mixdog 1.4.0 → 1.4.1

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 (236) hide show
  1. package/package.json +1 -1
  2. package/scripts/check-voice-runtime-inputs.mjs +25 -0
  3. package/scripts/gate-local.mjs +12 -3
  4. package/scripts/latest-guard.mjs +21 -9
  5. package/scripts/lib/isolated-test-env.mjs +10 -5
  6. package/scripts/lib/run-node-tests.mjs +37 -37
  7. package/scripts/lib/scoped-temp.d.mts +16 -0
  8. package/scripts/lib/scoped-temp.mjs +123 -0
  9. package/scripts/lib/stale-temp.mjs +44 -0
  10. package/scripts/post-release-check.mjs +1 -6
  11. package/scripts/pptx-kit-index.mjs +17 -4
  12. package/scripts/suite-health.mjs +10 -1
  13. package/scripts/test-data-dir.mjs +10 -7
  14. package/src/app.mjs +4 -0
  15. package/src/defaults/shipped-history.json +5 -0
  16. package/src/defaults/skills/browser-use/SKILL.md +6 -4
  17. package/src/defaults/skills/computer-use/SKILL.md +4 -2
  18. package/src/lib/rules-builder.cjs +29 -12
  19. package/src/output-styles/common.md +2 -2
  20. package/src/output-styles/simple.md +1 -1
  21. package/src/rules/agent/AGENT.md +5 -4
  22. package/src/rules/lead/LEAD.md +10 -5
  23. package/src/rules/shared/10-tool-workflow.md +3 -1
  24. package/src/rules/shared/45-destructive.md +0 -2
  25. package/src/rules/shared/50-execution.md +2 -0
  26. package/src/rules/shared/70-delivery.md +7 -9
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
  28. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
  36. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
  37. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
  47. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
  49. package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
  50. package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
  51. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
  62. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
  63. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
  64. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
  65. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
  66. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
  68. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
  76. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
  77. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
  78. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  79. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
  80. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
  81. package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
  82. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
  83. package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
  84. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
  85. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
  86. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
  87. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
  88. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
  89. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
  90. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
  92. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
  93. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
  97. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
  99. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
  102. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
  103. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
  104. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
  105. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
  106. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
  107. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
  108. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
  110. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
  111. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
  112. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
  115. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
  116. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
  118. package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
  121. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
  123. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
  125. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
  126. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  136. package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
  137. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
  138. package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
  139. package/src/runtime/channels/lib/whisper-port.mjs +10 -1
  140. package/src/runtime/computer-bridge/action-schema.mjs +2 -2
  141. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  142. package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
  143. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
  144. package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
  145. package/src/runtime/local-provider/server-process.mjs +2 -1
  146. package/src/runtime/media/adapters/gemini-video.mjs +4 -1
  147. package/src/runtime/media/adapters/xai-media.mjs +2 -1
  148. package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
  149. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
  150. package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
  151. package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
  152. package/src/runtime/office/design/design-art-direction.mjs +29 -27
  153. package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
  154. package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
  155. package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
  156. package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
  157. package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
  158. package/src/runtime/office/quality/design-review.mjs +1 -5
  159. package/src/runtime/office/quality/document-brief.mjs +1 -5
  160. package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
  161. package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
  162. package/src/runtime/shared/bot-owned-work.mjs +56 -0
  163. package/src/runtime/shared/defaults-separation.mjs +1 -4
  164. package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
  165. package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
  166. package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
  167. package/src/runtime/shared/handoff-artifacts.mjs +18 -8
  168. package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
  169. package/src/runtime/shared/llm/cost.mjs +5 -1
  170. package/src/runtime/shared/loopback-listener.mjs +25 -2
  171. package/src/runtime/shared/model-route-chain.d.mts +7 -2
  172. package/src/runtime/shared/model-route-chain.mjs +22 -4
  173. package/src/runtime/shared/open-url.mjs +10 -0
  174. package/src/runtime/shared/session-activity-signal.mjs +14 -0
  175. package/src/runtime/shared/session-scratch.mjs +37 -0
  176. package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
  177. package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
  178. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  179. package/src/runtime/shared/tool-surface.mjs +3 -0
  180. package/src/runtime/shared/tool-work-units.mjs +2 -0
  181. package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
  182. package/src/runtime/shared/worktrees/contract.mjs +4 -1
  183. package/src/runtime/shared/worktrees/git.mjs +15 -0
  184. package/src/runtime/shared/worktrees/manager.mjs +87 -38
  185. package/src/runtime/shared/worktrees/test-support.mjs +6 -10
  186. package/src/runtime/visualize-bridge/client.mjs +2 -0
  187. package/src/session-runtime/agent-home-delivery.mjs +41 -0
  188. package/src/session-runtime/agent-task-sessions.mjs +152 -63
  189. package/src/session-runtime/agent-tool-routing.mjs +8 -41
  190. package/src/session-runtime/boot/tools.mjs +5 -1
  191. package/src/session-runtime/bot-status.mjs +12 -6
  192. package/src/session-runtime/bot-target.mjs +38 -0
  193. package/src/session-runtime/channel-config-api.mjs +6 -1
  194. package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
  195. package/src/session-runtime/conversation-import/discovery.mjs +271 -25
  196. package/src/session-runtime/conversation-import/service.mjs +26 -5
  197. package/src/session-runtime/handoff-attachments.mjs +82 -31
  198. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
  199. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
  200. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
  201. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
  202. package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
  203. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
  204. package/src/session-runtime/model-route/set-route.mjs +11 -2
  205. package/src/session-runtime/provider-catalog-cache.mjs +82 -36
  206. package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
  207. package/src/session-runtime/provider-models.mjs +4 -2
  208. package/src/session-runtime/resource-recall-api.mjs +6 -1
  209. package/src/session-runtime/runtime-feature-gates.mjs +5 -1
  210. package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
  211. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
  212. package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
  213. package/src/session-runtime/services/agent-tool/render.mjs +71 -17
  214. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
  215. package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
  216. package/src/session-runtime/services/provider-admin.mjs +2 -1
  217. package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
  218. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
  219. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
  220. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
  221. package/src/session-runtime/workspace/transitions.mjs +25 -0
  222. package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
  223. package/src/standalone/daemon-session-control.mjs +18 -2
  224. package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
  225. package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
  226. package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
  227. package/src/tui/app/app-view/floating-panel.jsx +7 -4
  228. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
  229. package/src/tui/app/model-picker/route-selection.mjs +5 -6
  230. package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
  231. package/src/tui/app/provider-setup/provider-items.mjs +8 -2
  232. package/src/tui/app/provider-setup-picker.mjs +9 -5
  233. package/src/tui/components/QuestionPanel.jsx +13 -4
  234. package/src/tui/dist/index.mjs +75 -28
  235. package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
  236. package/src/ui/statusline-agents.mjs +1 -3
@@ -24,6 +24,8 @@ import {
24
24
  inheritedFromSessionId,
25
25
  isStoredSessionId,
26
26
  positiveNumber as _positiveNumber,
27
+ compactCount,
28
+ compactCountField,
27
29
  realTurnCount,
28
30
  workspaceSummary,
29
31
  } from './store-summary-fields.mjs';
@@ -54,26 +56,50 @@ const _computeCleanPreview = cleanSummaryPreview;
54
56
  // cleaning runs a chain of regex replaces that each allocate a copy. Those
55
57
  // short strings repeat from flush to flush, so the pure result is memoized per
56
58
  // (max, text). Long raw message text is not kept here (see _previewFromMessage).
59
+ // Every new message's raw text (up to the text limit) also passes through, so
60
+ // the memo is bounded by the characters it holds, least recently used first
61
+ // out: at 8192 entries alone it kept ~16–32 MB of message text per `max`.
57
62
  const CLEAN_PREVIEW_MEMO_TEXT_LIMIT = 2048;
58
63
  const CLEAN_PREVIEW_MEMO_ENTRIES = 8192;
59
- const cleanPreviewMemo = new Map(); // max → Map<text, cleaned>
64
+ const CLEAN_PREVIEW_MEMO_MAX_CHARS = 512 * 1024;
65
+ const cleanPreviewMemo = new Map(); // max → { byText: Map<text, cleaned>, chars }
60
66
 
61
67
  function _cleanPreview(text, max = 240) {
62
68
  if (typeof text !== 'string' || text.length > CLEAN_PREVIEW_MEMO_TEXT_LIMIT) return _computeCleanPreview(text, max);
63
- let byText = cleanPreviewMemo.get(max);
64
- if (!byText) {
65
- byText = new Map();
66
- cleanPreviewMemo.set(max, byText);
69
+ let memo = cleanPreviewMemo.get(max);
70
+ if (!memo) {
71
+ memo = { byText: new Map(), chars: 0 };
72
+ cleanPreviewMemo.set(max, memo);
67
73
  }
74
+ const { byText } = memo;
68
75
  let cleaned = byText.get(text);
69
- if (cleaned === undefined) {
70
- cleaned = _computeCleanPreview(text, max);
71
- if (byText.size >= CLEAN_PREVIEW_MEMO_ENTRIES) byText.clear();
76
+ if (cleaned !== undefined) {
77
+ byText.delete(text);
72
78
  byText.set(text, cleaned);
79
+ return cleaned;
80
+ }
81
+ cleaned = _computeCleanPreview(text, max);
82
+ byText.set(text, cleaned);
83
+ memo.chars += text.length + cleaned.length;
84
+ for (const [oldText, oldCleaned] of byText) {
85
+ if (byText.size <= CLEAN_PREVIEW_MEMO_ENTRIES && memo.chars <= CLEAN_PREVIEW_MEMO_MAX_CHARS) break;
86
+ byText.delete(oldText);
87
+ memo.chars -= oldText.length + oldCleaned.length;
73
88
  }
74
89
  return cleaned;
75
90
  }
76
91
 
92
+ /** Entries and characters the preview memo holds, summed over every `max` (tests). */
93
+ export function _cleanPreviewMemoStatsForTest() {
94
+ let entries = 0;
95
+ let chars = 0;
96
+ for (const memo of cleanPreviewMemo.values()) {
97
+ entries += memo.byText.size;
98
+ chars += memo.chars;
99
+ }
100
+ return { entries, chars };
101
+ }
102
+
77
103
  const sessionMessageProjectionMemo = new WeakMap();
78
104
  // message → { raw, preview }. A session's first real user message is
79
105
  // re-checked on every save; its text is re-derived (cheap) and the preview is
@@ -265,6 +291,8 @@ export function _sessionSummary(session) {
265
291
  taskType: session.taskType || null,
266
292
  messageCount: messageProjection.count,
267
293
  ...(messageProjection.incoming ? { incomingTail: messageProjection.incoming.slice() } : {}),
294
+ // The numbering epoch of messageCount: a compaction restarts it.
295
+ compactCount: compactCount(session.compaction) ?? 0,
268
296
  realTurnCount: messageProjection.real,
269
297
  title: _cleanPreview(session.title || '', 100),
270
298
  preview: messageProjection.preview,
@@ -324,6 +352,7 @@ function _normalizeSummaryRow(row) {
324
352
  taskType: row.taskType || null,
325
353
  messageCount: Math.max(0, Math.floor(Number(row.messageCount) || 0)),
326
354
  ...(Array.isArray(row.incomingTail) ? { incomingTail: normalizedIncomingTail(row.incomingTail) } : {}),
355
+ ...compactCountField(row),
327
356
  realTurnCount: realTurnCount(row),
328
357
  title: _cleanPreview(row.title || '', 100),
329
358
  preview: _cleanPreview(row.preview || ''),
@@ -44,7 +44,11 @@ import {
44
44
  import { attachmentHistoryRows } from './stored-attachment-history.mjs';
45
45
  import { currentSessionCatalogDb, onSessionCatalogDbChange, sessionCatalogDb } from './catalog-db/index.mjs';
46
46
  import { decodeCatalogCursor, encodeCatalogCursor, isAfterCursor } from './catalog-db/schema.mjs';
47
- import { onSessionActivityChange, onSessionApprovalCardChange } from '../../../shared/session-activity-signal.mjs';
47
+ import {
48
+ onSessionActivityChange,
49
+ onSessionApprovalCardChange,
50
+ onSessionBotWorkChange,
51
+ } from '../../../shared/session-activity-signal.mjs';
48
52
  import { inFlightSessionActivity } from './manager/runtime-state.mjs';
49
53
  import {
50
54
  readCanonicalSessionRecord as readCanonicalLifecycle,
@@ -136,6 +140,10 @@ onSessionActivityChange(() => {
136
140
  // A session started or stopped waiting on the user's approval: its catalog row
137
141
  // (`awaitingApproval`) changed, though no store file did.
138
142
  onSessionApprovalCardChange(() => notifyCatalogListeners('approval'));
143
+ // A bot started or stopped having a send in flight in a session: its catalog
144
+ // row (`handedBy`) changed, though neither a file nor the set of working
145
+ // sessions did (the work may already be running, or continue past the end).
146
+ onSessionBotWorkChange(() => notifyCatalogListeners('bot-work'));
139
147
 
140
148
  /** `listener()` runs when a background pass finds a session file that changed
141
149
  * without a directory or index change, i.e. when an ordinary listing would
@@ -44,6 +44,7 @@ import {
44
44
  } from '../../../../shared/transcript-metadata.mjs';
45
45
  import { routineTriggerField } from '../../../../shared/routine-trigger.mjs';
46
46
  import { isImportedMessage } from '../imported-message.mjs';
47
+ import { toolRunLine } from '../conversation-import/turns.mjs';
47
48
  import { isContextAckMessage } from '../compact/user-message-kind.mjs';
48
49
  import { isRoutineResultMessage, routineResultNotice } from '../routine-result.mjs';
49
50
  import { heldRequestNotice, isHeldRequestMessage } from '../human-gate/held-message.mjs';
@@ -140,7 +141,64 @@ function restoredContentText(message) {
140
141
  return typeof message?.content === 'string' ? message.content : toolResultText(message?.content);
141
142
  }
142
143
 
144
+ // The runs of tool use an imported assistant message recorded (conversation-import
145
+ // turns.mjs), kept only where the text still holds exactly their canonical line.
146
+ function importedToolRuns(message) {
147
+ const runs = message?.meta?.importedToolRuns;
148
+ if (!isImportedMessage(message) || typeof message.content !== 'string' || !Array.isArray(runs)) return [];
149
+ let cursor = 0;
150
+ return runs.filter((run) => {
151
+ const valid =
152
+ Number.isInteger(run?.start) &&
153
+ Number.isInteger(run?.end) &&
154
+ run.start >= cursor &&
155
+ run.end > run.start &&
156
+ run.end <= message.content.length &&
157
+ Array.isArray(run.tools) &&
158
+ run.tools.length > 0 &&
159
+ run.tools.every((tool) => typeof tool?.name === 'string' && Number.isInteger(tool.count) && tool.count > 0) &&
160
+ message.content.slice(run.start, run.end) === toolRunLine(run.tools);
161
+ if (valid) cursor = run.end;
162
+ return valid;
163
+ });
164
+ }
165
+
166
+ // An imported reply with tool use: its text around each run as assistant rows,
167
+ // each run as tool rows (one per tool, counted). The calls' results were never
168
+ // imported; the `imported` mark lets the row say so.
169
+ function restoredImportedAssistantItems(message, runs, nextId) {
170
+ const { completion: _completion, ...metadata } = restoredTranscriptMetadata(message);
171
+ const at = Number(message?.meta?.transcript?.at);
172
+ const items = [];
173
+ const pushText = (raw) => {
174
+ const text = raw.trim();
175
+ if (text) items.push({ kind: 'assistant', id: nextId(), text, ...metadata });
176
+ };
177
+ let cursor = 0;
178
+ for (const run of runs) {
179
+ pushText(message.content.slice(cursor, run.start));
180
+ for (const { name, count } of run.tools) {
181
+ if (isTranscriptHiddenControlToolName(name)) continue;
182
+ items.push({
183
+ kind: 'tool',
184
+ id: nextId(),
185
+ name,
186
+ imported: true,
187
+ expanded: false,
188
+ count,
189
+ completedCount: count,
190
+ ...(Number.isFinite(at) ? { at, startedAt: at, completedAt: at } : {}),
191
+ });
192
+ }
193
+ cursor = run.end;
194
+ }
195
+ pushText(message.content.slice(cursor));
196
+ return items;
197
+ }
198
+
143
199
  function restoredAssistantTranscriptItems(message, nextId) {
200
+ const runs = importedToolRuns(message);
201
+ if (runs.length) return restoredImportedAssistantItems(message, runs, nextId);
144
202
  const text = restoredContentText(message).trim();
145
203
  if (!text) return [];
146
204
  const { completion, ...metadata } = restoredTranscriptMetadata(message);
@@ -339,7 +397,7 @@ function mergeRestoredToolItems(items) {
339
397
  // Result-dependent hiding (built-in skill loads) resolves here, after
340
398
  // every role:'tool' result has been attached to its restored call.
341
399
  if (item?.kind === 'tool' && isTranscriptHiddenToolItem(item)) continue;
342
- const mergeable = item?.kind === 'tool' && item.aggregate !== true;
400
+ const mergeable = item?.kind === 'tool' && item.aggregate !== true && item.imported !== true;
343
401
  if (!mergeable) {
344
402
  flushRun();
345
403
  merged.push(item);
@@ -379,7 +437,8 @@ function restoredMessageItemUpperBound(message) {
379
437
  const calls = restoredMessageToolCalls(message);
380
438
  const content = message?.content;
381
439
  const hasContent = typeof content === 'string' ? content.length > 0 : Array.isArray(content) && content.length > 0;
382
- return Number(hasContent) + completionCount + calls.length + boundaries;
440
+ const importedRows = importedToolRuns(message).reduce((total, run) => total + run.tools.length + 1, 0);
441
+ return Number(hasContent) + completionCount + calls.length + importedRows + boundaries;
383
442
  }
384
443
 
385
444
  // One restored tool card for a parsed notification/completion envelope.
@@ -259,6 +259,29 @@ export const PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS = resolveTimeoutMs(
259
259
  { minMs: 10_000, maxMs: Math.max(10_000, STALL_ABORT_MS - STALL_TICK_MS) }
260
260
  );
261
261
 
262
+ // First-OUTPUT window of one provider stream: from the stream opening until
263
+ // the model's first semantic output (reasoning, text or a tool call). Server
264
+ // metadata and keepalives (response.created / in_progress, an empty reasoning
265
+ // or message item, a role-only chunk, pings) neither end this phase nor start
266
+ // the semantic-idle clock, so a reasoning model may think silently past the
267
+ // idle window (issue #244: 4–6 min on a large post-tool-result prompt). The
268
+ // window sits one tick under the stall abort — the same ceiling the semantic
269
+ // idle uses — so the provider still fails (and may retry or fall back) before
270
+ // the agent watchdog's first-output deadline (600s default), and it is never
271
+ // shorter than the idle window it replaces. Not separately tunable: it
272
+ // follows STALL_TIMEOUT_S.
273
+ export const PROVIDER_FIRST_OUTPUT_TIMEOUT_MS = Math.max(
274
+ PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
275
+ PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS,
276
+ STALL_ABORT_MS - STALL_TICK_MS
277
+ );
278
+
279
+ /** The first-output window of a stream whose semantic idle is `idleMs`: a
280
+ * longer idle window (a provider profile) is never cut short by it. */
281
+ export function providerFirstOutputTimeoutMs(idleMs) {
282
+ return Math.max(idleMs, PROVIDER_FIRST_OUTPUT_TIMEOUT_MS);
283
+ }
284
+
262
285
  /**
263
286
  * The longest a provider's own first-byte handling may legitimately run
264
287
  * before it gives up, over every attempt it permits: `windowMs(n)` is the wait
@@ -28,9 +28,9 @@ function prefixPowerShellUtf8(command) {
28
28
  // UTF-8 prefix (+ optional filter-tee rescue), or a POSIX bash/zsh snapshot
29
29
  // wrapper delivered through the environment. Returns `{ failure }` when the
30
30
  // snapshot wrapper cannot be built.
31
- export async function planShellInvocation({ command, resolvedSpec, cwd }) {
31
+ export async function planShellInvocation({ command, resolvedSpec, cwd, sessionId = null }) {
32
32
  const { shell, shellArg, shellArgs, shellType } = resolvedSpec;
33
- const spawnEnv = buildShellSpawnEnv(cwd);
33
+ const spawnEnv = buildShellSpawnEnv(cwd, process.env, sessionId);
34
34
  const plan = {
35
35
  execShell: shell,
36
36
  execShellArg: shellArg,
@@ -1,3 +1,4 @@
1
+ import { applySessionScratchEnv } from '../../../../../shared/session-scratch.mjs';
1
2
  import { withSkillLauncherPath } from '../../../../../shared/skill-script-runtime.mjs';
2
3
  import {
3
4
  applyShellEgressPolicy,
@@ -6,7 +7,8 @@ import {
6
7
  scrubRuntimeRootVars,
7
8
  } from '../../env-scrub.mjs';
8
9
 
9
- export function buildShellSpawnEnv(cwd, baseEnv = process.env) {
10
+ // `sessionId` points TEMP/TMP/TMPDIR at that session's scratch directory.
11
+ export function buildShellSpawnEnv(cwd, baseEnv = process.env, sessionId = null) {
10
12
  const spawnEnv = {
11
13
  ...baseEnv,
12
14
  LANG: 'C.UTF-8',
@@ -24,5 +26,5 @@ export function buildShellSpawnEnv(cwd, baseEnv = process.env) {
24
26
  // `mixdog-skill-node` (a skill's ${MIXDOG_SKILL_NODE}) resolves in every
25
27
  // model shell; only this env gets the entry, never the daemon's own.
26
28
  withSkillLauncherPath(spawnEnv);
27
- return spawnEnv;
29
+ return applySessionScratchEnv(spawnEnv, sessionId);
28
30
  }
@@ -116,7 +116,12 @@ export async function executeBashTool(args, workDir, options = {}) {
116
116
  // A queued run, or a foreground tree that may outlive its result, settles later.
117
117
  let runSettled = null;
118
118
  try {
119
- const invocation = await planShellInvocation({ command, resolvedSpec, cwd: bashWorkDir });
119
+ const invocation = await planShellInvocation({
120
+ command,
121
+ resolvedSpec,
122
+ cwd: bashWorkDir,
123
+ sessionId: options?.callerSessionId || options?.sessionId || null,
124
+ });
120
125
  if (invocation.failure) return invocation.failure;
121
126
  combinedAbort = combineAbortSignals(await sessionAbortSignal(options?.sessionId), options?.abortSignal || null);
122
127
  const startedAtMs = Date.now();
@@ -5,7 +5,7 @@
5
5
  // behavior, argument shapes, and the usage boundaries that only apply to that
6
6
  // tool; cross-tool policy lives in rules/shared/*.md.
7
7
  // Platform-specific command syntax belongs next to the command argument.
8
- import { GIT_TOOL_DEF } from './git-command-tool.mjs';
8
+ import { GIT_AGENT_TOOL_DEF, GIT_TOOL_DEF } from './git-command-tool.mjs';
9
9
  import { PUBLIC_PATH_BATCH_LIMIT, PUBLIC_READ_WINDOW_MAX } from './arg-guard.mjs';
10
10
  import { GITHUB_TOOL_DEF } from '../../../../github/tool.mjs';
11
11
  import { envFlag } from '../../../../shared/env.mjs';
@@ -50,7 +50,7 @@ export function buildShellRoutingRules(kind = _defaultShellKind) {
50
50
  function buildShellDescription(kind) {
51
51
  const run = _shellBackgroundDisabled
52
52
  ? 'Commands run in the foreground until completion.'
53
- : `Holds ${_holdS}s; then, or on input/task notification, backgrounds with task_id (command keeps running). Completion is automatic; never poll, and never sleep/poll-loop inside the shell (background:true or wait_ms instead; for an external job, background a command that exits when it ends). Result needed now: task wait. Known-long finite run: wait_ms up to ${SHELL_HOLD_MAX_MS}. Independent long runs, servers/watchers: background:true; cancel them when done.`;
53
+ : `Holds ${_holdS}s; then, or on input/task notification, backgrounds with task_id (command keeps running). Completion is automatic; never poll, and never sleep/poll-loop inside the shell (background:true or wait_ms instead; for an external job, background a command that exits when it ends). Result needed now: task wait. Independent long runs: background:true; cancel when done.`;
54
54
  return `Run programs, builds, tests and computation. ${buildShellRoutingRules(kind)} ${run}`;
55
55
  }
56
56
 
@@ -88,6 +88,10 @@ export function bindShellKind(tool, kind) {
88
88
  return bound;
89
89
  }
90
90
 
91
+ function taskTimeoutDescription(defaultMs) {
92
+ return `Wait ceiling in ms; else current output at the ceiling. Default ${defaultMs}; clamped to ${TASK_WAIT_TIMEOUT_MIN_MS}-${TASK_WAIT_TIMEOUT_MAX_MS}.`;
93
+ }
94
+
91
95
  export const BUILTIN_TOOLS = [
92
96
  {
93
97
  name: 'read',
@@ -101,7 +105,7 @@ export const BUILTIN_TOOLS = [
101
105
  compressible: false,
102
106
  },
103
107
  description:
104
- "Read file windows or images. One call per read stage: {file_path, offset, limit} entries for every located site (≤10 files per call), never a whole large file. Content in context is never read again. Missing paths are reported, never replaced. Directories: list. Binaries: bounded hex. Rows carry no line numbers; `[lines a-b]` gives each block's range. Never count rows: before writing a line number, copy it from grep, code_graph or git diff output; otherwise cite path and symbol.",
108
+ "Read file windows or images. One call per read stage: {file_path, offset, limit} entries for every located site, never a whole large file. Content in context is never read again. Missing paths are reported, never replaced. Directories: list. Binaries: bounded hex. Rows carry no line numbers; `[lines a-b]` gives each block's range. Never count rows: before writing a line number, copy it from grep, code_graph or git diff output; otherwise cite path and symbol.",
105
109
  inputSchema: {
106
110
  type: 'object',
107
111
  properties: {
@@ -129,8 +133,7 @@ export const BUILTIN_TOOLS = [
129
133
  },
130
134
  },
131
135
  ],
132
- description:
133
- 'Path(s) or {file_path, offset, limit} entries; overlapping windows of one file merge. A glob fans out to ≤10 newest files, 25 lines each.',
136
+ description: 'Overlapping windows of one file merge. A glob fans out to ≤10 newest files.',
134
137
  },
135
138
  offset: {
136
139
  type: 'integer',
@@ -167,14 +170,11 @@ export const BUILTIN_TOOLS = [
167
170
  compressibleLossless: true,
168
171
  },
169
172
  description:
170
- 'Replace exact text in one file. old_string must match once unless replace_all is true. Every non-overlapping edit of the stage in one response, using existing text, not text another edit creates; many files → one edit per file, all in that response. Widened replacements keep intervening lines verbatim.',
173
+ 'Replace exact text in one file. old_string must match once unless replace_all is true. Every non-overlapping edit of the stage in one response, using existing text, not text another edit creates; many files → one edit per file. Widened replacements keep intervening lines verbatim.',
171
174
  inputSchema: {
172
175
  type: 'object',
173
176
  properties: {
174
- file_path: {
175
- type: 'string',
176
- description: 'Path to the file to modify.',
177
- },
177
+ file_path: { type: 'string' },
178
178
  old_string: {
179
179
  type: 'string',
180
180
  description:
@@ -187,7 +187,7 @@ export const BUILTIN_TOOLS = [
187
187
  replace_all: {
188
188
  type: 'boolean',
189
189
  default: false,
190
- description: 'Replace all occurrences of old_string; default false.',
190
+ description: 'Replace all occurrences of old_string.',
191
191
  },
192
192
  },
193
193
  required: ['file_path', 'old_string', 'new_string'],
@@ -254,7 +254,7 @@ export const BUILTIN_TOOLS = [
254
254
  openWorldHint: false,
255
255
  },
256
256
  description:
257
- 'Manage shell tasks. Completion is delivered automatically; wait only when you need the result now, never poll with read. wait returns when the task finishes, or when new user input or any task notification arrives (the task keeps running). read/wait return new output, prioritizing the tail on completion. output:tail re-reads recent output without advancing the cursor; use read on the log path for omitted content.',
257
+ 'Manage shell tasks. Completion is delivered automatically; wait only when you need the result now, never poll with read. wait returns when the task finishes, or when new user input or any task notification arrives (the task keeps running). read/wait return new output, prioritizing the tail on completion. Use read on the log path for omitted content.',
258
258
  inputSchema: {
259
259
  type: 'object',
260
260
  properties: {
@@ -267,13 +267,13 @@ export const BUILTIN_TOOLS = [
267
267
  timeout_ms: {
268
268
  type: 'integer',
269
269
  minimum: 0,
270
- description: `Wait ceiling in ms; returns on completion, new user input or a task notification, else current output at the ceiling. Default ${TASK_WAIT_TIMEOUT_DEFAULT_MS} (agents ${AGENT_TASK_WAIT_DEFAULT_MS}); clamped to ${TASK_WAIT_TIMEOUT_MIN_MS}-${TASK_WAIT_TIMEOUT_MAX_MS}.`,
270
+ description: taskTimeoutDescription(TASK_WAIT_TIMEOUT_DEFAULT_MS),
271
271
  },
272
272
  output: {
273
273
  type: 'string',
274
274
  enum: ['new', 'tail'],
275
275
  description:
276
- 'read/wait only. Default new; tail replays recent output after an interruption or context loss. Up to 8 KiB per stream while running, 12 KiB on completion or replay; original logs are preserved.',
276
+ 'read/wait only. Default new; tail re-reads recent output without advancing the cursor, after an interruption or context loss. Up to 8 KiB per stream while running, 12 KiB on completion or replay; original logs are preserved.',
277
277
  },
278
278
  },
279
279
  required: ['action'],
@@ -324,7 +324,7 @@ export const BUILTIN_TOOLS = [
324
324
  description:
325
325
  'Requested results; default 250; 0 unlimited. Context output caps at 40 blocks; continue via the returned offset.',
326
326
  },
327
- offset: { type: 'integer', minimum: 0, description: 'Result offset.' },
327
+ offset: { type: 'integer', minimum: 0 },
328
328
  context: {
329
329
  type: 'integer',
330
330
  minimum: 0,
@@ -356,7 +356,7 @@ export const BUILTIN_TOOLS = [
356
356
  compressible: true,
357
357
  },
358
358
  description:
359
- 'Wildcard file-path lookup under a known directory; directories never match. Omit path for the current Project; no preliminary listing. Unknown base: find first with a known path fragment. Gitignored paths need include_noise:true.',
359
+ 'Wildcard file-path lookup under a known directory; directories never match. No preliminary listing. Unknown base: find first with a known path fragment.',
360
360
  inputSchema: {
361
361
  type: 'object',
362
362
  properties: {
@@ -374,7 +374,7 @@ export const BUILTIN_TOOLS = [
374
374
  description: 'mtime default (newest first); natural = raw walk order, cheaper on huge trees.',
375
375
  },
376
376
  limit: { type: 'integer', minimum: 0, description: 'Max entries; default 100; 0 unlimited.' },
377
- offset: { type: 'integer', minimum: 0, description: 'Entry offset.' },
377
+ offset: { type: 'integer', minimum: 0 },
378
378
  include_noise: {
379
379
  type: 'boolean',
380
380
  description: 'Also search gitignored trees.',
@@ -404,7 +404,7 @@ export const BUILTIN_TOOLS = [
404
404
  type: 'string',
405
405
  minLength: 1,
406
406
  pattern: '\\S',
407
- description: 'Non-empty filename/path fragment; space-separated fragments AND-match within one path.',
407
+ description: 'Filename/path fragment; space-separated fragments AND-match within one path.',
408
408
  },
409
409
  path: { type: 'string', description: 'Base directory; omit for the current Project.' },
410
410
  limit: { type: 'integer', minimum: 0, description: 'Max paths; default 25; 0 unlimited.' },
@@ -445,10 +445,36 @@ export const BUILTIN_TOOLS = [
445
445
  maximum: 100,
446
446
  description: 'Max entries; default 100; 0 = no page cap (absolute cap still applies).',
447
447
  },
448
- offset: { type: 'integer', minimum: 0, description: 'Entry offset.' },
448
+ offset: { type: 'integer', minimum: 0 },
449
449
  },
450
- required: [],
451
450
  additionalProperties: false,
452
451
  },
453
452
  },
454
453
  ];
454
+
455
+ // What a delegated agent's session sees of the tools whose text differs from
456
+ // the Lead's: git (worktree operations are Lead-only) and task (the agent wait
457
+ // default). Keyed by the shared definition, so a rebuilt list binds once.
458
+ const _taskTool = BUILTIN_TOOLS.find((tool) => tool.name === 'task');
459
+ const _agentTaskTool = {
460
+ ..._taskTool,
461
+ inputSchema: {
462
+ ..._taskTool.inputSchema,
463
+ properties: {
464
+ ..._taskTool.inputSchema.properties,
465
+ timeout_ms: {
466
+ ..._taskTool.inputSchema.properties.timeout_ms,
467
+ description: taskTimeoutDescription(AGENT_TASK_WAIT_DEFAULT_MS),
468
+ },
469
+ },
470
+ },
471
+ };
472
+ const _agentToolVariants = new Map([
473
+ [GIT_TOOL_DEF, GIT_AGENT_TOOL_DEF],
474
+ [_taskTool, _agentTaskTool],
475
+ ]);
476
+
477
+ /** The agent-session variant of `tool`, or `tool` itself when it has none. */
478
+ export function bindAgentOwner(tool) {
479
+ return _agentToolVariants.get(tool) ?? tool;
480
+ }
@@ -324,7 +324,13 @@ export function rawContentCacheSet(fullPath, stat, rawBuf, now = Date.now()) {
324
324
  if (!canPublishReadCache(fullPath)) return;
325
325
  if (!fullPath || !Buffer.isBuffer(rawBuf)) return;
326
326
  const version = fileVersion(stat);
327
- if (!version || !canCacheReadFile(version) || rawBuf.length !== version.size || rawBuf.length > RAW_CONTENT_CACHE_MAX_BYTES) return;
327
+ if (
328
+ !version ||
329
+ !canCacheReadFile(version) ||
330
+ rawBuf.length !== version.size ||
331
+ rawBuf.length > RAW_CONTENT_CACHE_MAX_BYTES
332
+ )
333
+ return;
328
334
  const key = canonicalCachePath(fullPath);
329
335
  rawContentCacheDelete(key);
330
336
  RAW_CONTENT_CACHE.set(key, {
@@ -83,6 +83,9 @@ const SERVER_OPERATIONS = new Set(['daemon', 'instaweb']);
83
83
  // probe into `git rev-parse exited 128` inside a plain directory.
84
84
  const REPO_FREE_OPERATIONS = new Set(['version', 'help']);
85
85
 
86
+ const GIT_DESCRIPTION_HEAD =
87
+ 'Run Git here, never through shell. An array runs in order and stops on failure; batch read-only commands in one. diff for known changes, status to discover them; history only when needed. git diff or git diff -- <paths> includes untracked files. Set include_stage_ids:true to select changes, then action:stage with diff_id/change_ids. Staging rejects stale or cross-Project diffs and returns per-file staged counts.';
88
+
86
89
  export const GIT_TOOL_DEF = {
87
90
  name: 'git',
88
91
  title: 'Git',
@@ -94,8 +97,7 @@ export const GIT_TOOL_DEF = {
94
97
  openWorldHint: true,
95
98
  compressible: true,
96
99
  },
97
- description:
98
- 'Run Git here, never through shell. An array (max 10) runs in order and stops on failure; batch read-only commands in one. diff for known changes, status to discover them; history only when needed. git diff or git diff -- <paths> includes untracked files. Set include_stage_ids:true to select changes, then action:stage with diff_id/change_ids. Staging rejects stale or cross-Project diffs and returns per-file staged counts. Worktrees are managed only: action:worktree (Lead only) moves a session into/out of its managed worktree, merges, removes, lists, prunes; git worktree add/remove/list/prune map to managed operations (add -b keeps the session in place; add --detach makes a read-only snapshot worktree, removed when its turn ends; list --porcelain stays raw git; move/lock/unlock/repair are refused). Merge or remove only when the user asks; carry and large-size answers come from the user; force only on explicit request.',
100
+ description: `${GIT_DESCRIPTION_HEAD} Worktrees are managed only: action:worktree; git worktree add/remove/list/prune map to managed operations (add -b keeps the session in place; add --detach makes a read-only snapshot worktree, removed when its turn ends; list --porcelain stays raw git; move/lock/unlock/repair are refused). Merge or remove only when the user asks.`,
99
101
  inputSchema: {
100
102
  type: 'object',
101
103
  properties: {
@@ -118,14 +120,14 @@ export const GIT_TOOL_DEF = {
118
120
  minimum: 1,
119
121
  maximum: GIT_OUTPUT_LIMIT_MAX,
120
122
  description:
121
- 'Body line cap, max 1000; staging IDs are not capped. Default 500; git log defaults to 100. A cut ends with [output capped …] naming how to continue.',
123
+ 'Body line cap; staging IDs are not capped. Default 500; git log defaults to 100. A cut ends with [output capped …] naming how to continue.',
122
124
  },
123
125
  include_stage_ids: {
124
126
  type: 'boolean',
125
127
  description:
126
128
  'Include staging IDs outside the body cap for git diff or git diff -- <paths> only. Default false.',
127
129
  },
128
- diff_id: { type: 'string', description: 'stage: exact diff_id from git diff with include_stage_ids:true.' },
130
+ diff_id: { type: 'string', description: 'stage: exact diff_id from git diff.' },
129
131
  change_ids: {
130
132
  anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 50 }],
131
133
  description: 'stage: exact change ID or IDs to stage, including new files.',
@@ -158,6 +160,29 @@ export const GIT_TOOL_DEF = {
158
160
  },
159
161
  };
160
162
 
163
+ // Only the Lead host runs enter/return/merge/remove/prune (git-worktree-tool.mjs
164
+ // leadHost); an agent keeps `list`, so its schema omits every field the other
165
+ // operations take.
166
+ const LEAD_ONLY_WORKTREE_FIELDS = ['session_id', 'name', 'branch', 'create', 'carry', 'confirm_large', 'force'];
167
+
168
+ function buildGitAgentToolDef(base) {
169
+ const properties = { ...base.inputSchema.properties };
170
+ for (const field of LEAD_ONLY_WORKTREE_FIELDS) delete properties[field];
171
+ properties.action = {
172
+ ...properties.action,
173
+ description: 'Default command. stage uses diff_id/change_ids instead of command. worktree uses worktree_action.',
174
+ };
175
+ properties.worktree_action = { type: 'string', enum: ['list'], description: 'list only.' };
176
+ return {
177
+ ...base,
178
+ description: `${GIT_DESCRIPTION_HEAD} Worktrees are Lead-managed: only worktree_action:list and git worktree list work; every other worktree operation is refused.`,
179
+ inputSchema: { ...base.inputSchema, properties },
180
+ };
181
+ }
182
+
183
+ /** The git tool as a delegated agent sees it (see bindAgentOwner). */
184
+ export const GIT_AGENT_TOOL_DEF = buildGitAgentToolDef(GIT_TOOL_DEF);
185
+
161
186
  function cleanText(value) {
162
187
  return String(value || '')
163
188
  .replace(/\r\n/g, '\n')
@@ -849,7 +874,8 @@ function snapshotContaining(repo) {
849
874
  }
850
875
  return (
851
876
  Object.values(entries).find(
852
- (record) => record?.kind === WORKTREE_SNAPSHOT_KIND && typeof record.path === 'string' && isWithin(record.path, repo)
877
+ (record) =>
878
+ record?.kind === WORKTREE_SNAPSHOT_KIND && typeof record.path === 'string' && isWithin(record.path, repo)
853
879
  ) ?? null
854
880
  );
855
881
  }
@@ -101,6 +101,8 @@ function worktreeFailureText(code, message, details) {
101
101
  return typeof details.path === 'string' && details.path
102
102
  ? `This branch is already checked out in ${details.path}.`
103
103
  : 'This branch is already checked out in another worktree.';
104
+ case 'WORKTREE_DEFAULT_BRANCH':
105
+ return "The repository's default branch can't be opened in a worktree: the project folder could not switch to it while the worktree exists. Check it out in the project folder, or create a new branch for the worktree.";
104
106
  case 'WORKTREE_REMOVAL_INCOMPLETE':
105
107
  return 'The worktree could not be removed completely because a file is still in use. Close it and remove the worktree again.';
106
108
  case 'WORKTREE_PATH_UNSAFE':
@@ -162,6 +164,8 @@ function nextStep(code, details, { raw }) {
162
164
  return 'The workspace changed meanwhile; call again (the workspace is read afresh).';
163
165
  case 'WORKTREE_BUSY_JOBS':
164
166
  return 'Nothing was changed; try again once the session and its workers are idle.';
167
+ case 'WORKTREE_DEFAULT_BRANCH':
168
+ return 'Nothing was created. Work on a new branch instead (worktree_action:"enter" with branch and create:true), or ask the user to check the default branch out in the project folder.';
165
169
  case 'MERGE_CONFLICT':
166
170
  return 'Nothing was merged or forced. Resolve the conflict in the worktree (merge the target branch into it there), or ask the user.';
167
171
  case 'WORKTREE_DIRTY':
@@ -316,7 +320,8 @@ async function rawAdd(plan, input, options) {
316
320
  throw new Error(`git worktree add: ${value} is not supported for managed worktrees`);
317
321
  }
318
322
  if (ADD_BARE_FLAGS.has(value)) continue;
319
- if (value.startsWith('-')) throw new Error(`git worktree add: option ${value} is not supported for managed worktrees`);
323
+ if (value.startsWith('-'))
324
+ throw new Error(`git worktree add: option ${value} is not supported for managed worktrees`);
320
325
  positional.push(value);
321
326
  }
322
327
  const [path, commitish = null] = positional;
@@ -436,8 +441,9 @@ async function rawPrune(plan, options) {
436
441
  const WORKTREE_HELP = Object.freeze({
437
442
  add: 'git worktree add [-b <branch>] [--detach] <path> [<commit-ish>]: a managed worktree for the branch (the session stays put), or a read-only snapshot for a detached commit.',
438
443
  remove: 'git worktree remove [-f] <path>: the managed removal of that worktree.',
439
- list: 'git worktree list [-v]: managed worktrees plus the repository\'s other worktrees. --porcelain stays raw git.',
440
- prune: 'git worktree prune [-n|--dry-run] [-v]: the managed prune of this repository (clean, merged worktrees no session retains); -n only lists what it would remove.',
444
+ list: "git worktree list [-v]: managed worktrees plus the repository's other worktrees. --porcelain stays raw git.",
445
+ prune:
446
+ 'git worktree prune [-n|--dry-run] [-v]: the managed prune of this repository (clean, merged worktrees no session retains); -n only lists what it would remove.',
441
447
  });
442
448
 
443
449
  /** -h/--help before any `--` (the help answers instead of the operation running). */
@@ -450,7 +456,9 @@ function wantsHelp(args) {
450
456
  function checkListArgs(args) {
451
457
  for (const value of args) {
452
458
  if (value === '-v' || value === '--verbose') continue;
453
- throw new Error(`git worktree list: ${value.startsWith('-') ? `option ${value} is not supported` : `unexpected argument ${value}`}`);
459
+ throw new Error(
460
+ `git worktree list: ${value.startsWith('-') ? `option ${value} is not supported` : `unexpected argument ${value}`}`
461
+ );
454
462
  }
455
463
  }
456
464
 
@@ -466,7 +474,8 @@ export async function executeRawWorktree(plan, input, options = {}) {
466
474
  if (sub === 'list' && plan.args.includes('--porcelain')) return null;
467
475
  try {
468
476
  if (sub === 'list') checkListArgs(plan.args.slice(1));
469
- if (sub === 'list') return { text: okJson({ mapped: plan.command, ...(await listRows(options, plan.cwd)) }), failed: false };
477
+ if (sub === 'list')
478
+ return { text: okJson({ mapped: plan.command, ...(await listRows(options, plan.cwd)) }), failed: false };
470
479
  if (sub === 'add') return { text: await rawAdd(plan, input, options), failed: false };
471
480
  if (sub === 'remove') return { text: await rawRemove(plan, options), failed: false };
472
481
  if (sub === 'prune') return { text: await rawPrune(plan, options), failed: false };
@@ -479,7 +488,9 @@ export async function executeRawWorktree(plan, input, options = {}) {
479
488
  };
480
489
  }
481
490
  return {
482
- text: fail(`git worktree ${sub || '(no subcommand)'} is not supported; use add, remove, list, prune or action:"worktree"`),
491
+ text: fail(
492
+ `git worktree ${sub || '(no subcommand)'} is not supported; use add, remove, list, prune or action:"worktree"`
493
+ ),
483
494
  failed: true,
484
495
  };
485
496
  } catch (error) {
@@ -529,7 +540,8 @@ export async function executeWorktreeAction(input, workDir, options = {}) {
529
540
  if (problem) return fail(problem);
530
541
  const action = input.worktree_action;
531
542
  try {
532
- if (action === 'list') return okJson({ worktree_action: action, ...(await listRows(options, resolve(workDir || process.cwd()))) });
543
+ if (action === 'list')
544
+ return okJson({ worktree_action: action, ...(await listRows(options, resolve(workDir || process.cwd()))) });
533
545
  const host = leadHost(options, `git worktree_action ${action}`);
534
546
  const sessionId = input.session_id?.trim() || null;
535
547
  const request = {
@@ -545,7 +557,9 @@ export async function executeWorktreeAction(input, workDir, options = {}) {
545
557
  }
546
558
  : {}),
547
559
  ...(action === 'remove' ? { force: input.force === true } : {}),
548
- ...(action === 'prune' ? { repoCommonDir: (await repositoryAt(resolve(workDir || process.cwd())))?.repoCommonDir ?? null } : {}),
560
+ ...(action === 'prune'
561
+ ? { repoCommonDir: (await repositoryAt(resolve(workDir || process.cwd())))?.repoCommonDir ?? null }
562
+ : {}),
549
563
  txId: txIdOf(options, action),
550
564
  };
551
565
  const result = await runOn(host, request, options);