A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.
The original chat panel answers one question well: what did I ask recently? Its
sidebar is a flat, reverse-chronological list with a client-side title filter and
three time buckets. That is adequate while a user has a handful of threads and
falls apart at fifty: there is no way to say this one matters, these three
belong to the same issue, or I am done with this, put it away.The knowledge base had a matching gap. Every KB screen sat behind
role:admin|super-admin, so a viewer or editor could read a document only
one at a time, through a chat citation — never browse the corpus.The Sessions workspace closes both. It is a new shell, not a new chat: the
rail carries the knowledge base above an organised session list, and everything
below the layout is the existing engine.
The requirement was explicit: do not duplicate anything the current chat
uses. That single constraint decided the architecture.A chat turn in AskMyDocs is not a request/response — it is a durable agent run
with a deferred-send queue, optimistic cache merges, auto-titling, retrieval
filter constraints, graph-aware citations, a realtime voice session and a
branch/edit/regenerate protocol. Roughly 350 lines of async orchestration lived
inside ChatView. Copying them into a second page would have meant every future
fix applied twice, and drift the first time someone touched one surface only.So the orchestration moved out of the view into a headless hook, and both
pages became presentation.
Both surfaces are mounted under the team route but deliberately skip
AppShell. The session rail is meant to be the only sidebar, so the
primary navigation would be a second, competing one — and the point of the
view is that you step into it, not that it is another admin page.TeamGate therefore chooses the chrome: isStandaloneSurface(pathname)
selects StandaloneShell for the sessions and knowledge segments and
AppShell for everything else. The tenant-sync logic is unchanged and shared,
so a deep link and a legacy hash-less URL behave identically on both sides.StandaloneShell keeps the irreducible minimum: a way back (a chrome-less
view without an exit is a trap), the team switcher (the tenant is the
outermost scope of everything below it, and a multi-tenant user would
otherwise be stranded), and the theme toggle. Absent by design: the nav
sidebar, breadcrumbs, the notification bell, the command palette and the
tweaks panel. A team switch swaps the hash in place, so it does not eject
you back into the frame you just left.The entry point stays in the main sidebar — Sessions and Browse KB in
the Workspace group — so /chat remains the page you arrive on and the door
into the workspace.ChatView went from 794 lines to about 220. The only thing the hook cannot own
is navigation: TanStack Router’s to is a typed literal, so each surface
injects its own targets through a ChatSessionNavigator. The admin-only Open in
Knowledge Base deep link is per-surface for the same reason — /chat sends
admins to the admin explorer, /sessions sends readers to the new Browse KB
page.
Switching to another chat or creating one leaves the original agent run and
its event subscription active. Each conversation owns its request, messages,
activity timeline and confirmation state; the open view subscribes to that
conversation without taking ownership of its lifetime. Searches in different
conversations can run simultaneously. The Stop action cancels only its own run.Both chat lists show ongoing searches, requests awaiting confirmation and
failures. A reply completed away from its conversation gets an unread badge
showing elapsed whole minutes from the server’s completion timestamp (0 min,
1 min, etc.). Opening the conversation clears the badge. Replies completed
while the conversation is open are already read. Partial answers use the same
rule; cancellations and failures do not produce unread-answer badges.Run status and read receipts are stored on the server. Refreshing or reopening
the app recovers active searches and replies completed while it was closed.
Recovery replays existing run events without posting the question again.
Only entering the corresponding conversation acknowledges its displayed reply;
loading or refreshing the chat list never clears badges. Read receipts name an
exact assistant message, so a late receipt cannot acknowledge a later reply.
Requests keep their original tenant when the foreground team changes, and
logout/user changes discard the local registry without deleting server state.chat_folders is a new tenant-aware table; conversations gains four additive
columns. Grouping by project already existed through
conversations.project_key and is unchanged — folders are the free-form axis on
top of it.Two indexes, chosen from the access pattern rather than the column list:
Index
Why
(tenant_id, user_id, archived_at)
the only selective predicate the sidebar issues
chat_folder_id
nullOnDelete sweeps conversations by it on every folder delete — unindexed, a full scan
pinned_at and importance are deliberately not indexed: neither is
selective, the ordering CASE cannot use an index anyway, and a user’s session
count is in the tens.
pinned_at / archived_at are nullable timestamps. That buys most recently
pinned first ordering and an archived-on date for free, indexes cleanly as
NULL-is-false, and needs no companion audit column.The HTTP contract is therefore asymmetric on purpose: the request carries a
boolean ({"pinned": true}), the response carries the timestamp. A client asks
for a state and reads back when it happened — it never invents a date the server
would disagree with.
updated_at is both the recency ordering key and the last activity the sidebar
shows. If pinning or archiving bumped it, archiving a thread and restoring it
would teleport an untouched conversation to the top of the list, and “last
activity” would stop meaning activity.So ConversationOrganizerService writes with timestamps = false and restores
the flag in a finally — leaving it off would make a rename later in the same
request go unrecorded. A rename still bumps it, because that is activity on
the thread.
The column is a string, so ordering on it directly would be alphabetical.
critical < high < normal happens to be almost correct today, which is precisely
the trap: it breaks silently the day a fourth level lands.
ConversationImportance::orderByCaseSql() builds the ORDER BY CASEfrom the
enum, and a unit test asserts every case appears in it.The stored value is the machine-readable identifier and never localizes. There
is deliberately no server-side label: the three user-visible strings live in
the Sessions UI alongside every other literal in that panel, so there is exactly
one translation surface rather than two that drift. A server-side label could
not serve the picker anyway, which needs labels for the two levels a session is
not at.The ordering also avoids ORDER BY pinned_at DESC, which is not portable:
PostgreSQL sorts NULLs first on DESC, SQLite last, MySQL has no NULLS LAST at
all. An explicit null-rank CASE behaves identically everywhere.
Only ?archived= is a server parameter. Folder grouping, search and sorting all
happen client-side: the server already returns the user’s sessions in display
order, and a list in the tens does not need a round-trip per keystroke — while a
server-side q would pull in full LIKE escaping for no user-visible gain.Archived exclusion is nonetheless server-side by default, so “archived” means
archived on every surface, and the older /chat sidebar inherits the behaviour
without a line of change.
chat_folder_id is nullOnDelete. Cascading would silently destroy a user’s
chat history, so deleting a folder leaves its sessions unfiled — and the
confirmation says so. A feature test asserts the sessions survive and the count
is unchanged.
This capability ships with a PHP surface
(ConversationOrganizerService, ChatFolderService) and an HTTP surface
(PATCH /conversations/{id}, /api/chat-folders), and deliberately no MCP
tool.Folders, pins, archive state and importance are per-user UI organisation
state: they change nothing an agent can retrieve, ground on, or reason about,
and exposing them would widen the write surface over someone’s private thread
list for zero agent benefit. The in-repo precedents are exact —
ChatFilterPresetController (per-user chat UI state, HTTP-only),
KbDocumentPreviewController (a documented single-surface chat-UI read), and
Admin\ProjectController (HTTP-only registry CRUD).GET /api/kb/tree needs no MCP tool either: the KB-tree capability already has
a PHP core (KbTreeService), an HTTP surface (the admin route) and MCP retrieval
tools. This is a second HTTP surface over the same core.
Everything is gated by authentication and tenant membership; no role gate.
The real boundaries are narrower than a role:
Folders and sessions — per-user ownership, enforced inside the services
with forTenant() plus where('user_id', …). Another user’s folder answers
404, never 403: a 403 would confirm the id exists.
chat_folder_id on the PATCH — validated against folders the acting user
owns in the active tenant, then re-verified in the service. The id is
attacker-chosen; without the ownership predicate user A could file a thread
into user B’s folder.
Browse KB — the SQL access scope, not a policy. GET /api/kb/tree runs
through the same KbTreeService as the admin tree, with KnowledgeDocument’s
global AccessScopeScope active and with_trashed neither accepted nor
forwarded (a reader never sees a soft-deleted document).
The visibility posture surprises people, so state it plainly. With
kb.project_isolation.enabled = false — the default every fresh deploy ships —
the viewer role holds kb.read.any, AccessScopeScope::apply()
short-circuits on canReadAllProjects(), and a reader sees every live
document in the tenant.That is the pre-existing retrieval posture: identical to what chat citations
and /api/kb/documents/{id}/preview already expose. The Browse KB page widens
nothing — it makes an existing posture visible. Enabling the flag narrows both.
Both states are pinned by tests/Feature/Api/KbTreeReaderTest.php, so a future
flip can never be silent.
GET /conversations returns a BARE array, not a {data: …} envelope. It
always has, so wrapping it would break every existing client in one step —
ConversationResource sets $wrap = null for exactly that reason. New keys
are additive, and created_at / updated_at pass through untouched, because
changing an existing key’s format breaks callers as surely as removing it.
The archived cache key is nested, ['conversations', 'archived'], not a
sibling like ['conversations-archived']. The chat engine already invalidates
['conversations'] after every settled turn, and TanStack’s prefix matching
then covers both lists. Flattening it leaves the archived drawer silently
stale.
conversations.chat_folder_id is not a pivot. A session belongs to at most
one folder; there is no multi-folder membership and no nesting.
An empty PATCH body is a 422, not a 200 no-op, so a caller can distinguish
“nothing applied” from “applied nothing”.
A freshly created model needs the attribute default.importance has a DB
default, but a create()d instance that never sent the column has the
attribute absent and the enum cast yields null — which is how
POST /conversations briefly answered 500 while every read path stayed green.
The default is mirrored on the model.
These two routes render without AppShell. Adding a third
full-screen surface means adding its segment to STANDALONE_SEGMENTS in
frontend/src/routes/index.tsx; forgetting it gives the new page two
sidebars, and adding a segment by mistake strips an admin page of its
navigation. standalone-surface.test.ts pins both directions.
/app/{team}/kb is not this page. That path is a legacy alias redirecting
into the admin explorer; the reader page is /app/{team}/knowledge.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"AskMyDocs","description":"Enterprise RAG + canonical knowledge graph + agentic tool use — self-hostable, MIT licensed.","colors":{"primary":"#6366f1","light":"#818cf8","dark":"#4f46e5"},"favicon":"https://mintcdn.com/padosoft/5lJ1TIL_Ft2f-U_h/favicon.svg?fit=max\u0026auto=format\u0026n=5lJ1TIL_Ft2f-U_h\u0026q=85\u0026s=2daf903727ff773a9ebe5503693559f8","navbar":{"links":[{"href":"https://github.com/lopadova/AskMyDocs","label":"GitHub"}]},"footer":{"socials":{"github":"https://github.com/lopadova/AskMyDocs"}},"styling":{"latex":true},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"padosoft","hasAudioNarration":false,"actualSubdomain":"padosoft","customDomains":["doc.askmydocs.padosoft.com"],"gitSource":{"type":"github","owner":"lopadova","repo":"askmydocs","deployBranch":"main","contentDirectory":"docs-site","isPrivate":true,"mountPath":"","label":""},"chroma":{"collectionId":"abdcb26f-5e79-4753-ba8a-7673e44b56ce","liveDeploymentHistoryId":"6aecb6c898140ff047bd1ab6","schemaVersion":"v3","latestBuiltDeploymentHistoryId":"6aecb6c898140ff047bd1ab6"},"entitlements":{"AI_CHAT":{"status":"DISABLED"}},"buildId":"6aecb6c898140ff047bd1ab6","clientVersion":"0.0.4074","searchFilterCounts":[{"language":"default","tag":"Guides","count":36},{"language":"default","tag":"Architecture","count":10},{"language":"default","tag":"Integrations","count":6},{"language":"default","tag":"Configuration \u0026 Operations","count":6},{"language":"default","tag":"Best Practices","count":5},{"language":"default","tag":"Get Started","count":4},{"language":"default","tag":"Reference","count":3}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"rbac-expansion","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-serve","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"live-agents","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"comment-mode","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[],"page":{"pagePath":"chat-sessions-workspace","slug":"/chat-sessions-workspace","pageMetadata":{"title":"Sessions workspace","description":"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.","icon":"layer-group","href":"/chat-sessions-workspace"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,em:`em`,li:`li`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,Heading,Mermaid,Table,Warning}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Mermaid)_missingMdxReference(`Mermaid`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsx(Heading,{level:`2`,id:`motivation`,children:`Motivation`}),`\n`,_jsxs(_components.p,{children:[`The original chat panel answers one question well: `,_jsx(_components.em,{children:`what did I ask recently?`}),` Its\nsidebar is a flat, reverse-chronological list with a client-side title filter and\nthree time buckets. That is adequate while a user has a handful of threads and\nfalls apart at fifty: there is no way to say `,_jsx(_components.em,{children:`this one matters`}),`, `,_jsx(_components.em,{children:`these three\nbelong to the same issue`}),`, or `,_jsx(_components.em,{children:`I am done with this, put it away`}),`.`]}),`\n`,_jsxs(_components.p,{children:[`The knowledge base had a matching gap. Every KB screen sat behind\n`,_jsx(_components.code,{children:`role:admin|super-admin`}),`, so a `,_jsx(_components.code,{children:`viewer`}),` or `,_jsx(_components.code,{children:`editor`}),` could read a document only\none at a time, through a chat citation — never browse the corpus.`]}),`\n`,_jsxs(_components.p,{children:[`The `,_jsx(_components.strong,{children:`Sessions workspace`}),` closes both. It is a new `,_jsx(_components.em,{children:`shell`}),`, not a new chat: the\nrail carries the knowledge base above an organised session list, and everything\nbelow the layout is the existing engine.`]}),`\n`,_jsx(Heading,{level:`2`,id:`the-constraint-that-shaped-the-design`,children:`The constraint that shaped the design`}),`\n`,_jsxs(_components.p,{children:[`The requirement was explicit: `,_jsx(_components.strong,{children:`do not duplicate anything the current chat\nuses.`}),` That single constraint decided the architecture.`]}),`\n`,_jsxs(_components.p,{children:[`A chat turn in AskMyDocs is not a request/response — it is a durable agent run\nwith a deferred-send queue, optimistic cache merges, auto-titling, retrieval\nfilter constraints, graph-aware citations, a realtime voice session and a\nbranch/edit/regenerate protocol. Roughly 350 lines of async orchestration lived\ninside `,_jsx(_components.code,{children:`ChatView`}),`. Copying them into a second page would have meant every future\nfix applied twice, and drift the first time someone touched one surface only.`]}),`\n`,_jsxs(_components.p,{children:[`So the orchestration moved `,_jsx(_components.strong,{children:`out`}),` of the view into a headless hook, and both\npages became presentation.`]}),`\n`,_jsx(Mermaid,{chart:`flowchart TB\n subgraph Shared[\"Shared engine — one implementation\"]\n H[\"useChatSession()\u003cbr/>URL sync · project scope · effectiveFilters\u003cbr/>deferred-send queue · auto-title · branch/edit\"]\n A[\"useAgentChat()\"]\n R[\"useRealtimeAgent()\"]\n C[\"Composer · FilterBar · MessageThread\u003cbr/>MessageBubble · Citations · CitationDocumentModal\"]\n H --> A\n H --> R\n end\n\n subgraph Surfaces[\"Presentation — two shells\"]\n CV[\"ChatView\u003cbr/>/chat · inside AppShell\"]\n SV[\"SessionsView\u003cbr/>/sessions · StandaloneShell\"]\n end\n\n CV -->|\"nav: /chat targets\"| H\n SV -->|\"nav: /sessions targets\"| H\n CV --> C\n SV --> C\n SV --> SB[\"SessionSidebar\u003cbr/>KB entry · folders · pin · importance · archive\"]\n SV --> KB[\"KnowledgeBrowseView\u003cbr/>/knowledge\"]`}),`\n`,_jsx(Heading,{level:`3`,id:`it-renders-outside-the-app-frame`,children:`It renders outside the app frame`}),`\n`,_jsxs(_components.p,{children:[`Both surfaces are mounted under the team route but deliberately skip\n`,_jsx(_components.code,{children:`AppShell`}),`. The session rail is meant to be the `,_jsx(_components.strong,{children:`only`}),` sidebar, so the\nprimary navigation would be a second, competing one — and the point of the\nview is that you step into it, not that it is another admin page.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`TeamGate`}),` therefore chooses the chrome: `,_jsx(_components.code,{children:`isStandaloneSurface(pathname)`}),`\nselects `,_jsx(_components.code,{children:`StandaloneShell`}),` for the `,_jsx(_components.code,{children:`sessions`}),` and `,_jsx(_components.code,{children:`knowledge`}),` segments and\n`,_jsx(_components.code,{children:`AppShell`}),` for everything else. The tenant-sync logic is unchanged and shared,\nso a deep link and a legacy hash-less URL behave identically on both sides.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`StandaloneShell`}),` keeps the irreducible minimum: a way `,_jsx(_components.strong,{children:`back`}),` (a chrome-less\nview without an exit is a trap), the `,_jsx(_components.strong,{children:`team switcher`}),` (the tenant is the\noutermost scope of everything below it, and a multi-tenant user would\notherwise be stranded), and the theme toggle. Absent by design: the nav\nsidebar, breadcrumbs, the notification bell, the command palette and the\ntweaks panel. A team switch swaps the hash `,_jsx(_components.strong,{children:`in place`}),`, so it does not eject\nyou back into the frame you just left.`]}),`\n`,_jsxs(_components.p,{children:[`The entry point stays in the main sidebar — `,_jsx(_components.strong,{children:`Sessions`}),` and `,_jsx(_components.strong,{children:`Browse KB`}),` in\nthe Workspace group — so `,_jsx(_components.code,{children:`/chat`}),` remains the page you arrive on and the door\ninto the workspace.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`ChatView`}),` went from 794 lines to about 220. The only thing the hook cannot own\nis `,_jsx(_components.strong,{children:`navigation`}),`: TanStack Router’s `,_jsx(_components.code,{children:`to`}),` is a typed literal, so each surface\ninjects its own targets through a `,_jsx(_components.code,{children:`ChatSessionNavigator`}),`. The admin-only `,_jsx(_components.em,{children:`Open in\nKnowledge Base`}),` deep link is per-surface for the same reason — `,_jsx(_components.code,{children:`/chat`}),` sends\nadmins to the admin explorer, `,_jsx(_components.code,{children:`/sessions`}),` sends readers to the new Browse KB\npage.`]}),`\n`,_jsx(Heading,{level:`2`,id:`data-model`,children:`Data model`}),`\n`,_jsx(Heading,{level:`3`,id:`background-searches-and-unread-replies`,children:`Background searches and unread replies`}),`\n`,_jsx(_components.p,{children:`Switching to another chat or creating one leaves the original agent run and\nits event subscription active. Each conversation owns its request, messages,\nactivity timeline and confirmation state; the open view subscribes to that\nconversation without taking ownership of its lifetime. Searches in different\nconversations can run simultaneously. The Stop action cancels only its own run.`}),`\n`,_jsxs(_components.p,{children:[`Both chat lists show ongoing searches, requests awaiting confirmation and\nfailures. A reply completed away from its conversation gets an unread badge\nshowing elapsed whole minutes from the server’s completion timestamp (`,_jsx(_components.code,{children:`0 min`}),`,\n`,_jsx(_components.code,{children:`1 min`}),`, etc.). Opening the conversation clears the badge. Replies completed\nwhile the conversation is open are already read. Partial answers use the same\nrule; cancellations and failures do not produce unread-answer badges.`]}),`\n`,_jsx(_components.p,{children:`Run status and read receipts are stored on the server. Refreshing or reopening\nthe app recovers active searches and replies completed while it was closed.\nRecovery replays existing run events without posting the question again.\nOnly entering the corresponding conversation acknowledges its displayed reply;\nloading or refreshing the chat list never clears badges. Read receipts name an\nexact assistant message, so a late receipt cannot acknowledge a later reply.\nRequests keep their original tenant when the foreground team changes, and\nlogout/user changes discard the local registry without deleting server state.`}),`\n`,_jsx(Mermaid,{chart:`erDiagram\n CHAT_FOLDERS ||--o{ CONVERSATIONS : \"files (nullOnDelete)\"\n USERS ||--o{ CHAT_FOLDERS : owns\n USERS ||--o{ CONVERSATIONS : owns\n\n CHAT_FOLDERS {\n bigint id PK\n string tenant_id\n bigint user_id FK\n string name\n smallint position\n }\n CONVERSATIONS {\n bigint id PK\n string tenant_id\n bigint user_id FK\n string project_key\n bigint chat_folder_id FK \"nullable — null = unfiled\"\n timestamp pinned_at \"nullable\"\n timestamp archived_at \"nullable\"\n string importance \"normal | high | critical\"\n }`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`chat_folders`}),` is a new tenant-aware table; `,_jsx(_components.code,{children:`conversations`}),` gains four additive\ncolumns. Grouping by `,_jsx(_components.strong,{children:`project`}),` already existed through\n`,_jsx(_components.code,{children:`conversations.project_key`}),` and is unchanged — folders are the free-form axis on\ntop of it.`]}),`\n`,_jsx(_components.p,{children:`Two indexes, chosen from the access pattern rather than the column list:`}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Index`}),_jsx(_components.th,{children:`Why`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`(tenant_id, user_id, archived_at)`})}),_jsx(_components.td,{children:`the only selective predicate the sidebar issues`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`chat_folder_id`})}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`nullOnDelete`}),` sweeps conversations by it on every folder delete — unindexed, a full scan`]})]})]})]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`pinned_at`}),` and `,_jsx(_components.code,{children:`importance`}),` are deliberately `,_jsx(_components.strong,{children:`not`}),` indexed: neither is\nselective, the ordering `,_jsx(_components.code,{children:`CASE`}),` cannot use an index anyway, and a user’s session\ncount is in the tens.`]}),`\n`,_jsx(Heading,{level:`2`,id:`decision-rationale`,children:`Decision rationale`}),`\n`,_jsx(Heading,{level:`3`,id:`timestamps-not-booleans-for-pin-and-archive`,children:`Timestamps, not booleans, for pin and archive`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`pinned_at`}),` / `,_jsx(_components.code,{children:`archived_at`}),` are nullable timestamps. That buys `,_jsx(_components.em,{children:`most recently\npinned first`}),` ordering and an archived-on date for free, indexes cleanly as\nNULL-is-false, and needs no companion audit column.`]}),`\n`,_jsxs(_components.p,{children:[`The HTTP contract is therefore `,_jsx(_components.strong,{children:`asymmetric on purpose`}),`: the request carries a\nboolean (`,_jsx(_components.code,{children:`{\"pinned\": true}`}),`), the response carries the timestamp. A client asks\nfor a state and reads back when it happened — it never invents a date the server\nwould disagree with.`]}),`\n`,_jsxs(Heading,{level:`3`,id:`organisation-writes-must-not-touch-updated_at`,children:[`Organisation writes must not touch `,_jsx(_components.code,{children:`updated_at`})]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`updated_at`}),` is both the recency ordering key and the `,_jsx(_components.em,{children:`last activity`}),` the sidebar\nshows. If pinning or archiving bumped it, archiving a thread and restoring it\nwould teleport an untouched conversation to the top of the list, and “last\nactivity” would stop meaning activity.`]}),`\n`,_jsxs(_components.p,{children:[`So `,_jsx(_components.code,{children:`ConversationOrganizerService`}),` writes with `,_jsx(_components.code,{children:`timestamps = false`}),` and restores\nthe flag in a `,_jsx(_components.code,{children:`finally`}),` — leaving it off would make a rename later in the same\nrequest go unrecorded. A `,_jsx(_components.strong,{children:`rename still bumps it`}),`, because that `,_jsx(_components.em,{children:`is`}),` activity on\nthe thread.`]}),`\n`,_jsx(Heading,{level:`3`,id:`importance-is-an-enum-with-explicit-weights`,children:`Importance is an enum with explicit weights`}),`\n`,_jsxs(_components.p,{children:[`The column is a `,_jsx(_components.code,{children:`string`}),`, so ordering on it directly would be alphabetical.\n`,_jsx(_components.code,{children:`critical \u003c high \u003c normal`}),` happens to be almost correct today, which is precisely\nthe trap: it breaks silently the day a fourth level lands.\n`,_jsx(_components.code,{children:`ConversationImportance::orderByCaseSql()`}),` builds the `,_jsx(_components.code,{children:`ORDER BY CASE`}),` `,_jsx(_components.strong,{children:`from the\nenum`}),`, and a unit test asserts every case appears in it.`]}),`\n`,_jsxs(_components.p,{children:[`The stored value is the machine-readable identifier and never localizes. There\nis deliberately `,_jsx(_components.strong,{children:`no`}),` server-side label: the three user-visible strings live in\nthe Sessions UI alongside every other literal in that panel, so there is exactly\none translation surface rather than two that drift. A server-side label could\nnot serve the picker anyway, which needs labels for the two levels a session is\n`,_jsx(_components.em,{children:`not`}),` at.`]}),`\n`,_jsxs(_components.p,{children:[`The ordering also avoids `,_jsx(_components.code,{children:`ORDER BY pinned_at DESC`}),`, which is not portable:\nPostgreSQL sorts NULLs first on `,_jsx(_components.code,{children:`DESC`}),`, SQLite last, MySQL has no `,_jsx(_components.code,{children:`NULLS LAST`}),` at\nall. An explicit null-rank `,_jsx(_components.code,{children:`CASE`}),` behaves identically everywhere.`]}),`\n`,_jsx(Heading,{level:`3`,id:`one-server-side-list-parameter`,children:`One server-side list parameter`}),`\n`,_jsxs(_components.p,{children:[`Only `,_jsx(_components.code,{children:`?archived=`}),` is a server parameter. Folder grouping, search and sorting all\nhappen client-side: the server already returns the user’s sessions in display\norder, and a list in the tens does not need a round-trip per keystroke — while a\nserver-side `,_jsx(_components.code,{children:`q`}),` would pull in full LIKE escaping for no user-visible gain.`]}),`\n`,_jsxs(_components.p,{children:[`Archived exclusion is nonetheless `,_jsx(_components.strong,{children:`server-side by default`}),`, so “archived” means\narchived on every surface, and the older `,_jsx(_components.code,{children:`/chat`}),` sidebar inherits the behaviour\nwithout a line of change.`]}),`\n`,_jsx(Heading,{level:`3`,id:`folder-delete-unfiles-it-never-cascades`,children:`Folder delete unfiles; it never cascades`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`chat_folder_id`}),` is `,_jsx(_components.code,{children:`nullOnDelete`}),`. Cascading would silently destroy a user’s\nchat history, so deleting a folder leaves its sessions unfiled — and the\nconfirmation says so. A feature test asserts the sessions survive and the count\nis unchanged.`]}),`\n`,_jsx(Heading,{level:`3`,id:`r44-—-a-documented-mcp-exception`,children:`R44 — a documented MCP exception`}),`\n`,_jsxs(_components.p,{children:[`This capability ships with a `,_jsx(_components.strong,{children:`PHP surface`}),`\n(`,_jsx(_components.code,{children:`ConversationOrganizerService`}),`, `,_jsx(_components.code,{children:`ChatFolderService`}),`) and an `,_jsx(_components.strong,{children:`HTTP surface`}),`\n(`,_jsx(_components.code,{children:`PATCH /conversations/{id}`}),`, `,_jsx(_components.code,{children:`/api/chat-folders`}),`), and deliberately `,_jsx(_components.strong,{children:`no MCP\ntool`}),`.`]}),`\n`,_jsxs(_components.p,{children:[`Folders, pins, archive state and importance are `,_jsx(_components.em,{children:`per-user UI organisation\nstate`}),`: they change nothing an agent can retrieve, ground on, or reason about,\nand exposing them would widen the write surface over someone’s private thread\nlist for zero agent benefit. The in-repo precedents are exact —\n`,_jsx(_components.code,{children:`ChatFilterPresetController`}),` (per-user chat UI state, HTTP-only),\n`,_jsx(_components.code,{children:`KbDocumentPreviewController`}),` (a documented single-surface chat-UI read), and\n`,_jsx(_components.code,{children:`Admin\\\\ProjectController`}),` (HTTP-only registry CRUD).`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`GET /api/kb/tree`}),` needs no MCP tool either: the KB-tree `,_jsx(_components.em,{children:`capability`}),` already has\na PHP core (`,_jsx(_components.code,{children:`KbTreeService`}),`), an HTTP surface (the admin route) and MCP retrieval\ntools. This is a second HTTP surface over the same core.`]}),`\n`,_jsx(Heading,{level:`2`,id:`access-and-visibility`,children:`Access and visibility`}),`\n`,_jsxs(_components.p,{children:[`Everything is gated by authentication and tenant membership; `,_jsx(_components.strong,{children:`no role gate`}),`.\nThe real boundaries are narrower than a role:`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Folders and sessions`}),` — per-user ownership, enforced inside the services\nwith `,_jsx(_components.code,{children:`forTenant()`}),` plus `,_jsx(_components.code,{children:`where('user_id', …)`}),`. Another user’s folder answers\n`,_jsx(_components.strong,{children:`404, never 403`}),`: a 403 would confirm the id exists.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`chat_folder_id`}),` on the PATCH`]}),` — validated against folders the `,_jsx(_components.em,{children:`acting user`}),`\nowns in the `,_jsx(_components.em,{children:`active tenant`}),`, then re-verified in the service. The id is\nattacker-chosen; without the ownership predicate user A could file a thread\ninto user B’s folder.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Browse KB`}),` — the SQL access scope, not a policy. `,_jsx(_components.code,{children:`GET /api/kb/tree`}),` runs\nthrough the same `,_jsx(_components.code,{children:`KbTreeService`}),` as the admin tree, with `,_jsx(_components.code,{children:`KnowledgeDocument`}),`’s\nglobal `,_jsx(_components.code,{children:`AccessScopeScope`}),` active and `,_jsx(_components.code,{children:`with_trashed`}),` neither accepted nor\nforwarded (a reader never sees a soft-deleted document).`]}),`\n`]}),`\n`,_jsxs(Warning,{children:[_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`The visibility posture surprises people, so state it plainly.`}),` With\n`,_jsx(_components.code,{children:`kb.project_isolation.enabled = false`}),` — the default every fresh deploy ships —\nthe `,_jsx(_components.code,{children:`viewer`}),` role holds `,_jsx(_components.code,{children:`kb.read.any`}),`, `,_jsx(_components.code,{children:`AccessScopeScope::apply()`}),`\nshort-circuits on `,_jsx(_components.code,{children:`canReadAllProjects()`}),`, and a reader sees `,_jsx(_components.strong,{children:`every live\ndocument in the tenant`}),`.`]}),_jsxs(_components.p,{children:[`That is the `,_jsx(_components.em,{children:`pre-existing`}),` retrieval posture: identical to what chat citations\nand `,_jsx(_components.code,{children:`/api/kb/documents/{id}/preview`}),` already expose. The Browse KB page widens\nnothing — it makes an existing posture visible. Enabling the flag narrows both.\nBoth states are pinned by `,_jsx(_components.code,{children:`tests/Feature/Api/KbTreeReaderTest.php`}),`, so a future\nflip can never be silent.`]})]}),`\n`,_jsx(Heading,{level:`2`,id:`worked-example`,children:`Worked example`}),`\n`,_jsx(_components.p,{children:`Filing a thread under an issue and flagging it:`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`19`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`19`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# 1. Create the folder (per-(tenant, user) unique name; a duplicate is 422).`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`curl`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -X`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` POST`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` https://askmydocs.example/api/chat-folders`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D7BA7D`},children:` \\\\`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -H`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'X-Tenant-Id: acme'`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -H`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'Content-Type: application/json'`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D7BA7D`},children:` \\\\`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` --cookie-jar`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` /tmp/c`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` --cookie`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` /tmp/c`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D7BA7D`},children:` \\\\`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -d`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` '{\"name\":\"Issue 42 — onboarding\"}'`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# → 201 {\"data\":{\"id\":7,\"name\":\"Issue 42 — onboarding\",\"position\":0,…}}`})}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# 2. File a session into it and mark it critical. One partial PATCH, and`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:\"# `chat_folder_id: null` would UNFILE it — omitting the key leaves it alone.\"})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`curl`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -X`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` PATCH`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` https://askmydocs.example/conversations/123`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D7BA7D`},children:` \\\\`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -H`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'X-Tenant-Id: acme'`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -H`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'Content-Type: application/json'`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D7BA7D`},children:` \\\\`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` --cookie`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` /tmp/c`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D7BA7D`},children:` \\\\`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -d`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` '{\"chat_folder_id\":7,\"importance\":\"critical\",\"pinned\":true}'`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# → 200 {\"id\":123,…,\"chat_folder_id\":7,\"pinned_at\":\"2026-09-16T11:00:00Z\",`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# \"importance\":\"critical\",\"updated_at\":\"\u003cunchanged>\"}`})}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# 3. The default listing still excludes archived threads.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`curl`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` https://askmydocs.example/conversations`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -H`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'X-Tenant-Id: acme'`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` --cookie`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` /tmp/c`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`curl`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'https://askmydocs.example/conversations?archived=1'`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -H`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'X-Tenant-Id: acme'`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` --cookie`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` /tmp/c`})]}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`Note `,_jsx(_components.code,{children:`updated_at`}),` in step 2: organising a session never moves it.`]}),`\n`,_jsx(Heading,{level:`2`,id:`gotchas`,children:`Gotchas`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`GET /conversations`}),` returns a BARE array`]}),`, not a `,_jsx(_components.code,{children:`{data: …}`}),` envelope. It\nalways has, so wrapping it would break every existing client in one step —\n`,_jsx(_components.code,{children:`ConversationResource`}),` sets `,_jsx(_components.code,{children:`$wrap = null`}),` for exactly that reason. New keys\nare additive, and `,_jsx(_components.code,{children:`created_at`}),` / `,_jsx(_components.code,{children:`updated_at`}),` pass through untouched, because\nchanging an existing key’s `,_jsx(_components.em,{children:`format`}),` breaks callers as surely as removing it.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`The archived cache key is nested`}),`, `,_jsx(_components.code,{children:`['conversations', 'archived']`}),`, not a\nsibling like `,_jsx(_components.code,{children:`['conversations-archived']`}),`. The chat engine already invalidates\n`,_jsx(_components.code,{children:`['conversations']`}),` after every settled turn, and TanStack’s prefix matching\nthen covers both lists. Flattening it leaves the archived drawer silently\nstale.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`conversations.chat_folder_id`}),` is not a pivot.`]}),` A session belongs to at most\none folder; there is no multi-folder membership and no nesting.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`An empty PATCH body is a 422`}),`, not a 200 no-op, so a caller can distinguish\n“nothing applied” from “applied nothing”.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`A freshly created model needs the attribute default.`}),` `,_jsx(_components.code,{children:`importance`}),` has a DB\ndefault, but a `,_jsx(_components.code,{children:`create()`}),`d instance that never sent the column has the\nattribute absent and the enum cast yields `,_jsx(_components.code,{children:`null`}),` — which is how\n`,_jsx(_components.code,{children:`POST /conversations`}),` briefly answered 500 while every read path stayed green.\nThe default is mirrored on the model.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[`These two routes render without `,_jsx(_components.code,{children:`AppShell`}),`.`]}),` Adding a third\nfull-screen surface means adding its segment to `,_jsx(_components.code,{children:`STANDALONE_SEGMENTS`}),` in\n`,_jsx(_components.code,{children:`frontend/src/routes/index.tsx`}),`; forgetting it gives the new page two\nsidebars, and adding a segment by mistake strips an admin page of its\nnavigation. `,_jsx(_components.code,{children:`standalone-surface.test.ts`}),` pins both directions.`]}),`\n`,_jsxs(_components.li,{children:[_jsxs(_components.strong,{children:[_jsx(_components.code,{children:`/app/{team}/kb`}),` is not this page.`]}),` That path is a legacy alias redirecting\ninto the admin explorer; the reader page is `,_jsx(_components.code,{children:`/app/{team}/knowledge`}),`.`]}),`\n`]}),`\n`,_jsx(Heading,{level:`2`,id:`see-also`,children:`See also`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.a,{href:`/chat-and-retrieval`,children:`Chat \u0026 retrieval`}),` — what actually happens inside a turn.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.a,{href:`/admin-panel`,children:`Admin panel`}),` — the admin KB explorer, editing and upload.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.a,{href:`/architecture/multi-tenancy`,children:`Multi-tenancy`}),` — how `,_jsx(_components.code,{children:`tenant_id`}),` scoping works.`]}),`\n`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Sessions workspace","description":"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.","icon":"layer-group","href":"/chat-sessions-workspace"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"Motivation","slug":"motivation","depth":2,"children":[]},{"title":"The constraint that shaped the design","slug":"the-constraint-that-shaped-the-design","depth":2,"children":[{"title":"It renders outside the app frame","slug":"it-renders-outside-the-app-frame","depth":3,"children":[]}]},{"title":"Data model","slug":"data-model","depth":2,"children":[{"title":"Background searches and unread replies","slug":"background-searches-and-unread-replies","depth":3,"children":[]}]},{"title":"Decision rationale","slug":"decision-rationale","depth":2,"children":[{"title":"Timestamps, not booleans, for pin and archive","slug":"timestamps-not-booleans-for-pin-and-archive","depth":3,"children":[]},{"title":"Organisation writes must not touch updated_at","slug":"organisation-writes-must-not-touch-updated_at","depth":3,"children":[]},{"title":"Importance is an enum with explicit weights","slug":"importance-is-an-enum-with-explicit-weights","depth":3,"children":[]},{"title":"One server-side list parameter","slug":"one-server-side-list-parameter","depth":3,"children":[]},{"title":"Folder delete unfiles; it never cascades","slug":"folder-delete-unfiles-it-never-cascades","depth":3,"children":[]},{"title":"R44 — a documented MCP exception","slug":"r44-—-a-documented-mcp-exception","depth":3,"children":[]}]},{"title":"Access and visibility","slug":"access-and-visibility","depth":2,"children":[]},{"title":"Worked example","slug":"worked-example","depth":2,"children":[]},{"title":"Gotchas","slug":"gotchas","depth":2,"children":[]},{"title":"See also","slug":"see-also","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":[],"subdomain":"padosoft","siteName":"AskMyDocs","title":"Sessions workspace - AskMyDocs","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-11-05T17:03:47.727Z","originalFileLocation":"chat-sessions-workspace.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.\">\u003cmeta data-mint-head name=\"application-name\" content=\"AskMyDocs\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Sessions workspace - AskMyDocs\">\u003cmeta data-mint-head property=\"og:description\" content=\"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"AskMyDocs\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://padosoft.mintlify.site/_mintlify/api/og/v2?division=Guides\u0026amp;title=Sessions+workspace\u0026amp;description=A+ChatGPT-style+chat+surface+with+the+knowledge+base+on+top+and+sessions+organised+into+folders%2C+pins+and+importance+%E2%80%94+built+as+a+second+shell+over+the+one+exis\u0026amp;theme=69afb4f64dc420baf20d4f78\u0026amp;signature=b438cfdfa6cad57f8babc3fcfdb407fb8c3eaa6e245637633225eeb28eaeabf3\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Sessions workspace - AskMyDocs\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://padosoft.mintlify.site/_mintlify/api/og/v2?division=Guides\u0026amp;title=Sessions+workspace\u0026amp;description=A+ChatGPT-style+chat+surface+with+the+knowledge+base+on+top+and+sessions+organised+into+folders%2C+pins+and+importance+%E2%80%94+built+as+a+second+shell+over+the+one+exis\u0026amp;theme=69afb4f64dc420baf20d4f78\u0026amp;signature=b438cfdfa6cad57f8babc3fcfdb407fb8c3eaa6e245637633225eeb28eaeabf3\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/padosoft/m2DETVtAyJ1cZl2Y/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"AskMyDocs\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#6366f1\">\u003clink data-mint-head rel=\"canonical\" href=\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/chat-sessions-workspace.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://doc.askmydocs.padosoft.com/#organization\",\"name\":\"AskMyDocs\",\"url\":\"https://doc.askmydocs.padosoft.com\"},{\"@type\":\"WebSite\",\"@id\":\"https://doc.askmydocs.padosoft.com#website\",\"name\":\"AskMyDocs\",\"url\":\"https://doc.askmydocs.padosoft.com\",\"publisher\":{\"@id\":\"https://doc.askmydocs.padosoft.com/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace#webpage\",\"url\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace\",\"name\":\"Sessions workspace\",\"description\":\"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.\",\"dateModified\":\"2026-11-05T17:03:47.727Z\",\"isPartOf\":{\"@id\":\"https://doc.askmydocs.padosoft.com#website\"},\"breadcrumb\":{\"@id\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Guides\",\"item\":\"https://doc.askmydocs.padosoft.com/ingestion\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Sessions workspace\",\"item\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace#article\",\"headline\":\"Sessions workspace\",\"name\":\"Sessions workspace\",\"description\":\"A ChatGPT-style chat surface with the knowledge base on top and sessions organised into folders, pins and importance — built as a second shell over the one existing chat engine.\",\"url\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace\",\"mainEntityOfPage\":{\"@id\":\"https://doc.askmydocs.padosoft.com/chat-sessions-workspace#webpage\"},\"dateModified\":\"2026-11-05T17:03:47.727Z\",\"publisher\":{\"@id\":\"https://doc.askmydocs.padosoft.com/#organization\"},\"isPartOf\":{\"@id\":\"https://doc.askmydocs.padosoft.com#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"AskMyDocs\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"AskMyDocs","nav":{"global":null,"groups":[{"group":"Get Started","icon":"rocket","pages":[{"title":"Introduction","icon":"book-open","href":"/introduction"},{"title":"Quickstart","icon":"bolt","href":"/quickstart"},{"title":"Installation","icon":"download","href":"/installation"},{"title":"Core concepts","icon":"diagram-project","href":"/core-concepts"}]},{"group":"Guides","icon":"book-open","pages":[{"title":"Ingesting documents","icon":"file-import","href":"/ingestion"},{"title":"Documents \u0026 OCR","icon":"file-magnifying-glass","href":"/documents-and-ocr"},{"title":"Digitization Review","icon":"clipboard-check","href":"/digitization-review"},{"title":"Portable Wiki Export","icon":"folder-tree","href":"/portable-wiki-export"},{"title":"Chat \u0026 retrieval","description":"A chat turn end to end — hybrid retrieval, reranker fusion, graph expansion, rejected-approach injection, the typed prompt, grounded citations, and the refusal contract.","icon":"comments","href":"/chat-and-retrieval"},{"title":"Sessions workspace","icon":"layer-group","href":"/chat-sessions-workspace"},{"title":"Canonical knowledge \u0026 promotion","description":"The typed canonical layer, the nine artifact types, and the human-gated three-stage promotion pipeline (ADR 0003) that keeps the LLM at 'draft'.","icon":"user-check","href":"/canonical-and-promotion"},{"title":"Auto-Wiki — the self-compiling tier","icon":"wand-magic-sparkles","href":"/auto-wiki"},{"title":"Universal connectors","icon":"plug","href":"/connectors"},{"title":"Credential-based connectors (IMAP)","icon":"key","href":"/connectors-credential"},{"title":"Multi-account \u0026 project-scoped connectors","icon":"layer-group","href":"/connectors-multi-account"},{"title":"Connector sync settings \u0026 folder discovery","icon":"sliders","href":"/connectors-sync-settings"},{"title":"Gmail, Microsoft Mail and shared email triage","icon":"envelope","href":"/connectors-email-api"},{"title":"Ingest provenance","href":"/ingest-provenance"},{"title":"Source Permissions","href":"/source-permissions"},{"title":"Provenance Tool Firewall","href":"/tool-firewall"},{"title":"Durable IMAP full-history imports","icon":"inbox-full","href":"/connectors-imap-backfill"},{"title":"Per-mailbox IMAP serialization","icon":"lock","href":"/connectors-imap-serialization"},{"title":"API connector — endpoints as live LLM tools","icon":"plug-circle-bolt","href":"/connectors-api"},{"title":"Flow orchestration","icon":"route","href":"/flow-orchestration"},{"title":"KITT — the embeddable agent","icon":"robot","href":"/kitt-widget"},{"title":"The admin cockpit","icon":"gauge","href":"/admin-panel"},{"title":"The project registry","icon":"folder-tree","href":"/projects-registry"},{"title":"The team switcher","icon":"users","href":"/team-switcher"},{"title":"Team and tenant management","icon":"building","href":"/team-management"},{"title":"System administration","icon":"shield-halved","href":"/system-administration"},{"title":"Engagement \u0026 Intelligence Suite","icon":"activity","href":"/engagement-suite"},{"title":"Multi-channel Digests","icon":"mail","href":"/digests"},{"title":"Engagement Dashboards","icon":"chart-line","href":"/dashboards"},{"title":"Gamification","icon":"award","href":"/gamification"},{"title":"Agentic Knowledge Reports","icon":"table-cells","href":"/agentic-knowledge-reports"},{"title":"Evidence \u0026 Risk Review","icon":"shield-check","href":"/evidence-risk-review"},{"title":"AI FinOps — spend governance","icon":"wallet","href":"/ai-finops"},{"title":"AI Guardrails — chat input + output firewall","icon":"shield-halved","href":"/ai-guardrails"},{"title":"Invitations \u0026 Referrals","icon":"ticket","href":"/invitations"},{"title":"PII redaction \u0026 compliance","icon":"shield-halved","href":"/pii-and-compliance"}]},{"group":"Best Practices","icon":"lightbulb","pages":[{"title":"Institutional memory \u0026 anti-repetition","icon":"brain","href":"/institutional-memory"},{"title":"Grounding \u0026 evidence tiers","icon":"scale-balanced","href":"/grounding-and-evidence-tiers"},{"title":"The anti-hallucination firewall","icon":"shield-halved","href":"/anti-hallucination-firewall"},{"title":"Multi-tenant isolation","icon":"building-shield","href":"/multi-tenant-isolation"},{"title":"Automated isolation testing","icon":"shield-check","href":"/isolation-testing"}]},{"group":"Integrations","icon":"puzzle-piece","pages":[{"title":"AI providers","icon":"microchip","href":"/ai-providers"},{"title":"MCP server","icon":"terminal","href":"/mcp-server"},{"title":"Connect AI tools to a project","icon":"plug","href":"/codex-project-connection"},{"title":"Desktop client \u0026 token authentication","icon":"desktop","href":"/desktop-client"},{"title":"Connect external applications with OAuth","icon":"key","href":"/external-oauth"},{"title":"Sister packages","icon":"cubes","href":"/sister-packages"}]},{"group":"Configuration \u0026 Operations","icon":"gear","pages":[{"title":"Configuration","icon":"sliders","href":"/configuration"},{"title":"Runtime configuration governance","icon":"sliders","href":"/runtime-config-governance"},{"title":"Self-hosting","icon":"server","href":"/self-hosting"},{"title":"Case-study email datasets","icon":"envelope-circle-check","href":"/case-study-email-datasets"},{"title":"Scheduler \u0026 maintenance","icon":"clock","href":"/scheduler-and-maintenance"},{"title":"Troubleshooting","icon":"wrench","href":"/troubleshooting"}]},{"group":"Architecture","icon":"diagram-project","pages":[{"title":"Architecture overview","icon":"sitemap","href":"/architecture/overview"},{"title":"Retrieval pipeline","icon":"layer-group","href":"/architecture/retrieval-pipeline"},{"title":"Canonical knowledge graph","icon":"share-nodes","href":"/architecture/canonical-graph"},{"title":"Auto-Wiki engine","icon":"wand-magic-sparkles","href":"/architecture/auto-wiki-engine"},{"title":"Ingestion pipeline","icon":"inbox-in","href":"/architecture/ingestion-pipeline"},{"title":"Agent capability planner","icon":"route","href":"/architecture/agent-capability-planner"},{"title":"Database schema","icon":"database","href":"/architecture/database-schema"},{"title":"Security \u0026 threat model","icon":"shield-halved","href":"/architecture/security-and-threat-model"},{"title":"Governed AI actions","icon":"file-signature","href":"/architecture/governed-actions"},{"title":"Architecture decisions","icon":"scroll","href":"/architecture/decisions"}]},{"group":"Reference","icon":"book-bookmark","pages":[{"title":"HTTP API reference","icon":"plug","href":"/reference/api"},{"title":"CLI reference","icon":"terminal","href":"/reference/cli"},{"title":"ADR index","icon":"list","href":"/reference/adr-index"}]}]},"basePath":"","title":"Sessions workspace - AskMyDocs","buildId":"dpl_BLQvU86QFwq3AhPofSTw6q8dGWKm","deploymentId":"dpl_BLQvU86QFwq3AhPofSTw6q8dGWKm","dynamics":[],"chunks":[]}