@rubytech/create-maxy-code 0.1.57 → 0.1.59

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 (77) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +30 -19
  3. package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +35 -90
  4. package/payload/platform/plugins/docs/references/admin-session.md +17 -16
  5. package/payload/platform/plugins/docs/references/platform.md +3 -3
  6. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  7. package/payload/platform/plugins/preval/.claude-plugin/plugin.json +8 -0
  8. package/payload/platform/plugins/preval/PLUGIN.md +39 -0
  9. package/payload/platform/plugins/preval/skills/property-preval/SKILL.md +78 -0
  10. package/payload/platform/plugins/preval/skills/property-preval/references/render.py +265 -0
  11. package/payload/platform/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
  12. package/payload/platform/plugins/preval/skills/property-preval/references/template.html +253 -0
  13. package/payload/platform/plugins/property-data/.claude-plugin/plugin.json +1 -1
  14. package/payload/platform/plugins/property-data/PLUGIN.md +46 -1
  15. package/payload/platform/plugins/property-data/mcp/dist/index.js +81 -1
  16. package/payload/platform/plugins/property-data/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
  18. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
  19. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js +13 -0
  20. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
  21. package/payload/platform/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
  22. package/payload/platform/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
  23. package/payload/platform/plugins/property-data/mcp/src/index.ts +155 -0
  24. package/payload/platform/plugins/property-data/mcp/src/tools/endpoints.ts +20 -0
  25. package/payload/platform/plugins/property-data/skills/property-data/SKILL.md +19 -0
  26. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  27. package/payload/platform/services/claude-session-manager/dist/http-server.js +25 -260
  28. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  29. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +3 -7
  30. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
  31. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +6 -32
  32. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +6 -24
  34. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -91
  36. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/reaper.js +0 -11
  39. package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +0 -21
  41. package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
  43. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  44. package/payload/platform/templates/agents/admin/SOUL.md +1 -1
  45. package/payload/premium-plugins/real-agent/BUNDLE.md +5 -3
  46. package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
  47. package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
  48. package/payload/premium-plugins/real-agent/plugins/.claude-plugin/marketplace.json +5 -0
  49. package/payload/premium-plugins/real-agent/plugins/preval/.claude-plugin/plugin.json +8 -0
  50. package/payload/premium-plugins/real-agent/plugins/preval/PLUGIN.md +39 -0
  51. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +78 -0
  52. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +265 -0
  53. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
  54. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template.html +253 -0
  55. package/payload/premium-plugins/real-agent/plugins/property-data/.claude-plugin/plugin.json +1 -1
  56. package/payload/premium-plugins/real-agent/plugins/property-data/PLUGIN.md +46 -1
  57. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js +81 -1
  58. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js.map +1 -1
  59. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
  60. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
  61. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js +13 -0
  62. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
  63. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
  64. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
  65. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/index.ts +155 -0
  66. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/endpoints.ts +20 -0
  67. package/payload/premium-plugins/real-agent/plugins/property-data/skills/property-data/SKILL.md +19 -0
  68. package/payload/server/public/assets/{admin-2ZJmzJ_g.js → admin-nqCBFiSl.js} +1 -1
  69. package/payload/server/public/assets/{data-DlmoKxmN.js → data-BGUAGVkV.js} +1 -1
  70. package/payload/server/public/assets/{graph-BVvcFSEQ.js → graph-MvYxZOBF.js} +1 -1
  71. package/payload/server/public/assets/{graph-labels-cCvgu38Y.js → graph-labels-D865qb3K.js} +1 -1
  72. package/payload/server/public/assets/{page-BnAxPkmg.js → page--hOVRrgN.js} +1 -1
  73. package/payload/server/public/assets/{page-BnaN3u86.js → page-C2b1nlOc.js} +1 -1
  74. package/payload/server/public/data.html +3 -3
  75. package/payload/server/public/graph.html +3 -3
  76. package/payload/server/public/index.html +4 -4
  77. package/payload/server/server.js +19 -83
@@ -8,14 +8,13 @@
8
8
  // POST /resume { senderId, role, channel, sessionId } -> Session
9
9
  // POST /:id/stop -> 204 (kill PTY; idempotent on already-dead row)
10
10
  // 404 if no row matches any of the 4 keys
11
- // DELETE /:id -> 204 (rmSync JSONL + sidecar + subdir)
11
+ // DELETE /:id -> 204 (rmSync JSONL + subdir)
12
12
  // 409 if PTY still alive
13
13
  // 404 if no transcript on disk
14
14
  // POST /:id/input { text }
15
15
  // GET /:id/log -> { jsonlPath, sizeBytes, exists }
16
16
  // GET /:id/log?follow=1 -> streamed file content (current + new)
17
17
  // GET /:id/meta -> SessionMeta payload
18
- // PATCH /:id { displayName } -> 204 (writes sidecar)
19
18
  // POST /:id/archive { mode }
20
19
  //
21
20
  // Any of the four ids — intrinsic sessionId, bridgeSessionId, bridgeSuffix,
@@ -24,10 +23,9 @@
24
23
  // manager does not know.
25
24
  import { Hono } from 'hono';
26
25
  import { stream } from 'hono/streaming';
27
- import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, writeFileSync, mkdirSync, renameSync, readFileSync, openSync, readSync, closeSync } from 'node:fs';
28
- import { createHash } from 'node:crypto';
26
+ import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, mkdirSync, renameSync, readFileSync } from 'node:fs';
29
27
  import { spawnClaudeSession, stopSession, PERMISSION_MODES } from './pty-spawner.js';
30
- import { readJsonlSession, readSidecar, sidecarPathFor } from './jsonl-enumerator.js';
28
+ import { readJsonlSession } from './jsonl-enumerator.js';
31
29
  import { claudeStateRoot, projectSlugForCwd } from './jsonl-path.js';
32
30
  import { basename, join } from 'node:path';
33
31
  const ROLES = ['admin', 'public'];
@@ -116,7 +114,6 @@ function parseSpecialistDomains(raw) {
116
114
  // bridgeSuffix, or pid (numeric). All resolve to a sessionId via watcher.
117
115
  const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
118
116
  const LIST_TAIL_BYTES = 64 * 1024;
119
- const DISPLAY_NAME_MAX = 256;
120
117
  function timed(logger, method, path, status, ms) {
121
118
  logger(`api method=${method} path=${path} outcome=${status} ms=${ms}`);
122
119
  }
@@ -140,98 +137,18 @@ function resolveRow(watcher, id) {
140
137
  }
141
138
  return undefined;
142
139
  }
143
- // Recorder-only auto-archive (Task 114; discriminator updated Task 131).
144
- // The database-operator session run by `turn-completed-graph-write.sh`
145
- // is structurally identical to a user-facing admin session Task 131
146
- // brings it byte-for-byte close to a Sidebar-shape spawn (real
147
- // senderId, hidden=false). Its deterministic single-turn lifecycle is
148
- // enforced here: as soon as the recorder's JSONL contains an assistant
149
- // message whose `stop_reason === "end_turn"`, the manager stops the PTY
150
- // against itself and lets the normal archive path run. Recorder
151
- // detection keys on `originatingSessionId` presence, the one field
152
- // that is set only for recorder spawns.
140
+ // Task 147 — auto-archive for the database-operator specialist on
141
+ // `end_turn`. The recorder is a Sidebar-equivalent admin spawn whose only
142
+ // override is the specialist value. Once its JSONL contains an assistant
143
+ // message with `stop_reason === "end_turn"`, stop the PTY and let the
144
+ // normal archive path run. No tool-call tally, no recorder-only log
145
+ // surfaces the Sidebar's `pty-spawned jsonl … end_turn …
146
+ // auto-archive` lines cover the lifecycle.
153
147
  const END_TURN_SIGNATURE = '"stop_reason":"end_turn"';
154
- /** Hash tool arguments for log correlation without leaking content
155
- * (Task 129). Uses sha256(JSON.stringify(args)).slice(0,8). */
156
- function argHash(toolArgs) {
157
- let serialized;
158
- try {
159
- serialized = JSON.stringify(toolArgs ?? null);
160
- }
161
- catch {
162
- serialized = '<unserializable>';
163
- }
164
- return createHash('sha256').update(serialized).digest('hex').slice(0, 8);
165
- }
166
- /** Sanitize a record's error string for the log line. Strip newlines,
167
- * cap at 200 chars. */
168
- function sanitizeErr(value) {
169
- if (typeof value !== 'string')
170
- return String(value ?? 'unknown');
171
- return value.replace(/[\r\n\t]+/g, ' ').slice(0, 200);
172
- }
173
- /** Per-recorder JSONL observer (Task 129). Parses newly-appended bytes
174
- * on each modify event and emits one `[turn-recorder] tool-call …` line
175
- * per JSONL record carrying a `tool_use` content block, and one
176
- * `[turn-recorder] tool-result …` line per record carrying a
177
- * `tool_result` content block. Maintains the session's `recorderTally`
178
- * as side-effect so `attachRecorderAutoArchive` can read final counts
179
- * at `write-complete` time.
180
- *
181
- * Position tracking lives on the StoredSession (`recorderJsonlPosition`)
182
- * so re-entry on a modify storm cannot double-emit the same record. */
183
- function readNewRecords(jsonlPath, fromPos) {
184
- if (!existsSync(jsonlPath))
185
- return { records: [], newPos: fromPos };
186
- const size = statSync(jsonlPath).size;
187
- if (size <= fromPos)
188
- return { records: [], newPos: fromPos };
189
- let buf;
190
- try {
191
- const fd = openSync(jsonlPath, 'r');
192
- try {
193
- buf = Buffer.alloc(size - fromPos);
194
- readSync(fd, buf, 0, buf.length, fromPos);
195
- }
196
- finally {
197
- closeSync(fd);
198
- }
199
- }
200
- catch {
201
- return { records: [], newPos: fromPos };
202
- }
203
- const text = buf.toString('utf8');
204
- // If the last line lacks a newline it is a partial append; rewind so
205
- // we re-read it on the next modify.
206
- const lastNl = text.lastIndexOf('\n');
207
- const completeText = lastNl === -1 ? '' : text.slice(0, lastNl + 1);
208
- const advance = Buffer.byteLength(completeText, 'utf8');
209
- const lines = completeText.split('\n').filter((l) => l.length > 0);
210
- const records = [];
211
- for (const line of lines) {
212
- try {
213
- records.push(JSON.parse(line));
214
- }
215
- catch {
216
- // skip malformed jsonl line — recorder may truncate
217
- }
218
- }
219
- return { records, newPos: fromPos + advance };
220
- }
221
- function attachRecorderAutoArchive(deps, session) {
222
- // Task 131 — recorder discriminator switched from senderId to
223
- // originatingSessionId. The recorder's senderId is now the operator's
224
- // accountId (Sidebar-shape parity); originatingSessionId is the only
225
- // structural marker that this session is a recorder.
226
- if (!session.originatingSessionId)
227
- return;
228
- if (!session.recorderTally) {
229
- deps.logger(`[turn-recorder] auto-archive-attach-skipped recorderSessionId=${session.sessionId} reason=missing-recorder-tally`);
148
+ const AUTO_ARCHIVE_SPECIALIST = 'database-operator';
149
+ function attachSpecialistEndTurnAutoArchive(deps, session) {
150
+ if (session.specialist !== AUTO_ARCHIVE_SPECIALIST)
230
151
  return;
231
- }
232
- const op = session.originatingSessionId;
233
- const rec = session.sessionId;
234
- const tally = session.recorderTally;
235
152
  let archived = false;
236
153
  function endTurnSeen(jsonlPath) {
237
154
  try {
@@ -242,72 +159,7 @@ function attachRecorderAutoArchive(deps, session) {
242
159
  return false;
243
160
  }
244
161
  }
245
- // Drain newly-appended JSONL records, emit one line per tool_use /
246
- // tool_result content block, and update the running tally. The record
247
- // shape follows Claude Code's transcript: top-level `type`, a `message`
248
- // object with `content[]`, each block carrying `type: 'tool_use' |
249
- // 'tool_result'`. Tool results carry `is_error: boolean` on the block;
250
- // when absent we treat the call as `ok`.
251
- function drainJsonl(jsonlPath) {
252
- const startedAtMs = new Map();
253
- const { records, newPos } = readNewRecords(jsonlPath, session.recorderJsonlPosition ?? 0);
254
- session.recorderJsonlPosition = newPos;
255
- for (const rec0 of records) {
256
- if (!rec0 || typeof rec0 !== 'object')
257
- continue;
258
- const top = rec0;
259
- const ts = typeof top.timestamp === 'string' ? Date.parse(top.timestamp) : Date.now();
260
- const message = top.message;
261
- if (!message || typeof message !== 'object')
262
- continue;
263
- const content = message.content;
264
- if (!Array.isArray(content))
265
- continue;
266
- for (const block of content) {
267
- if (!block || typeof block !== 'object')
268
- continue;
269
- const b = block;
270
- if (b.type === 'tool_use') {
271
- tally.calls += 1;
272
- const name = typeof b.name === 'string' ? b.name : 'unknown';
273
- const id = typeof b.id === 'string' ? b.id : '';
274
- if (id)
275
- startedAtMs.set(id, ts);
276
- deps.logger(`[turn-recorder] tool-call sessionId=${op} recorderSessionId=${rec} tool=${name} argHash=${argHash(b.input)}`);
277
- }
278
- else if (b.type === 'tool_result') {
279
- const isError = b.is_error === true;
280
- if (isError)
281
- tally.errors += 1;
282
- else
283
- tally.oks += 1;
284
- const useId = typeof b.tool_use_id === 'string' ? b.tool_use_id : '';
285
- const startMs = useId ? startedAtMs.get(useId) : undefined;
286
- const elapsedMs = startMs ? Math.max(0, ts - startMs) : 0;
287
- // Tool name is not on the result block; the call line that
288
- // preceded it already named the tool — leave `tool=unknown`
289
- // here rather than guess. Operators correlate via the
290
- // adjacent argHash + tool_use_id ordering.
291
- let line = `[turn-recorder] tool-result sessionId=${op} recorderSessionId=${rec} tool=unknown outcome=${isError ? 'error' : 'ok'} ms=${elapsedMs}`;
292
- if (isError) {
293
- const errText = typeof b.content === 'string'
294
- ? b.content
295
- : Array.isArray(b.content)
296
- ? b.content
297
- .map((c) => c && typeof c === 'object' && 'text' in c
298
- ? String(c.text ?? '')
299
- : '')
300
- .join(' ')
301
- : '';
302
- line += ` err=${JSON.stringify(sanitizeErr(errText))}`;
303
- }
304
- deps.logger(line);
305
- }
306
- }
307
- }
308
- }
309
162
  function tryArchive(jsonlPath) {
310
- drainJsonl(jsonlPath);
311
163
  if (archived)
312
164
  return;
313
165
  if (!existsSync(jsonlPath))
@@ -315,22 +167,7 @@ function attachRecorderAutoArchive(deps, session) {
315
167
  if (!endTurnSeen(jsonlPath))
316
168
  return;
317
169
  archived = true;
318
- // Spec §52-53 — `write-complete` precedes `auto-archive` on the
319
- // end-turn path. Read the running tally; if zero successful tool
320
- // calls also emit `write-empty` (§65). On `tally.oks === 0` the
321
- // `write-empty` reason is the cheapest derivable one. The richer
322
- // reasons (`tool-surface-missing`, `no-extractable-entities`) are
323
- // emitted earlier on dedicated paths; this is the catch-all.
324
- deps.logger(`[turn-recorder] write-complete sessionId=${op} recorderSessionId=${rec} toolCalls=${tally.calls} toolOks=${tally.oks} toolErrors=${tally.errors}`);
325
- if (tally.oks === 0) {
326
- const reason = tally.calls === 0
327
- ? 'no-extractable-entities'
328
- : tally.errors > 0 && tally.errors === tally.calls
329
- ? 'tool-errors-only'
330
- : 'no-extractable-entities';
331
- deps.logger(`[turn-recorder] write-empty sessionId=${op} recorderSessionId=${rec} reason=${reason}`);
332
- }
333
- deps.logger(`[turn-recorder] auto-archive sessionId=${op} recorderSessionId=${rec} reason=end-turn archivedAt=${new Date().toISOString()}`);
170
+ deps.logger(`auto-archive sessionId=${session.sessionId} specialist=${AUTO_ARCHIVE_SPECIALIST} reason=end-turn archivedAt=${new Date().toISOString()}`);
334
171
  void stopSession({ store: deps.store, killGraceMs: deps.killGraceMs, logger: deps.logger }, session.sessionId);
335
172
  }
336
173
  if (session.jsonlPath)
@@ -384,18 +221,15 @@ export function buildHttpApp(deps) {
384
221
  deps.logger(`[fs-watcher] live-row-jsonl-attached sessionId=${event.sessionId} path=${event.row.jsonlPath}`);
385
222
  attachPublicAudit(deps, live);
386
223
  });
387
- function liveDisplayName(s, sidecarName) {
388
- if (sidecarName && sidecarName.trim())
389
- return sidecarName.trim();
224
+ function liveDisplayName(s) {
390
225
  if (s.jsonlPath && existsSync(s.jsonlPath)) {
391
226
  const enriched = readJsonlSession(deps.spawnCwd, s.sessionId, LIST_TAIL_BYTES, deps.logger);
392
227
  if (enriched?.aiTitle?.trim())
393
228
  return enriched.aiTitle.trim();
394
229
  }
395
- return s.sessionId.slice(0, 8);
230
+ return null;
396
231
  }
397
232
  function toLivePayload(s) {
398
- const sidecar = s.jsonlPath ? readSidecar(s.jsonlPath) : null;
399
233
  const hasJsonl = s.jsonlPath !== null && existsSync(s.jsonlPath);
400
234
  return {
401
235
  sessionId: s.sessionId,
@@ -407,7 +241,7 @@ export function buildHttpApp(deps) {
407
241
  channel: s.channel,
408
242
  jsonlPath: s.jsonlPath,
409
243
  status: 'alive',
410
- displayName: liveDisplayName(s, sidecar?.displayName),
244
+ displayName: liveDisplayName(s),
411
245
  lastMessageAt: null,
412
246
  turnCount: 0,
413
247
  capped: false,
@@ -417,7 +251,7 @@ export function buildHttpApp(deps) {
417
251
  };
418
252
  }
419
253
  function toArchivedPayload(row, senderId) {
420
- let displayName = row.sessionId.slice(0, 8);
254
+ let displayName = null;
421
255
  let lastMessageAt = null;
422
256
  let turnCount = 0;
423
257
  let capped = false;
@@ -471,8 +305,8 @@ export function buildHttpApp(deps) {
471
305
  const specialist = typeof body.specialist === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(body.specialist)
472
306
  ? body.specialist
473
307
  : undefined;
474
- const originatingSessionId = typeof body.originatingSessionId === 'string' && body.originatingSessionId.length > 0
475
- ? body.originatingSessionId
308
+ const model = typeof body.model === 'string' && /^[A-Za-z0-9._-]{1,64}$/.test(body.model)
309
+ ? body.model
476
310
  : undefined;
477
311
  const aboutOwnerShape = body.aboutOwner == null ? 'absent' : (typeof body.aboutOwner === 'object' ? 'object' : typeof body.aboutOwner);
478
312
  const dormantPluginsShape = Array.isArray(body.dormantPlugins)
@@ -512,7 +346,7 @@ export function buildHttpApp(deps) {
512
346
  : body.tunnelUrl === null
513
347
  ? null
514
348
  : undefined;
515
- const result = await spawnClaudeSession({ ...deps, store: deps.store, watcher: deps.watcher, onSessionReady: (s) => { attachPublicAudit(deps, s); attachRecorderAutoArchive(deps, s); }, tunnelUrlOverride }, {
349
+ const result = await spawnClaudeSession({ ...deps, store: deps.store, watcher: deps.watcher, onSessionReady: (s) => { attachPublicAudit(deps, s); attachSpecialistEndTurnAutoArchive(deps, s); }, tunnelUrlOverride }, {
516
350
  senderId,
517
351
  role: role,
518
352
  channel: channel,
@@ -525,7 +359,7 @@ export function buildHttpApp(deps) {
525
359
  specialistDomains,
526
360
  hidden,
527
361
  specialist,
528
- originatingSessionId,
362
+ model,
529
363
  });
530
364
  if (!result.ok) {
531
365
  if ('rejected' in result) {
@@ -535,18 +369,6 @@ export function buildHttpApp(deps) {
535
369
  }
536
370
  return c.json({ error: 'operator-slots-reserved', total: result.rejected.total, reserve: result.rejected.reserve, totalCap: result.rejected.totalCap }, 503);
537
371
  }
538
- // Task 129 — recorder preflight failures return distinct HTTP codes
539
- // so the loopback caller can route the error to the correct named
540
- // log line. The spawn-failure log was already emitted inside the
541
- // preflight before returning.
542
- if (result.reason === 'missing-originating-session-id') {
543
- timed(deps.logger, 'POST', '/spawn', 400, Date.now() - start);
544
- return c.json({ error: 'missing-originating-session-id' }, 400);
545
- }
546
- if (result.reason === 'tool-surface-missing') {
547
- timed(deps.logger, 'POST', '/spawn', 503, Date.now() - start);
548
- return c.json({ error: 'tool-surface-missing', detail: result.stderrTail }, 503);
549
- }
550
372
  timed(deps.logger, 'POST', '/spawn', 500, Date.now() - start);
551
373
  return c.json({ error: 'spawn-failed', reason: result.reason, stderrTail: result.stderrTail }, 500);
552
374
  }
@@ -568,8 +390,8 @@ export function buildHttpApp(deps) {
568
390
  // watcher's `idle | busy | gone`. `agentLabel` is the resolution
569
391
  // `agent ?? cachedAgentSetting`; the UI maps null → "admin",
570
392
  // non-null → the specialist name. `firstPrompt` is the resolved
571
- // displayName (sidecar ai-title 8-char slice) so the row title is
572
- // computed once on the server, not by every browser tab.
393
+ // displayName (tail.aiTitle verbatim, else null Task 146) so the row
394
+ // title is computed once on the server, not by every browser tab.
573
395
  app.get('/events', (c) => {
574
396
  const start = Date.now();
575
397
  const clientIp = c.req.header('x-forwarded-for') ?? 'loopback';
@@ -583,10 +405,6 @@ export function buildHttpApp(deps) {
583
405
  if (enriched)
584
406
  firstPrompt = enriched.displayName;
585
407
  }
586
- if (!firstPrompt) {
587
- const sidecar = row.jsonlPath ? readSidecar(row.jsonlPath) : null;
588
- firstPrompt = sidecar?.displayName ?? row.sessionId.slice(0, 8);
589
- }
590
408
  return {
591
409
  sessionId: row.sessionId,
592
410
  live: row.state === 'live',
@@ -757,7 +575,7 @@ export function buildHttpApp(deps) {
757
575
  : body.tunnelUrl === null
758
576
  ? null
759
577
  : undefined;
760
- const result = await spawnClaudeSession({ ...deps, store: deps.store, watcher: deps.watcher, onSessionReady: (s) => { attachPublicAudit(deps, s); attachRecorderAutoArchive(deps, s); }, tunnelUrlOverride }, {
578
+ const result = await spawnClaudeSession({ ...deps, store: deps.store, watcher: deps.watcher, onSessionReady: (s) => { attachPublicAudit(deps, s); attachSpecialistEndTurnAutoArchive(deps, s); }, tunnelUrlOverride }, {
761
579
  senderId,
762
580
  role: role,
763
581
  channel: channel,
@@ -822,51 +640,6 @@ export function buildHttpApp(deps) {
822
640
  timed(deps.logger, 'GET', `/${id}/meta`, 200, Date.now() - start);
823
641
  return c.json(payload, 200);
824
642
  });
825
- app.patch('/:sessionId', async (c) => {
826
- const start = Date.now();
827
- const id = c.req.param('sessionId');
828
- if (!SESSION_ID_PATTERN.test(id)) {
829
- timed(deps.logger, 'PATCH', `/${id}`, 400, Date.now() - start);
830
- return c.json({ error: 'invalid-session-id' }, 400);
831
- }
832
- let body;
833
- try {
834
- body = await c.req.json();
835
- }
836
- catch {
837
- timed(deps.logger, 'PATCH', `/${id}`, 400, Date.now() - start);
838
- return c.json({ error: 'body-not-json' }, 400);
839
- }
840
- if (typeof body.displayName !== 'string' || body.displayName.length === 0 || body.displayName.length > DISPLAY_NAME_MAX) {
841
- timed(deps.logger, 'PATCH', `/${id}`, 400, Date.now() - start);
842
- return c.json({ error: 'invalid-displayName', detail: { maxLen: DISPLAY_NAME_MAX } }, 400);
843
- }
844
- const row = resolveRow(deps.watcher, id);
845
- if (!row || !row.jsonlPath) {
846
- timed(deps.logger, 'PATCH', `/${id}`, 404, Date.now() - start);
847
- return c.json({ error: 'session-not-found' }, 404);
848
- }
849
- const sidecarPath = sidecarPathFor(row.jsonlPath);
850
- const prior = readSidecar(row.jsonlPath)?.displayName ?? null;
851
- try {
852
- writeFileSync(sidecarPath, JSON.stringify({ displayName: body.displayName }, null, 2) + '\n', 'utf8');
853
- }
854
- catch (err) {
855
- const msg = err instanceof Error ? err.message : String(err);
856
- deps.logger(`rename-failed sessionId=${row.sessionId.slice(0, 8)} message=${msg}`);
857
- timed(deps.logger, 'PATCH', `/${id}`, 500, Date.now() - start);
858
- return c.json({ error: 'sidecar-write-failed', detail: msg }, 500);
859
- }
860
- deps.logger(`rename sessionId=${row.sessionId.slice(0, 8)} from=${prior ?? ''} to=${body.displayName}`);
861
- if (prior === null) {
862
- // First write — sidebar's auto-stamp from `/spawn`. Distinct log so
863
- // an operator can grep this to confirm the brand-stamped label path
864
- // ran, separately from manual renames via the modal. Task 120.
865
- deps.logger(`sidecar-displayname-stamped sessionId=${row.sessionId.slice(0, 8)} displayName=${body.displayName}`);
866
- }
867
- timed(deps.logger, 'PATCH', `/${id}`, 200, Date.now() - start);
868
- return c.json({ ok: true, displayName: body.displayName }, 200);
869
- });
870
643
  app.post('/:sessionId/archive', async (c) => {
871
644
  const start = Date.now();
872
645
  const id = c.req.param('sessionId');
@@ -901,11 +674,8 @@ export function buildHttpApp(deps) {
901
674
  const slugDir = join(claudeStateRoot(), 'projects', slug);
902
675
  const archiveDir = join(slugDir, 'archive');
903
676
  const fileName = `${row.sessionId}.jsonl`;
904
- const sidecarName = `${row.sessionId}.meta.json`;
905
677
  const fromJsonl = mode === 'archive' ? join(slugDir, fileName) : join(archiveDir, fileName);
906
678
  const toJsonl = mode === 'archive' ? join(archiveDir, fileName) : join(slugDir, fileName);
907
- const fromSidecar = mode === 'archive' ? join(slugDir, sidecarName) : join(archiveDir, sidecarName);
908
- const toSidecar = mode === 'archive' ? join(archiveDir, sidecarName) : join(slugDir, sidecarName);
909
679
  if (!existsSync(fromJsonl)) {
910
680
  timed(deps.logger, 'POST', `/${id}/archive`, 404, Date.now() - start);
911
681
  return c.json({ error: 'session-not-found' }, 404);
@@ -914,8 +684,6 @@ export function buildHttpApp(deps) {
914
684
  if (mode === 'archive')
915
685
  mkdirSync(archiveDir, { recursive: true });
916
686
  renameSync(fromJsonl, toJsonl);
917
- if (existsSync(fromSidecar))
918
- renameSync(fromSidecar, toSidecar);
919
687
  }
920
688
  catch (err) {
921
689
  const msg = err instanceof Error ? err.message : String(err);
@@ -964,7 +732,6 @@ export function buildHttpApp(deps) {
964
732
  timed(deps.logger, 'DELETE', `/${id}`, 404, Date.now() - start);
965
733
  return c.json({ error: 'session-not-found' }, 404);
966
734
  }
967
- const sidecarPath = sidecarPathFor(row.jsonlPath);
968
735
  const subdir = join(claudeStateRoot(), 'projects', projectSlugForCwd(deps.spawnCwd), sessionId);
969
736
  let bytes = 0;
970
737
  try {
@@ -976,8 +743,6 @@ export function buildHttpApp(deps) {
976
743
  try {
977
744
  if (existsSync(row.jsonlPath))
978
745
  rmSync(row.jsonlPath);
979
- if (existsSync(sidecarPath))
980
- rmSync(sidecarPath);
981
746
  if (existsSync(subdir))
982
747
  rmSync(subdir, { recursive: true, force: true });
983
748
  }