@rubytech/create-sitedesk-code 0.1.524 → 0.1.526

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 (126) hide show
  1. package/dist/__tests__/samba-provision.test.js +46 -1
  2. package/dist/__tests__/websockify-bind-wiring.test.js +47 -0
  3. package/dist/index.js +39 -1
  4. package/dist/samba-provision.js +37 -0
  5. package/package.json +1 -1
  6. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +8 -7
  7. package/payload/platform/plugins/admin/skills/superpowers-sprint/SKILL.md +26 -0
  8. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  9. package/payload/platform/plugins/memory/PLUGIN.md +10 -0
  10. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +5 -1
  11. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts +2 -0
  13. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.d.ts.map +1 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js +141 -0
  15. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/temporal-type-preserve.test.js.map +1 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +1 -1
  17. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +6 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  20. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts +45 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.d.ts.map +1 -0
  22. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js +122 -0
  23. package/payload/platform/plugins/memory/mcp/dist/lib/temporal-type-preserve.js.map +1 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js +26 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-by-name.test.js.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts +2 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.d.ts.map +1 -0
  28. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js +201 -0
  29. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-type-preserve.test.js.map +1 -0
  30. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  31. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +89 -33
  32. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
  34. package/payload/platform/plugins/scheduling/PLUGIN.md +6 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts +2 -0
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.d.ts.map +1 -0
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js +148 -0
  38. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/meeting-retype.test.js.map +1 -0
  39. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +83 -6
  40. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
  41. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +90 -5
  42. package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
  43. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +173 -2
  44. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +73 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +194 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  49. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +25 -1
  50. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +81 -8
  52. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  53. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js +39 -0
  54. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-event-agent.test.js.map +1 -1
  55. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  56. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +9 -1
  57. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  58. package/payload/platform/scripts/__tests__/check-canonical-tool-names.test.sh +162 -0
  59. package/payload/platform/scripts/__tests__/check-no-task-id-leaks.test.sh +53 -0
  60. package/payload/platform/scripts/__tests__/task-id-citation.test.sh +72 -0
  61. package/payload/platform/scripts/check-canonical-tool-names.mjs +36 -4
  62. package/payload/platform/scripts/check-no-task-id-leaks.mjs +80 -62
  63. package/payload/platform/scripts/lib/canonical-tool-names.mjs +130 -26
  64. package/payload/platform/scripts/lib/task-id-citation.mjs +67 -0
  65. package/payload/platform/scripts/vnc.sh +45 -1
  66. package/payload/server/{chunk-VUXKXOJU.js → chunk-7MGR5M6G.js} +4 -2
  67. package/payload/server/{manager-4TJK3YOY.js → manager-6XOWHREK.js} +1 -1
  68. package/payload/server/server.js +10 -4
  69. package/payload/platform/.docs/search-surface-contract.md +0 -58
  70. package/payload/platform/docs/superpowers/plans/2026-06-02-task-610-follower-202-retry.md +0 -372
  71. package/payload/platform/docs/superpowers/plans/2026-06-04-public-agent-knowledge-delivery.md +0 -230
  72. package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +0 -544
  73. package/payload/platform/docs/superpowers/plans/2026-07-11-task-1557-operator-on-behalf-preference-attribution.md +0 -542
  74. package/payload/platform/docs/superpowers/plans/2026-07-13-account-schema-ontology-projection.md +0 -547
  75. package/payload/platform/docs/superpowers/plans/2026-07-14-graph-top-level-labels-ontology-single-source.md +0 -458
  76. package/payload/platform/docs/superpowers/plans/2026-07-17-task-1736-retire-mcp-stderr-tee.md +0 -397
  77. package/payload/platform/docs/superpowers/plans/2026-07-18-graph-native-ledger.md +0 -807
  78. package/payload/platform/docs/superpowers/plans/2026-07-18-ledger-write-path-fix.md +0 -109
  79. package/payload/platform/docs/superpowers/plans/2026-07-20-storage-pages-create.md +0 -82
  80. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +0 -673
  81. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +0 -1244
  82. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +0 -462
  83. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +0 -321
  84. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1829-platform-ui-typecheck.md +0 -830
  85. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +0 -2177
  86. package/payload/platform/docs/superpowers/plans/2026-07-21-graph-caption-resolver.md +0 -124
  87. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1877-bash-schema-enforcement.md +0 -107
  88. package/payload/platform/docs/superpowers/plans/2026-07-21-task-1887-reconcile-allowed-top-level.md +0 -205
  89. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +0 -748
  90. package/payload/platform/docs/superpowers/plans/2026-07-22-task-1910-two-way-portal-exchange.md +0 -1716
  91. package/payload/platform/docs/superpowers/plans/2026-07-23-task-1930-adherence-enforcement.md +0 -422
  92. package/payload/platform/docs/superpowers/plans/2026-07-24-task-1942-subaccount-switcher-brand-head.md +0 -327
  93. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1974-uploads-intake-inbox.md +0 -235
  94. package/payload/platform/docs/superpowers/plans/2026-07-25-task-1976-intra-folder-hygiene.md +0 -215
  95. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +0 -555
  96. package/payload/platform/docs/superpowers/plans/2026-07-26-task-1926-portal-push-concurrency-guard.md +0 -482
  97. package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +0 -394
  98. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2016-disabled-agent-routing.md +0 -624
  99. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2028-declared-file-write-deny.md +0 -303
  100. package/payload/platform/docs/superpowers/plans/2026-07-27-task-2052-account-machinery-write-fence.md +0 -250
  101. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +0 -65
  102. package/payload/platform/docs/superpowers/specs/2026-06-02-task-610-follower-202-retry-design.md +0 -116
  103. package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +0 -170
  104. package/payload/platform/docs/superpowers/specs/2026-07-11-task-1557-operator-on-behalf-preference-attribution-design.md +0 -90
  105. package/payload/platform/docs/superpowers/specs/2026-07-13-account-schema-ontology-projection-design.md +0 -178
  106. package/payload/platform/docs/superpowers/specs/2026-07-14-graph-top-level-labels-ontology-single-source-design.md +0 -119
  107. package/payload/platform/docs/superpowers/specs/2026-07-17-task-1736-retire-mcp-stderr-tee-design.md +0 -128
  108. package/payload/platform/docs/superpowers/specs/2026-07-18-graph-native-ledger-design.md +0 -241
  109. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +0 -177
  110. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +0 -201
  111. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +0 -217
  112. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +0 -80
  113. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +0 -97
  114. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1829-platform-ui-typecheck-design.md +0 -163
  115. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +0 -163
  116. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +0 -121
  117. package/payload/platform/docs/superpowers/specs/2026-07-22-task-1910-two-way-portal-exchange-design.md +0 -103
  118. package/payload/platform/docs/superpowers/specs/2026-07-23-task-1930-adherence-enforcement-design.md +0 -81
  119. package/payload/platform/docs/superpowers/specs/2026-07-24-task-1942-subaccount-switcher-brand-head-design.md +0 -35
  120. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +0 -232
  121. package/payload/platform/docs/superpowers/specs/2026-07-26-task-1926-portal-push-concurrency-guard-design.md +0 -200
  122. package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +0 -169
  123. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2016-disabled-agent-routing-design.md +0 -139
  124. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2028-declared-file-write-deny-design.md +0 -127
  125. package/payload/platform/docs/superpowers/specs/2026-07-27-task-2052-account-machinery-write-fence-design.md +0 -150
  126. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +0 -155
@@ -1,394 +0,0 @@
1
- # Task 2023 — sidebar-sessions async reads Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Stop `computeSessionList` from blocking the one event loop for the whole duration of its filesystem walk, without changing what the endpoint returns.
6
-
7
- **Architecture:** The route already sits behind the Task 1986 per-account cache and its compute is already `async` and awaited. The block is not the awaiting; it is that every read inside the compute is synchronous, so the loop cannot run anything else from the first `readFileSync` to the last. Replacing the per-row reads with `node:fs/promises` gives the loop a yield point per row. Directory enumeration is left alone on measurement, not preference (see Global Constraints).
8
-
9
- **Tech Stack:** TypeScript, Node 22, Hono, vitest.
10
-
11
- ## Global Constraints
12
-
13
- - **Measured cost split, maxy 2026-07-26, 52 files / 133.9 MB / 38,042 lines, two identical runs:** `enumMs=1`, `readMs=284.5` then `277.4`, `parseMs=212.3` then `216.6`.
14
- - **The four `readdirSync` calls cost 1 ms and are NOT converted.** `enumerateJsonls` is imported by six other server modules; forking it for 1 ms would trade a measurable regression risk for no measurable gain. This is a deliberate deviation from the task file's problem statement, which names lines 216, 227, 240 and 255 alongside the reads that do cost.
15
- - **JSON parsing is ~215 ms of pure CPU and no async change removes it.** The deliverable is that it stops arriving as one 215 ms block and arrives as ~52 chunks separated by I/O yields. Do not claim the parse cost is eliminated.
16
- - **Reads stay sequential.** The current loop holds one `body` at a time and lets it go out of scope each iteration. `Promise.all` over 52 files would hold 133.9 MB at once. Sequential `await` yields the loop without changing the memory profile.
17
- - **No behaviour change.** Response shape, field values, ordering, census log lines and cache states are all byte-identical.
18
- - **Existing sync exports keep their signatures.** `enumerateJsonls`, `loadUserTitles`, `readSidecarMeta` and `readBridgeIds` are consumed by `server/index.ts`, `public-reader.ts`, `webchat.ts`, `whatsapp-reader.ts`, `files.ts` and two test files. None of those are in scope.
19
- - **One parser per format.** Where a sync and an async reader both exist, they share a single exported pure parse function. Two copies of a parse body is a drift registry and is not acceptable.
20
-
21
- ---
22
-
23
- ## File Structure
24
-
25
- - `server/routes/admin/sidebar-sessions.ts` — the only production file modified. Gains `parseUserTitles`, `parseSidecarMeta`, `loadUserTitlesAsync`, `readSidecarMetaAsync`; `computeSessionList`'s row loop switches to `fs/promises`.
26
- - `server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts` — new, holds this task's own tests.
27
-
28
- ---
29
-
30
- ### Task 1: Split the two parsers out of their sync readers
31
-
32
- **Files:**
33
- - Modify: `platform/ui/server/routes/admin/sidebar-sessions.ts:308-335` (`loadUserTitles`), `:371-402` (`readSidecarMeta`)
34
- - Test: `platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
35
-
36
- **Interfaces:**
37
- - Produces: `parseUserTitles(raw: string): Map<string, string>` and `parseSidecarMeta(raw: string): SidecarMeta`. Both take the file's raw text and never touch the filesystem. Both degrade to empty/null on a parse failure or wrong shape, exactly as the current readers do.
38
- - Consumes: nothing.
39
-
40
- - [ ] **Step 1: Write the failing test**
41
-
42
- ```ts
43
- import { describe, it, expect } from 'vitest'
44
- import { parseUserTitles, parseSidecarMeta } from '../sidebar-sessions'
45
-
46
- describe('parseUserTitles', () => {
47
- it('keeps a trimmed string title and drops every malformed entry', () => {
48
- const raw = JSON.stringify({
49
- good: { title: ' renamed ' },
50
- blankTitle: { title: ' ' },
51
- wrongType: { title: 42 },
52
- notAnObject: 'nope',
53
- '': { title: 'no id' },
54
- })
55
- const out = parseUserTitles(raw)
56
- expect(out.get('good')).toBe('renamed')
57
- expect([...out.keys()]).toEqual(['good'])
58
- })
59
-
60
- it('returns an empty map for unparseable or non-object JSON', () => {
61
- expect(parseUserTitles('{not json').size).toBe(0)
62
- expect(parseUserTitles('[1,2,3]').size).toBe(0)
63
- expect(parseUserTitles('null').size).toBe(0)
64
- })
65
- })
66
-
67
- describe('parseSidecarMeta', () => {
68
- it('reads every field and filters non-string bridgeIds', () => {
69
- const raw = JSON.stringify({
70
- bridgeIds: ['a', '', 7, 'b'],
71
- role: 'admin',
72
- channel: 'webchat',
73
- senderId: '+447700900000',
74
- startedAt: '2026-07-26T00:00:00.000Z',
75
- personId: 'p1',
76
- adminUserId: 'u1',
77
- visitorId: 'v1',
78
- agentSlug: 'slug',
79
- accountId: 'acct',
80
- })
81
- expect(parseSidecarMeta(raw)).toEqual({
82
- bridgeIds: ['a', 'b'],
83
- role: 'admin',
84
- channel: 'webchat',
85
- senderId: '+447700900000',
86
- startedAt: '2026-07-26T00:00:00.000Z',
87
- personId: 'p1',
88
- adminUserId: 'u1',
89
- visitorId: 'v1',
90
- agentSlug: 'slug',
91
- accountId: 'acct',
92
- })
93
- })
94
-
95
- it('returns the empty shape for unparseable JSON', () => {
96
- expect(parseSidecarMeta('{not json')).toEqual({
97
- bridgeIds: [], role: null, channel: null, senderId: null, startedAt: null,
98
- personId: null, adminUserId: null, visitorId: null, agentSlug: null, accountId: null,
99
- })
100
- })
101
- })
102
- ```
103
-
104
- - [ ] **Step 2: Run test to verify it fails**
105
-
106
- Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
107
- Expected: FAIL — `parseUserTitles is not a function` (the export does not exist yet).
108
-
109
- - [ ] **Step 3: Write minimal implementation**
110
-
111
- Replace the body of `loadUserTitles` from the `JSON.parse` onward with a call to a new exported pure function, and do the same for `readSidecarMeta`. The sync readers keep their exact signatures and their `readFileSync` + catch.
112
-
113
- ```ts
114
- /** Pure half of {@link loadUserTitles}: parse the on-disk `session-titles.json`
115
- * text. Shared with the async reader so one format has one parser. */
116
- export function parseUserTitles(raw: string): Map<string, string> {
117
- const out = new Map<string, string>()
118
- let parsed: unknown
119
- try { parsed = JSON.parse(raw) } catch { return out }
120
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return out
121
- for (const [sessionId, value] of Object.entries(parsed as Record<string, unknown>)) {
122
- if (!sessionId) continue
123
- if (!value || typeof value !== 'object') continue
124
- const v = value as { title?: unknown }
125
- if (typeof v.title !== 'string') continue
126
- const trimmed = v.title.trim()
127
- if (!trimmed) continue
128
- out.set(sessionId, trimmed)
129
- }
130
- return out
131
- }
132
-
133
- export function loadUserTitles(accountDir: string | null): Map<string, string> {
134
- if (!accountDir) return new Map()
135
- let raw: string
136
- try { raw = readFileSync(join(accountDir, 'session-titles.json'), 'utf8') } catch { return new Map() }
137
- return parseUserTitles(raw)
138
- }
139
- ```
140
-
141
- ```ts
142
- // A factory, not a shared constant. `{ ...CONST }` would copy the REFERENCE to
143
- // one `bridgeIds` array into every empty result, so a single caller mutating it
144
- // would corrupt every future empty sidecar in the process.
145
- function emptySidecarMeta(): SidecarMeta {
146
- return {
147
- bridgeIds: [], role: null, channel: null, senderId: null, startedAt: null,
148
- personId: null, adminUserId: null, visitorId: null, agentSlug: null, accountId: null,
149
- }
150
- }
151
-
152
- /** Pure half of {@link readSidecarMeta}: parse a `.meta.json` sidecar's text.
153
- * Shared with the async reader so one format has one parser. */
154
- export function parseSidecarMeta(raw: string): SidecarMeta {
155
- let parsed: unknown
156
- try { parsed = JSON.parse(raw) } catch { return emptySidecarMeta() }
157
- if (!parsed || typeof parsed !== 'object') return emptySidecarMeta()
158
- const r = parsed as { bridgeIds?: unknown; role?: unknown; channel?: unknown; senderId?: unknown; startedAt?: unknown; personId?: unknown; adminUserId?: unknown; visitorId?: unknown; agentSlug?: unknown; accountId?: unknown }
159
- const bridgeIds = Array.isArray(r.bridgeIds)
160
- ? r.bridgeIds.filter((b): b is string => typeof b === 'string' && b.length > 0)
161
- : []
162
- return {
163
- bridgeIds,
164
- role: typeof r.role === 'string' ? r.role : null,
165
- channel: typeof r.channel === 'string' ? r.channel : null,
166
- senderId: typeof r.senderId === 'string' ? r.senderId : null,
167
- startedAt: typeof r.startedAt === 'string' ? r.startedAt : null,
168
- personId: typeof r.personId === 'string' ? r.personId : null,
169
- adminUserId: typeof r.adminUserId === 'string' ? r.adminUserId : null,
170
- visitorId: typeof r.visitorId === 'string' ? r.visitorId : null,
171
- agentSlug: typeof r.agentSlug === 'string' ? r.agentSlug : null,
172
- accountId: typeof r.accountId === 'string' ? r.accountId : null,
173
- }
174
- }
175
-
176
- export function readSidecarMeta(metaPath: string): SidecarMeta {
177
- let raw: string
178
- try { raw = readFileSync(metaPath, 'utf8') } catch { return emptySidecarMeta() }
179
- return parseSidecarMeta(raw)
180
- }
181
- ```
182
-
183
- - [ ] **Step 4: Run tests to verify they pass**
184
-
185
- Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts server/routes/admin/__tests__/sidebar-sessions.test.ts server/routes/admin/__tests__/resolve-title.test.ts`
186
- Expected: PASS on all three files. The two pre-existing files are the regression guard on the sync readers whose bodies just moved.
187
-
188
- - [ ] **Step 5: Commit**
189
-
190
- ```bash
191
- git add maxy-code/platform/ui/server/routes/admin/sidebar-sessions.ts maxy-code/platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts
192
- git commit -m "refactor(2023): split the sidecar and user-title parsers out of their sync readers"
193
- ```
194
-
195
- ---
196
-
197
- ### Task 2: Add the async readers and switch the row loop onto them
198
-
199
- **Files:**
200
- - Modify: `platform/ui/server/routes/admin/sidebar-sessions.ts:70` (imports), `:572` (`loadUserTitles` call site), `:642-655` (row loop reads)
201
- - Test: `platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
202
-
203
- **Interfaces:**
204
- - Consumes: `parseUserTitles`, `parseSidecarMeta`, `emptySidecarMeta` from Task 1.
205
- - Produces: `loadUserTitlesAsync(accountDir: string | null): Promise<Map<string, string>>` and `readSidecarMetaAsync(metaPath: string): Promise<SidecarMeta>`. Same degradation rules as their sync twins.
206
-
207
- - [ ] **Step 1: Write the failing test**
208
-
209
- The load-bearing assertion is that the loop yields. A test that only checks the returned rows would pass just as well against the synchronous version, which is the dead-signal case of Task 1730. So the test races a timer against the compute: with synchronous reads the timer cannot fire until the walk finishes; with async reads it fires during it.
210
-
211
- ```ts
212
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
213
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
214
- import { tmpdir } from 'node:os'
215
- import { join } from 'node:path'
216
- import { loadUserTitlesAsync, readSidecarMetaAsync } from '../sidebar-sessions'
217
-
218
- let dir: string
219
- beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'sbs-async-')) })
220
- afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
221
-
222
- describe('loadUserTitlesAsync', () => {
223
- it('reads and parses the same file as the sync twin', async () => {
224
- writeFileSync(join(dir, 'session-titles.json'), JSON.stringify({ s1: { title: 'renamed' } }))
225
- expect((await loadUserTitlesAsync(dir)).get('s1')).toBe('renamed')
226
- })
227
-
228
- it('returns an empty map for a null accountDir and for a missing file', async () => {
229
- expect((await loadUserTitlesAsync(null)).size).toBe(0)
230
- expect((await loadUserTitlesAsync(join(dir, 'absent'))).size).toBe(0)
231
- })
232
- })
233
-
234
- describe('readSidecarMetaAsync', () => {
235
- it('reads and parses the same file as the sync twin', async () => {
236
- const p = join(dir, 'x.meta.json')
237
- writeFileSync(p, JSON.stringify({ role: 'admin', channel: 'webchat' }))
238
- const meta = await readSidecarMetaAsync(p)
239
- expect(meta.role).toBe('admin')
240
- expect(meta.channel).toBe('webchat')
241
- })
242
-
243
- it('returns the empty shape for a missing file', async () => {
244
- expect((await readSidecarMetaAsync(join(dir, 'absent.meta.json'))).bridgeIds).toEqual([])
245
- })
246
- })
247
-
248
- describe('the row loop yields to the event loop', () => {
249
- it('lets a pending timer fire while the walk is still running', async () => {
250
- const root = join(dir, 'projects', 'slug')
251
- mkdirSync(root, { recursive: true })
252
- // Bodies large enough that the walk cannot complete inside one tick.
253
- const line = JSON.stringify({ type: 'user', message: { role: 'user', content: 'x'.repeat(400) } })
254
- for (let i = 0; i < 40; i++) {
255
- const id = `0000000${i.toString().padStart(2, '0')}-0000-4000-8000-000000000000`
256
- writeFileSync(join(root, `${id}.jsonl`), `${line}\n`.repeat(500))
257
- }
258
-
259
- let firedDuringWalk = false
260
- const walk = readAllBodiesForTest(join(dir, 'projects'))
261
- setTimeout(() => { firedDuringWalk = true }, 0)
262
- await walk
263
- expect(firedDuringWalk).toBe(true)
264
- })
265
- })
266
- ```
267
-
268
- - [ ] **Step 2: Run test to verify it fails**
269
-
270
- Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
271
- Expected: FAIL — `loadUserTitlesAsync is not a function`, and `readAllBodiesForTest is not defined`.
272
-
273
- - [ ] **Step 3: Write minimal implementation**
274
-
275
- Add the promises import beside the existing sync one, add the two async readers, and export the loop's read half as `readAllBodiesForTest` so the yield test drives the shipped code rather than a hand-copied twin.
276
-
277
- ```ts
278
- import { readdirSync, readFileSync, statSync } from 'node:fs'
279
- import { readFile, stat } from 'node:fs/promises'
280
- ```
281
-
282
- ```ts
283
- /** Async twin of {@link loadUserTitles}. Same parser, same degradation. Used by
284
- * computeSessionList so the request path never blocks the loop on this read. */
285
- export async function loadUserTitlesAsync(accountDir: string | null): Promise<Map<string, string>> {
286
- if (!accountDir) return new Map()
287
- let raw: string
288
- try { raw = await readFile(join(accountDir, 'session-titles.json'), 'utf8') } catch { return new Map() }
289
- return parseUserTitles(raw)
290
- }
291
-
292
- /** Async twin of {@link readSidecarMeta}. Same parser, same degradation. */
293
- export async function readSidecarMetaAsync(metaPath: string): Promise<SidecarMeta> {
294
- let raw: string
295
- try { raw = await readFile(metaPath, 'utf8') } catch { return emptySidecarMeta() }
296
- return parseSidecarMeta(raw)
297
- }
298
-
299
- /** The row loop's read half, exported only so the yield test drives the shipped
300
- * code. Sequential by design: `Promise.all` here would hold every body in
301
- * memory at once (133.9 MB measured on maxy 2026-07-26). */
302
- export async function readAllBodiesForTest(projectsRoot: string): Promise<number> {
303
- let n = 0
304
- for (const { path } of enumerateJsonls(projectsRoot)) {
305
- try { await readFile(path, 'utf8'); await stat(path); n++ } catch { continue }
306
- }
307
- return n
308
- }
309
- ```
310
-
311
- In `computeSessionList`, change line 572 and the row loop's reads:
312
-
313
- ```ts
314
- const userTitles = await loadUserTitlesAsync(accountDir)
315
- ```
316
-
317
- ```ts
318
- let body: string
319
- let mtimeMs: number
320
- try {
321
- body = await readFile(path, 'utf8')
322
- mtimeMs = (await stat(path)).mtimeMs
323
- } catch {
324
- continue
325
- }
326
- ```
327
-
328
- ```ts
329
- const { bridgeIds, role, channel: sidecarChannel, senderId, personId, adminUserId, accountId: rowAccountId } = await readSidecarMetaAsync(metaPath)
330
- ```
331
-
332
- - [ ] **Step 4: Run tests to verify they pass**
333
-
334
- Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
335
- Expected: PASS, including the yield test.
336
-
337
- - [ ] **Step 5: Mutation-check the yield test**
338
-
339
- Temporarily revert `readAllBodiesForTest` to `readFileSync` / `statSync` and re-run. Expected: the yield test FAILS with `firedDuringWalk` false. Restore the async version. A yield test that passes against synchronous reads is measuring nothing.
340
-
341
- - [ ] **Step 6: Typecheck and run the full change surface**
342
-
343
- Run: `cd maxy-code/platform/ui && npx tsc --noEmit -p tsconfig.json`
344
- Expected: 0 errors.
345
-
346
- Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/ server/lib/__tests__/route-cache.test.ts server/__tests__/edge-request-timing.test.ts`
347
- Expected: PASS. `route-cache-watchers.test.ts` fails only when batched with its siblings and passes 4/4 alone; that is Task 2014 and predates this change.
348
-
349
- - [ ] **Step 7: Commit**
350
-
351
- ```bash
352
- git add maxy-code/platform/ui/server/routes/admin/sidebar-sessions.ts maxy-code/platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts
353
- git commit -m "perf(2023): read session bodies and sidecars asynchronously so the walk yields the loop"
354
- ```
355
-
356
- ---
357
-
358
- ### Task 3: Correct the task file's own criterion and figures
359
-
360
- **Files:**
361
- - Modify: `maxy-code/.tasks/2023-sidebar-sessions-sync-fs-blocks-event-loop.md`
362
-
363
- **Interfaces:** none.
364
-
365
- - [ ] **Step 1: Replace the p99 verification criterion**
366
-
367
- The task says a p99 approaching the request durations confirms and a low p99 refutes. Fourteen consecutive windows measured on maxy read `p99=51` or `52` including one with `max=663`, because one blocked tick out of ~590 samples sits nowhere near the 99th percentile. Rewrite the Observability and Verification sections to turn on `max`, which is the statistic that sees a single sub-second block and which Task 2024 also shipped.
368
-
369
- - [ ] **Step 2: Replace the 620 ms figure**
370
-
371
- The `p50 620 ms across 27,280 requests` sample is undatable and mostly predates the Task 1986 route cache, which landed 2026-07-25. State the datable post-cache distribution instead: `n=38 p50=2 p90=603 p99=678 max=1161`, and that the cost is one full walk per 30 s poll because the poll interval equals `BACKSTOP_MS`.
372
-
373
- - [ ] **Step 3: Record the enumeration deviation**
374
-
375
- State that the four `readdirSync` calls were measured at 1 ms and deliberately left synchronous, with the six other importers of `enumerateJsonls` as the reason.
376
-
377
- - [ ] **Step 4: Commit**
378
-
379
- ```bash
380
- git add maxy-code/.tasks/2023-sidebar-sessions-sync-fs-blocks-event-loop.md
381
- git commit -m "docs(2023): correct the p99 criterion and the pre-cache 620ms figure"
382
- ```
383
-
384
- ---
385
-
386
- ## Verification (Phase 4, on-device)
387
-
388
- The unit tests prove the loop yields. They cannot prove the lag falls on real data, so the sprint's claim rests on a device reading taken the same way as the before reading:
389
-
390
- - Before, already recorded: fourteen windows, `max` 310 to 708 ms in every window containing a `cache=stale-backstop` recompute, `max` 52 to 53 ms in every window without one.
391
- - After: the same grep across a window of real dashboard traffic. A window containing a `stale-backstop` recompute should no longer show a `max` near the recompute's own `ms=`.
392
- - Refutation case: if `max` still tracks the recompute duration after the change, the remaining block is the ~215 ms of parsing and the change did not achieve its goal. That is a reportable result, not a failed change.
393
-
394
- This step needs an installer publish to reach the device and is therefore operator-gated.