@rubytech/create-maxy-code 0.1.314 → 0.1.316

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/__tests__/plugin-install.test.js +23 -1
  2. package/dist/index.js +47 -2
  3. package/dist/lib/plugin-install.js +16 -0
  4. package/package.json +1 -1
  5. package/payload/platform/lib/graph-search/dist/index.d.ts +18 -31
  6. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
  7. package/payload/platform/lib/graph-search/dist/index.js +20 -36
  8. package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
  9. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +55 -64
  10. package/payload/platform/lib/graph-search/src/index.ts +19 -37
  11. package/payload/platform/neo4j/schema.cypher +15 -41
  12. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +12 -13
  13. package/payload/platform/plugins/docs/references/admin-session.md +1 -1
  14. package/payload/platform/plugins/docs/references/admin-ui.md +3 -3
  15. package/payload/platform/plugins/docs/references/internals.md +9 -10
  16. package/payload/platform/plugins/memory/mcp/dist/index.js +12 -15
  17. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/memory/references/schema-estate-agent.md +32 -0
  19. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/http-server.js +14 -6
  21. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/index.js +1 -0
  23. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +11 -11
  25. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  26. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +96 -47
  27. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -6
  29. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +6 -6
  31. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  32. package/payload/platform/services/webchat-channel/dist/permission.d.ts +46 -0
  33. package/payload/platform/services/webchat-channel/dist/permission.d.ts.map +1 -0
  34. package/payload/platform/services/webchat-channel/dist/permission.js +56 -0
  35. package/payload/platform/services/webchat-channel/dist/permission.js.map +1 -0
  36. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  37. package/payload/platform/services/webchat-channel/dist/server.js +47 -1
  38. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  39. package/payload/platform/templates/specialists/agents/data-manager.md +3 -1
  40. package/payload/platform/templates/specialists/agents/public-session-reviewer.md +2 -2
  41. package/payload/server/public/assets/{AccessGate-B_sL5-0I.js → AccessGate-bu988Xo-.js} +1 -1
  42. package/payload/server/public/assets/{AdminLoginScreens-BgicJfUu.js → AdminLoginScreens-BCYaOOuk.js} +1 -1
  43. package/payload/server/public/assets/AdminShell-DSjJ4Biu.js +1 -0
  44. package/payload/server/public/assets/{Checkbox-BJdwS4v2.js → Checkbox-B4Dbvjls.js} +1 -1
  45. package/payload/server/public/assets/{OperatorConversations-DPlZIELy.js → OperatorConversations-XsXP7-P-.js} +1 -1
  46. package/payload/server/public/assets/{admin-DAFDivnO.js → admin-m0797WGA.js} +1 -1
  47. package/payload/server/public/assets/{audio-attachment-mime-h53tpH_4.js → audio-attachment-mime-Jm4KJ4YC.js} +3 -3
  48. package/payload/server/public/assets/{brand-DqQPSWa7.js → brand-D8LClgr_.js} +1 -1
  49. package/payload/server/public/assets/{brand-bye0l_rp.css → brand-DvttVeGB.css} +1 -1
  50. package/payload/server/public/assets/{browser-CmFjVZYo.js → browser-CBjNMPsz.js} +1 -1
  51. package/payload/server/public/assets/chat-Sg-RGuV6.js +1 -0
  52. package/payload/server/public/assets/{data-C4v6AdQ-.js → data-CEqE4BDw.js} +1 -1
  53. package/payload/server/public/assets/{graph-BJyMQdRx.js → graph-BZNc3EXa.js} +3 -3
  54. package/payload/server/public/assets/{graph-labels-BTpv_U3g.js → graph-labels-Bj5yM9bI.js} +1 -1
  55. package/payload/server/public/assets/operator-Bph7mx87.js +1 -0
  56. package/payload/server/public/assets/page-CtWCeXdg.js +1 -0
  57. package/payload/server/public/assets/{public-IRKxc9hu.js → public-gxHCJCs3.js} +3 -3
  58. package/payload/server/public/assets/public-next-C3clISGQ.js +1 -0
  59. package/payload/server/public/assets/{useSelectionMode-CTJhWnNp.js → useSelectionMode-BMn70cch.js} +1 -1
  60. package/payload/server/public/browser.html +6 -6
  61. package/payload/server/public/chat.html +8 -8
  62. package/payload/server/public/data.html +5 -5
  63. package/payload/server/public/graph.html +8 -8
  64. package/payload/server/public/index.html +8 -8
  65. package/payload/server/public/operator.html +10 -10
  66. package/payload/server/public/public-next.html +9 -9
  67. package/payload/server/public/public.html +7 -7
  68. package/payload/server/server.js +101 -15
  69. package/payload/server/public/assets/AdminShell-C0gZ5JKc.js +0 -1
  70. package/payload/server/public/assets/chat-CSw2u48Q.js +0 -1
  71. package/payload/server/public/assets/operator-Bj88rdnI.js +0 -1
  72. package/payload/server/public/assets/page-hmlVCBRQ.js +0 -1
  73. package/payload/server/public/assets/public-next-DqQ4PZLk.js +0 -1
@@ -11,15 +11,13 @@
11
11
  * The fulltext index `knowledge_fulltext` was renamed to `entity_search`
12
12
  * and its label union expanded from 3 labels (KnowledgeDocument | Section |
13
13
  * Chunk) to the full operator-meaningful label set (~40 labels) on every
14
- * textual property the schema's writers assign. Task 416 then split that
15
- * single `entity_search` into `entity_search_admin` and
16
- * `entity_search_public` so public BM25 ranking is shaped only by
17
- * `compiledTruthPublic`, not by the admin-side `compiledTruth` text the
18
- * public agent can never read. The lib references both via
19
- * FULLTEXT_INDEX_ADMIN / FULLTEXT_INDEX_PUBLIC below and routes between
20
- * them with `pickFulltextIndex(allowedScopes)`. The doctrine test at
21
- * `__tests__/fulltext-coverage.test.ts` parses both declarations and
22
- * asserts the universal label union plus the compiledTruth field split.
14
+ * textual property the schema's writers assign. Task 416 split that single
15
+ * `entity_search` into an admin index plus a public twin; the public twin
16
+ * and its whole read path were retired once public agents became toolless,
17
+ * so the lib now targets the single `entity_search_admin` index via
18
+ * FULLTEXT_INDEX_ADMIN below. The doctrine test at
19
+ * `__tests__/fulltext-coverage.test.ts` parses that declaration and asserts
20
+ * the universal label union.
23
21
  *
24
22
  * QUERY --> EMBED --> VECTOR SEARCH (per index) --> ┐
25
23
  * │ ├--> MERGE --> EXPAND --> RESULTS
@@ -83,36 +81,20 @@ function readFlags() {
83
81
  export type { DedupLayer, RetrievalClass };
84
82
  export { DEFAULT_LAYERS };
85
83
  /**
86
- * Names of the two fulltext indexes (Task 416). Mirrors the `CREATE
87
- * FULLTEXT INDEX` declarations in `platform/neo4j/schema.cypher`. The
88
- * doctrine test asserts each name matches its constant and that the field
89
- * split is correct — drift between schema and these constants breaks
90
- * BM25 silently.
91
- *
92
- * `entity_search_admin` carries `n.compiledTruth`; `entity_search_public`
93
- * carries `n.compiledTruthPublic`. Other indexed text fields are shared.
84
+ * Name of the fulltext index (Task 416 admin index; the public twin was
85
+ * retired with the public read path). Mirrors the `CREATE FULLTEXT INDEX`
86
+ * declaration in `platform/neo4j/schema.cypher`. The doctrine test asserts
87
+ * the name matches this constant — drift between schema and this constant
88
+ * breaks BM25 silently. `entity_search_admin` carries `n.compiledTruth`.
94
89
  */
95
90
  export const FULLTEXT_INDEX_ADMIN = "entity_search_admin";
96
- export const FULLTEXT_INDEX_PUBLIC = "entity_search_public";
97
-
98
- /**
99
- * Pick the BM25 index for a read. Returns the public index iff the caller
100
- * is in the verified public-only scope (`allowedScopes` is exactly
101
- * `['public']` — the public-agent spawn shape). Anything else — including
102
- * `undefined` (admin route default), `[]`, `['shared']`, `['public','shared']`
103
- * — routes to the admin index, so ranking only reflects `compiledTruthPublic`
104
- * for callers whose Cypher scope filter already restricts them to public
105
- * rows. Mirrors `isPublicOnlyScope` semantics in memory-search.ts; that
106
- * file imports this predicate via `isPublicOnlyScope` below.
107
- */
108
- export function pickFulltextIndex(
109
- allowedScopes: readonly string[] | undefined,
110
- ): string {
111
- return isPublicOnlyScope(allowedScopes) ? FULLTEXT_INDEX_PUBLIC : FULLTEXT_INDEX_ADMIN;
112
- }
113
91
 
114
- /** Detect the public-agent read shape. Hoisted out of memory-search.ts so
115
- * the BM25 picker and the property-bag projection share one predicate. */
92
+ /** Detect the public-agent read shape (`allowedScopes` exactly `['public']`).
93
+ * The BM25 index picker that used this predicate was retired with the public
94
+ * read path. Its one remaining consumer — the `projectPublicProperties` swap
95
+ * in the memory MCP's memory-search.ts — is now unreachable: every live read
96
+ * passes `allowedScopes: undefined`, so this returns false on every call. Kept
97
+ * as that projection's gate for when a public read surface is restored. */
116
98
  export function isPublicOnlyScope(
117
99
  allowedScopes: readonly string[] | undefined,
118
100
  ): boolean {
@@ -481,7 +463,7 @@ export async function bm25Only(
481
463
  ORDER BY score DESC
482
464
  LIMIT $limit`,
483
465
  {
484
- indexName: pickFulltextIndex(allowedScopes),
466
+ indexName: FULLTEXT_INDEX_ADMIN,
485
467
  query: escaped,
486
468
  accountId,
487
469
  limit: int(limit),
@@ -429,24 +429,20 @@ OPTIONS {
429
429
  // `role` = discriminator on KnowledgeDocument projections
430
430
  // ('identity'|'soul'|'knowledge'|'knowledge-summary') so BM25 hits surface
431
431
  // which file backed the result. Distinct from Person.role (no shadow).
432
- // Public/admin BM25 scoring isolation (Task 416). Two fulltext indexes
433
- // over the same universal label union, differing in one column: the admin
434
- // index carries `n.compiledTruth`; the public index carries
435
- // `n.compiledTruthPublic`. The graph-search lib picks between them based
436
- // on `allowedScopes` public reads (allowedScopes exactly ['public'])
437
- // rank against the public twin only; everything else (admin reads pass no
438
- // scope filter) ranks against the admin field. Before Task 416 a single
439
- // `entity_search` carried both columns, which let admin notes silently
440
- // shape the BM25 score of public-twin rows even though the memory-search
441
- // projection swapped the field on the way out. Drop the pre-416 single
442
- // index so existing deployments don't keep a populated dead third copy.
432
+ // Admin BM25 fulltext index. Task 416 split a single `entity_search` into an
433
+ // admin index plus a public twin (`entity_search_public`) so a public read
434
+ // ranked only against `n.compiledTruthPublic`. Task 654 retired the public twin
435
+ // and its whole read path once public agents became toolless; `entity_search_admin`
436
+ // is now the only fulltext index, carrying `n.compiledTruth`. Both the pre-416
437
+ // single index and the retired public twin keep a permanent `DROP ... IF EXISTS`
438
+ // below so existing deployments stop populating a dead index that consumes disk.
443
439
  DROP INDEX entity_search IF EXISTS;
440
+ DROP INDEX entity_search_public IF EXISTS;
444
441
 
445
- // Task 397 added :ConversationArchive to the label union. DROP + recreate so
446
- // existing installs pick up the new label without manual reindex; fresh
447
- // installs no-op the DROP via IF EXISTS.
442
+ // Task 397 added :ConversationArchive to the label union. DROP + recreate the
443
+ // admin index so existing installs pick up new labels without manual reindex;
444
+ // fresh installs no-op the DROP via IF EXISTS.
448
445
  DROP INDEX entity_search_admin IF EXISTS;
449
- DROP INDEX entity_search_public IF EXISTS;
450
446
 
451
447
  CREATE FULLTEXT INDEX entity_search_admin IF NOT EXISTS
452
448
  FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
@@ -470,33 +466,11 @@ ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
470
466
  n.displayName, n.slug, n.role,
471
467
  n.client, n.address, n.supplier, n.clientName,
472
468
  n.compiledTruth];
473
-
474
- CREATE FULLTEXT INDEX entity_search_public IF NOT EXISTS
475
- FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
476
- |Listing|Property|Viewing|Offer|PostalAddress
477
- |Job|LineItem|Valuation|Milestone|QuoteDocument|VariationNote|InboundInvoice|CheckIn|WhatsAppGroup
478
- |Person|UserProfile|Preference|AdminUser|AccessGrant
479
- |KnowledgeDocument|ConversationArchive|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
480
- |Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
481
- |Task|Project|Event|Meeting|Attachment
482
- |Workflow|WorkflowStep|WorkflowRun|StepResult
483
- |EmailAccount
484
- |Position|Credential|Agent|Idea|Concept
485
- |VoiceProfile|VoiceEdit|SocialPost|AEOAudit|Page
486
- |TimelineEvent|CompiledTruthRevision|Report|OrphanCandidate|CitationProposal)
487
- ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
488
- n.title, n.currentTitle, n.summary, n.body, n.content, n.text, n.description, n.headline, n.abstract,
489
- n.email, n.note, n.label, n.value, n.message, n.preview, n.tagline,
490
- n.subject, n.bodyPreview, n.fromName, n.fromAddress, n.agentAddress, n.screeningReason,
491
- n.authority, n.contactValue, n.toolName,
492
- n.displayName, n.slug, n.role,
493
- n.compiledTruthPublic];
494
469
  // `compiledTruth` (Task 306) is the authoritative admin-side summary on
495
- // Person/Company/Concept/Project/LocalBusiness; `compiledTruthPublic`
496
- // (Task 392) is the customer-facing twin on the four public-twin labels
497
- // (Organization | Concept | Project | LocalBusiness). Each is indexed in
498
- // exactly one of the two indexes above, so the BM25 score a query sees
499
- // is shaped only by the text the caller's scope is allowed to read.
470
+ // Person/Company/Concept/Project/LocalBusiness, indexed above. Its customer-facing
471
+ // twin `compiledTruthPublic` (Task 392) is still written, but is no longer
472
+ // fulltext-indexed: the public index it fed was retired with the public read
473
+ // path (Task 654), so no BM25 surface ranks against it.
500
474
 
501
475
  // Project node — a standalone creative-output node distinct from
502
476
  // :Section. Anchored via (:UserProfile)-[:CREATED]->(:Project), with optional
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:6b3de8da9e586c39ea62705d90262664efecf5318cb8b573d7d3912114b5f654
4
+ content-hash: sha256:4c25ba31f010fd3042621f035682a6df66d7ec88011a11984ef197f056a61b60
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2314,7 +2314,7 @@ either is a regression.
2314
2314
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. The Sidebar navigates the opened tab to `claude.ai/code/session_<id>` on `outcome=bound`; on `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
2315
2315
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
2316
2316
 
2317
- **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
2317
+ **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. On an existing chat each picker re-seats the live session and carries the operator's current model+mode+effort so picking one never resets the others. When the live session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny card in the composer (tool, description, argument preview); a click sends the verdict over Claude Code's channel permission relay and the tool runs or is refused — webchat is the only interactive ask surface (WhatsApp stays text-only; Telegram has no agent channel). Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
2318
2318
 
2319
2319
  **Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
2320
2320
 
@@ -2567,8 +2567,8 @@ authoritative. This section names the surfaces and what backs each.
2567
2567
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
2568
2568
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
2569
2569
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
2570
- | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** each row also carries a `SessionReseatControl` (sliders icon) beside the action cluster a model/mode/effort popover that forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive). Its model picker defaults to the row's current model (a new `model` field on each row, read from the JSONL tail); mode and effort each carry a "Keep" option that omits the field. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
2571
- | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". **Re-seat:** each channel conversation row also carries the `SessionReseatControl` beside its click-to-open area (the row is a `conv-with-actions` shell whose open click moved onto an inner button so the popover can sit beside it), so a WhatsApp/Telegram session's model/mode/effort is re-seatable from the dashboard exactly like a webchat row; the channel reader row carries the same `model` field for the picker default. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
2570
+ | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
2571
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". **Re-seat:** each channel conversation row carries a `SessionRowActions` cluster whose sole action is Re-seat (the row is a `conv-with-actions` shell whose open click moved onto an inner button so the cluster can sit beside it), so a WhatsApp/Telegram session's model/mode/effort is re-seatable from the dashboard exactly like a webchat row — wide it is the inline sliders icon, and below the 400 px width it folds into the `…` overflow menu identically to the Sessions rows; the channel reader row carries the same `model` field for the picker default. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
2572
2572
  | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
2573
2573
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
2574
2574
  | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal. | `sidebar-artefacts.ts`, `files.ts` |
@@ -3241,16 +3241,15 @@ The admin agent runs via Claude Code CLI, which manages its own system prompt as
3241
3241
 
3242
3242
  This is assembled as a `<previous-context>` block in the system prompt on each admin turn.
3243
3243
 
3244
- ### fetchMemoryContext the MCP bridge
3244
+ ### Public-agent memory context (retired)
3245
3245
 
3246
- For public agents, the server calls the memory MCP server via JSON-RPC over stdin/stdout:
3247
-
3248
- 1. Spawn the memory MCP server as a subprocess with environment variables: `ACCOUNT_ID`, `ALLOWED_SCOPES=public,shared`, `AGENT_SLUG`, `KNOWLEDGE_KEYWORDS`, `SESSION_ID`
3249
- 2. Send `initialize` + `tools/call` (name: `memory-search`, arguments: `{query, account_id}`)
3250
- 3. Read the tool result text
3251
- 4. Timeout: 8 seconds. On any failure, returns null the agent proceeds without memory context.
3252
-
3253
- This subprocess model means each public agent query gets an isolated, short-lived memory server instance with the correct scope constraints baked into its environment.
3246
+ Public agents historically had their memory context injected by a server-side bridge
3247
+ (`fetchMemoryContext`) that spawned the memory MCP server with `ALLOWED_SCOPES=public,shared`
3248
+ and ran a `memory-search` on the agent's behalf. That bridge has been removed: public agents
3249
+ are now toolless (`buildPublicDeny` denies `mcp__memory__*`), so neither the agent nor a
3250
+ server-side proxy runs a public-scope memory read. The public-scope fulltext index that this
3251
+ path ranked against was retired alongside it. Restoring read-back for gated visitors is
3252
+ tracked separately — see the gated-public-agents doc.
3254
3253
 
3255
3254
  ---
3256
3255
 
@@ -95,7 +95,7 @@ The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/
95
95
 
96
96
  The metadata pane surfaces two distinct identifier values. The three-id model was collapsed back to a single-identity contract: claude's bridge suffix and the JSONL basename UUID are two phases of one claude-side identity, not two operator-visible identifiers. The manager resolver accepts either phase on any route. The wire emits one canonical `sessionId` — whichever phase is current — and the pane shows one row.
97
97
 
98
- **Re-seat mints a new id.** Model, mode, and effort are inception-only levers — claude reads them when a session is born and the `claude rc` daemon exposes no per-session switch — so changing one on an *existing* session forks it: `/api/admin/session-reseat` pre-mints a fresh `sessionId`, the manager `/rc-spawn` fork branch runs `--resume <old> --fork-session --session-id <new> --model <model>` (copying history into the new id), and the operator lands on the fork. The fork pins the chosen model and, when supplied, the chosen `permissionMode` (pushed as `--permission-mode`, one of the 5 composer-writable modes) and `effort` (`low|medium|high|xhigh`, merged into the per-spawn inline `--settings.effortLevel`); each is validated against its allowlist before reaching the argv (a present-but-disallowed value is a 400). Both `/chat`'s composer pickers and the dashboard's per-row Re-seat control drive this one fork, for webchat and WhatsApp/Telegram sessions alike. The full write path is in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
98
+ **Re-seat mints a new id.** Model, mode, and effort are inception-only levers — claude reads them when a session is born and the `claude rc` daemon exposes no per-session switch — so changing one on an *existing* session forks it: `/api/admin/session-reseat` pre-mints a fresh `sessionId`, the manager `/rc-spawn` fork branch runs `--resume <old> --fork-session --session-id <new> --model <model>` (copying history into the new id), and the operator lands on the fork. The fork pins the chosen model and, when supplied, the chosen `permissionMode` (pushed as `--permission-mode`, one of the 5 composer-writable modes) and `effort` (`low|medium|high|xhigh`, merged into the per-spawn inline `--settings.effortLevel`); each is validated against its allowlist before reaching the argv (a present-but-disallowed value is a 400). Both `/chat`'s composer pickers and the dashboard's per-row Re-seat control drive this one fork, for webchat and WhatsApp/Telegram sessions alike. On `/chat`, every picker now carries the operator's *current* model, mode, and effort and overrides only the picked lever, so changing one never silently resets the other two (a non-writable current mode is omitted rather than rejected). When a re-seated webchat session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny prompt in `/chat` and blocks until the operator answers — over Claude Code's channel permission relay, the only interactive ask surface (WhatsApp stays text-only). The full write path is in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
99
99
 
100
100
  | Operator label | What it is | Manager wire field | Log key |
101
101
  |---|---|---|---|
@@ -49,7 +49,7 @@ either is a regression.
49
49
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. The Sidebar navigates the opened tab to `claude.ai/code/session_<id>` on `outcome=bound`; on `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
50
50
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
51
51
 
52
- **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
52
+ **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. All three levers are now changeable from the `/chat` composer (the model is a friendly-labelled checkmark dropdown via `MODEL_DISPLAY_NAMES`, effort a Faster↔Smarter slider, mode a 5-mode checkmark dropdown), each writing its `account.json` key then `POST /reapply-levers` so the next spawn honours it; the full write path is in `.docs/admin-webchat-native-channel.md`. On an existing chat each picker re-seats the live session and carries the operator's current model+mode+effort so picking one never resets the others. When the live session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny card in the composer (tool, description, argument preview); a click sends the verdict over Claude Code's channel permission relay and the tool runs or is refused — webchat is the only interactive ask surface (WhatsApp stays text-only; Telegram has no agent channel). Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
53
53
 
54
54
  **Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
55
55
 
@@ -302,8 +302,8 @@ authoritative. This section names the surfaces and what backs each.
302
302
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
303
303
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
304
304
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
305
- | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** each row also carries a `SessionReseatControl` (sliders icon) beside the action cluster a model/mode/effort popover that forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive). Its model picker defaults to the row's current model (a new `model` field on each row, read from the JSONL tail); mode and effort each carry a "Keep" option that omits the field. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
306
- | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". **Re-seat:** each channel conversation row also carries the `SessionReseatControl` beside its click-to-open area (the row is a `conv-with-actions` shell whose open click moved onto an inner button so the popover can sit beside it), so a WhatsApp/Telegram session's model/mode/effort is re-seatable from the dashboard exactly like a webchat row; the channel reader row carries the same `model` field for the picker default. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
305
+ | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
306
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". **Re-seat:** each channel conversation row carries a `SessionRowActions` cluster whose sole action is Re-seat (the row is a `conv-with-actions` shell whose open click moved onto an inner button so the cluster can sit beside it), so a WhatsApp/Telegram session's model/mode/effort is re-seatable from the dashboard exactly like a webchat row — wide it is the inline sliders icon, and below the 400 px width it folds into the `…` overflow menu identically to the Sessions rows; the channel reader row carries the same `model` field for the picker default. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
307
307
  | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
308
308
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
309
309
  | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal. | `sidebar-artefacts.ts`, `files.ts` |
@@ -373,16 +373,15 @@ The admin agent runs via Claude Code CLI, which manages its own system prompt as
373
373
 
374
374
  This is assembled as a `<previous-context>` block in the system prompt on each admin turn.
375
375
 
376
- ### fetchMemoryContext the MCP bridge
377
-
378
- For public agents, the server calls the memory MCP server via JSON-RPC over stdin/stdout:
379
-
380
- 1. Spawn the memory MCP server as a subprocess with environment variables: `ACCOUNT_ID`, `ALLOWED_SCOPES=public,shared`, `AGENT_SLUG`, `KNOWLEDGE_KEYWORDS`, `SESSION_ID`
381
- 2. Send `initialize` + `tools/call` (name: `memory-search`, arguments: `{query, account_id}`)
382
- 3. Read the tool result text
383
- 4. Timeout: 8 seconds. On any failure, returns null the agent proceeds without memory context.
384
-
385
- This subprocess model means each public agent query gets an isolated, short-lived memory server instance with the correct scope constraints baked into its environment.
376
+ ### Public-agent memory context (retired)
377
+
378
+ Public agents historically had their memory context injected by a server-side bridge
379
+ (`fetchMemoryContext`) that spawned the memory MCP server with `ALLOWED_SCOPES=public,shared`
380
+ and ran a `memory-search` on the agent's behalf. That bridge has been removed: public agents
381
+ are now toolless (`buildPublicDeny` denies `mcp__memory__*`), so neither the agent nor a
382
+ server-side proxy runs a public-scope memory read. The public-scope fulltext index that this
383
+ path ranked against was retired alongside it. Restoring read-back for gated visitors is
384
+ tracked separately — see the gated-public-agents doc.
386
385
 
387
386
  ---
388
387
 
@@ -140,18 +140,15 @@ const validator = {
140
140
  liveSource: liveSchemaRuntime.source,
141
141
  };
142
142
  const userId = process.env.USER_ID; // Optional — present for admin sessions with users.json auth
143
- // Scope filtering: comma-separated list of allowed scopes (e.g. "public,shared").
144
- // When set, memory-search only returns nodes whose scope is in this list.
145
- // When absent (admin), no scope filtering is applied.
146
- const allowedScopes = process.env.ALLOWED_SCOPES
147
- ? process.env.ALLOWED_SCOPES.split(",").map((s) => s.trim()).filter(Boolean)
148
- : undefined;
149
- // Task 485 per-visitor slice ringfence. Stamped by pty-spawner on
150
- // gated public-agent spawns. When present, every memory-search WHERE
151
- // composes `(n.sliceToken = $sliceToken OR n.scope IN $allowedScopes)`
152
- // so the agent sees its visitor's user-scoped nodes alongside public-scope
153
- // rows. When absent (admin path + open-mode public), behaviour is
154
- // unchanged.
143
+ // Scope filtering via `ALLOWED_SCOPES` was retired with the public read path
144
+ // (Task 654): no spawn stamps it any more, so every memory read runs unscoped
145
+ // (admin behaviour). `allowedScopes` is passed as `undefined` below.
146
+ // Task 485 — per-visitor slice ringfence. When `ACCESS_SLICE_TOKEN` is set,
147
+ // every memory-search WHERE composes `(n.sliceToken = $sliceToken OR n.scope
148
+ // IS NULL)` so the agent sees its visitor's user-scoped nodes alongside
149
+ // scope-less rows. The read machinery is retained, but currently inert: no
150
+ // spawn stamps `ACCESS_SLICE_TOKEN` since the public read path was retired
151
+ // (Task 654), so `accessSliceToken` is always undefined and reads run admin-wide.
155
152
  const accessSliceToken = typeof process.env.ACCESS_SLICE_TOKEN === "string" && process.env.ACCESS_SLICE_TOKEN.length > 0
156
153
  ? process.env.ACCESS_SLICE_TOKEN
157
154
  : undefined;
@@ -259,7 +256,7 @@ eagerTool(server, "memory-search", "Search the knowledge graph using semantic ve
259
256
  accountId,
260
257
  limit,
261
258
  expandHops,
262
- allowedScopes,
259
+ allowedScopes: undefined,
263
260
  sliceToken: accessSliceToken,
264
261
  keywords,
265
262
  keywordMatch,
@@ -368,7 +365,7 @@ eagerTool(server, "memory-lookup-by-name", "Deterministic by-name entity lookup.
368
365
  name,
369
366
  accountId,
370
367
  labels,
371
- allowedScopes,
368
+ allowedScopes: undefined,
372
369
  sliceToken: accessSliceToken,
373
370
  agentSlug: effectiveAgentSlug,
374
371
  limit,
@@ -376,7 +373,7 @@ eagerTool(server, "memory-lookup-by-name", "Deterministic by-name entity lookup.
376
373
  process.stderr.write(`[memory-lookup-by-name] name=${JSON.stringify(name)} ` +
377
374
  `labels=${labels && labels.length > 0 ? labels.join("|") : "any"} ` +
378
375
  `resultCount=${hits.length} ` +
379
- `scope=${accessSliceToken ? accessSliceToken.slice(0, 8) : allowedScopes ? allowedScopes.join("|") : "admin"}\n`);
376
+ `scope=${accessSliceToken ? accessSliceToken.slice(0, 8) : "admin"}\n`);
380
377
  if (hits.length === 0) {
381
378
  return {
382
379
  content: [