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
@@ -0,0 +1,37 @@
1
+ // Per-session scratch directory under Mixdog's data directory. Processes the
2
+ // `shell` tool starts see it as TEMP/TMP/TMPDIR, so scratch files never land in
3
+ // the project, and it is removed with its session (session-delete.mjs).
4
+ import { mkdirSync, rmSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { resolvePluginData } from './plugin-paths.mjs';
7
+
8
+ const SESSION_ID = /^[A-Za-z0-9_-]+$/;
9
+
10
+ /** The session's scratch path (not created), or '' for an unusable id. */
11
+ export function sessionScratchDir(sessionId) {
12
+ const id = typeof sessionId === 'string' ? sessionId : '';
13
+ return SESSION_ID.test(id) ? join(resolvePluginData(), 'scratch', id) : '';
14
+ }
15
+
16
+ /** Create the session's scratch directory if absent; returns its path ('' without a session id). */
17
+ export function ensureSessionScratchDir(sessionId) {
18
+ const dir = sessionScratchDir(sessionId);
19
+ if (dir) mkdirSync(dir, { recursive: true });
20
+ return dir;
21
+ }
22
+
23
+ export function removeSessionScratchDir(sessionId) {
24
+ const dir = sessionScratchDir(sessionId);
25
+ if (dir) rmSync(dir, { recursive: true, force: true });
26
+ }
27
+
28
+ /** Point an env's temp variables at the session's scratch directory. */
29
+ export function applySessionScratchEnv(env, sessionId) {
30
+ const dir = ensureSessionScratchDir(sessionId);
31
+ if (dir) {
32
+ env.TEMP = dir;
33
+ env.TMP = dir;
34
+ env.TMPDIR = dir;
35
+ }
36
+ return env;
37
+ }
@@ -38,9 +38,7 @@ function agentNameFromResult(resultText) {
38
38
  // The agent identity a card shows: the call's own fields, else the result
39
39
  // envelope when the call only carried a tag/task id.
40
40
  function agentDisplayName(args, resultText = '') {
41
- return (
42
- titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '') || agentNameFromResult(resultText)
43
- );
41
+ return titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '') || agentNameFromResult(resultText);
44
42
  }
45
43
 
46
44
  function agentModelLabel(args) {
@@ -7,13 +7,26 @@ import { backgroundTaskFailureStatusLabel } from '../err-text.mjs';
7
7
  import { agentTerminalDetail, isAgentTool } from './agent-surface.mjs';
8
8
  import { backgroundTaskElapsed, backgroundTaskFailureDetail, isBackgroundTaskTool } from './background-task.mjs';
9
9
  import { shellDisplayStatus } from './shell-surface.mjs';
10
- import { normalizeTerminalStatus, resultControlStatus, resultEnvelopeStatus, shellResultElapsed } from './terminal-status.mjs';
10
+ import {
11
+ normalizeTerminalStatus,
12
+ resultControlStatus,
13
+ resultEnvelopeStatus,
14
+ shellResultElapsed,
15
+ } from './terminal-status.mjs';
11
16
  import { mediaTerminalStatus } from './media-result.mjs';
12
17
  import { gitTerminalStatus } from './git-result.mjs';
13
18
  import { isUserControlCancellation } from '../tool-status.mjs';
14
19
 
15
20
  // Tools whose result opens with a task/bridge envelope that reports the call's status.
16
- const ENVELOPE_STATUS_TOOLS = new Set(['agent', 'bridge', 'task', 'web_search', 'search_query', 'image_query', 'web_search_call']);
21
+ const ENVELOPE_STATUS_TOOLS = new Set([
22
+ 'agent',
23
+ 'bridge',
24
+ 'task',
25
+ 'web_search',
26
+ 'search_query',
27
+ 'image_query',
28
+ 'web_search_call',
29
+ ]);
17
30
 
18
31
  function shellFragments(base, display, isShellSurface) {
19
32
  if (!isShellSurface) return { shellStatus: '', shellElapsed: '' };
@@ -648,7 +648,7 @@ const summarizeAskUserResult = ({ text, translate }) => {
648
648
 
649
649
  const TOOL_RESULT_SUMMARIZERS = new Map(
650
650
  [
651
- [['ask_user'], summarizeAskUserResult],
651
+ [['ask_user', 'handoff_user'], summarizeAskUserResult],
652
652
  [['read', 'view_image', 'read_mcp_resource'], summarizeReadResult],
653
653
  [['apply_patch'], summarizePatchResult],
654
654
  [['grep'], summarizeGrepResult],
@@ -151,6 +151,7 @@ export function displayToolName(name, args = {}) {
151
151
  case 'request_user_input':
152
152
  return 'Ask User';
153
153
  case 'ask_user':
154
+ case 'handoff_user':
154
155
  return 'Question';
155
156
  case 'update_plan':
156
157
  return 'Plan';
@@ -239,6 +240,8 @@ export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}
239
240
  return summarizeGithub(a, max);
240
241
  case 'goal':
241
242
  return compactParts([String(a.action || ''), truncateToolText(a.objective, max)]);
243
+ case 'handoff_user':
244
+ return '';
242
245
  case 'ask_user':
243
246
  return truncateToolText(
244
247
  (Array.isArray(a.questions) ? a.questions : [])
@@ -91,6 +91,7 @@ const TOOL_CATEGORY = new Map([
91
91
  ['goal', 'Setup'],
92
92
  ['request_user_input', 'Setup'],
93
93
  ['ask_user', 'Setup'],
94
+ ['handoff_user', 'Setup'],
94
95
  ['update_plan', 'Setup'],
95
96
  ['skill', 'Skill'],
96
97
  ['skill_execute', 'Skill'],
@@ -734,6 +735,7 @@ const TOOL_UNITS = new Map([
734
735
  ['code_graph', codeGraphUnit],
735
736
  ['request_user_input', () => unitDescriptor('Setup', { active: 'Asking', done: 'Asked', noun: 'user' })],
736
737
  ['ask_user', () => unitDescriptor('Setup', { active: 'Asking', done: 'Asked', noun: 'user' })],
738
+ ['handoff_user', () => unitDescriptor('Setup', { active: 'Asking', done: 'Asked', noun: 'user' })],
737
739
  ['update_plan', () => unitDescriptor('Setup', { active: 'Updating', done: 'Updated', noun: 'plan' })],
738
740
  ['list_mcp_resources', () => unitDescriptor('Setup', { active: 'Listing', done: 'Listed', noun: 'MCP resource' })],
739
741
  [
@@ -117,16 +117,23 @@ function recordPath(sessionId) {
117
117
  function chain(sessionId, task) {
118
118
  const previous = writeChains.get(sessionId) || Promise.resolve();
119
119
  const next = previous.then(task, task);
120
- writeChains.set(
121
- sessionId,
122
- next.then(
123
- () => undefined,
124
- () => undefined
125
- )
120
+ const settled = next.then(
121
+ () => undefined,
122
+ () => undefined
126
123
  );
124
+ writeChains.set(sessionId, settled);
125
+ // A settled chain with nothing queued behind it holds no order to keep.
126
+ void settled.then(() => {
127
+ if (writeChains.get(sessionId) === settled) writeChains.delete(sessionId);
128
+ });
127
129
  return next;
128
130
  }
129
131
 
132
+ /** Sessions with a write chain still held (tests). */
133
+ export function _turnSnapshotWriteChainCountForTest() {
134
+ return writeChains.size;
135
+ }
136
+
130
137
  async function readRecordFile(path) {
131
138
  try {
132
139
  const parsed = JSON.parse(await readFile(path, 'utf8'));
@@ -27,6 +27,7 @@ const WORKTREE_ENTRY_STATES = Object.freeze(['creating', 'ready', 'deleting', 'r
27
27
  export const WORKTREE_ERROR_CODES = Object.freeze({
28
28
  WORKTREE_BUSY_JOBS: 'WORKTREE_BUSY_JOBS',
29
29
  WORKTREE_BRANCH_OCCUPIED: 'WORKTREE_BRANCH_OCCUPIED',
30
+ WORKTREE_DEFAULT_BRANCH: 'WORKTREE_DEFAULT_BRANCH',
30
31
  WORKTREE_DISK_INSUFFICIENT: 'WORKTREE_DISK_INSUFFICIENT',
31
32
  WORKTREE_LARGE_REPO_CONFIRM: 'WORKTREE_LARGE_REPO_CONFIRM',
32
33
  WORKTREE_DIRTY: 'WORKTREE_DIRTY',
@@ -54,6 +55,8 @@ export const WORKTREE_ERROR_CODES = Object.freeze({
54
55
  const DEFAULT_ERROR_MESSAGES = Object.freeze({
55
56
  WORKTREE_BUSY_JOBS: 'The session has running jobs; wait for them or stop them before changing its workspace.',
56
57
  WORKTREE_BRANCH_OCCUPIED: 'The branch is checked out in another worktree.',
58
+ WORKTREE_DEFAULT_BRANCH:
59
+ "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.",
57
60
  WORKTREE_DISK_INSUFFICIENT: 'Not enough free disk space to create the worktree.',
58
61
  WORKTREE_LARGE_REPO_CONFIRM: 'The repository is large; confirm before creating a worktree.',
59
62
  WORKTREE_DIRTY: 'The worktree has uncommitted changes.',
@@ -193,7 +196,7 @@ function record(value, name) {
193
196
  * @property {string|null} worktreeId Managed worktree id; null when running in the project folder.
194
197
  * @property {string|null} branch Checked-out branch at `cwd`; null when detached or not a repository.
195
198
  * @property {string|null} baseCommit Commit the worktree branch was created from.
196
- * @property {string|null} mergeTarget Branch the worktree merges into (the project folder's branch).
199
+ * @property {string|null} mergeTarget Branch the worktree merges into (the project folder's branch); null for an existing branch until a merge records it.
197
200
  * @property {number} revision Monotonic; advances on every committed workspace change.
198
201
  * @property {WorkspaceTransition} transition In-flight transition marker.
199
202
  */
@@ -186,6 +186,21 @@ export async function localBranches(cwd, { signal, prefix = '' } = {}) {
186
186
  return lines(result.stdout);
187
187
  }
188
188
 
189
+ /** The local branch names the repository's remotes declare as their default (`refs/remotes/<remote>/HEAD`). */
190
+ export async function remoteDefaultBranches(cwd) {
191
+ const result = await runGit(cwd, ['for-each-ref', '--format=%(refname) %(symref)', 'refs/remotes'], {
192
+ readOnly: true,
193
+ });
194
+ const names = [];
195
+ for (const line of lines(result.stdout)) {
196
+ const [ref, target] = line.split(' ');
197
+ if (!ref.endsWith('/HEAD') || !target) continue;
198
+ const remote = ref.slice(0, -'HEAD'.length);
199
+ if (target.startsWith(remote)) names.push(target.slice(remote.length));
200
+ }
201
+ return names;
202
+ }
203
+
189
204
  export async function validBranchName(cwd, branch) {
190
205
  if (typeof branch !== 'string' || !branch.trim() || branch.startsWith('-')) return false;
191
206
  const result = await runGit(cwd, ['check-ref-format', '--branch', branch], { allowFailure: true });
@@ -49,6 +49,7 @@ import {
49
49
  WORKTREE_SNAPSHOT_KIND,
50
50
  worktreeBranchName,
51
51
  worktreeError,
52
+ worktreeRefusal,
52
53
  } from './contract.mjs';
53
54
  import {
54
55
  assertCarryable,
@@ -68,6 +69,7 @@ import {
68
69
  listWorktrees,
69
70
  localBranches,
70
71
  parseStatusPaths,
72
+ remoteDefaultBranches,
71
73
  runGit,
72
74
  throwIfAborted,
73
75
  validBranchName,
@@ -757,6 +759,11 @@ export async function resolveBranchWorkspace({
757
759
  repoCommonDir: repo.repoCommonDir,
758
760
  };
759
761
  }
762
+ // Checked out in a managed worktree, the branch a remote declares its
763
+ // default would stay locked away from the project folder.
764
+ if (!create && (await remoteDefaultBranches(repo.projectPath)).includes(branch)) {
765
+ throw worktreeError('WORKTREE_DEFAULT_BRANCH', undefined, { branch });
766
+ }
760
767
  const entry = await createManagedWorktree({
761
768
  projectPath,
762
769
  sessionId,
@@ -770,10 +777,6 @@ export async function resolveBranchWorkspace({
770
777
  confirmLarge,
771
778
  signal,
772
779
  });
773
- const record = await getRecord(entry.id);
774
- record.mergeTarget = repo.currentBranch;
775
- await putRecord(record);
776
- entry.mergeTarget = repo.currentBranch;
777
780
  return { path: entry.path, branch, entry, created: true, repoCommonDir: repo.repoCommonDir };
778
781
  });
779
782
  }
@@ -1219,7 +1222,10 @@ export async function createManagedWorktree({
1219
1222
  path: managedWorktreePath(repo.repoCommonDir, slug),
1220
1223
  branch,
1221
1224
  baseCommit,
1222
- mergeTarget,
1225
+ // An existing branch has no base it branched from: it merges where a
1226
+ // merge first takes it (recorded then), and until that its own work is
1227
+ // what it gained here since baseCommit.
1228
+ mergeTarget: existingBranch ? null : mergeTarget,
1223
1229
  createdAt: Date.now(),
1224
1230
  ownerSessionId: owner.sessionId,
1225
1231
  existingBranch,
@@ -1400,9 +1406,7 @@ export async function removeSessionSnapshots(sessionId, { txIds = null } = {}) {
1400
1406
  const only = txIds ? new Set(txIds) : null;
1401
1407
  const held = Object.values((await readRegistry()).entries).filter(
1402
1408
  (record) =>
1403
- record.kind === WORKTREE_SNAPSHOT_KIND &&
1404
- record.holderSessionId === sessionId &&
1405
- (!only || only.has(record.txId))
1409
+ record.kind === WORKTREE_SNAPSHOT_KIND && record.holderSessionId === sessionId && (!only || only.has(record.txId))
1406
1410
  );
1407
1411
  for (const record of held) {
1408
1412
  const row = { id: record.id, txId: record.txId };
@@ -1854,19 +1858,59 @@ export async function removeManagedWorktree({
1854
1858
  if (!current && !expected) throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
1855
1859
  return skipped;
1856
1860
  }
1857
- assertNotShared(current, sessionId);
1858
- if (current.state === 'creating' || current.state === 'recovery') {
1859
- const rollback = await rollbackCreation(current);
1860
- return rollback.complete ? { removed: true } : { removed: false, retryable: true, remaining: rollback.remaining };
1861
+ return removeLocked(current, { force, sessionId, preserveBranch, safeOnly });
1862
+ });
1863
+ }
1864
+
1865
+ /** The removal of `current`, under its repository lock. */
1866
+ async function removeLocked(current, { force, sessionId, preserveBranch = false, safeOnly = false }) {
1867
+ assertNotShared(current, sessionId);
1868
+ if (current.state === 'creating' || current.state === 'recovery') {
1869
+ const rollback = await rollbackCreation(current);
1870
+ return rollback.complete ? { removed: true } : { removed: false, retryable: true, remaining: rollback.remaining };
1871
+ }
1872
+ // Every deletion attempt of a snapshot, a resumed one included, waits
1873
+ // for (refuses on) its consumers.
1874
+ const release = current.kind === WORKTREE_SNAPSHOT_KIND ? holdSnapshotFolder(current) : () => {};
1875
+ try {
1876
+ return await decideAndFinishDeletion(current, { force, preserveBranch, safeOnly });
1877
+ } finally {
1878
+ release();
1879
+ }
1880
+ }
1881
+
1882
+ /**
1883
+ * Remove a worktree whose owner is gone, on an explicit request: under the
1884
+ * repository lock its retention is read again right before anything moves,
1885
+ * so an owner that came back (a live session, a running lease holder) or a
1886
+ * session the store still keeps it for refuses (WORKTREE_BUSY_JOBS). Then
1887
+ * the ordinary removal guards apply (dirty or unmerged work needs `force`).
1888
+ * @param {{ entryId: string, force?: boolean, liveSessionIds?: string[], retainedSessionIds?: string[] }} input
1889
+ */
1890
+ export async function removeOrphanedWorktree({
1891
+ entryId,
1892
+ force = false,
1893
+ liveSessionIds = [],
1894
+ retainedSessionIds = [],
1895
+ } = {}) {
1896
+ const record = await getRecord(entryId);
1897
+ if (!record) throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
1898
+ return withRepoLock(record.repoCommonDir, async () => {
1899
+ const current = await getRecord(entryId);
1900
+ if (!current || current.kind === WORKTREE_SNAPSHOT_KIND) {
1901
+ throw worktreeError('WORKTREE_MISSING', undefined, { entryId });
1861
1902
  }
1862
- // Every deletion attempt of a snapshot, a resumed one included, waits
1863
- // for (refuses on) its consumers.
1864
- const release = current.kind === WORKTREE_SNAPSHOT_KIND ? holdSnapshotFolder(current) : () => {};
1865
- try {
1866
- return await decideAndFinishDeletion(current, { force, preserveBranch, safeOnly });
1867
- } finally {
1868
- release();
1903
+ const { ownerLive, removable } = await retentionOf(current, new Set(liveSessionIds), new Set(retainedSessionIds));
1904
+ if (ownerLive) throw worktreeRefusal('workspace-lease', { entryId });
1905
+ if (!removable) {
1906
+ // Retained recovery data names itself; otherwise sessions still keep it.
1907
+ await assertRecoveryStoresClear(current);
1908
+ throw worktreeRefusal('workspace-retained', {
1909
+ entryId,
1910
+ sessions: [current.ownerSessionId, ...(current.sharedSessionIds ?? [])].filter(Boolean),
1911
+ });
1869
1912
  }
1913
+ return removeLocked(current, { force });
1870
1914
  });
1871
1915
  }
1872
1916
 
@@ -1900,25 +1944,7 @@ export async function listManagedWorktrees({ liveSessionIds = [], retainedSessio
1900
1944
  const retained = new Set(retainedSessionIds);
1901
1945
  const rows = [];
1902
1946
  for (const record of Object.values((await readRegistry()).entries)) {
1903
- const snapshot = record.kind === WORKTREE_SNAPSHOT_KIND;
1904
- const ownerLive = snapshot
1905
- ? await snapshotHolderLive(record, live)
1906
- : (!!record.ownerSessionId && live.has(record.ownerSessionId)) ||
1907
- leaseProcessAlive(await readOwnerLease(record));
1908
- const isRetained = !!record.ownerSessionId && retained.has(record.ownerSessionId);
1909
- // A snapshot outlives no holder: the prune removes it, changes and all.
1910
- let removable = snapshot
1911
- ? !ownerLive
1912
- : !record.sharedSessionIds?.length &&
1913
- (record.state !== 'ready' || (!ownerLive && (record.disposable === true || !isRetained)));
1914
- if (removable) {
1915
- try {
1916
- await assertRecoveryStoresClear(record);
1917
- } catch (error) {
1918
- if (error?.code !== 'WORKTREE_FILE_RECOVERY') throw error;
1919
- removable = false;
1920
- }
1921
- }
1947
+ const { ownerLive, retained: isRetained, removable } = await retentionOf(record, live, retained);
1922
1948
  rows.push({
1923
1949
  ...entryView(record),
1924
1950
  projectPath: record.projectPath,
@@ -1930,6 +1956,29 @@ export async function listManagedWorktrees({ liveSessionIds = [], retainedSessio
1930
1956
  return rows;
1931
1957
  }
1932
1958
 
1959
+ /** Who keeps `record` (listManagedWorktrees): `live` and `retained` are session id sets. */
1960
+ async function retentionOf(record, live, retained) {
1961
+ const snapshot = record.kind === WORKTREE_SNAPSHOT_KIND;
1962
+ const ownerLive = snapshot
1963
+ ? await snapshotHolderLive(record, live)
1964
+ : (!!record.ownerSessionId && live.has(record.ownerSessionId)) || leaseProcessAlive(await readOwnerLease(record));
1965
+ const isRetained = !!record.ownerSessionId && retained.has(record.ownerSessionId);
1966
+ // A snapshot outlives no holder: the prune removes it, changes and all.
1967
+ let removable = snapshot
1968
+ ? !ownerLive
1969
+ : !record.sharedSessionIds?.length &&
1970
+ (record.state !== 'ready' || (!ownerLive && (record.disposable === true || !isRetained)));
1971
+ if (removable) {
1972
+ try {
1973
+ await assertRecoveryStoresClear(record);
1974
+ } catch (error) {
1975
+ if (error?.code !== 'WORKTREE_FILE_RECOVERY') throw error;
1976
+ removable = false;
1977
+ }
1978
+ }
1979
+ return { ownerLive, retained: isRetained, removable };
1980
+ }
1981
+
1933
1982
  /**
1934
1983
  * Conservative reconcile (daemon startup, and the manual prune). Always:
1935
1984
  * resumes interrupted deletions, rolls back interrupted creations of no
@@ -1,26 +1,22 @@
1
1
  // Temporary git repositories and data dirs for the worktree suites.
2
2
  import '../git-direct.test-support.mjs';
3
3
  import { execFileSync } from 'node:child_process';
4
- import { appendFileSync, chmodSync, cpSync, existsSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
5
- import { tmpdir } from 'node:os';
4
+ import { appendFileSync, chmodSync, cpSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
6
5
  import { dirname, join } from 'node:path';
6
+ import { makeTempDirSync, removeTempDirSync } from '../../../../scripts/lib/scoped-temp.mjs';
7
7
 
8
8
  const created = [];
9
9
 
10
+ // A suite that never calls cleanupTempDirs still has its directories removed
11
+ // when the process exits.
10
12
  export function tempDir(prefix = 'mixdog-wt-') {
11
- const dir = mkdtempSync(join(tmpdir(), prefix));
13
+ const dir = makeTempDirSync(prefix);
12
14
  created.push(dir);
13
15
  return dir;
14
16
  }
15
17
 
16
18
  export function cleanupTempDirs() {
17
- for (const dir of created.splice(0)) {
18
- try {
19
- rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
20
- } catch {
21
- /* best effort */
22
- }
23
- }
19
+ for (const dir of created.splice(0)) removeTempDirSync(dir);
24
20
  }
25
21
 
26
22
  const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_EDITOR: 'true' };
@@ -163,6 +163,8 @@ export async function executeVisualizeTool(args, options = {}) {
163
163
  cwd: options.cwd || process.cwd(),
164
164
  action: validated.action,
165
165
  input: validated.input,
166
+ // The call's id: the host names it in its turn-end record of opens whose tab is gone.
167
+ ...(options.callId ? { callId: String(options.callId) } : {}),
166
168
  };
167
169
  let discovery = readBridgeDiscovery(DISCOVERY_FILE);
168
170
  if (!discovery) return toolError(unavailableMessage());
@@ -332,6 +332,9 @@ export function createRoutineResultQueue(
332
332
  rounds: section(doc, 'rounds'),
333
333
  // Reporting turns not yet queued, by submission id (agent-task-sessions.mjs).
334
334
  watches: section(doc, 'watches'),
335
+ // The bot-sent turns that hold no watch (reports off, or a watch that could
336
+ // not be stored), by submission id: work a restart re-attaches.
337
+ owned: section(doc, 'owned'),
335
338
  // Posted task results whose wake turn was not taken yet, per home.
336
339
  wakes: section(doc, 'wakes'),
337
340
  });
@@ -559,6 +562,26 @@ export function createRoutineResultQueue(
559
562
  return { ...doc, watches };
560
563
  });
561
564
  },
565
+ /** Keep `work` ({ runId, agentId, taskSessionId, owner, at }): a bot-sent
566
+ * turn that reports nothing, until its work ends. It is no watch: it
567
+ * owes no report, starts no hand-off and takes no part in a watch cap. */
568
+ async own(work) {
569
+ await update((doc) => ({ ...doc, owned: { ...doc.owned, [work.runId]: work } }));
570
+ },
571
+ /** Every stored record of bot-sent work that reports nothing, oldest first. */
572
+ async owned() {
573
+ return Object.values((await read()).owned);
574
+ },
575
+ /** Drop the records of `runIds` (a run id or a list; their work ended), in one write. */
576
+ async disown(runIds) {
577
+ const ids = [runIds].flat();
578
+ await update((doc) => {
579
+ if (!ids.some((runId) => doc.owned[runId])) return undefined;
580
+ const owned = { ...doc.owned };
581
+ for (const runId of ids) delete owned[runId];
582
+ return { ...doc, owned };
583
+ });
584
+ },
562
585
  /** The user spoke in `homeId`: its round trips count from zero again. */
563
586
  async resetRounds(homeId) {
564
587
  await update((doc) => {
@@ -1351,6 +1374,9 @@ export function createAgentHomeDelivery({
1351
1374
  unwatchTask: (runId) => queue.unwatch(runId),
1352
1375
  followTask: (runId, submissionId) => queue.followWatch(runId, submissionId),
1353
1376
  taskWatches: () => queue.watches(),
1377
+ ownTask: (work) => queue.own(work),
1378
+ disownTask: (runId) => queue.disown(runId),
1379
+ ownedTasks: () => queue.owned(),
1354
1380
  resetRounds: (homeId) => queue.resetRounds(homeId),
1355
1381
  };
1356
1382
  }
@@ -1446,6 +1472,21 @@ export async function followAgentTask(runId, submissionId) {
1446
1472
  return (await processDelivery()).followTask(runId, submissionId);
1447
1473
  }
1448
1474
 
1475
+ /** Keep the record of one bot-sent turn that reports nothing until its work ends. */
1476
+ export async function ownAgentTask(work) {
1477
+ return (await processDelivery()).ownTask(work);
1478
+ }
1479
+
1480
+ /** Drop the record of one bot-sent turn that reports nothing (its work ended). */
1481
+ export async function disownAgentTask(runId) {
1482
+ return (await processDelivery()).disownTask(runId);
1483
+ }
1484
+
1485
+ /** The bot-sent turns that report nothing and whose work was not seen to end. */
1486
+ export async function agentOwnedTasks() {
1487
+ return (await processDelivery()).ownedTasks();
1488
+ }
1489
+
1449
1490
  /** A user's own message in `homeId`: its round trips with every target restart. */
1450
1491
  export async function resetAgentTaskRounds(homeId) {
1451
1492
  return (await processDelivery()).resetRounds(homeId);