@rubytech/create-maxy-code 0.1.91 → 0.1.92

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 (50) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/package-lock.json +18 -4
  3. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +4 -3
  4. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  5. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +159 -219
  6. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  7. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +9 -0
  8. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  9. package/payload/platform/services/claude-session-manager/dist/http-server.js +262 -67
  10. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  11. package/payload/platform/services/claude-session-manager/dist/index.js +10 -0
  12. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  13. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +10 -4
  14. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
  15. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +27 -8
  16. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts +8 -0
  18. package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/jsonl-path.js +11 -0
  20. package/payload/platform/services/claude-session-manager/dist/jsonl-path.js.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +14 -6
  23. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts +2 -0
  25. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
  26. package/payload/platform/services/claude-session-manager/dist/reaper.js +6 -3
  27. package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
  28. package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +6 -4
  29. package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/session-store.js +8 -10
  31. package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
  32. package/payload/platform/services/claude-session-manager/dist/types.d.ts +0 -4
  33. package/payload/platform/services/claude-session-manager/dist/types.d.ts.map +1 -1
  34. package/payload/platform/services/claude-session-manager/dist/user-title-store.d.ts +39 -0
  35. package/payload/platform/services/claude-session-manager/dist/user-title-store.d.ts.map +1 -0
  36. package/payload/platform/services/claude-session-manager/dist/user-title-store.js +138 -0
  37. package/payload/platform/services/claude-session-manager/dist/user-title-store.js.map +1 -0
  38. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +3 -0
  39. package/payload/server/public/assets/admin-BhEvxMrC.js +217 -0
  40. package/payload/server/public/assets/{data-3n20lir5.js → data-Cbo0tTS3.js} +1 -1
  41. package/payload/server/public/assets/{graph-ChwMpxn_.js → graph-Beucu3_J.js} +1 -1
  42. package/payload/server/public/assets/graph-labels-D15-eFQK.js +1 -0
  43. package/payload/server/public/assets/{page-DNjUtT5q.js → page-Bu8kO4jO.js} +2 -2
  44. package/payload/server/public/assets/{page-D3bFmqF4.js → page-C1hj89Gf.js} +1 -1
  45. package/payload/server/public/data.html +3 -3
  46. package/payload/server/public/graph.html +3 -3
  47. package/payload/server/public/index.html +4 -4
  48. package/payload/server/server.js +17 -0
  49. package/payload/server/public/assets/admin-C57bDUgs.js +0 -217
  50. package/payload/server/public/assets/graph-labels-CUgJgIwl.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -113,10 +113,6 @@
113
113
  "resolved": "plugins/scheduling/mcp",
114
114
  "link": true
115
115
  },
116
- "node_modules/@maxy/tasks": {
117
- "resolved": "plugins/tasks/mcp",
118
- "link": true
119
- },
120
116
  "node_modules/@maxy/waitlist": {
121
117
  "resolved": "plugins/waitlist/mcp",
122
118
  "link": true
@@ -125,6 +121,10 @@
125
121
  "resolved": "plugins/whatsapp/mcp",
126
122
  "link": true
127
123
  },
124
+ "node_modules/@maxy/work": {
125
+ "resolved": "plugins/work/mcp",
126
+ "link": true
127
+ },
128
128
  "node_modules/@maxy/workflows": {
129
129
  "resolved": "plugins/workflows/mcp",
130
130
  "link": true
@@ -3454,6 +3454,7 @@
3454
3454
  "plugins/tasks/mcp": {
3455
3455
  "name": "@maxy/tasks",
3456
3456
  "version": "0.1.0",
3457
+ "extraneous": true,
3457
3458
  "dependencies": {
3458
3459
  "@modelcontextprotocol/sdk": "^1.12.1",
3459
3460
  "neo4j-driver": "^5.28.1",
@@ -3503,6 +3504,19 @@
3503
3504
  "typescript": "^5.7.0"
3504
3505
  }
3505
3506
  },
3507
+ "plugins/work/mcp": {
3508
+ "name": "@maxy/work",
3509
+ "version": "0.1.0",
3510
+ "dependencies": {
3511
+ "@modelcontextprotocol/sdk": "^1.12.1",
3512
+ "neo4j-driver": "^5.28.1",
3513
+ "zod": "^3.24.0"
3514
+ },
3515
+ "devDependencies": {
3516
+ "@types/node": "^22.0.0",
3517
+ "typescript": "^5.7.0"
3518
+ }
3519
+ },
3506
3520
  "plugins/workflows/mcp": {
3507
3521
  "name": "@maxy/workflows",
3508
3522
  "version": "0.2.0",
@@ -2,7 +2,9 @@ import type { Logger } from './types.js';
2
2
  export type RowState = 'live' | 'archived';
3
3
  export type RowStatus = 'idle' | 'busy' | 'gone';
4
4
  export interface SessionRow {
5
- /** Intrinsic Claude session id (UUID). Canonical key. */
5
+ /** Intrinsic Claude session id (UUID). Canonical key. For a top-level
6
+ * jsonl row, this is the file basename. For a pre-first-turn live row,
7
+ * this is the sessionId from the PID file. */
6
8
  sessionId: string;
7
9
  /** Full bridge session id ("session_<suffix>") from PID file, or null. */
8
10
  bridgeSessionId: string | null;
@@ -11,7 +13,7 @@ export interface SessionRow {
11
13
  /** Specialist agent name from PID file (`--agent <name>`), or null.
12
14
  * Always null on archived rows — PID file is gone by then. */
13
15
  agent: string | null;
14
- /** Specialist agent name cached from the first JSONL `agent-setting`
16
+ /** Specialist agent name cached from the JSONL `agent-setting`
15
17
  * record (read once at row-archive transition or boot-seed of an
16
18
  * archived row). Null when the JSONL has no `agent-setting` record
17
19
  * (operator-driven session). UI's resolveAgentLabel falls back to
@@ -19,7 +21,6 @@ export interface SessionRow {
19
21
  cachedAgentSetting: string | null;
20
22
  cwd: string | null;
21
23
  pid: number | null;
22
- jsonlPath: string | null;
23
24
  pidFilePath: string | null;
24
25
  state: RowState;
25
26
  status: RowStatus;
@@ -1 +1 @@
1
- {"version":3,"file":"fs-watcher.d.ts","sourceRoot":"","sources":["../src/fs-watcher.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAsCxC,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAA;AAC1C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAEhD,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,wEAAwE;IACxE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B;mEAC+D;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;qCAIiC;IACjC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,QAAQ,CAAA;IACf,MAAM,EAAE,SAAS,CAAA;IACjB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;IACpC,MAAM,EAAE,KAAK,GAAG,OAAO,CAAA;IACvB,GAAG,EAAE,UAAU,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;AAEtD,MAAM,WAAW,SAAS;IACxB,QAAQ,IAAI,IAAI,CAAA;IAChB,GAAG,IAAI,IAAI,CAAA;IACX,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,IAAI,CAAA;IACtC,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IAClD,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IACxD,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IACzD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IAC7C,GAAG,IAAI,UAAU,EAAE,CAAA;IACnB,6EAA6E;IAC7E,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IAC/D,KAAK,IAAI,IAAI,CAAA;IACb;;;;2EAIuE;IACvE,UAAU,EAAE;QACV,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;QACpC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;KACvC,CAAA;CACF;AA0DD,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,SAAS,CAoaZ"}
1
+ {"version":3,"file":"fs-watcher.d.ts","sourceRoot":"","sources":["../src/fs-watcher.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAsCxC,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAA;AAC1C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAEhD,MAAM,WAAW,UAAU;IACzB;;mDAE+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,wEAAwE;IACxE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B;mEAC+D;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;qCAIiC;IACjC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,QAAQ,CAAA;IACf,MAAM,EAAE,SAAS,CAAA;IACjB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;IACpC,MAAM,EAAE,KAAK,GAAG,OAAO,CAAA;IACvB,GAAG,EAAE,UAAU,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;AAEtD,MAAM,WAAW,SAAS;IACxB,QAAQ,IAAI,IAAI,CAAA;IAChB,GAAG,IAAI,IAAI,CAAA;IACX,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,IAAI,CAAA;IACtC,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IAClD,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IACxD,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IACzD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAA;IAC7C,GAAG,IAAI,UAAU,EAAE,CAAA;IACnB,6EAA6E;IAC7E,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IAC/D,KAAK,IAAI,IAAI,CAAA;IACb;;;;2EAIuE;IACvE,UAAU,EAAE;QACV,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;QACpC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;KACvC,CAAA;CACF;AAyDD,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,SAAS,CA2XZ"}
@@ -1,23 +1,28 @@
1
- // Filesystem watcher — single source of truth for the manager's row index.
1
+ // Filesystem watcher — disk is the source of truth (Task 258).
2
2
  //
3
3
  // Two directories are watched continuously:
4
4
  //
5
5
  // ${CLAUDE_CONFIG_DIR}/sessions/<pid>.json — written by Claude Code at PTY
6
- // launch for entrypoint=cli. Carries sessionId, bridgeSessionId, agent,
7
- // status, cwd, updatedAt directly. Created at process init, refreshed on
8
- // status/agent change, deleted when the process exits cleanly.
6
+ // launch. Carries sessionId, bridgeSessionId, agent, status, cwd,
7
+ // updatedAt directly. Created at process init, refreshed on status/agent
8
+ // change, deleted when the process exits cleanly.
9
9
  //
10
10
  // ${CLAUDE_CONFIG_DIR}/projects/<slug>/<sessionId>.jsonl — written on the
11
11
  // first operator turn; persists after process exit as the audit
12
12
  // transcript.
13
13
  //
14
- // The watcher unifies the two surfaces into one row indexed by the intrinsic
15
- // `sessionId` (the UUID Claude Code mints, identical to the JSONL basename).
16
- // PID-file events drive the live half of the lifecycle; JSONL events drive
17
- // the archived half. A row exists whenever EITHER file exists.
14
+ // Row identity rule:
15
+ // - A top-level JSONL file at `<projectsDir>/<sid>.jsonl` is a row whose
16
+ // `sessionId` IS the basename. Always.
17
+ // - A PID file at `<sessionsDir>/<pid>.json` decorates a row: the row
18
+ // becomes 'live' when the PID file's `sessionId` field matches a
19
+ // top-level JSONL basename, OR when the PID file's `sessionId` field
20
+ // has no matching JSONL yet (fresh /spawn pre-first-turn).
21
+ // - The fs-watcher carries no path-anchored maps and no merge state
22
+ // machine. After Task 258, paths are derived; the watcher reads disk.
18
23
  //
19
24
  // Boot sequence is sync-then-arm: bootSeed() reads both directories
20
- // synchronously and rebuilds the in-memory index before arm() turns on the
25
+ // synchronously and rebuilds the in-memory cache before arm() turns on the
21
26
  // fs.watch handlers. Without this ordering, watch events fire before the
22
27
  // seed completes and either land before any subscriber attaches (silent
23
28
  // drop) or race with seed inserts (double-emit). The deterministic order
@@ -28,16 +33,6 @@
28
33
  // emits when the PID file is rewritten (write-temp-rename is two events
29
34
  // from fs.watch's perspective on macOS, more on Linux), and folds the
30
35
  // CREATE→MODIFY pair an editor-style writer can emit.
31
- //
32
- // Identifiers carried on every row:
33
- // - sessionId (canonical, never null)
34
- // - bridgeSessionId ("session_<suffix>" form, null for entrypoint=cli
35
- // rows that have not yet written URL-bound state)
36
- // - bridgeSuffix ("<suffix>" — derived; bridgeSessionId minus the
37
- // "session_" prefix)
38
- // - pid (null on archived JSONL-only rows)
39
- // The store/HTTP layer resolves an inbound id against all four keys so a
40
- // caller holding any one of them resolves to the same row.
41
36
  import { existsSync, mkdirSync, openSync, closeSync, readSync, readFileSync, readdirSync, statSync, watch } from 'node:fs';
42
37
  import { join } from 'node:path';
43
38
  const DEBOUNCE_MS = 200;
@@ -119,39 +114,16 @@ function parsePidFile(path) {
119
114
  : typeof parsed.startedAt === 'number'
120
115
  ? parsed.startedAt
121
116
  : 0;
122
- return {
123
- sessionId,
124
- bridgeSessionId,
125
- bridgeSuffix,
126
- agent,
127
- cachedAgentSetting: null,
128
- cwd,
129
- pid,
130
- jsonlPath: null,
131
- pidFilePath: path,
132
- state: 'live',
133
- status,
134
- updatedAt,
135
- };
117
+ return { sessionId, pid, bridgeSessionId, bridgeSuffix, agent, cwd, status, updatedAt, pidFilePath: path };
136
118
  }
137
119
  export function createFsWatcher(opts) {
138
120
  const { sessionsDir, projectsDir, logger } = opts;
139
- // Canonical store keyed on sessionId. Secondary maps re-derived from the
140
- // canonical row so a removeRow drops every key in one shot.
121
+ // Transient row cache, keyed on sessionId. Built by bootSeed and kept
122
+ // in sync by fs event reconciliation. Not the source of truth — disk is.
141
123
  const rowsBySessionId = new Map();
124
+ // Reverse indexes derived from row content. Rebuilt on indexRow/unindexRow.
142
125
  const sessionIdByPid = new Map();
143
126
  const sessionIdByBridgeSuffix = new Map();
144
- const sessionIdByPidFilePath = new Map();
145
- // Task 190 — jsonl-path-anchored prior-row lookup. Closes the symmetric
146
- // gap to Task 186's pid-file swap: when `claude --resume` rewrites the
147
- // pid file, the fork sid stays canonical, but Claude continues writing
148
- // turns to `<source>.jsonl`. The admin server archives the source jsonl
149
- // during resume, so the first post-resume turn re-creates the file at
150
- // the projects-dir path. Without this map, that create event lands as
151
- // a stray archived row. The pid-file swap branch pre-seeds this map
152
- // with `jsonlPathFor(parsed.sessionId) → priorSessionIdAtPath` so the
153
- // jsonl event redirects onto the fork row.
154
- const sessionIdByJsonlPath = new Map();
155
127
  const subscribers = new Set();
156
128
  const pidWaiters = new Map();
157
129
  const debounceTimers = new Map();
@@ -161,7 +133,7 @@ export function createFsWatcher(opts) {
161
133
  let seeded = false;
162
134
  function emit(event) {
163
135
  logger(`[fs-watcher] event=${event.kind} kind=${event.source} sessionId=${event.sessionId} agent=${event.row?.agent ?? 'none'} ` +
164
- `status=${event.row?.status ?? 'gone'} path=${event.row?.[event.source === 'pid' ? 'pidFilePath' : 'jsonlPath'] ?? 'none'}`);
136
+ `status=${event.row?.status ?? 'gone'} path=${event.row?.pidFilePath ?? 'derived'}`);
165
137
  for (const sub of subscribers) {
166
138
  try {
167
139
  sub(event);
@@ -186,10 +158,6 @@ export function createFsWatcher(opts) {
186
158
  sessionIdByPid.set(row.pid, row.sessionId);
187
159
  if (row.bridgeSuffix)
188
160
  sessionIdByBridgeSuffix.set(row.bridgeSuffix, row.sessionId);
189
- if (row.pidFilePath)
190
- sessionIdByPidFilePath.set(row.pidFilePath, row.sessionId);
191
- if (row.jsonlPath)
192
- sessionIdByJsonlPath.set(row.jsonlPath, row.sessionId);
193
161
  }
194
162
  function unindexRow(row) {
195
163
  rowsBySessionId.delete(row.sessionId);
@@ -197,36 +165,90 @@ export function createFsWatcher(opts) {
197
165
  sessionIdByPid.delete(row.pid);
198
166
  if (row.bridgeSuffix)
199
167
  sessionIdByBridgeSuffix.delete(row.bridgeSuffix);
200
- if (row.pidFilePath)
201
- sessionIdByPidFilePath.delete(row.pidFilePath);
202
- if (row.jsonlPath)
203
- sessionIdByJsonlPath.delete(row.jsonlPath);
204
168
  }
205
169
  function jsonlPathFor(sessionId) {
206
170
  return join(projectsDir, `${sessionId}.jsonl`);
207
171
  }
172
+ /** Find the pid file (if any) whose content names `sessionId`. Used at
173
+ * bootSeed to correlate jsonls with their live pid files, and on jsonl
174
+ * events to detect a freshly-created jsonl whose sessionId matches a
175
+ * pre-first-turn live row. */
176
+ function findPidFileForSessionId(sessionId) {
177
+ try {
178
+ for (const entry of readdirSync(sessionsDir, { withFileTypes: true })) {
179
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
180
+ continue;
181
+ const parsed = parsePidFile(join(sessionsDir, entry.name));
182
+ if (parsed && parsed.sessionId === sessionId)
183
+ return parsed;
184
+ }
185
+ }
186
+ catch {
187
+ // sessionsDir absent — fine.
188
+ }
189
+ return null;
190
+ }
191
+ function buildLiveRow(pidContent) {
192
+ return {
193
+ sessionId: pidContent.sessionId,
194
+ bridgeSessionId: pidContent.bridgeSessionId,
195
+ bridgeSuffix: pidContent.bridgeSuffix,
196
+ agent: pidContent.agent,
197
+ cachedAgentSetting: null,
198
+ cwd: pidContent.cwd,
199
+ pid: pidContent.pid,
200
+ pidFilePath: pidContent.pidFilePath,
201
+ state: 'live',
202
+ status: pidContent.status,
203
+ updatedAt: pidContent.updatedAt,
204
+ };
205
+ }
206
+ function buildArchivedRow(sessionId, jsonlPath) {
207
+ return {
208
+ sessionId,
209
+ bridgeSessionId: null,
210
+ bridgeSuffix: null,
211
+ agent: null,
212
+ cachedAgentSetting: readAgentSettingFromJsonl(jsonlPath),
213
+ cwd: null,
214
+ pid: null,
215
+ pidFilePath: null,
216
+ state: 'archived',
217
+ status: 'gone',
218
+ updatedAt: (() => {
219
+ try {
220
+ return statSync(jsonlPath).mtimeMs;
221
+ }
222
+ catch {
223
+ return 0;
224
+ }
225
+ })(),
226
+ };
227
+ }
208
228
  /** Apply the current on-disk state for one PID-file path to the index.
209
- * Reads the file if present, merges with any existing row (by sessionId),
210
- * or removes the row when the file is gone. Emits create/modify/delete. */
229
+ * Reads the file if present; on delete, finds the row via pid (from the
230
+ * filename) since we no longer carry a path-keyed index. */
211
231
  function reconcilePidFile(path) {
212
232
  const onDisk = existsSync(path);
213
- const priorSessionId = sessionIdByPidFilePath.get(path);
214
233
  if (!onDisk) {
234
+ // pid is encoded in the filename. Look up by pid → sessionId.
235
+ const fileName = path.slice(path.lastIndexOf('/') + 1);
236
+ const pidStr = fileName.replace(/\.json$/, '');
237
+ const pid = /^\d+$/.test(pidStr) ? Number(pidStr) : null;
238
+ if (pid === null)
239
+ return;
240
+ const priorSessionId = sessionIdByPid.get(pid);
215
241
  if (!priorSessionId)
216
242
  return;
217
243
  const prior = rowsBySessionId.get(priorSessionId);
218
- if (!prior) {
219
- sessionIdByPidFilePath.delete(path);
244
+ if (!prior)
220
245
  return;
221
- }
222
- // PID file gone. If JSONL still exists, demote row to archived;
223
- // else drop it entirely.
224
- const jsonlPath = prior.jsonlPath ?? jsonlPathFor(prior.sessionId);
246
+ const jsonlPath = jsonlPathFor(prior.sessionId);
225
247
  if (existsSync(jsonlPath)) {
226
248
  // Cache agent identity from JSONL head before the PID file is
227
- // forgotten. `agent` on a live row comes from the PID file
228
- // (`--agent <name>`); after demote it's null, so without this
229
- // cache the UI would lose the specialist label.
249
+ // forgotten. `agent` on a live row comes from the PID file; after
250
+ // demote it's null, so without this cache the UI would lose the
251
+ // specialist label.
230
252
  const cachedAgentSetting = prior.cachedAgentSetting
231
253
  ?? readAgentSettingFromJsonl(jsonlPath);
232
254
  const updated = {
@@ -236,7 +258,6 @@ export function createFsWatcher(opts) {
236
258
  state: 'archived',
237
259
  status: 'gone',
238
260
  cachedAgentSetting,
239
- jsonlPath,
240
261
  };
241
262
  unindexRow(prior);
242
263
  indexRow(updated);
@@ -251,58 +272,31 @@ export function createFsWatcher(opts) {
251
272
  const parsed = parsePidFile(path);
252
273
  if (!parsed)
253
274
  return;
254
- const jsonlPath = jsonlPathFor(parsed.sessionId);
255
- if (existsSync(jsonlPath))
256
- parsed.jsonlPath = jsonlPath;
257
- // Task 186 — `claude --resume` rewrites the pid file's sessionId field
258
- // to the resumed source id at the same path. Hold the prior sessionId
259
- // as canonical: it matches `SessionStore.add(...)` from `/spawn` (and
260
- // the `/resume` response payload echoed to the client URL). Merge the
261
- // rewritten content's mutable fields (agent, status, updatedAt) into
262
- // the prior row; do NOT index a second row.
263
- const priorSessionIdAtPath = sessionIdByPidFilePath.get(path);
264
- if (priorSessionIdAtPath && priorSessionIdAtPath !== parsed.sessionId) {
265
- const priorAtPath = rowsBySessionId.get(priorSessionIdAtPath);
266
- if (priorAtPath) {
267
- logger(`[fs-watcher] pid-file-sessionId-changed path=${path} prior=${priorSessionIdAtPath} next=${parsed.sessionId}`);
268
- // Task 190 — pre-register the resumed source jsonl path against
269
- // the prior canonical sid so the eventual jsonl create event
270
- // (Claude writes turns to `<parsed.sessionId>.jsonl`) routes back
271
- // to this row instead of indexing a stray archived row.
272
- sessionIdByJsonlPath.set(jsonlPathFor(parsed.sessionId), priorSessionIdAtPath);
273
- const merged = {
274
- ...priorAtPath,
275
- agent: parsed.agent,
276
- status: parsed.status,
277
- updatedAt: parsed.updatedAt,
278
- };
279
- rowsBySessionId.set(priorSessionIdAtPath, merged);
280
- emit({ kind: 'modify', source: 'pid', row: merged, sessionId: priorSessionIdAtPath });
281
- return;
275
+ // If this pid was already mapped to a different sessionId (claude
276
+ // rewrote the file's sessionId mid-life, e.g. Task 186 resume
277
+ // convergence), drop the stale row first. The new content is the
278
+ // authoritative state.
279
+ const stalePriorByPid = sessionIdByPid.get(parsed.pid);
280
+ if (stalePriorByPid && stalePriorByPid !== parsed.sessionId) {
281
+ const stale = rowsBySessionId.get(stalePriorByPid);
282
+ if (stale) {
283
+ unindexRow(stale);
284
+ emit({ kind: 'delete', source: 'pid', row: null, sessionId: stalePriorByPid });
282
285
  }
283
- // Prior row was unindexed (e.g. by the reaper) between events. Drop
284
- // the stale path mapping and fall through to the standard new-row
285
- // path so the rewritten content lands as a fresh row rather than
286
- // permanently leaking the swap log line.
287
- logger(`[fs-watcher] pid-file-sessionId-changed path=${path} prior=${priorSessionIdAtPath} next=${parsed.sessionId} prior-row=gone`);
288
- sessionIdByPidFilePath.delete(path);
289
286
  }
290
287
  const prior = rowsBySessionId.get(parsed.sessionId);
291
- if (!prior) {
292
- indexRow(parsed);
293
- emit({ kind: 'create', source: 'pid', row: parsed, sessionId: parsed.sessionId });
288
+ const merged = buildLiveRow(parsed);
289
+ if (prior) {
290
+ // Preserve cachedAgentSetting from the prior row (read once at
291
+ // archive transition or boot-seed; pid file rewrites don't carry it).
292
+ merged.cachedAgentSetting = prior.cachedAgentSetting ?? merged.cachedAgentSetting;
293
+ unindexRow(prior);
294
+ indexRow(merged);
295
+ emit({ kind: 'modify', source: 'pid', row: merged, sessionId: merged.sessionId });
294
296
  return;
295
297
  }
296
- // Merge: keep jsonlPath from prior if file existed; otherwise from
297
- // parsed (which already checked disk above).
298
- const merged = {
299
- ...prior,
300
- ...parsed,
301
- jsonlPath: parsed.jsonlPath ?? prior.jsonlPath,
302
- };
303
- unindexRow(prior);
304
298
  indexRow(merged);
305
- emit({ kind: 'modify', source: 'pid', row: merged, sessionId: merged.sessionId });
299
+ emit({ kind: 'create', source: 'pid', row: merged, sessionId: merged.sessionId });
306
300
  }
307
301
  function reconcileJsonlFile(path) {
308
302
  const onDisk = existsSync(path);
@@ -314,12 +308,11 @@ export function createFsWatcher(opts) {
314
308
  if (!onDisk) {
315
309
  if (!prior)
316
310
  return;
317
- // JSONL gone. If PID file still present, row stays live but loses
318
- // transcript; else drop row entirely.
311
+ // JSONL gone. If PID file still present, row stays live (transient
312
+ // state the live PTY may write a new jsonl on the next turn);
313
+ // else drop the row entirely.
319
314
  if (prior.pidFilePath && existsSync(prior.pidFilePath)) {
320
- const updated = { ...prior, jsonlPath: null };
321
- rowsBySessionId.set(updated.sessionId, updated);
322
- emit({ kind: 'delete', source: 'jsonl', row: updated, sessionId });
315
+ emit({ kind: 'delete', source: 'jsonl', row: prior, sessionId });
323
316
  return;
324
317
  }
325
318
  unindexRow(prior);
@@ -327,62 +320,26 @@ export function createFsWatcher(opts) {
327
320
  return;
328
321
  }
329
322
  if (prior) {
330
- if (prior.jsonlPath === path) {
331
- emit({ kind: 'modify', source: 'jsonl', row: prior, sessionId });
332
- return;
333
- }
334
- const updated = { ...prior, jsonlPath: path };
335
- unindexRow(prior);
336
- indexRow(updated);
337
- emit({ kind: 'modify', source: 'jsonl', row: updated, sessionId });
323
+ // Row already exists at this sessionId. If the row was live
324
+ // (pid-file-derived), the jsonl appearing is the first-turn flush;
325
+ // emit a modify so subscribers re-render. If the row was archived,
326
+ // the jsonl is the same file we already knew about; no-op except
327
+ // for the modify emission (subscribers may want to re-poll the tail).
328
+ emit({ kind: prior.state === 'archived' ? 'modify' : 'modify', source: 'jsonl', row: prior, sessionId });
338
329
  return;
339
330
  }
340
- // Task 190 before falling into the archived-row fallback, consult
341
- // the jsonl-path map. A prior canonical sid may have pre-registered
342
- // this path via the Task 186 pid-file swap branch (the resume case)
343
- // or a row indexed earlier may name this path. Either way: redirect
344
- // the event onto the prior canonical sid rather than indexing a
345
- // second row at the basename-derived sid.
346
- const priorCanonicalSid = sessionIdByJsonlPath.get(path);
347
- if (priorCanonicalSid && priorCanonicalSid !== sessionId) {
348
- const priorCanonical = rowsBySessionId.get(priorCanonicalSid);
349
- if (priorCanonical) {
350
- logger(`[fs-watcher] jsonl-path-sessionId-changed path=${path} prior=${priorCanonicalSid} next=${sessionId}`);
351
- const updated = { ...priorCanonical, jsonlPath: path };
352
- unindexRow(priorCanonical);
353
- indexRow(updated);
354
- emit({ kind: 'modify', source: 'jsonl', row: updated, sessionId: updated.sessionId });
355
- return;
356
- }
357
- // Prior canonical row vanished between the swap pre-seed and the
358
- // jsonl event (e.g. pid file deleted, reaper). Drop the stale map
359
- // entry and fall through to the standard archived-row path so the
360
- // file lands as an ordinary archived row at the basename sid.
361
- logger(`[fs-watcher] jsonl-path-sessionId-changed path=${path} prior=${priorCanonicalSid} next=${sessionId} prior-row=gone`);
362
- sessionIdByJsonlPath.delete(path);
331
+ // Fresh jsonl at this sessionId. If a pid file already exists with this
332
+ // sessionId, the row is live (claude wrote its first turn before the
333
+ // pid-file create event was observed rare but possible). Otherwise
334
+ // the row is archived (jsonl with no live PTY behind it).
335
+ const matchingPid = findPidFileForSessionId(sessionId);
336
+ if (matchingPid) {
337
+ const row = buildLiveRow(matchingPid);
338
+ indexRow(row);
339
+ emit({ kind: 'create', source: 'jsonl', row, sessionId });
340
+ return;
363
341
  }
364
- // JSONL appeared without a corresponding PID file → archived row.
365
- const row = {
366
- sessionId,
367
- bridgeSessionId: null,
368
- bridgeSuffix: null,
369
- agent: null,
370
- cachedAgentSetting: readAgentSettingFromJsonl(path),
371
- cwd: null,
372
- pid: null,
373
- jsonlPath: path,
374
- pidFilePath: null,
375
- state: 'archived',
376
- status: 'gone',
377
- updatedAt: (() => {
378
- try {
379
- return statSync(path).mtimeMs;
380
- }
381
- catch {
382
- return 0;
383
- }
384
- })(),
385
- };
342
+ const row = buildArchivedRow(sessionId, path);
386
343
  indexRow(row);
387
344
  emit({ kind: 'create', source: 'jsonl', row, sessionId });
388
345
  }
@@ -404,68 +361,51 @@ export function createFsWatcher(opts) {
404
361
  function bootSeed() {
405
362
  if (seeded)
406
363
  return;
407
- let pidCount = 0;
364
+ // Walk the projects dir first so jsonl rows exist as the canonical key
365
+ // surface. Then walk the sessions dir; each pid file's sessionId either
366
+ // matches an already-seeded jsonl row (promote to live) or has no jsonl
367
+ // yet (a pre-first-turn live row, no transcript).
408
368
  let jsonlCount = 0;
369
+ let pidCount = 0;
409
370
  try {
410
- for (const entry of readdirSync(sessionsDir, { withFileTypes: true })) {
411
- if (!entry.isFile() || !entry.name.endsWith('.json'))
371
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
372
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
412
373
  continue;
413
- const path = join(sessionsDir, entry.name);
414
- const row = parsePidFile(path);
415
- if (!row)
374
+ const sessionId = entry.name.slice(0, -'.jsonl'.length);
375
+ if (!sessionId)
416
376
  continue;
417
- const jsonlPath = jsonlPathFor(row.sessionId);
418
- if (existsSync(jsonlPath))
419
- row.jsonlPath = jsonlPath;
420
- indexRow(row);
421
- pidCount += 1;
377
+ const jsonlPath = join(projectsDir, entry.name);
378
+ if (!rowsBySessionId.has(sessionId)) {
379
+ indexRow(buildArchivedRow(sessionId, jsonlPath));
380
+ }
381
+ jsonlCount += 1;
422
382
  }
423
383
  }
424
384
  catch {
425
- // sessionsDir absent — fine. Watcher creates it via mkdir at arm time.
385
+ // projectsDir absent on first boot Claude Code creates it on first
386
+ // transcript write. Watcher's mkdirSync at arm() avoids fs.watch ENOENT.
426
387
  }
427
388
  try {
428
- for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
429
- if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
389
+ for (const entry of readdirSync(sessionsDir, { withFileTypes: true })) {
390
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
430
391
  continue;
431
- const sessionId = entry.name.slice(0, -'.jsonl'.length);
432
- const path = join(projectsDir, entry.name);
433
- const prior = rowsBySessionId.get(sessionId);
392
+ const path = join(sessionsDir, entry.name);
393
+ const parsed = parsePidFile(path);
394
+ if (!parsed)
395
+ continue;
396
+ const prior = rowsBySessionId.get(parsed.sessionId);
397
+ const row = buildLiveRow(parsed);
434
398
  if (prior) {
435
- if (prior.jsonlPath !== path) {
436
- rowsBySessionId.set(sessionId, { ...prior, jsonlPath: path });
437
- }
438
- }
439
- else {
440
- const row = {
441
- sessionId,
442
- bridgeSessionId: null,
443
- bridgeSuffix: null,
444
- agent: null,
445
- cachedAgentSetting: readAgentSettingFromJsonl(path),
446
- cwd: null,
447
- pid: null,
448
- jsonlPath: path,
449
- pidFilePath: null,
450
- state: 'archived',
451
- status: 'gone',
452
- updatedAt: (() => {
453
- try {
454
- return statSync(path).mtimeMs;
455
- }
456
- catch {
457
- return 0;
458
- }
459
- })(),
460
- };
461
- indexRow(row);
399
+ // Preserve cached agent setting we already read from the jsonl head.
400
+ row.cachedAgentSetting = prior.cachedAgentSetting ?? row.cachedAgentSetting;
401
+ unindexRow(prior);
462
402
  }
463
- jsonlCount += 1;
403
+ indexRow(row);
404
+ pidCount += 1;
464
405
  }
465
406
  }
466
407
  catch {
467
- // projectsDir absent on first boot Claude Code creates it on first
468
- // transcript write. Watcher's mkdirSync at arm() avoids fs.watch ENOENT.
408
+ // sessionsDir absent — fine. Watcher creates it via mkdir at arm time.
469
409
  }
470
410
  seeded = true;
471
411
  logger(`[fs-watcher] boot-seed sessions-dir=${pidCount} projects-dir=${jsonlCount} rows=${rowsBySessionId.size}`);