aicodeman 1.24.7 → 1.26.0

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 (195) hide show
  1. package/dist/agent-case-marker.d.ts +91 -0
  2. package/dist/agent-case-marker.d.ts.map +1 -0
  3. package/dist/agent-case-marker.js +151 -0
  4. package/dist/agent-case-marker.js.map +1 -0
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +17 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config/base-path.d.ts +55 -0
  9. package/dist/config/base-path.d.ts.map +1 -0
  10. package/dist/config/base-path.js +105 -0
  11. package/dist/config/base-path.js.map +1 -0
  12. package/dist/config/cli-registry/schema.d.ts +2 -0
  13. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  14. package/dist/config/cli-registry/schema.js +1 -1
  15. package/dist/config/cli-registry/schema.js.map +1 -1
  16. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  17. package/dist/config/cli-registry/stock.js +4 -1
  18. package/dist/config/cli-registry/stock.js.map +1 -1
  19. package/dist/config/cli-registry/types.d.ts +9 -0
  20. package/dist/config/cli-registry/types.d.ts.map +1 -1
  21. package/dist/daemon-control.d.ts +2 -0
  22. package/dist/daemon-control.d.ts.map +1 -1
  23. package/dist/daemon-control.js +2 -0
  24. package/dist/daemon-control.js.map +1 -1
  25. package/dist/docker-export.d.ts.map +1 -1
  26. package/dist/docker-export.js +8 -1
  27. package/dist/docker-export.js.map +1 -1
  28. package/dist/docker-hosts.d.ts +97 -3
  29. package/dist/docker-hosts.d.ts.map +1 -1
  30. package/dist/docker-hosts.js +260 -4
  31. package/dist/docker-hosts.js.map +1 -1
  32. package/dist/hooks-config.d.ts +25 -0
  33. package/dist/hooks-config.d.ts.map +1 -1
  34. package/dist/hooks-config.js +106 -6
  35. package/dist/hooks-config.js.map +1 -1
  36. package/dist/session.d.ts +31 -1
  37. package/dist/session.d.ts.map +1 -1
  38. package/dist/session.js +101 -5
  39. package/dist/session.js.map +1 -1
  40. package/dist/tmux-manager.d.ts.map +1 -1
  41. package/dist/tmux-manager.js +54 -8
  42. package/dist/tmux-manager.js.map +1 -1
  43. package/dist/types/api.d.ts +51 -1
  44. package/dist/types/api.d.ts.map +1 -1
  45. package/dist/types/api.js.map +1 -1
  46. package/dist/types/session.d.ts +49 -0
  47. package/dist/types/session.d.ts.map +1 -1
  48. package/dist/types/session.js.map +1 -1
  49. package/dist/web/middleware/auth.d.ts +3 -3
  50. package/dist/web/middleware/auth.d.ts.map +1 -1
  51. package/dist/web/middleware/auth.js +13 -11
  52. package/dist/web/middleware/auth.js.map +1 -1
  53. package/dist/web/public/admin-ui.js.gz +0 -0
  54. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  55. package/dist/web/public/app.3a89ac57.js +38 -0
  56. package/dist/web/public/app.3a89ac57.js.br +0 -0
  57. package/dist/web/public/app.3a89ac57.js.gz +0 -0
  58. package/dist/web/public/approvals-ui.js.gz +0 -0
  59. package/dist/web/public/{constants.7bd482dc.js → constants.7a635e34.js} +58 -0
  60. package/dist/web/public/constants.7a635e34.js.br +0 -0
  61. package/dist/web/public/constants.7a635e34.js.gz +0 -0
  62. package/dist/web/public/cron-ui.js.gz +0 -0
  63. package/dist/web/public/entrance-animations.js.gz +0 -0
  64. package/dist/web/public/home-sessions.js.gz +0 -0
  65. package/dist/web/public/i18n.9db8c4e0.js +1 -0
  66. package/dist/web/public/i18n.9db8c4e0.js.br +0 -0
  67. package/dist/web/public/i18n.9db8c4e0.js.gz +0 -0
  68. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  69. package/dist/web/public/index.html +46 -17
  70. package/dist/web/public/index.html.br +0 -0
  71. package/dist/web/public/index.html.gz +0 -0
  72. package/dist/web/public/input-cjk.87f14251.js.gz +0 -0
  73. package/dist/web/public/{keyboard-accessory.0454a3e9.js → keyboard-accessory.daf18302.js} +8 -4
  74. package/dist/web/public/keyboard-accessory.daf18302.js.br +0 -0
  75. package/dist/web/public/keyboard-accessory.daf18302.js.gz +0 -0
  76. package/dist/web/public/manifest.json +2 -1
  77. package/dist/web/public/mobile-handlers.0f83a608.js.gz +0 -0
  78. package/dist/web/public/mobile-overview.js.gz +0 -0
  79. package/dist/web/public/{mobile.87674154.css → mobile.7112f27d.css} +1 -1
  80. package/dist/web/public/{mobile.87674154.css.br → mobile.7112f27d.css.br} +0 -0
  81. package/dist/web/public/mobile.7112f27d.css.gz +0 -0
  82. package/dist/web/public/{notification-manager.5d229063.js → notification-manager.36ea4624.js} +1 -1
  83. package/dist/web/public/notification-manager.36ea4624.js.br +0 -0
  84. package/dist/web/public/notification-manager.36ea4624.js.gz +0 -0
  85. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  86. package/dist/web/public/{panels-ui.e6e06034.js → panels-ui.db4785bf.js} +8 -8
  87. package/dist/web/public/panels-ui.db4785bf.js.br +0 -0
  88. package/dist/web/public/panels-ui.db4785bf.js.gz +0 -0
  89. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  90. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  91. package/dist/web/public/readmymind-ui.js.gz +0 -0
  92. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  93. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  94. package/dist/web/public/session-lineage.js.gz +0 -0
  95. package/dist/web/public/session-ui.a9c126c6.js +77 -0
  96. package/dist/web/public/session-ui.a9c126c6.js.br +0 -0
  97. package/dist/web/public/session-ui.a9c126c6.js.gz +0 -0
  98. package/dist/web/public/{settings-ui.1ed208dd.js → settings-ui.554f9cdd.js} +1 -1
  99. package/dist/web/public/settings-ui.554f9cdd.js.br +0 -0
  100. package/dist/web/public/settings-ui.554f9cdd.js.gz +0 -0
  101. package/dist/web/public/styles.298f81f2.css +1 -0
  102. package/dist/web/public/styles.298f81f2.css.br +0 -0
  103. package/dist/web/public/{styles.421b36d4.css.gz → styles.298f81f2.css.gz} +0 -0
  104. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  105. package/dist/web/public/sw.js +13 -6
  106. package/dist/web/public/sw.js.br +0 -0
  107. package/dist/web/public/sw.js.gz +0 -0
  108. package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
  109. package/dist/web/public/terminal-ui.70445f81.js.gz +0 -0
  110. package/dist/web/public/ultracode-panel.js.gz +0 -0
  111. package/dist/web/public/ultracode-windows.js.gz +0 -0
  112. package/dist/web/public/upload.html.gz +0 -0
  113. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  114. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  115. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  116. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  117. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  118. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  119. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  120. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  121. package/dist/web/public/vendor/xterm.css.gz +0 -0
  122. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  123. package/dist/web/public/{voice-input.4ec670b0.js → voice-input.c4b51eb6.js} +1 -1
  124. package/dist/web/public/voice-input.c4b51eb6.js.br +0 -0
  125. package/dist/web/public/voice-input.c4b51eb6.js.gz +0 -0
  126. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  127. package/dist/web/public/webview-tabs.js +3 -1
  128. package/dist/web/public/webview-tabs.js.br +0 -0
  129. package/dist/web/public/webview-tabs.js.gz +0 -0
  130. package/dist/web/route-helpers.d.ts +18 -0
  131. package/dist/web/route-helpers.d.ts.map +1 -1
  132. package/dist/web/route-helpers.js +28 -0
  133. package/dist/web/route-helpers.js.map +1 -1
  134. package/dist/web/routes/case-routes.d.ts.map +1 -1
  135. package/dist/web/routes/case-routes.js +261 -6
  136. package/dist/web/routes/case-routes.js.map +1 -1
  137. package/dist/web/routes/file-routes.d.ts.map +1 -1
  138. package/dist/web/routes/file-routes.js +62 -4
  139. package/dist/web/routes/file-routes.js.map +1 -1
  140. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  141. package/dist/web/routes/hook-event-routes.js +29 -3
  142. package/dist/web/routes/hook-event-routes.js.map +1 -1
  143. package/dist/web/routes/session-routes.d.ts.map +1 -1
  144. package/dist/web/routes/session-routes.js +139 -59
  145. package/dist/web/routes/session-routes.js.map +1 -1
  146. package/dist/web/routes/webview-routes.d.ts +2 -2
  147. package/dist/web/routes/webview-routes.d.ts.map +1 -1
  148. package/dist/web/routes/webview-routes.js +23 -21
  149. package/dist/web/routes/webview-routes.js.map +1 -1
  150. package/dist/web/schemas.d.ts +30 -0
  151. package/dist/web/schemas.d.ts.map +1 -1
  152. package/dist/web/schemas.js +78 -0
  153. package/dist/web/schemas.js.map +1 -1
  154. package/dist/web/server.d.ts +4 -2
  155. package/dist/web/server.d.ts.map +1 -1
  156. package/dist/web/server.js +75 -12
  157. package/dist/web/server.js.map +1 -1
  158. package/dist/web/sse-events.d.ts +4 -2
  159. package/dist/web/sse-events.d.ts.map +1 -1
  160. package/dist/web/sse-events.js +4 -2
  161. package/dist/web/sse-events.js.map +1 -1
  162. package/dist/web/webview-proxy.d.ts +16 -7
  163. package/dist/web/webview-proxy.d.ts.map +1 -1
  164. package/dist/web/webview-proxy.js +28 -15
  165. package/dist/web/webview-proxy.js.map +1 -1
  166. package/package.json +8 -7
  167. package/skills/codeman/SKILL.md +14 -10
  168. package/skills/codeman/preamble.sh +6 -3
  169. package/skills/codeman/reference/endpoints.md +10 -0
  170. package/skills/codeman/reference/recipes.md +1 -1
  171. package/skills/codeman/reference/verbs.md +26 -1
  172. package/dist/web/public/app.a5d2fc08.js +0 -38
  173. package/dist/web/public/app.a5d2fc08.js.br +0 -0
  174. package/dist/web/public/app.a5d2fc08.js.gz +0 -0
  175. package/dist/web/public/constants.7bd482dc.js.br +0 -0
  176. package/dist/web/public/constants.7bd482dc.js.gz +0 -0
  177. package/dist/web/public/i18n.9ae15b51.js +0 -1
  178. package/dist/web/public/i18n.9ae15b51.js.br +0 -0
  179. package/dist/web/public/i18n.9ae15b51.js.gz +0 -0
  180. package/dist/web/public/keyboard-accessory.0454a3e9.js.br +0 -0
  181. package/dist/web/public/keyboard-accessory.0454a3e9.js.gz +0 -0
  182. package/dist/web/public/mobile.87674154.css.gz +0 -0
  183. package/dist/web/public/notification-manager.5d229063.js.br +0 -0
  184. package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
  185. package/dist/web/public/panels-ui.e6e06034.js.br +0 -0
  186. package/dist/web/public/panels-ui.e6e06034.js.gz +0 -0
  187. package/dist/web/public/session-ui.c558f981.js +0 -68
  188. package/dist/web/public/session-ui.c558f981.js.br +0 -0
  189. package/dist/web/public/session-ui.c558f981.js.gz +0 -0
  190. package/dist/web/public/settings-ui.1ed208dd.js.br +0 -0
  191. package/dist/web/public/settings-ui.1ed208dd.js.gz +0 -0
  192. package/dist/web/public/styles.421b36d4.css +0 -1
  193. package/dist/web/public/styles.421b36d4.css.br +0 -0
  194. package/dist/web/public/voice-input.4ec670b0.js.br +0 -0
  195. package/dist/web/public/voice-input.4ec670b0.js.gz +0 -0
@@ -19,7 +19,8 @@ import { ownerLayoutKey } from '../../tab-layout-persistence.js';
19
19
  import { TabLayoutValidationError } from '../../tab-layout.js';
20
20
  import { sessionWaits, resolveWaitSignals, sessionHookOptions, signalForStatus, WaitCapacityError, } from '../session-wait-registry.js';
21
21
  import { clampWaitMs, MAX_BUFFER_SCAN_BYTES } from '../../config/agent-wait.js';
22
- import { autoConfigureRalph, canAccessOwned, CASES_DIR, findPersistedSessionOrFail, findSessionOrFail, getAuthUser, isAdmin, isWorkingDirAllowed, ownerFor, parseBody, persistAndBroadcastSession, resolveCasesDir, resolveParentSessionId, sessionCapacityMessage, SETTINGS_PATH, validatePathWithinBase, } from '../route-helpers.js';
22
+ import { autoConfigureRalph, canAccessOwned, CASES_DIR, findPersistedSessionOrFail, findSessionOrFail, getAuthUser, isAdmin, isWorkingDirAllowed, ownerFor, parseBody, persistAndBroadcastSession, resolveAgentCaseOrigin, resolveCasesDir, resolveParentSessionId, sessionCapacityMessage, SETTINGS_PATH, validatePathWithinBase, } from '../route-helpers.js';
23
+ import { buildAgentCaseMarker, writeAgentCaseMarker } from '../../agent-case-marker.js';
23
24
  import { canUsernameRunPrivilegedCommands, resolveClaudeModeForUsername } from '../../user-store.js';
24
25
  import { enabledClis, getCli } from '../../config/cli-registry/registry.js';
25
26
  import { resolveCliLaunchError } from '../../utils/cli-launcher.js';
@@ -38,7 +39,7 @@ import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal
38
39
  import { MAX_PASTE_IMAGE_BYTES } from '../../config/buffer-limits.js';
39
40
  import { dataPath, getDataDir } from '../../config/instance.js';
40
41
  import { checkRemoteTmuxAvailable, readRemoteCases, readRemoteHosts, toAttachedSessionRemote, toSessionRemote, } from '../../remote-hosts.js';
41
- import { checkDockerAvailable, checkDockerConfigDrift, checkDockerTmuxAvailable, ensureAgentBaseImage, DEFAULT_AGENT_IMAGE, persistDockerCaseClaudeSessionId, readDockerCases, readDockerHosts, toSessionDocker, } from '../../docker-hosts.js';
42
+ import { checkDockerAvailable, checkDockerConfigDrift, probeAdoptableContainer, checkDockerTmuxAvailable, ensureAgentBaseImage, DEFAULT_AGENT_IMAGE, persistDockerCaseClaudeSessionId, readDockerCases, readDockerHosts, toSessionDocker, } from '../../docker-hosts.js';
42
43
  import { LRUMap } from '../../utils/lru-map.js';
43
44
  import { findLatestOmpSessionId } from '../../utils/omp-session-resolver.js';
44
45
  import { scanOmpSessionsHistory } from '../../omp-transcript.js';
@@ -1552,9 +1553,18 @@ export function registerSessionRoutes(app, ctx) {
1552
1553
  // turn behind beats a viewer showing someone else's conversation.
1553
1554
  const claudeHistoryPinCache = new LRUMap({ maxSize: 1024 });
1554
1555
  async function resolveActiveClaudeSessionIdFromHistory(session, projectsDir) {
1556
+ // A pane whose conversation id came from its OWN hook needs no correlation:
1557
+ // $CODEMAN_SESSION_ID (the pane's env) -> data.session_id (the CLI's own
1558
+ // stdin JSON) is a first-hand binding that never looks at cwd, so it cannot
1559
+ // be stolen by a sibling pane, a closed tab, or a bare `claude` in a
1560
+ // terminal. Guessing can only be worse than the fact. This is also what
1561
+ // closes the hole below for a pane driven straight from tmux: it never
1562
+ // reaches `if (!submitAt)`.
1563
+ if (session.claudeSessionIdIsFirstHand)
1564
+ return null;
1555
1565
  const submitAt = session.lastSubmitAt;
1556
1566
  if (!submitAt)
1557
- return null; // never typed through Codeman — nothing to credit
1567
+ return null; // no anchor at all — nothing to credit
1558
1568
  const cached = claudeHistoryPinCache.get(session.id);
1559
1569
  if (cached && cached.submitAt === submitAt)
1560
1570
  return cached.claudeSessionId;
@@ -1652,17 +1662,40 @@ export function registerSessionRoutes(app, ctx) {
1652
1662
  return /^(?:<local-command|<command-name>|<task-notification>|<system-reminder>|<teammate-message\b|Another Claude session sent a message:|Base directory for this skill:)/i.test(text);
1653
1663
  }
1654
1664
  /**
1655
- * Claude writes one logical turn as many JSONL rows: text, thinking and tool
1656
- * blocks share message ids, while tool results are represented as user rows.
1657
- * Build viewer cards from real user boundaries instead of treating every row
1658
- * as a separate chat message.
1665
+ * Claude writes an append-only event log: tool results arrive as user rows,
1666
+ * thinking/tool_use rows carry no text, and a prompt typed while the agent is
1667
+ * working is only ever recorded as an `attachment/queued_command` row. But one
1668
+ * assistant row IS one whole model message: measured across ~/.claude/projects
1669
+ * (CLI 2.1.220-2.1.251) no assistant row carries more than one content block
1670
+ * and no message id carries more than one text block, so there is nothing to
1671
+ * reassemble. Emit one card per row and group them with `turn` instead of
1672
+ * concatenating a human turn's replies into a single card (#169), which fused
1673
+ * up to 74 distinct model messages into one card. Splitting is safe for
1674
+ * markdown: no adjacent pair of assistant text rows in the corpus continues a
1675
+ * table, a list, or an open code fence.
1659
1676
  */
1660
1677
  function parseClaudeResponseTranscript(content, full) {
1661
1678
  let lastText = '';
1662
1679
  let lastTimestamp = '';
1663
1680
  const messages = [];
1664
- let currentUserFragments = new Set();
1665
- let currentAssistantFragments = new Set();
1681
+ // #169's replay guards, kept: they now SKIP a duplicated row instead of
1682
+ // concatenating it into the previous card.
1683
+ const currentUserFragments = new Set();
1684
+ const currentAssistantFragments = new Set();
1685
+ // Turn 0 is reserved for anything emitted before the first human prompt.
1686
+ let turn = 0;
1687
+ const pushUserMessage = (text, timestamp, queued) => {
1688
+ // A run of consecutive human inputs (a mid-turn queued burst) is ONE turn,
1689
+ // so the viewer renders it under one badge instead of one badge per line.
1690
+ if (messages.at(-1)?.role !== 'user')
1691
+ turn += 1;
1692
+ const message = { kind: 'prompt', label: 'Prompt', role: 'user', text, timestamp, turn };
1693
+ if (queued)
1694
+ message.queued = true;
1695
+ messages.push(message);
1696
+ currentUserFragments.add(text);
1697
+ currentAssistantFragments.clear();
1698
+ };
1666
1699
  for (const line of content.split('\n')) {
1667
1700
  if (!line)
1668
1701
  continue;
@@ -1677,6 +1710,30 @@ export function registerSessionRoutes(app, ctx) {
1677
1710
  // rows include repeated image dimensions and other UI-generated context.
1678
1711
  if (entry.isSidechain)
1679
1712
  continue;
1713
+ // A prompt typed while Claude is working is absorbed mid-turn and lives
1714
+ // ONLY in an attachment row, so it was lost outright. `origin.kind` and
1715
+ // `commandMode` separate the human's queue entries from the CLI's own:
1716
+ // measured over 57 real transcripts on 2026-09-01, 322 queued_command rows
1717
+ // split 163 `prompt`/`human` and 159 `task-notification`, and not one of
1718
+ // those 159 carries an `origin` key. The 163 human rows become 162 user
1719
+ // cards here — one is a verbatim repeat inside a still-unanswered user run
1720
+ // and is collapsed by the dedup guard below — out of 353 user cards total.
1721
+ if (entry.type === 'attachment') {
1722
+ if (!full)
1723
+ continue;
1724
+ const queued = entry.attachment;
1725
+ if (!queued || queued.type !== 'queued_command')
1726
+ continue;
1727
+ if (queued.origin?.kind !== 'human' || queued.commandMode !== 'prompt')
1728
+ continue;
1729
+ const text = typeof queued.prompt === 'string' ? queued.prompt.trim() : '';
1730
+ if (!text || isClaudeSyntheticUserMessage(entry, text))
1731
+ continue;
1732
+ if (currentUserFragments.has(text))
1733
+ continue;
1734
+ pushUserMessage(text, queued.timestamp || entry.timestamp, true);
1735
+ continue;
1736
+ }
1680
1737
  if (entry.type === 'user') {
1681
1738
  const text = extractClaudeText(entry.message?.content, '\n').trim();
1682
1739
  // A tool_result block has no text block and naturally drops out here.
@@ -1684,21 +1741,13 @@ export function registerSessionRoutes(app, ctx) {
1684
1741
  continue;
1685
1742
  if (!full)
1686
1743
  continue;
1687
- const previous = messages.at(-1);
1688
- if (previous?.role === 'user') {
1689
- // Claude can replay the initial user row while restoring a transcript.
1690
- // Only collapse duplicates within the same unanswered user turn; the
1691
- // same prompt after an assistant response remains a legitimate turn.
1692
- if (currentUserFragments.has(text))
1693
- continue;
1694
- previous.text += `\n\n${text}`;
1695
- currentUserFragments.add(text);
1696
- }
1697
- else {
1698
- messages.push({ role: 'user', text, timestamp: entry.timestamp });
1699
- currentUserFragments = new Set([text]);
1700
- }
1701
- currentAssistantFragments.clear();
1744
+ // Claude replays the initial user row while restoring a transcript, and
1745
+ // a CLI that also wrote an absorbed prompt as a user row would double it.
1746
+ // Both collapse here. The same prompt sent again AFTER a reply is a
1747
+ // legitimate second turn, because that reply cleared the set.
1748
+ if (currentUserFragments.has(text))
1749
+ continue;
1750
+ pushUserMessage(text, entry.timestamp, false);
1702
1751
  continue;
1703
1752
  }
1704
1753
  if (entry.type !== 'assistant')
@@ -1710,20 +1759,11 @@ export function registerSessionRoutes(app, ctx) {
1710
1759
  lastTimestamp = entry.timestamp || '';
1711
1760
  if (!full)
1712
1761
  continue;
1713
- const previous = messages.at(-1);
1714
- if (previous?.role === 'assistant') {
1715
- // Replayed snapshots sometimes repeat an identical text block. Distinct
1716
- // progress/final blocks are kept, but remain inside one Claude card.
1717
- if (currentAssistantFragments.has(text))
1718
- continue;
1719
- previous.text += `\n\n${text}`;
1720
- previous.timestamp = entry.timestamp || previous.timestamp;
1721
- currentAssistantFragments.add(text);
1722
- }
1723
- else {
1724
- messages.push({ role: 'assistant', text, timestamp: entry.timestamp });
1725
- currentAssistantFragments = new Set([text]);
1726
- }
1762
+ // Replayed snapshots repeat an identical text block inside one turn.
1763
+ if (currentAssistantFragments.has(text))
1764
+ continue;
1765
+ messages.push({ kind: 'response', label: 'Response', role: 'assistant', text, timestamp: entry.timestamp, turn });
1766
+ currentAssistantFragments.add(text);
1727
1767
  currentUserFragments.clear();
1728
1768
  }
1729
1769
  return full ? { text: lastText, timestamp: lastTimestamp, messages } : { text: lastText, timestamp: lastTimestamp };
@@ -2527,7 +2567,10 @@ export function registerSessionRoutes(app, ctx) {
2527
2567
  const capMsg = sessionCapacityMessage(ctx.sessions, owner);
2528
2568
  if (capMsg)
2529
2569
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, capMsg);
2530
- const { caseName = 'testcase', sessionName, mode = 'claude', modelOverride, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, deepSeekConfig, ompConfig, envOverrides, effort, parentSessionId, } = parseBody(QuickStartSchema, req.body);
2570
+ const { caseName = 'testcase', sessionName, mode = 'claude', modelOverride, openCodeConfig, codexConfig, geminiConfig, antigravityConfig, piConfig, grokConfig, deepSeekConfig, ompConfig, envOverrides, effort, parentSessionId, agentOrigin, } = parseBody(QuickStartSchema, req.body);
2571
+ // Resolved ONCE here: the same value labels a case directory this request creates
2572
+ // (agent-case-marker.ts) and draws the tab lineage line on the session below.
2573
+ const qsParentSessionId = resolveParentSessionId(ctx, req, parentSessionId, owner);
2531
2574
  // Multi-user: shell mode is arbitrary host-account execution, gated by the grant.
2532
2575
  // Resolve the owner's grant from the store so a GRANTED regular user is not wrongly denied.
2533
2576
  if (getCli(mode)?.capabilities.privilegedCommandGate && !(await canUsernameRunPrivilegedCommands(owner))) {
@@ -2606,25 +2649,46 @@ export function registerSessionRoutes(app, ctx) {
2606
2649
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, availability.error || 'docker daemon is not available');
2607
2650
  }
2608
2651
  const sessionDocker = toSessionDocker(host, dockerCase);
2609
- // Ensure the base image exists, auto-building the default image on first use so
2610
- // it is never a blocker. Dedup'd with any build kicked off at case-create, so
2611
- // this awaits the SAME in-flight build rather than starting a second one.
2612
- const ensured = await ensureAgentBaseImage(sessionDocker, sessionDocker.image, {
2613
- onProgress: (line) => ctx.broadcast(SseEvent.DockerImageBuildProgress, { name: dockerCase.name, line }),
2614
- });
2615
- if (!ensured.ok) {
2616
- return createErrorResponse(ApiErrorCode.OPERATION_FAILED, ensured.error || 'base image not available');
2617
- }
2618
- if (ensured.built) {
2619
- ctx.broadcast(SseEvent.DockerImageBuildComplete, { name: dockerCase.name, image: sessionDocker.image });
2620
- }
2621
- // tmux is a hard prerequisite (the in-container tmux makes reconnect durable).
2622
- // Skip the extra container-run probe for our OWN default image (the baked
2623
- // Dockerfile always contains tmux); still verify a custom image.
2624
- if (sessionDocker.image !== DEFAULT_AGENT_IMAGE) {
2625
- const tmuxCheck = await checkDockerTmuxAvailable(sessionDocker);
2626
- if (!tmuxCheck.ok) {
2627
- return createErrorResponse(ApiErrorCode.OPERATION_FAILED, tmuxCheck.error || 'base image is missing tmux');
2652
+ // An ADOPTED container skips every image-side gate: we never run `docker
2653
+ // create`, so the image is the user's business, and `ensureAgentBaseImage`
2654
+ // would build/require an image that has nothing to do with their container.
2655
+ // The prerequisite that DOES still hold is tmux inside it, so probe the live
2656
+ // container (not the image) and refuse before launch rather than dead-paning.
2657
+ if (sessionDocker.owned === false) {
2658
+ const probe = await probeAdoptableContainer(sessionDocker, [mode]);
2659
+ if (!probe.ok) {
2660
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, probe.error || 'container is not usable');
2661
+ }
2662
+ // The probe already exec'd into the container; carry its facts onto the
2663
+ // live session so the launch chain does not have to re-ask.
2664
+ sessionDocker.runsAsRoot = probe.runsAsRoot;
2665
+ // No `mode !== 'shell'` arm: a mode with no binary of its own is reported
2666
+ // available by the probe unconditionally, so this reads the same answer for it.
2667
+ if (!probe.availableModes?.includes(mode)) {
2668
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `"${mode}" is not installed in container "${sessionDocker.containerName}". Adoption never modifies the container — install it inside, or pick another mode.`);
2669
+ }
2670
+ }
2671
+ else {
2672
+ // Ensure the base image exists, auto-building the default image on first use so
2673
+ // it is never a blocker. Dedup'd with any build kicked off at case-create, so
2674
+ // this awaits the SAME in-flight build rather than starting a second one.
2675
+ const ensured = await ensureAgentBaseImage(sessionDocker, sessionDocker.image, {
2676
+ onProgress: (line) => ctx.broadcast(SseEvent.DockerImageBuildProgress, { name: dockerCase.name, line }),
2677
+ });
2678
+ if (!ensured.ok) {
2679
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, ensured.error || 'base image not available');
2680
+ }
2681
+ if (ensured.built) {
2682
+ ctx.broadcast(SseEvent.DockerImageBuildComplete, { name: dockerCase.name, image: sessionDocker.image });
2683
+ }
2684
+ // tmux is a hard prerequisite (the in-container tmux makes reconnect durable).
2685
+ // Skip the extra container-run probe for our OWN default image (the baked
2686
+ // Dockerfile always contains tmux); still verify a custom image.
2687
+ if (sessionDocker.image !== DEFAULT_AGENT_IMAGE) {
2688
+ const tmuxCheck = await checkDockerTmuxAvailable(sessionDocker);
2689
+ if (!tmuxCheck.ok) {
2690
+ return createErrorResponse(ApiErrorCode.OPERATION_FAILED, tmuxCheck.error || 'base image is missing tmux');
2691
+ }
2628
2692
  }
2629
2693
  }
2630
2694
  // Config drift (docs/docker-cases-plan.md §4): the desired create-config no
@@ -2716,6 +2780,22 @@ export function registerSessionRoutes(app, ctx) {
2716
2780
  mode !== 'omp') {
2717
2781
  await writeHooksConfig(resolvedCasePath);
2718
2782
  }
2783
+ // Label a directory an AGENT asked us to create, so the scratch workspaces a
2784
+ // long orchestration leaves behind can be told apart from the user's real
2785
+ // projects later (see agent-case-marker.ts). This is the only branch that may
2786
+ // write it: it is the only one that creates the directory, and a pre-existing
2787
+ // case must never be labelled. Best-effort — a failed marker must not fail the
2788
+ // spawn it decorates.
2789
+ const qsAgentOrigin = resolveAgentCaseOrigin(req, agentOrigin, qsParentSessionId);
2790
+ if (qsAgentOrigin) {
2791
+ await writeAgentCaseMarker(resolvedCasePath, buildAgentCaseMarker({
2792
+ createdBy: qsAgentOrigin,
2793
+ parentSessionId: qsParentSessionId,
2794
+ parentSessionName: qsParentSessionId ? ctx.sessions.get(qsParentSessionId)?.name : undefined,
2795
+ mode,
2796
+ owner,
2797
+ }));
2798
+ }
2719
2799
  ctx.broadcast(SseEvent.CaseCreated, { name: caseName, path: resolvedCasePath });
2720
2800
  }
2721
2801
  catch (err) {
@@ -2833,7 +2913,7 @@ export function registerSessionRoutes(app, ctx) {
2833
2913
  docker,
2834
2914
  resumeSessionId: dockerResumeId,
2835
2915
  tmuxHistoryLimit: qsTerminalHistoryConfig.tmuxHistoryLimit,
2836
- parentSessionId: resolveParentSessionId(ctx, req, parentSessionId, owner),
2916
+ parentSessionId: qsParentSessionId,
2837
2917
  });
2838
2918
  // Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
2839
2919
  // so the initial state already has the phrase configured (only if globally enabled)