commandmate 0.30.1 → 0.30.2

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 (190) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +141 -141
  3. package/.next/app-path-routes-manifest.json +29 -29
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/api/agents/update/route_client-reference-manifest.js +1 -1
  9. package/.next/server/app/api/agents/versions/route_client-reference-manifest.js +1 -1
  10. package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
  11. package/.next/server/app/api/app/update-check/route.js +1 -1
  12. package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/capabilities/route.js +1 -1
  24. package/.next/server/app/api/capabilities/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/hooks/agent-event/route.js +1 -1
  33. package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/hooks/claude-done/route.js +1 -1
  35. package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/hooks/permission-request/route.js +1 -1
  37. package/.next/server/app/api/hooks/permission-request/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/push/escalation/route_client-reference-manifest.js +1 -1
  41. package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/remote/pair/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
  49. package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
  52. package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
  55. package/.next/server/app/api/settings/default-agents/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/settings/default-surface-mode/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
  58. package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
  61. package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
  64. package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  66. package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
  67. package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
  70. package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
  73. package/.next/server/app/api/worktrees/[id]/clear-composer/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/worktrees/[id]/cli-reference/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
  76. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
  78. package/.next/server/app/api/worktrees/[id]/env/route_client-reference-manifest.js +1 -1
  79. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
  82. package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
  84. package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
  85. package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
  87. package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
  88. package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
  90. package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
  91. package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
  94. package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
  96. package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
  97. package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
  99. package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
  100. package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
  102. package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
  103. package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
  105. package/.next/server/app/api/worktrees/[id]/instances/opencode/route_client-reference-manifest.js +1 -1
  106. package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
  108. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
  109. package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
  111. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
  112. package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
  113. package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
  114. package/.next/server/app/api/worktrees/[id]/opencode/diff/route_client-reference-manifest.js +1 -1
  115. package/.next/server/app/api/worktrees/[id]/opencode/session/route_client-reference-manifest.js +1 -1
  116. package/.next/server/app/api/worktrees/[id]/opencode/share/route_client-reference-manifest.js +1 -1
  117. package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
  118. package/.next/server/app/api/worktrees/[id]/resolve-target/route_client-reference-manifest.js +1 -1
  119. package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
  120. package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
  121. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
  122. package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
  123. package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
  124. package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
  125. package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
  126. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
  127. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
  128. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
  129. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
  130. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
  131. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
  132. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
  133. package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
  134. package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
  135. package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
  136. package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
  137. package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
  138. package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
  139. package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
  140. package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  141. package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
  142. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
  143. package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
  144. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
  145. package/.next/server/app/api/worktrees/[id]/verify/config/route_client-reference-manifest.js +1 -1
  146. package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
  147. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/cancel/route_client-reference-manifest.js +1 -1
  148. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
  149. package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
  150. package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
  151. package/.next/server/app/api/worktrees/apply-default-agents/route_client-reference-manifest.js +1 -1
  152. package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
  153. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  154. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  155. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  156. package/.next/server/app/more/page_client-reference-manifest.js +1 -1
  157. package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  158. package/.next/server/app/page_client-reference-manifest.js +1 -1
  159. package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
  160. package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
  161. package/.next/server/app/review/page_client-reference-manifest.js +1 -1
  162. package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  163. package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
  164. package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
  165. package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  166. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  167. package/.next/server/app/worktrees/[id]/page.js +1 -1
  168. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  169. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  170. package/.next/server/app-paths-manifest.json +29 -29
  171. package/.next/server/chunks/3099.js +3 -3
  172. package/.next/server/chunks/5043.js +1 -1
  173. package/.next/server/chunks/6130.js +1 -1
  174. package/.next/server/chunks/6894.js +1 -1
  175. package/.next/server/chunks/9696.js +2 -2
  176. package/.next/server/middleware-manifest.json +5 -5
  177. package/.next/server/pages/500.html +1 -1
  178. package/.next/server/pages-manifest.json +1 -1
  179. package/.next/server/server-reference-manifest.json +1 -1
  180. package/.next/static/chunks/{6381-dbf04a99647f6018.js → 6381-147dde2a967e9458.js} +1 -1
  181. package/.next/static/chunks/app/worktrees/[id]/{page-aac399392ec8c273.js → page-2f73d69187936627.js} +1 -1
  182. package/.next/trace +13 -13
  183. package/dist/server/src/lib/hooks/sources/antigravity/history.js +24 -5
  184. package/dist/server/src/lib/hooks/sources/antigravity/transcript.js +39 -0
  185. package/dist/server/src/lib/hooks/sources/claude/history.js +152 -4
  186. package/dist/server/src/lib/hooks/sources/claude/transcript.js +175 -13
  187. package/dist/server/src/lib/hooks/stop-history-capture.js +58 -25
  188. package/package.json +1 -1
  189. /package/.next/static/{pcpB4K6KreMzu2qbhvuRY → QXxQbXWoo7ldUyOcv7PCZ}/_buildManifest.js +0 -0
  190. /package/.next/static/{pcpB4K6KreMzu2qbhvuRY → QXxQbXWoo7ldUyOcv7PCZ}/_ssgManifest.js +0 -0
@@ -532,12 +532,31 @@ function resolveAssistantTimestampMs(turn, userRow) {
532
532
  */
533
533
  async function writeAntigravityTurn(target, turn, rendered, timestampMs, path) {
534
534
  const instanceId = target.instanceId ?? target.cliToolId;
535
+ if (!(0, transcript_1.isAntigravityTurnWritable)(turn)) {
536
+ // agy has not finished this answer and no later prompt has taken over, so
537
+ // what is in the file is a turn in progress. Writing it would put a reply
538
+ // with its last paragraph missing into History permanently — the row is
539
+ // keyed on `(conversationId, step_index)`, so every later read finds it and
540
+ // answers "already saved". That is Issue #2264, reported against claude and
541
+ // structurally identical here: a turn cut off after its `tool_calls` renders
542
+ // a *non-empty* body, so the emptiness guard below cannot see it.
543
+ logger.info('antigravity-transcript-turn-open', {
544
+ worktreeId: target.worktreeId,
545
+ instanceId,
546
+ conversationId: rendered.conversationId,
547
+ stepIndex: rendered.stepIndex,
548
+ records: turn.records.length,
549
+ textBlocks: rendered.textBlocks,
550
+ toolBlocks: rendered.toolBlocks,
551
+ });
552
+ return false;
553
+ }
535
554
  if (rendered.body.length === 0) {
536
- // A turn that said nothing yet. Unlike codex there is no `task_complete` to
537
- // tell an unfinished turn from a silent one, so the body is the only
538
- // evidence there is — and answering false is right for both readings: the
539
- // scraper keeps the reply if there was one, and an empty row that would show
540
- // as a blank answer forever is never written.
555
+ // A turn that said nothing. agy has no `task_complete`, so a closed turn
556
+ // that rendered to nothing and an interrupted one look alike here and
557
+ // answering false is right for both readings: the scraper keeps the reply if
558
+ // there was one, and an empty row that would show as a blank answer forever
559
+ // is never written.
541
560
  logger.info('antigravity-transcript-turn-empty', {
542
561
  worktreeId: target.worktreeId,
543
562
  instanceId,
@@ -72,6 +72,8 @@ exports.readAntigravityTranscriptRecord = readAntigravityTranscriptRecord;
72
72
  exports.parseAntigravityTranscript = parseAntigravityTranscript;
73
73
  exports.extractAntigravityUserRequest = extractAntigravityUserRequest;
74
74
  exports.buildAntigravityTurns = buildAntigravityTurns;
75
+ exports.isAntigravityTurnClosingRecord = isAntigravityTurnClosingRecord;
76
+ exports.isAntigravityTurnWritable = isAntigravityTurnWritable;
75
77
  exports.isAntigravityPromptRecord = isAntigravityPromptRecord;
76
78
  exports.renderAntigravityTurn = renderAntigravityTurn;
77
79
  const event_mapper_1 = require("../event-mapper");
@@ -298,6 +300,8 @@ function buildAntigravityTurns(records, conversationId) {
298
300
  ? { stepIndex: record.stepIndex, text, timestampMs: record.timestampMs ?? startedAt }
299
301
  : null,
300
302
  records: [],
303
+ closed: false,
304
+ superseded: false,
301
305
  overflowed: false,
302
306
  });
303
307
  continue;
@@ -307,14 +311,49 @@ function buildAntigravityTurns(records, conversationId) {
307
311
  preludeRecords += 1;
308
312
  continue;
309
313
  }
314
+ // Assigned rather than or-ed, so what survives is the last `MODEL` record's
315
+ // answer. `SYSTEM` records are ignored here for the same reason they render
316
+ // to nothing: agy injected them, they are not agy speaking.
317
+ if (record.source === exports.ANTIGRAVITY_MODEL_SOURCE) {
318
+ turn.closed = isAntigravityTurnClosingRecord(record);
319
+ }
310
320
  if (turn.records.length >= exports.MAX_ANTIGRAVITY_TURN_RECORDS) {
311
321
  turn.overflowed = true;
312
322
  continue;
313
323
  }
314
324
  turn.records.push(record);
315
325
  }
326
+ // Every turn but the last has a later prompt behind it, and a prompt is proof
327
+ // agy moved on. See {@link AntigravityTurnAccumulator.superseded}.
328
+ for (let index = 0; index < turns.length - 1; index += 1) {
329
+ turns[index].superseded = true;
330
+ }
316
331
  return { turns, preludeRecords };
317
332
  }
333
+ /**
334
+ * Whether this `MODEL` record is agy finishing its answer (Issue #2264).
335
+ *
336
+ * See {@link AntigravityTurnAccumulator.closed} for the argument; the emptiness
337
+ * test is the one {@link renderAntigravityTurn} applies to `content`, so a
338
+ * record that renders to nothing cannot be what makes a turn look finished.
339
+ */
340
+ function isAntigravityTurnClosingRecord(record) {
341
+ if (record.source !== exports.ANTIGRAVITY_MODEL_SOURCE)
342
+ return false;
343
+ if (record.type !== exports.ANTIGRAVITY_PLANNER_TYPE)
344
+ return false;
345
+ if (record.toolCalls.length > 0)
346
+ return false;
347
+ return (record.content ?? '').trim().length > 0;
348
+ }
349
+ /**
350
+ * Whether a writer may record this turn (Issue #2264).
351
+ *
352
+ * Either proof that nothing more is coming — agy answered, or it moved on.
353
+ */
354
+ function isAntigravityTurnWritable(turn) {
355
+ return turn.closed || turn.superseded;
356
+ }
318
357
  /** Whether this record is the one the operator's own text arrived on. */
319
358
  function isAntigravityPromptRecord(record) {
320
359
  return record.source === exports.ANTIGRAVITY_USER_SOURCE && record.type === exports.ANTIGRAVITY_USER_TYPE;
@@ -90,7 +90,7 @@ var __importStar = (this && this.__importStar) || (function () {
90
90
  };
91
91
  })();
92
92
  Object.defineProperty(exports, "__esModule", { value: true });
93
- exports.CLAUDE_TRANSCRIPT_TAIL_BYTES = void 0;
93
+ exports.CLAUDE_TURN_RECHECK_LIMIT = exports.CLAUDE_TRANSCRIPT_TAIL_BYTES = void 0;
94
94
  exports.resetClaudeTranscriptSessions = resetClaudeTranscriptSessions;
95
95
  exports.resolveClaudeSessionId = resolveClaudeSessionId;
96
96
  exports.claudeProjectsRoot = claudeProjectsRoot;
@@ -324,6 +324,12 @@ async function captureClaudeTranscriptTurn(target, capture) {
324
324
  });
325
325
  }
326
326
  const pending = await selectUnwrittenClaudeTurns(target, built.turns);
327
+ // Before anything is written: the rows that are already there (#2264). This
328
+ // is deliberately ahead of the early return below, because "the newest turn
329
+ // is already a row" is the state the nine short rows the Issue measured were
330
+ // stuck in — the reader answered true and did nothing while the transcript
331
+ // beside it held the missing paragraph.
332
+ await refreshClaudeTurnRows(target, built.turns.slice(0, built.turns.length - pending.turns.length).slice(-exports.CLAUDE_TURN_RECHECK_LIMIT), path);
327
333
  if (pending.turns.length === 0) {
328
334
  // The anchor is the newest turn in the window, so there is nothing to
329
335
  // write and the newest turn is a row. True, for the reason
@@ -357,7 +363,7 @@ async function captureClaudeTranscriptTurn(target, capture) {
357
363
  const turn = pending.turns[index];
358
364
  const previousStartedAt = index === 0 ? pending.previousStartedAt : pending.turns[index - 1].startedAt;
359
365
  const userRow = await recordClaudeUserTurn(target, turn, previousStartedAt);
360
- captured = await writeClaudeTurn(target, (0, transcript_1.renderClaudeTurn)(turn), resolveAssistantTimestampMs(turn, userRow), path);
366
+ captured = await writeClaudeTurn(target, turn, (0, transcript_1.renderClaudeTurn)(turn), resolveAssistantTimestampMs(turn, userRow), path);
361
367
  }
362
368
  return captured;
363
369
  }
@@ -493,6 +499,11 @@ function collectHeadlessClaudeTurn(records, sessionId) {
493
499
  promptIsOperatorInput: false,
494
500
  blocks,
495
501
  assistantRecords,
502
+ // Never handed to a writer, so neither flag can be read off it; false is the
503
+ // value that would refuse the write if one were ever attempted.
504
+ closed: false,
505
+ superseded: false,
506
+ stopReasonObserved: false,
496
507
  overflowed,
497
508
  };
498
509
  }
@@ -717,6 +728,113 @@ async function readClaudeTranscriptTail(path) {
717
728
  await handle?.close().catch(() => { });
718
729
  }
719
730
  }
731
+ /**
732
+ * How many already-written turns are re-rendered and compared (Issue #2264).
733
+ *
734
+ * The repair half of #2264 has to look at rows the reader has **already**
735
+ * written, which is the one thing the anchor rule deliberately does not do — so
736
+ * it is bounded here rather than by the window. Three, because the poller runs
737
+ * every two seconds and re-rendering the whole 4 MiB window on every tick would
738
+ * turn a repair into the most expensive thing the poll does, while a row that is
739
+ * going to grow grows within a second of being written.
740
+ *
741
+ * The cost of the bound is only paid by rows the old code left short: those are
742
+ * repaired on the next read that still has the turn among its newest three, and
743
+ * a session that has moved four turns past one of them keeps it. The Issue's own
744
+ * nine rows were all the newest turn at the time, so all nine are inside it.
745
+ */
746
+ exports.CLAUDE_TURN_RECHECK_LIMIT = 3;
747
+ /**
748
+ * Replace a saved row whose body has since grown (Issue #2264).
749
+ *
750
+ * The second half of the fix, and the half that repairs what the first half
751
+ * only stops happening again. A row keyed `claude-turn:<uuid>` is written once
752
+ * and every later read answers "already saved" — so the nine short rows the
753
+ * Issue measured were frozen, and the scrape that could have replaced them was
754
+ * suppressed two seconds later by that same idempotency answer.
755
+ *
756
+ * **Strictly longer, never merely different.** Equality is the ordinary case and
757
+ * must cost nothing, and a body that got *shorter* between two reads is not a
758
+ * turn that grew — it is a window that slid, or a truncation marker, and
759
+ * overwriting a full reply with a shorter one is the one outcome worse than the
760
+ * bug. Longer implies different, so one comparison covers both halves of the
761
+ * Issue's "differs and is longer".
762
+ *
763
+ * `message_updated`, never `message`: the row already existed and was already
764
+ * delivered when it was created (#2195), so a client that appended instead of
765
+ * replacing would show the reply twice.
766
+ *
767
+ * @param existing - The row `findMessageByRequestId` answered with
768
+ * @returns Whether the row was replaced
769
+ */
770
+ async function growClaudeTurnRow(target, existing, rendered, path) {
771
+ const instanceId = target.instanceId ?? target.cliToolId;
772
+ const previousLength = existing.content.length;
773
+ if (rendered.body.length <= previousLength)
774
+ return false;
775
+ const [{ getDbInstance }, { updateMessageContent }, { broadcastMessage }] = await Promise.all([
776
+ Promise.resolve().then(() => __importStar(require('../../../../lib/db/db-instance'))),
777
+ Promise.resolve().then(() => __importStar(require('../../../../lib/db'))),
778
+ Promise.resolve().then(() => __importStar(require('../../../../lib/ws-server'))),
779
+ ]);
780
+ updateMessageContent(getDbInstance(), existing.id, rendered.body);
781
+ broadcastMessage('message_updated', {
782
+ worktreeId: target.worktreeId,
783
+ message: { ...existing, content: rendered.body },
784
+ });
785
+ logger.info('claude-transcript-turn-updated', {
786
+ worktreeId: target.worktreeId,
787
+ instanceId,
788
+ sessionId: rendered.sessionId,
789
+ requestId: existing.requestId,
790
+ path,
791
+ previousLength,
792
+ bodyLength: rendered.body.length,
793
+ textBlocks: rendered.textBlocks,
794
+ toolBlocks: rendered.toolBlocks,
795
+ });
796
+ return true;
797
+ }
798
+ /**
799
+ * Re-read the newest already-written turns and grow the short ones (#2264).
800
+ *
801
+ * Runs before the pending turns are written and independently of whether there
802
+ * are any — the case it exists for is precisely the one
803
+ * {@link captureClaudeTranscriptTurn} used to return `true` from without doing
804
+ * anything: the newest turn already has a row, and that row is missing its last
805
+ * paragraph.
806
+ *
807
+ * Turns that are still open are skipped rather than compared. Their body is by
808
+ * definition not the final one, and a repair that raced the agent would rewrite
809
+ * the row on every poll of a long turn.
810
+ *
811
+ * The database is asked before the turn is rendered, so a candidate with no row
812
+ * — every candidate, in a session this reader has never written to — costs one
813
+ * indexed lookup and no Markdown.
814
+ *
815
+ * @param candidates - Already-written turns, oldest first, at most {@link CLAUDE_TURN_RECHECK_LIMIT}
816
+ * @returns How many rows were replaced
817
+ */
818
+ async function refreshClaudeTurnRows(target, candidates, path) {
819
+ if (candidates.length === 0)
820
+ return 0;
821
+ const [{ getDbInstance }, { findMessageByRequestId }] = await Promise.all([
822
+ Promise.resolve().then(() => __importStar(require('../../../../lib/db/db-instance'))),
823
+ Promise.resolve().then(() => __importStar(require('../../../../lib/db'))),
824
+ ]);
825
+ const db = getDbInstance();
826
+ let updated = 0;
827
+ for (const turn of candidates) {
828
+ if (!(0, transcript_1.isClaudeTurnWritable)(turn))
829
+ continue;
830
+ const existing = findMessageByRequestId(db, target.worktreeId, (0, agent_transcript_1.claudeTurnRequestId)(turn.promptUuid));
831
+ if (!existing)
832
+ continue;
833
+ if (await growClaudeTurnRow(target, existing, (0, transcript_1.renderClaudeTurn)(turn), path))
834
+ updated += 1;
835
+ }
836
+ return updated;
837
+ }
720
838
  /**
721
839
  * Write one rendered turn, unless it is already there.
722
840
  *
@@ -731,8 +849,33 @@ async function readClaudeTranscriptTail(path) {
731
849
  *
732
850
  * @returns Whether History holds this turn as the agent's own Markdown
733
851
  */
734
- async function writeClaudeTurn(target, rendered, timestampMs, path) {
852
+ async function writeClaudeTurn(target, turn, rendered, timestampMs, path) {
735
853
  const instanceId = target.instanceId ?? target.cliToolId;
854
+ if (!(0, transcript_1.isClaudeTurnWritable)(turn)) {
855
+ // The agent has not said `end_turn` for this prompt and no later prompt has
856
+ // taken over, so what is in the file is a turn in progress. Writing it would
857
+ // put a reply with its last paragraph missing into History **permanently** —
858
+ // the row is keyed on the prompt's `uuid`, so every later read finds it and
859
+ // answers "already saved". That is Issue #2264 exactly: 9 of `claude-2`'s 20
860
+ // turns on 2026-09-03 were saved as a bare `> **Tool calls (1)**` with not
861
+ // one character of prose, because the Stop hook beat the last append and the
862
+ // emptiness guard below cannot see the difference — a turn cut off after its
863
+ // tool calls renders a *non-empty* body.
864
+ //
865
+ // Handing it back to the scraper costs the Markdown rendering for this turn
866
+ // and nothing else: the Stop receiver asks again after a short delay, and
867
+ // the poller asks again when the pane returns to the composer.
868
+ logger.info('claude-transcript-turn-open', {
869
+ worktreeId: target.worktreeId,
870
+ instanceId,
871
+ sessionId: rendered.sessionId,
872
+ promptUuid: rendered.promptUuid,
873
+ assistantRecords: turn.assistantRecords,
874
+ textBlocks: rendered.textBlocks,
875
+ toolBlocks: rendered.toolBlocks,
876
+ });
877
+ return false;
878
+ }
736
879
  if (rendered.body.length === 0) {
737
880
  // The turn is open but the agent has not written anything to the file yet —
738
881
  // the prompt record is there and the assistant records are not. Answering
@@ -762,12 +905,17 @@ async function writeClaudeTurn(target, rendered, timestampMs, path) {
762
905
  Promise.resolve().then(() => __importStar(require('../../../../lib/ws-server'))),
763
906
  ]);
764
907
  const db = getDbInstance();
765
- if (findMessageByRequestId(db, target.worktreeId, requestId)) {
908
+ const existing = findMessageByRequestId(db, target.worktreeId, requestId);
909
+ if (existing) {
766
910
  logger.debug('claude-transcript-turn-already-saved', {
767
911
  worktreeId: target.worktreeId,
768
912
  instanceId,
769
913
  requestId,
770
914
  });
915
+ // The row may still be one of the short ones #2264 was reported for, and
916
+ // this is the one place that knows both the row and the turn. See
917
+ // {@link growClaudeTurnRow}.
918
+ await growClaudeTurnRow(target, existing, rendered, path);
771
919
  return true;
772
920
  }
773
921
  const message = createMessage(db, {
@@ -36,9 +36,12 @@
36
36
  * is therefore keyed on the *prompt* record, never on a `requestId`.
37
37
  * 3. **`type: "user"` is mostly not the user.** Of 19 user records in the
38
38
  * sampled session, 18 were `tool_result` payloads. Tool results, `isMeta`
39
- * placeholders (`[Image: original 1440x2170…]`), and the slash-command
39
+ * placeholders (`[Image: original 1440x2170…]`), and the built-in command
40
40
  * bookkeeping records (`<command-name>`, `<local-command-stdout>`) all have
41
- * `type: "user"` and none of them is a prompt. See {@link isClaudePromptRecord}.
41
+ * `type: "user"` and none of them is a prompt. The one `<command-…>` shape
42
+ * that *is* a prompt — a project command, recorded `<command-message>` first
43
+ * — is Issue #2265; see {@link CLAUDE_SLASH_COMMAND_PREFIX} and
44
+ * {@link isClaudePromptRecord}.
42
45
  * 4. **`thinking` blocks can be empty.** The block arrives with a `signature`
43
46
  * and `thinking: ""` when the text is not retained, so an empty one is
44
47
  * skipped rather than rendered as a blank quote.
@@ -53,13 +56,16 @@
53
56
  * @module lib/hooks/sources/claude/transcript
54
57
  */
55
58
  Object.defineProperty(exports, "__esModule", { value: true });
56
- exports.CLAUDE_HUMAN_PROMPT_SOURCES = exports.CLAUDE_HUMAN_ORIGIN_KIND = exports.CLAUDE_THINKING_LABEL = exports.MAX_CLAUDE_TOOL_DETAIL_LENGTH = exports.MAX_CLAUDE_TURN_BLOCKS = exports.CLAUDE_TURN_TRUNCATION_MARKER = exports.MAX_CLAUDE_TURN_BODY_LENGTH = exports.CLAUDE_PROJECTS_DIR_SEGMENTS = void 0;
59
+ exports.CLAUDE_END_TURN_STOP_REASON = exports.CLAUDE_HUMAN_PROMPT_SOURCES = exports.CLAUDE_HUMAN_ORIGIN_KIND = exports.CLAUDE_SLASH_COMMAND_PREFIX = exports.CLAUDE_THINKING_LABEL = exports.MAX_CLAUDE_TOOL_DETAIL_LENGTH = exports.MAX_CLAUDE_TURN_BLOCKS = exports.CLAUDE_TURN_TRUNCATION_MARKER = exports.MAX_CLAUDE_TURN_BODY_LENGTH = exports.CLAUDE_PROJECTS_DIR_SEGMENTS = void 0;
57
60
  exports.claudeProjectSlug = claudeProjectSlug;
61
+ exports.claudeSlashCommandPrompt = claudeSlashCommandPrompt;
58
62
  exports.readClaudeContentBlock = readClaudeContentBlock;
59
63
  exports.readClaudeTranscriptRecord = readClaudeTranscriptRecord;
60
64
  exports.parseClaudeTranscript = parseClaudeTranscript;
61
65
  exports.isClaudePromptRecord = isClaudePromptRecord;
62
66
  exports.isClaudeOperatorPromptRecord = isClaudeOperatorPromptRecord;
67
+ exports.isClaudeTurnClosingRecord = isClaudeTurnClosingRecord;
68
+ exports.isClaudeTurnWritable = isClaudeTurnWritable;
63
69
  exports.createClaudeTurn = createClaudeTurn;
64
70
  exports.buildClaudeTurns = buildClaudeTurns;
65
71
  exports.renderClaudeTurn = renderClaudeTurn;
@@ -111,18 +117,97 @@ exports.CLAUDE_THINKING_LABEL = 'Thinking';
111
117
  /**
112
118
  * Prefixes that mark a `type: "user"` record as bookkeeping rather than a prompt.
113
119
  *
114
- * All four were observed on real transcripts. `<local-command-caveat>` and the
115
- * `<command-…>` trio are how a slash command is recorded; `<local-command-stdout>`
116
- * is its output. Treating any of them as a prompt would open a turn that the
117
- * operator never typed, and the reply to the *next* real prompt would be filed
118
- * under it.
120
+ * Three since Issue #2265, which took `<command-message>` off the list — see
121
+ * {@link CLAUDE_SLASH_COMMAND_PREFIX} for the census that separated the two
122
+ * shapes the `<command-…>` trio is written in. What is left is the bookkeeping
123
+ * of a **built-in** command: `<command-name>` first is how Claude records
124
+ * `/model`, `/compact`, `/clear`, `/login` and `/plan`, `<local-command-stdout>`
125
+ * is that command's output, and `<local-command-caveat>` is the preamble in
126
+ * front of it. Treating any of them as a prompt would open a turn that the agent
127
+ * never answers, and the reply to the *next* real prompt would be filed under it.
119
128
  */
120
129
  const CLAUDE_SYNTHETIC_PROMPT_PREFIXES = [
121
130
  '<local-command-caveat>',
122
131
  '<local-command-stdout>',
123
132
  '<command-name>',
124
- '<command-message>',
125
133
  ];
134
+ /**
135
+ * The tag a slash command the operator typed opens with (Issue #2265).
136
+ *
137
+ * ## The two shapes, and why the leading tag is the discriminator
138
+ *
139
+ * Claude writes the same three tags for two quite different events, and it
140
+ * writes them in a different **order** for each. Measured over every
141
+ * `type: "user"` record under `~/.claude/projects` on 2026-09-03 whose text
142
+ * opens with one of the `<command-…>` tags — 222 records:
143
+ *
144
+ * | leading tag | count | `<command-name>` values |
145
+ * |---|---|---|
146
+ * | `<command-message>` | 118 | `/orchestrate`, `/release`, `/uat`, `/worktree-cleanup`, `/worktree-new`, `/multi-stage-design-review`, `/cmate-*` |
147
+ * | `<command-name>` | 104 | `/model`, `/compact`, `/clear`, `/login`, `/plan` |
148
+ *
149
+ * The split is exact and it is the split that matters here. The 118 are
150
+ * *project and user commands* — a `.claude/commands/*.md` or a skill, expanded
151
+ * into instructions the agent then works through, which is a turn with a reply
152
+ * in it. The 104 are commands Claude Code executes itself; their whole output is
153
+ * the `<local-command-stdout>` record that follows and the agent never answers
154
+ * them.
155
+ *
156
+ * Before #2265 both were excluded, on the reading that a slash command is not
157
+ * "the operator's text". That cost the 118 their entire turn: the reply was
158
+ * folded into whichever turn came before — already saved, so
159
+ * `selectUnwrittenClaudeTurns` answered "nothing to write", and the answer *this*
160
+ * reader gives is also what suppresses the scraper's copy. The measured loss was
161
+ * a `/release v0.30.1` turn of 7 text blocks and an `/orchestrate` turn of 8,
162
+ * neither of which reached History by any path at all.
163
+ *
164
+ * ## `origin`, and the versions that carry it
165
+ *
166
+ * The 118 also answer {@link isClaudeOperatorPromptRecord}, but only on recent
167
+ * Claude Code: of them, every record from 2.1.238 onwards (46) carries
168
+ * `origin: {"kind": "human"}` and every record before it (72, versions 2.1.220
169
+ * … 2.1.235) carries neither marker — those predate the field. On an old
170
+ * version the turn is still opened and its reply still written; only the `user`
171
+ * row is skipped, which is #2196's stated degradation and not a new one.
172
+ */
173
+ exports.CLAUDE_SLASH_COMMAND_PREFIX = '<command-message>';
174
+ /** `<command-name>/release</command-name>` — the command itself. */
175
+ const CLAUDE_COMMAND_NAME_PATTERN = /<command-name>([\s\S]*?)<\/command-name>/;
176
+ /** `<command-args>v0.30.1</command-args>` — absent on a bare invocation. */
177
+ const CLAUDE_COMMAND_ARGS_PATTERN = /<command-args>([\s\S]*?)<\/command-args>/;
178
+ /**
179
+ * The line the operator typed, rebuilt from a slash-command record (Issue #2265).
180
+ *
181
+ * `<command-name>` and `<command-args>` back into `/release v0.30.1`, which is
182
+ * the string that was in the composer and — for a command CommandMate sent — the
183
+ * string `sendUserMessage` already wrote to `chat_messages`. That equality is
184
+ * what makes the `/send` row *adoptable* instead of duplicated: `recordUserTurn`
185
+ * compares on normalised content, and the raw record text
186
+ * (`<command-message>release</command-message>\n<command-name>…`) matches nothing
187
+ * a person ever sent.
188
+ *
189
+ * Returns null for anything that is not this shape, including a
190
+ * `<command-message>` record with no `<command-name>` in it. Null is what keeps
191
+ * {@link isClaudePromptRecord} from opening a turn it could not name: a record
192
+ * whose command cannot be read would otherwise reach History as its own XML,
193
+ * which is exactly the class of mistake #2196's positive-evidence rule exists to
194
+ * avoid. No such record was observed — all 118 carried both tags.
195
+ *
196
+ * @param text - The record's text, as {@link ClaudeTranscriptRecord.text} holds it
197
+ * @returns `/name args`, `/name` when there are no arguments, or null
198
+ */
199
+ function claudeSlashCommandPrompt(text) {
200
+ const trimmed = text.trim();
201
+ if (!trimmed.startsWith(exports.CLAUDE_SLASH_COMMAND_PREFIX))
202
+ return null;
203
+ const name = CLAUDE_COMMAND_NAME_PATTERN.exec(trimmed)?.[1].trim() ?? '';
204
+ if (name.length === 0)
205
+ return null;
206
+ // Absent on a bare invocation, and empty on a command invoked with no
207
+ // arguments; both mean the prompt is the command name on its own.
208
+ const args = CLAUDE_COMMAND_ARGS_PATTERN.exec(trimmed)?.[1].trim() ?? '';
209
+ return args.length === 0 ? name : `${name} ${args}`;
210
+ }
126
211
  /**
127
212
  * Read one `message.content` element.
128
213
  *
@@ -235,6 +320,7 @@ function readClaudeTranscriptRecord(value) {
235
320
  isCompactSummary: value.isCompactSummary === true,
236
321
  isInterruption: (0, event_mapper_1.readStringField)(value, 'interruptedMessageId') !== null,
237
322
  timestampMs: Number.isFinite(parsed) ? parsed : null,
323
+ stopReason: message ? (0, event_mapper_1.readStringField)(message, 'stop_reason') : null,
238
324
  blocks,
239
325
  text,
240
326
  };
@@ -279,11 +365,19 @@ function parseClaudeTranscript(text) {
279
365
  *
280
366
  * - it carries a `tool_result` block — 18 of the 19 user records in the sampled
281
367
  * session were these;
282
- * - `isMeta` is true — the image placeholders and the local-command caveat;
283
- * - its text opens with one of {@link CLAUDE_SYNTHETIC_PROMPT_PREFIXES} the
284
- * slash-command bookkeeping;
368
+ * - `isMeta` is true — the image placeholders, the local-command caveat, and
369
+ * the body of the `.claude/commands/*.md` or skill a slash command expands
370
+ * to, which arrives on its own record right behind the one below;
371
+ * - its text opens with one of {@link CLAUDE_SYNTHETIC_PROMPT_PREFIXES} — a
372
+ * built-in command and its output;
285
373
  * - it has no text at all, or no `uuid` to name the turn with.
286
374
  *
375
+ * A record opening with {@link CLAUDE_SLASH_COMMAND_PREFIX} is the fifth case
376
+ * and the one Issue #2265 changed: it **is** a prompt, because the operator
377
+ * typed `/release v0.30.1` and the agent answers it at length. It qualifies only
378
+ * when {@link claudeSlashCommandPrompt} can actually read the command out of it,
379
+ * so a shape this reader could not name never opens a turn.
380
+ *
287
381
  * Everything else is treated as a prompt, including `[Request interrupted by
288
382
  * user]`. That is deliberate: an interruption really does end the turn, and the
289
383
  * records after it really do belong to whatever came next.
@@ -300,6 +394,8 @@ function isClaudePromptRecord(record) {
300
394
  const text = record.text.trim();
301
395
  if (text.length === 0)
302
396
  return false;
397
+ if (text.startsWith(exports.CLAUDE_SLASH_COMMAND_PREFIX))
398
+ return claudeSlashCommandPrompt(text) !== null;
303
399
  return !CLAUDE_SYNTHETIC_PROMPT_PREFIXES.some((prefix) => text.startsWith(prefix));
304
400
  }
305
401
  /**
@@ -345,6 +441,14 @@ exports.CLAUDE_HUMAN_PROMPT_SOURCES = ['typed', 'queued'];
345
441
  * is the pre-#2196 behaviour — an orphaned assistant pair, which is untidy, and
346
442
  * not a fabricated user message, which is wrong.
347
443
  *
444
+ * Issue #2265 widened what reaches here by one shape — the project-command
445
+ * record `isClaudePromptRecord` now accepts — and the rule holds on it without
446
+ * change: of the 118 such records under `~/.claude/projects` on 2026-09-03, the
447
+ * 46 written by Claude Code 2.1.238 and later carry `origin: {"kind": "human"}`
448
+ * and answer true, and the 72 written before the field existed carry no marker
449
+ * and answer false. The second group loses its `user` row and keeps its reply,
450
+ * which is this function's designed degradation rather than a new gap.
451
+ *
348
452
  * `isCompactSummary` and `isInterruption` are then checked explicitly even
349
453
  * though nothing observed carries a human marker *and* either flag. They are not
350
454
  * redundant by construction: compaction and interruption are both things the
@@ -362,16 +466,62 @@ function isClaudeOperatorPromptRecord(record) {
362
466
  return record.originKind === exports.CLAUDE_HUMAN_ORIGIN_KIND;
363
467
  return record.promptSource !== null && exports.CLAUDE_HUMAN_PROMPT_SOURCES.includes(record.promptSource);
364
468
  }
469
+ /**
470
+ * `message.stop_reason` on the record that ends a reply (Issue #2264).
471
+ *
472
+ * The other value this reader cares about is `tool_use`, which is not named as
473
+ * a constant because nothing branches on it: everything that is not `end_turn`
474
+ * leaves the turn open.
475
+ */
476
+ exports.CLAUDE_END_TURN_STOP_REASON = 'end_turn';
477
+ /**
478
+ * Whether this assistant record is the one that ended the reply (Issue #2264).
479
+ *
480
+ * `end_turn` **and** prose on the same record. See
481
+ * {@link ClaudeTurnAccumulator.closed} for why neither half alone is the rule,
482
+ * and note that the emptiness test here is the one `renderClaudeTurn` applies to
483
+ * a text block — a block whose `text` trims to nothing is not rendered, so it
484
+ * cannot be what makes a turn look finished either.
485
+ */
486
+ function isClaudeTurnClosingRecord(record) {
487
+ if (record.type !== 'assistant')
488
+ return false;
489
+ if (record.stopReason !== exports.CLAUDE_END_TURN_STOP_REASON)
490
+ return false;
491
+ return record.blocks.some((block) => block.type === 'text' && (block.text ?? '').trim().length > 0);
492
+ }
493
+ /**
494
+ * Whether a writer may record this turn (Issue #2264).
495
+ *
496
+ * Either proof that nothing more is coming: the agent said so
497
+ * ({@link ClaudeTurnAccumulator.closed}), or it has moved on to another prompt
498
+ * ({@link ClaudeTurnAccumulator.superseded}).
499
+ */
500
+ function isClaudeTurnWritable(turn) {
501
+ if (turn.closed || turn.superseded)
502
+ return true;
503
+ // No record of this turn said why it stopped, so there is no evidence to
504
+ // refuse it on. See {@link ClaudeTurnAccumulator.stopReasonObserved}: this is
505
+ // unreachable against every Claude version measured, and it is what keeps a
506
+ // version that stops writing the field from silently disabling the reader.
507
+ return !turn.stopReasonObserved;
508
+ }
365
509
  /** A brand-new accumulator for one turn. */
366
510
  function createClaudeTurn(record, sessionId) {
367
511
  return {
368
512
  sessionId: record.sessionId ?? sessionId,
369
513
  promptUuid: record.uuid,
370
514
  startedAt: record.timestampMs ?? 0,
371
- promptText: record.text,
515
+ // A slash command's record is XML and the operator typed a line; #2265 puts
516
+ // the line on the turn so that the `user` row reads `/release v0.30.1` and
517
+ // the `/send` row holding those same bytes is adopted rather than doubled.
518
+ promptText: claudeSlashCommandPrompt(record.text) ?? record.text,
372
519
  promptIsOperatorInput: isClaudeOperatorPromptRecord(record),
373
520
  blocks: [],
374
521
  assistantRecords: 0,
522
+ closed: false,
523
+ superseded: false,
524
+ stopReasonObserved: false,
375
525
  overflowed: false,
376
526
  };
377
527
  }
@@ -416,6 +566,13 @@ function buildClaudeTurns(records, sessionId) {
416
566
  continue;
417
567
  }
418
568
  current.assistantRecords += 1;
569
+ // Assigned rather than or-ed, so the value left standing is the *last*
570
+ // assistant record's answer. A turn that ends on a `tool_use` record after
571
+ // an earlier `end_turn` — which is what a turn Claude resumed looks like —
572
+ // is open again, and that is the reading the Issue's census supports.
573
+ current.closed = isClaudeTurnClosingRecord(record);
574
+ if (record.stopReason !== null)
575
+ current.stopReasonObserved = true;
419
576
  for (const block of record.blocks) {
420
577
  if (current.blocks.length >= exports.MAX_CLAUDE_TURN_BLOCKS) {
421
578
  current.overflowed = true;
@@ -424,6 +581,11 @@ function buildClaudeTurns(records, sessionId) {
424
581
  current.blocks.push(block);
425
582
  }
426
583
  }
584
+ // Every turn but the last one has a later prompt behind it, and a prompt is
585
+ // proof the agent moved on. See {@link ClaudeTurnAccumulator.superseded}.
586
+ for (let index = 0; index < turns.length - 1; index += 1) {
587
+ turns[index].superseded = true;
588
+ }
427
589
  return { turns, orphanedAssistantRecords, sidechainRecords };
428
590
  }
429
591
  /**