mixdog 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (423) hide show
  1. package/NOTICE.md +52 -9
  2. package/README.md +8 -4
  3. package/package.json +2 -2
  4. package/scripts/native-release-status.mjs +238 -0
  5. package/src/defaults/shipped-history.json +8 -1
  6. package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
  7. package/src/defaults/skills/setup/SKILL.md +23 -1
  8. package/src/defaults/skills/setup/references/actions.md +54 -4
  9. package/src/defaults/skills/setup/references/surfaces.md +23 -3
  10. package/src/defaults/skills/setup-guide/SKILL.md +17 -10
  11. package/src/defaults/skills/visualize/SKILL.md +9 -6
  12. package/src/defaults/skills/visualize/references/craft.md +8 -1
  13. package/src/help.mjs +1 -1
  14. package/src/lib/keychain-cjs.cjs +4 -1
  15. package/src/lib/rules-builder.cjs +14 -7
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent-mode/AGENT_MODE.md +9 -6
  18. package/src/rules/agent-mode/MIXY.md +11 -6
  19. package/src/rules/lead/LEAD.md +2 -2
  20. package/src/rules/shared/10-tool-workflow.md +4 -0
  21. package/src/rules/shared/20-research.md +2 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
  25. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
  26. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
  27. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
  29. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
  32. package/src/runtime/agent/orchestrator/config.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +81 -16
  34. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -4
  35. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
  36. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  37. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
  38. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  43. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
  44. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +34 -16
  45. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
  46. package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
  47. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
  49. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -1
  50. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
  51. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  52. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
  53. package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
  54. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +10 -2
  60. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
  65. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  68. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
  69. package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
  72. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +5 -5
  74. package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
  75. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +17 -6
  78. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +41 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +6 -4
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
  83. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
  84. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
  85. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +21 -2
  86. package/src/runtime/agent/orchestrator/session/agent-room.mjs +149 -45
  87. package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
  88. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
  90. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
  91. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
  92. package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
  93. package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
  94. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
  96. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
  97. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
  99. package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
  100. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +156 -25
  101. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
  102. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
  103. package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
  104. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
  105. package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
  106. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
  107. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
  108. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
  110. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +111 -9
  111. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -14
  112. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
  116. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  119. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
  120. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  124. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
  125. package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
  126. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
  128. package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  131. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
  132. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  133. package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
  134. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +62 -47
  135. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +6 -8
  136. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
  137. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
  138. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  139. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
  140. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  141. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
  142. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  143. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
  144. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
  145. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +35 -4
  146. package/src/runtime/agent/orchestrator/session/routine-result.mjs +179 -16
  147. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
  148. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
  149. package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
  150. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
  151. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
  152. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
  153. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
  154. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
  155. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
  156. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
  157. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
  158. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
  159. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
  160. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
  161. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +49 -14
  162. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +134 -17
  163. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
  164. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
  165. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
  166. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
  167. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
  168. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
  169. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
  170. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
  171. package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
  175. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
  176. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
  177. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
  178. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
  179. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
  180. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
  181. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
  182. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
  183. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
  184. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
  185. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
  186. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
  187. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
  188. package/src/runtime/attachments/attachment-reference.mjs +155 -0
  189. package/src/runtime/attachments/limits.mjs +5 -1
  190. package/src/runtime/attachments/office-extract.mjs +3 -6
  191. package/src/runtime/attachments/pdf-extract.mjs +84 -10
  192. package/src/runtime/attachments/store.mjs +55 -30
  193. package/src/runtime/browser-bridge/client.mjs +73 -20
  194. package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
  195. package/src/runtime/channels/lib/scheduler.mjs +14 -7
  196. package/src/runtime/channels/lib/state-file.mjs +15 -15
  197. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
  198. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
  199. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
  200. package/src/runtime/channels/lib/worker-main.mjs +3 -2
  201. package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
  202. package/src/runtime/effort-judge/judge-client.mjs +6 -3
  203. package/src/runtime/effort-judge/model-install.mjs +16 -8
  204. package/src/runtime/effort-judge/model-manifest.json +7 -7
  205. package/src/runtime/local-provider/asset-installer.mjs +10 -4
  206. package/src/runtime/local-provider/catalog.mjs +5 -2
  207. package/src/runtime/local-provider/registered-models.mjs +64 -27
  208. package/src/runtime/local-provider/server.mjs +2 -3
  209. package/src/runtime/media/adapters/image-api.mjs +89 -0
  210. package/src/runtime/media/auth.mjs +64 -1
  211. package/src/runtime/media/catalog.mjs +66 -4
  212. package/src/runtime/media/jobs.mjs +4 -0
  213. package/src/runtime/media/lanes.mjs +46 -5
  214. package/src/runtime/media/media-usage.mjs +17 -0
  215. package/src/runtime/media/renditions.mjs +15 -15
  216. package/src/runtime/media/store.mjs +36 -35
  217. package/src/runtime/media/tool.mjs +8 -1
  218. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
  219. package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
  220. package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
  221. package/src/runtime/memory/lib/pg/process.mjs +8 -4
  222. package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
  223. package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
  224. package/src/runtime/office/core/journal.mjs +7 -14
  225. package/src/runtime/office/design/library/design-library-core.mjs +4 -16
  226. package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
  227. package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
  228. package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
  229. package/src/runtime/office/pdf/pdf-render.mjs +30 -18
  230. package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
  231. package/src/runtime/office/portable/portable-soffice.mjs +4 -0
  232. package/src/runtime/shared/agent-kind-migration.mjs +200 -0
  233. package/src/runtime/shared/agent-kind.mjs +152 -0
  234. package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
  235. package/src/runtime/shared/agent-message-envelope.mjs +44 -0
  236. package/src/runtime/shared/agent-reap-state.d.mts +4 -0
  237. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  238. package/src/runtime/shared/agent-route-config.mjs +22 -8
  239. package/src/runtime/shared/agent-store.mjs +149 -0
  240. package/src/runtime/shared/agent-task-starts.mjs +27 -0
  241. package/src/runtime/shared/atomic-file.d.mts +50 -0
  242. package/src/runtime/shared/atomic-file.mjs +394 -132
  243. package/src/runtime/shared/background-tasks.mjs +7 -4
  244. package/src/runtime/shared/bot-hops.mjs +30 -0
  245. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  246. package/src/runtime/shared/bounded-download.mjs +4 -2
  247. package/src/runtime/shared/config-patch.mjs +20 -1
  248. package/src/runtime/shared/config.mjs +39 -9
  249. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  250. package/src/runtime/shared/connector-catalog.mjs +2 -4
  251. package/src/runtime/shared/connector-triggers.mjs +2 -0
  252. package/src/runtime/shared/defaults-separation.mjs +6 -2
  253. package/src/runtime/shared/delegated-approval-host.mjs +74 -0
  254. package/src/runtime/shared/err-text.mjs +7 -1
  255. package/src/runtime/shared/error-presentation.mjs +113 -10
  256. package/src/runtime/shared/file-has-data.mjs +28 -0
  257. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  258. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  259. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  260. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  261. package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
  262. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  263. package/src/runtime/shared/pending-agent-work.mjs +54 -0
  264. package/src/runtime/shared/plain-preview.mjs +22 -17
  265. package/src/runtime/shared/process-lifecycle.mjs +8 -12
  266. package/src/runtime/shared/projects.mjs +50 -41
  267. package/src/runtime/shared/provider-accounts.mjs +17 -12
  268. package/src/runtime/shared/routine-trigger.d.mts +6 -1
  269. package/src/runtime/shared/routine-trigger.mjs +14 -6
  270. package/src/runtime/shared/run-child.d.mts +3 -0
  271. package/src/runtime/shared/run-child.mjs +24 -3
  272. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  273. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  274. package/src/runtime/shared/session-baseline-store.mjs +8 -2
  275. package/src/runtime/shared/singleton-owner.mjs +7 -6
  276. package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
  277. package/src/runtime/shared/staged-update.mjs +4 -2
  278. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
  279. package/src/runtime/shared/tool-execution-contract.mjs +6 -0
  280. package/src/runtime/shared/transcript-metadata.mjs +28 -3
  281. package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
  282. package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
  283. package/src/runtime/shared/turn-snapshot.mjs +44 -2
  284. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  285. package/src/runtime/shared/user-cwd.mjs +9 -4
  286. package/src/runtime/shared/user-data-guard.mjs +46 -44
  287. package/src/runtime/shared/utf8-cut.mjs +10 -0
  288. package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
  289. package/src/runtime/shared/worktrees/registry.mjs +26 -26
  290. package/src/runtime/tidy/core-install.mjs +26 -4
  291. package/src/runtime/tidy/install.mjs +91 -23
  292. package/src/runtime/web-search/lib/config.mjs +4 -16
  293. package/src/session-runtime/agent-home-delivery.mjs +859 -136
  294. package/src/session-runtime/agent-room-delivery.mjs +167 -19
  295. package/src/session-runtime/agent-task-sessions.mjs +831 -69
  296. package/src/session-runtime/agent-tool-routing.mjs +1 -0
  297. package/src/session-runtime/boot/apis.mjs +58 -24
  298. package/src/session-runtime/boot/config.mjs +1 -0
  299. package/src/session-runtime/boot/facade.mjs +11 -1
  300. package/src/session-runtime/boot/infrastructure.mjs +14 -2
  301. package/src/session-runtime/boot/providers.mjs +16 -6
  302. package/src/session-runtime/boot/shared.mjs +12 -6
  303. package/src/session-runtime/boot/tools.mjs +2 -2
  304. package/src/session-runtime/bot-status.mjs +432 -0
  305. package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
  306. package/src/session-runtime/config-lifecycle.mjs +78 -32
  307. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
  308. package/src/session-runtime/handoff-attachments.mjs +593 -0
  309. package/src/session-runtime/held-requests.mjs +36 -33
  310. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
  311. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
  312. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +45 -6
  313. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
  314. package/src/session-runtime/lifecycle/teardown.mjs +11 -3
  315. package/src/session-runtime/mcp-glue/connect.mjs +100 -186
  316. package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
  317. package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
  318. package/src/session-runtime/media-api.mjs +5 -2
  319. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  320. package/src/session-runtime/model-route/route-tuning.mjs +11 -2
  321. package/src/session-runtime/model-route/set-route.mjs +82 -2
  322. package/src/session-runtime/native-web-search.mjs +2 -2
  323. package/src/session-runtime/new-session-config.mjs +6 -1
  324. package/src/session-runtime/owned-automation-run.mjs +8 -1
  325. package/src/session-runtime/room-member-gate.mjs +16 -3
  326. package/src/session-runtime/route-resolution.mjs +21 -19
  327. package/src/session-runtime/route-state.mjs +1 -59
  328. package/src/session-runtime/routine-home-turn.mjs +5 -3
  329. package/src/session-runtime/runtime-bootstrap.mjs +5 -3
  330. package/src/session-runtime/runtime-paths.mjs +11 -5
  331. package/src/session-runtime/runtime-tunables.mjs +6 -0
  332. package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
  333. package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
  334. package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
  335. package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
  336. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  337. package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
  338. package/src/session-runtime/services/agent-tool/send-flow.mjs +36 -24
  339. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
  340. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
  341. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +12 -6
  342. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  343. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  344. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
  345. package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
  346. package/src/session-runtime/services/agent-tool.mjs +6 -0
  347. package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
  348. package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
  349. package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
  350. package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
  351. package/src/session-runtime/services/plugin-admin.mjs +26 -26
  352. package/src/session-runtime/services/provider-admin.mjs +14 -4
  353. package/src/session-runtime/services/schedule-access.mjs +33 -4
  354. package/src/session-runtime/services/seeds.mjs +4 -3
  355. package/src/session-runtime/services/usage-dashboard.mjs +1 -1
  356. package/src/session-runtime/services/usage-stats-model.mjs +2 -1
  357. package/src/session-runtime/session-create/materialize.mjs +2 -3
  358. package/src/session-runtime/session-create/session-options.mjs +15 -2
  359. package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
  360. package/src/session-runtime/settings-compaction-api.mjs +86 -39
  361. package/src/session-runtime/setup-tool/executor.mjs +20 -3
  362. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  363. package/src/session-runtime/setup-tool/settings-contract.mjs +37 -1
  364. package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
  365. package/src/session-runtime/skills-api.mjs +12 -5
  366. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
  367. package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
  368. package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
  369. package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
  370. package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
  371. package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
  372. package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
  373. package/src/session-runtime/tool-policy-refresh.mjs +47 -358
  374. package/src/session-runtime/tool-surface.mjs +5 -3
  375. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +99 -81
  376. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  377. package/src/session-runtime/turn/turn-run.mjs +2 -1
  378. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
  379. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +578 -301
  380. package/src/session-runtime/workflow-agents-api/agent-route.mjs +113 -53
  381. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  382. package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
  383. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
  384. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
  385. package/src/session-runtime/workspace/workspace-state.mjs +7 -6
  386. package/src/standalone/daemon-agent-control.mjs +4 -1
  387. package/src/standalone/daemon-session-control.mjs +60 -11
  388. package/src/standalone/daemon.mjs +34 -4
  389. package/src/standalone/local-session-runtime.mjs +22 -14
  390. package/src/standalone/session-client.mjs +7 -2
  391. package/src/standalone/session-protocol.mjs +7 -0
  392. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  393. package/src/standalone/session-service/agent-tree.mjs +4 -3
  394. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  395. package/src/standalone/session-service.mjs +11 -0
  396. package/src/standalone/session-state-projection.mjs +19 -9
  397. package/src/standalone/session-tool-output.mjs +35 -9
  398. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
  399. package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
  400. package/src/tui/app/slash-commands.mjs +1 -1
  401. package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
  402. package/src/tui/dist/index.mjs +599 -342
  403. package/src/tui/prompt-history-store.mjs +16 -21
  404. package/src/tui/session/context-state.mjs +4 -0
  405. package/src/tui/session/notice-surface.mjs +3 -4
  406. package/src/tui/session/session-api/intake/abort.mjs +19 -1
  407. package/src/tui/session/session-api/media.mjs +2 -1
  408. package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
  409. package/src/tui/session/session-api/routes-write.mjs +203 -0
  410. package/src/tui/session/session-api/routes.mjs +23 -236
  411. package/src/tui/session/session-api/settings.mjs +14 -1
  412. package/src/tui/session/session-flow/drain.mjs +28 -3
  413. package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
  414. package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
  415. package/src/tui/session/session-flow/steering.mjs +8 -3
  416. package/src/tui/session/tool-approval.mjs +185 -20
  417. package/src/tui/session/transcript-intake.mjs +4 -0
  418. package/src/tui/session/turn-live-call.mjs +11 -3
  419. package/src/tui/session/turn.mjs +36 -17
  420. package/src/tui/transcript-tool-failures.mjs +8 -2
  421. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
  422. package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
  423. package/src/rules/agent-mode/ROUTINE.md +0 -6
@@ -0,0 +1,256 @@
1
+ // The credential / secret file deny list, shared by every lane that hands a
2
+ // local file to someone else: the desktop's remote (paired-device) file lanes
3
+ // (apps/desktop/src/main/remote-sensitive-paths.ts adds its registered private
4
+ // roots and async canonicalization) and the runtime's attachment hand-offs
5
+ // between bots and agents (session-runtime/handoff-attachments.mjs).
6
+ //
7
+ // Matching is on SEGMENTS of a path, lower-cased on every platform, so a
8
+ // denied name cannot hide behind letter case, trailing dots/spaces or an NTFS
9
+ // stream suffix. Callers pass the canonical (realpath) form as well as the
10
+ // lexical one, so a link cannot reach a denied place under an innocent name.
11
+ import { realpathSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { dirname, join, resolve } from 'node:path';
14
+
15
+ /** A directory with this name anywhere on the path holds credentials. `.git`
16
+ * is one of them: its config carries remotes with embedded tokens and
17
+ * credential helpers, and its objects hold every secret ever committed. */
18
+ const SECRET_DIRECTORIES = new Set([
19
+ '.ssh',
20
+ '.gnupg',
21
+ '.aws',
22
+ '.azure',
23
+ '.kube',
24
+ '.mozilla',
25
+ '.password-store',
26
+ '.git',
27
+ ]);
28
+
29
+ /** Consecutive segments anywhere on the path that name a credential store:
30
+ * CLI configs, browser profiles and OS credential stores. */
31
+ const SECRET_SEQUENCES = [
32
+ // Tool credentials in the user's home.
33
+ ['.docker', 'config.json'],
34
+ ['.config', 'gcloud'],
35
+ ['appdata', 'roaming', 'gcloud'],
36
+ ['.config', 'gh'],
37
+ ['appdata', 'roaming', 'github cli'],
38
+ ['.config', 'git', 'credentials'],
39
+ ['.cargo', 'credentials'],
40
+ ['.cargo', 'credentials.toml'],
41
+ ['.terraform.d', 'credentials.tfrc.json'],
42
+ ['.claude', '.credentials.json'],
43
+ ['.codex', 'auth.json'],
44
+ ['.gemini', 'oauth_creds.json'],
45
+ // Browser profiles: cookies, saved logins, session tokens.
46
+ ['google', 'chrome', 'user data'],
47
+ ['microsoft', 'edge', 'user data'],
48
+ ['bravesoftware', 'brave-browser'],
49
+ ['chromium', 'user data'],
50
+ ['vivaldi', 'user data'],
51
+ ['opera software'],
52
+ ['mozilla', 'firefox'],
53
+ ['application support', 'google', 'chrome'],
54
+ ['application support', 'microsoft edge'],
55
+ ['application support', 'firefox'],
56
+ ['application support', 'chromium'],
57
+ ['application support', 'bravesoftware'],
58
+ ['application support', 'vivaldi'],
59
+ ['library', 'safari'],
60
+ ['library', 'cookies'],
61
+ ['.config', 'google-chrome'],
62
+ ['.config', 'chromium'],
63
+ ['.config', 'microsoft-edge'],
64
+ ['.config', 'bravesoftware'],
65
+ ['.config', 'vivaldi'],
66
+ ['snap', 'firefox'],
67
+ ['snap', 'chromium'],
68
+ ['.var', 'app', 'org.mozilla.firefox'],
69
+ ['.var', 'app', 'com.google.chrome'],
70
+ // OS credential stores.
71
+ ['microsoft', 'credentials'],
72
+ ['microsoft', 'protect'],
73
+ ['microsoft', 'vault'],
74
+ ['windows', 'system32', 'config'],
75
+ ['library', 'keychains'],
76
+ ['.local', 'share', 'keyrings'],
77
+ ['.local', 'share', 'kwalletd'],
78
+ ['etc', 'shadow'],
79
+ ['etc', 'gshadow'],
80
+ ['etc', 'ssh'],
81
+ ['etc', 'ssl', 'private'],
82
+ ];
83
+
84
+ const KEY_EXTENSIONS = new Set([
85
+ '.pem',
86
+ '.key',
87
+ '.p12',
88
+ '.pfx',
89
+ '.p8',
90
+ '.ppk',
91
+ '.jks',
92
+ '.keystore',
93
+ '.kdbx',
94
+ '.keychain',
95
+ '.keychain-db',
96
+ ]);
97
+ const CREDENTIAL_FILES = new Set([
98
+ '.netrc',
99
+ '_netrc',
100
+ '.npmrc',
101
+ '.pypirc',
102
+ '.git-credentials',
103
+ '.pgpass',
104
+ '.vault-token',
105
+ '.htpasswd',
106
+ '.dockercfg',
107
+ '.envrc',
108
+ '.s3cfg',
109
+ 'kubeconfig',
110
+ ]);
111
+ /** Environment files: every `.env*` name (`.env`, `.env.local`, `.envrc`,
112
+ * `.environment`, and the `.env.example` templates too — a template is
113
+ * checked in often enough with real values that no name is exempt) and
114
+ * `<stage>.env`. */
115
+ const ENV_FILE = /^(?:\.env.*|[^.]{1,64}(?:\.[^.]{1,64}){0,3}\.env)$/;
116
+ /** The dotenv templates a person may still open on purpose from a paired
117
+ * device (the desktop's remote file lanes, `envTemplates: true`); automatic
118
+ * transfers between bots and agents never get this allowance. */
119
+ const ENV_TEMPLATES = new Set(['.env.example', '.env.sample', '.env.template', '.env.dist']);
120
+ /** Credential JSON a cloud or OAuth client writes or downloads:
121
+ * `credentials.json`, `*credentials*.json`, `service-account*.json`,
122
+ * `client_secret*.json`. */
123
+ const CREDENTIAL_JSON = /^(?:.*credential.*|service[-_ ]?account.*|client[-_]?secret.*)\.json$/;
124
+ /** OpenSSH private keys (`id_ed25519`, `id_rsa_work`, `id_ecdsa_sk`, any
125
+ * `id_<type>`): no extension, so the public `.pub` half and documents named
126
+ * after a key type (`id_rsa_notes.md`) are not keys. */
127
+ const SSH_PRIVATE_KEY = /^id_[a-z0-9]{2,16}(?:[_-][^._-]{1,32}){0,3}$/;
128
+ /** A copy an editor or tool leaves beside a file (`x.bak`, `x.old`, `x~`,
129
+ * `x.1`): it holds what the original holds. */
130
+ const BACKUP_SUFFIX = /(?:\.(?:bak|backup|old|orig|prev|save|sav|swp|swo|tmp|copy|\d{1,3})|~)$/;
131
+ const MAX_BACKUP_SUFFIXES = 3;
132
+
133
+ /** The name a backup copy was made of (`.npmrc.bak.1` → `.npmrc`). */
134
+ function backupOriginal(name) {
135
+ let original = name;
136
+ for (let strip = 0; strip < MAX_BACKUP_SUFFIXES; strip += 1) {
137
+ const next = original.replace(BACKUP_SUFFIX, '');
138
+ if (next === original || !next) break;
139
+ original = next;
140
+ }
141
+ return original;
142
+ }
143
+
144
+ /** A file whose own name says it holds a secret, wherever it lives: a
145
+ * credential file, a dotenv file, a key or keystore, or a backup of one. */
146
+ function secretFileName(name, { envTemplates = false } = {}) {
147
+ const original = backupOriginal(name);
148
+ if (CREDENTIAL_FILES.has(original) || CREDENTIAL_JSON.test(original)) return true;
149
+ if (ENV_FILE.test(original)) return !(envTemplates && ENV_TEMPLATES.has(original));
150
+ const dot = original.lastIndexOf('.');
151
+ if (dot >= 0 && KEY_EXTENSIONS.has(original.slice(dot))) return true;
152
+ return SSH_PRIVATE_KEY.test(original);
153
+ }
154
+
155
+ // Mixdog's own state. Its home and data folders also hold what tools write
156
+ // (tool results, shell output, media), which stay reachable; only the files
157
+ // and folders that carry provider tokens, API keys, pairing and bridge secrets
158
+ // are denied.
159
+ const MIXDOG_SECRET_DIRECTORIES = new Set([
160
+ 'secrets',
161
+ 'mcp-oauth',
162
+ 'provider-accounts',
163
+ 'telegram',
164
+ 'relay-data',
165
+ 'dev-profile',
166
+ ]);
167
+ const MIXDOG_SECRET_FILE =
168
+ /^\.?(mixdog-config|provider-accounts|relay-hook-device|auth\.json|[a-z0-9-]+-oauth(-credentials)?\.json|[a-z0-9-]+-bridge\.json)/;
169
+ const MIXDOG_SECRET_EXTENSIONS = new Set(['.dpapi', '.token']);
170
+
171
+ function mixdogSecret(rest) {
172
+ if (rest.some((segment) => MIXDOG_SECRET_DIRECTORIES.has(segment))) return true;
173
+ const name = rest[rest.length - 1] ?? '';
174
+ const dot = name.lastIndexOf('.');
175
+ return MIXDOG_SECRET_FILE.test(name) || MIXDOG_SECRET_EXTENSIONS.has(dot >= 0 ? name.slice(dot) : '');
176
+ }
177
+
178
+ /** The segments of a path as the rules compare them: no extended-path prefix,
179
+ * no NTFS stream suffix, no trailing dots or spaces, lower case. */
180
+ export function sensitivePathSegments(path) {
181
+ return String(path)
182
+ .replace(/^[\\/]{2}[?.][\\/]/, '')
183
+ .split(/[\\/]+/)
184
+ .map((segment) =>
185
+ segment
186
+ .split(':')[0]
187
+ .replace(/[. ]+$/, '')
188
+ .toLowerCase()
189
+ )
190
+ .filter(Boolean);
191
+ }
192
+
193
+ function hasRun(segments, run) {
194
+ for (let start = 0; start + run.length <= segments.length; start += 1) {
195
+ if (run.every((segment, offset) => segments[start + offset] === segment)) return true;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ /**
201
+ * Whether `path` is denied under `within`: scopes `{ root: segments, except:
202
+ * subfolder names, whole }` — Mixdog's home and data folders (`whole: false`,
203
+ * only their secret subset is denied) and registered private roots (`whole:
204
+ * true`, everything but `except`). Pass both the lexical and canonical form.
205
+ * `envTemplates`: a user-initiated remote open may read the dotenv
206
+ * templates; nothing else gets that allowance.
207
+ */
208
+ export function matchesSensitivePath(path, within = [], { envTemplates = false } = {}) {
209
+ const segments = sensitivePathSegments(resolve(path));
210
+ const name = segments[segments.length - 1] ?? '';
211
+ if (segments.some((segment) => SECRET_DIRECTORIES.has(segment))) return true;
212
+ if (secretFileName(name, { envTemplates }) || SECRET_SEQUENCES.some((run) => hasRun(segments, run))) return true;
213
+ const containing = within.filter(
214
+ ({ root }) => root.length > 0 && root.every((segment, index) => segments[index] === segment)
215
+ );
216
+ // Mixdog's home and data folders follow Mixdog's own rule even inside a
217
+ // registered private root: a dev or isolated profile keeps them in the app
218
+ // profile, and the tool output and media there stay reachable.
219
+ const own = containing.filter(({ whole }) => !whole);
220
+ if (own.length) return own.some(({ root }) => mixdogSecret(segments.slice(root.length)));
221
+ return containing.some(({ root, except }) => {
222
+ const rest = segments.slice(root.length);
223
+ return rest.length > 0 && !except.includes(rest[0]);
224
+ });
225
+ }
226
+
227
+ function realOrSelf(path) {
228
+ try {
229
+ return realpathSync.native(path);
230
+ } catch {
231
+ const parent = dirname(path);
232
+ return parent === path ? path : join(realOrSelf(parent), path.slice(parent.length).replace(/^[\\/]+/, ''));
233
+ }
234
+ }
235
+
236
+ /** Mixdog's home and data folders (MIXDOG_HOME / MIXDOG_DATA_DIR, plus any
237
+ * extra data folder the caller names) as `matchesSensitivePath` scopes. */
238
+ export function mixdogSensitiveScopes(extraData = []) {
239
+ const home = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
240
+ const roots = [home, process.env.MIXDOG_DATA_DIR || join(home, 'data'), ...extraData].filter(Boolean);
241
+ const scopes = [];
242
+ for (const root of roots) {
243
+ for (const form of new Set([resolve(root), realOrSelf(resolve(root))])) {
244
+ scopes.push({ root: sensitivePathSegments(form), except: [], whole: false });
245
+ }
246
+ }
247
+ return scopes;
248
+ }
249
+
250
+ /** Whether `path` — its own name or what its links lead to — is a
251
+ * credential/secret file under the shared deny list and Mixdog's own scopes. */
252
+ export function isSensitiveLocalPath(path, { extraData = [] } = {}) {
253
+ const within = mixdogSensitiveScopes(extraData);
254
+ const absolute = resolve(path);
255
+ return matchesSensitivePath(absolute, within) || matchesSensitivePath(realOrSelf(absolute), within);
256
+ }
@@ -809,8 +809,14 @@ export function _unsavedBytesForTest(sessionId) {
809
809
  async function readBlob(sessionId, entry) {
810
810
  if (!entry?.blob) return undefined;
811
811
  try {
812
- const bytes = await readFile(join(blobDir(sessionId), entry.blob));
813
- return digest(bytes) === entry.blob ? bytes : undefined;
812
+ const path = join(blobDir(sessionId), entry.blob);
813
+ const bytes = await readFile(path);
814
+ if (digest(bytes) === entry.blob) return bytes;
815
+ // A torn blob (unflushed write cut by a crash). Blobs are published whole
816
+ // and never replaced, so a mismatch is damage: drop it, or the create-only
817
+ // write of a later capture of the same content would skip over it.
818
+ await rm(path, { force: true });
819
+ return undefined;
814
820
  } catch {
815
821
  return undefined;
816
822
  }
@@ -1,6 +1,6 @@
1
- import { mkdirSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
1
+ import { rmdirSync, rmSync, statSync } from 'node:fs';
2
2
  import { readFile, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
3
- import { dirname } from 'node:path';
3
+ import { writeJsonAtomicSync } from './atomic-file.mjs';
4
4
  import { withFileLock, withFileLockSync } from './file-lock.mjs';
5
5
  import { readJsonSafe } from './json-file.mjs';
6
6
  import { isPidAlive } from './pid-liveness.mjs';
@@ -19,11 +19,12 @@ const CLAIM_LOCK_STALE_MS = 5000;
19
19
  const CLAIM_LOCK_OPTS = Object.freeze({ timeoutMs: CLAIM_LOCK_TIMEOUT_MS, staleMs: CLAIM_LOCK_STALE_MS });
20
20
  const BUSY = Symbol('claim lock busy');
21
21
 
22
+ // A claim or handoff is flushed before it is published. The heartbeat's async
23
+ // refresh below is not: it rewrites the record every few seconds, and a record
24
+ // a crash leaves NUL reads as "no owner" (readJsonSafe), so the next claim
25
+ // simply takes over.
22
26
  function writeJsonAtomic(path, value) {
23
- mkdirSync(dirname(path), { recursive: true });
24
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
25
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
26
- renameSync(tmp, path);
27
+ writeJsonAtomicSync(path, value);
27
28
  }
28
29
 
29
30
  // Held by another live process or this one's async holder ('busy'). Any other
@@ -1,8 +1,9 @@
1
1
  // Explicit skill dependencies. Imported manifests are read, never rewritten.
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readFileSync, rmSync, statSync } from 'node:fs';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { parseDocument } from 'yaml';
6
+ import { writeFileAtomicSync } from './atomic-file.mjs';
6
7
  import { resolvePluginData } from './plugin-paths.mjs';
7
8
 
8
9
  const MAX_METADATA_BYTES = 64 * 1024;
@@ -119,12 +120,5 @@ export function saveSkillToolDependencies(filePath, dependencies) {
119
120
  const tools = normalizeSkillToolDependencies(dependencies, { strict: true });
120
121
  const content = `${JSON.stringify({ dependencies: { tools } }, null, 2)}\n`;
121
122
  if (Buffer.byteLength(content) > MAX_METADATA_BYTES) throw new Error('Skill dependency metadata is too large.');
122
- mkdirSync(dirname(path), { recursive: true });
123
- const temporary = `${path}.${randomUUID()}.tmp`;
124
- try {
125
- writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
126
- renameSync(temporary, path);
127
- } finally {
128
- rmSync(temporary, { force: true });
129
- }
123
+ writeFileAtomicSync(path, content);
130
124
  }
@@ -50,7 +50,7 @@ import { resolvePluginData } from './plugin-paths.mjs';
50
50
  import { isPidAlive as pidAlive } from './pid-liveness.mjs';
51
51
  import { sleepSync } from './sleep.mjs';
52
52
  import { detachedSpawnOpts, hiddenSpawnOpts } from './spawn-flags.mjs';
53
- import { renameWithRetrySync } from './atomic-file.mjs';
53
+ import { renameWithRetrySync, writeJsonAtomicSync } from './atomic-file.mjs';
54
54
  import {
55
55
  isDevInstall,
56
56
  localPackageVersion,
@@ -295,7 +295,9 @@ export async function runStagedInstall(version) {
295
295
 
296
296
  if (!existsSync(join(pkgDir, 'src', 'cli.mjs'))) return { ok: false, error: 'staged cli.mjs missing' };
297
297
  // Marker written LAST — its presence is the completion signal for the swap.
298
- writeFileSync(markerPath(verDir), JSON.stringify({ version: v, pkgDir, stagedAt: Date.now() }), 'utf8');
298
+ // Published whole and flushed: a NUL marker would read as "not staged" and
299
+ // force a full re-install.
300
+ writeJsonAtomicSync(markerPath(verDir), { version: v, pkgDir, stagedAt: Date.now() }, { compact: true });
299
301
  return { ok: true, version: v, dir: pkgDir };
300
302
  } catch (err) {
301
303
  return { ok: false, error: err?.message || String(err) };
@@ -197,11 +197,11 @@ async function refreshShellJobsStatus(ownerPid) {
197
197
  }
198
198
  // A killed command whose processes are not confirmed gone has already
199
199
  // reported its result, so its tool card is closed: it is listed until
200
- // its owner retires the record. Its root pid may be gone while a
201
- // descendant lives, so the owner's liveness decides, not the root's.
200
+ // its owner retires the record. An incomplete observation can leave no
201
+ // known survivors even after the owner session closes; only the owning
202
+ // runtime can confirm termination, so its liveness decides, not the root's.
202
203
  const unconfirmed = detail?.terminationUnconfirmed === true;
203
- if (unconfirmed ? !isHeldShellJobAlive(detail, ownerByJob.get(id)) : !(await isShellJobAlive(detail, p, dir, id)))
204
- continue;
204
+ if (unconfirmed ? !isPidAlive(ownerByJob.get(id) || 0) : !(await isShellJobAlive(detail, p, dir, id))) continue;
205
205
  // A foreground command is awaited in its own tool card; listing it here
206
206
  // made every short command flash in and out of the shell list. Only
207
207
  // backgrounded jobs (including promoted ones) are background work.
@@ -327,17 +327,6 @@ function summarizeShellJobs({ count, oldestMs, jobs, bySession }) {
327
327
  return { count, elapsedLabel, jobs, sessions };
328
328
  }
329
329
 
330
- // A held record lives while its owner process does. Once its owner session
331
- // has closed, it also needs a process left to hold: the root or a survivor
332
- // its termination watch last reported. Without one it is an orphan whose
333
- // processes are gone, and it resolves instead of staying unconfirmed.
334
- function isHeldShellJobAlive(detail, ownerPid) {
335
- if (!isPidAlive(ownerPid || 0)) return false;
336
- if (detail?.ownerSessionClosed !== true) return true;
337
- const survivors = Array.isArray(detail?.survivorPids) ? detail.survivorPids : [];
338
- return [detail?.pid, ...survivors].some((pid) => isPidAlive(positiveInt(pid)));
339
- }
340
-
341
330
  async function isShellJobAlive(detail, detailPath, dir, id) {
342
331
  const pid = positiveInt(detail?.pid);
343
332
  if (!pid) return false;
@@ -236,6 +236,12 @@ export function isTranscriptHiddenToolItem(item) {
236
236
  // stopped, and the restored transcript must keep that row
237
237
  // (scripts/turn-checkpoint-crash-test.mjs).
238
238
  const INTERNAL_TRANSCRIPT_INTERRUPT_RE = /^\[request interrupted by user(?: for tool use)?\]$/i;
239
+ // Every runtime control row written as a user turn when a turn is cut short
240
+ // (including the bare "[Request interrupted]" a kept Stop prompt gets). One
241
+ // pattern for "not real conversation": realTurnCount and the preview/title
242
+ // noise filter both use it.
243
+ export const CONTROL_MARKER_RE =
244
+ /^\[(?:truncated|request interrupted(?: by user(?: for tool use)?| by process restart)?)\]$/i;
239
245
  const TRANSCRIPT_CANCELLED_STATUS_RE = /^\[request interrupted(?: by process restart)?\]$/i;
240
246
 
241
247
  // Crash/implicit interruption markers remain in model-visible history for
@@ -53,6 +53,20 @@ export function normalizeMessageAttachments(value) {
53
53
  return out;
54
54
  }
55
55
 
56
+ /**
57
+ * Who a user-kind row came from, as the row's own fields: the sender, the
58
+ * submitting device and the bot that sent it. One definition for the delivered
59
+ * steering row and the still-queued one, so both render the same.
60
+ */
61
+ export function transcriptSourceFields(value) {
62
+ if (!value || typeof value !== 'object') return {};
63
+ return {
64
+ ...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
65
+ ...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
66
+ ...(typeof value.fromAgent === 'string' && value.fromAgent ? { fromAgent: value.fromAgent } : {}),
67
+ };
68
+ }
69
+
56
70
  /** A new stable message id (`meta.transcript.messageId`). */
57
71
  export function newTranscriptMessageId() {
58
72
  return `msg_${globalThis.crypto.randomUUID().replaceAll('-', '')}`;
@@ -71,14 +85,19 @@ export function persistedUserTranscriptMetadata(value) {
71
85
  ...(typeof value.attemptId === 'string' && value.attemptId ? { attemptId: value.attemptId } : {}),
72
86
  ...(Number.isFinite(Number(value.at)) ? { at: Number(value.at) } : {}),
73
87
  ...transcriptRouteMetadataFields(value),
74
- ...(typeof value.sender === 'string' && value.sender ? { sender: value.sender } : {}),
75
- // The device that submitted the prompt (host-stamped display name).
76
- ...(typeof value.device === 'string' && value.device ? { device: value.device } : {}),
88
+ // Sender, the submitting device (host-stamped display name) and the bot
89
+ // that sent this message to this session (agent-task-sessions.mjs).
90
+ ...transcriptSourceFields(value),
91
+ // Bot-to-bot chain position of a bot's message (session-runtime/bot-hops.mjs).
92
+ ...(Number.isInteger(value.hop) && value.hop > 0 ? { hop: value.hop } : {}),
77
93
  // The turn's change-record key (the outer prompt id), so a restored user
78
94
  // row still addresses its turn's Undo/Reapply.
79
95
  ...(typeof value.checkpointId === 'string' && value.checkpointId ? { checkpointId: value.checkpointId } : {}),
80
96
  // Every submission a merged queued batch delivered as this turn.
81
97
  ...submissionIdsField(value.submissionIds),
98
+ // The executions a woken batch delivered (session-flow/drain.mjs): the
99
+ // provenance a task session's report follows (agent-task-sessions.mjs).
100
+ ...consumedExecutionIdsField(value.consumedExecutionIds),
82
101
  ...attachmentsField(value.attachments),
83
102
  // A routine fire delivered as this turn (routine-trigger.mjs).
84
103
  ...routineTriggerField(value),
@@ -90,6 +109,12 @@ function submissionIdsField(value) {
90
109
  return ids.length ? { submissionIds: ids } : {};
91
110
  }
92
111
 
112
+ // Positions are kept: a null stands for a delivered entry that named no execution.
113
+ function consumedExecutionIdsField(value) {
114
+ if (!Array.isArray(value) || !value.length) return {};
115
+ return { consumedExecutionIds: value.map((id) => (typeof id === 'string' && id ? id : null)) };
116
+ }
117
+
93
118
  function attachmentsField(value) {
94
119
  const attachments = normalizeMessageAttachments(value);
95
120
  return attachments.length ? { attachments } : {};
@@ -3,8 +3,7 @@
3
3
  * record that names this session's transcript for discovery, plus the
4
4
  * first-occurrence failure logger every transcript writer shares.
5
5
  */
6
- import { mkdirSync, writeFileSync } from 'node:fs';
7
- import { dirname } from 'node:path';
6
+ import { writeJsonAtomicSync } from '../atomic-file.mjs';
8
7
 
9
8
  // Repeated-failure guard: log the first time each distinct message is seen,
10
9
  // then stay quiet so a persistently-broken path does not flood stderr.
@@ -27,10 +26,11 @@ export function createSessionRecord({ sessionRecordPath, sessionId, resolvedCwd,
27
26
 
28
27
  function write() {
29
28
  try {
30
- mkdirSync(dirname(sessionRecordPath), { recursive: true });
31
- writeFileSync(
29
+ // Published whole and flushed: a discovery reader never meets a torn or
30
+ // NUL record, and every refresh rewrites it anyway.
31
+ writeJsonAtomicSync(
32
32
  sessionRecordPath,
33
- JSON.stringify({
33
+ {
34
34
  sessionId,
35
35
  cwd: resolvedCwd,
36
36
  transcriptPath,
@@ -38,7 +38,8 @@ export function createSessionRecord({ sessionRecordPath, sessionId, resolvedCwd,
38
38
  updatedAt: Date.now(),
39
39
  kind: 'interactive',
40
40
  entrypoint: 'cli',
41
- })
41
+ },
42
+ { compact: true }
42
43
  );
43
44
  } catch (err) {
44
45
  logOnce(err);
@@ -13,8 +13,9 @@
13
13
  // list is what lets a SHARED worktree still revert: the baseline describes the
14
14
  // whole tree, but only these paths may be restored from it.
15
15
  import { createHash } from 'node:crypto';
16
- import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
16
+ import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises';
17
17
  import { join, resolve } from 'node:path';
18
+ import { writeFileAtomicAsync } from './atomic-file.mjs';
18
19
  import { cleanString as clean } from './clean.mjs';
19
20
  import { resolvePluginData } from './plugin-paths.mjs';
20
21
 
@@ -173,10 +174,10 @@ async function sweepRecords() {
173
174
  await sweepDirectory(recordRoot(), RECORD_TTL_MS, MAX_RECORD_FILES);
174
175
  }
175
176
 
177
+ // Flushed before it is published: a crash must not leave a record that reads as
178
+ // NUL (every reader below answers a damaged record as "no record").
176
179
  async function writeJsonAtomically(path, payload) {
177
- const temporary = `${path}.${process.pid}.tmp`;
178
- await writeFile(temporary, JSON.stringify(payload), 'utf8');
179
- await rename(temporary, path);
180
+ await writeFileAtomicAsync(path, JSON.stringify(payload));
180
181
  }
181
182
 
182
183
  /**
@@ -54,6 +54,7 @@ import {
54
54
  reviewSectionTruncated,
55
55
  } from './review-diff.mjs';
56
56
  import { PATCH_EDIT_MAX_BYTES } from './bounded-file-read.mjs';
57
+ import { runGitOffThread } from './git-runner.mjs';
57
58
  import { cleanString as clean } from './clean.mjs';
58
59
  import { createLineDiffBudget, lineDiffHunks, unifiedHunksBody } from './bounded-line-diff.mjs';
59
60
 
@@ -1718,13 +1719,52 @@ export async function getTurnChanges(sessionId, options = {}) {
1718
1719
  let status = 'missing';
1719
1720
  if (ownerSessionId && turnChangesPending(ownerSessionId, checkpointId)) status = 'pending';
1720
1721
  else if (record) status = record.files?.length || record.outside?.length ? 'ready' : 'none';
1721
- return turnChangesReply({
1722
+ const reply = turnChangesReply({
1722
1723
  status,
1723
1724
  checkpointId,
1724
1725
  record,
1725
1726
  patch: options?.patch === true,
1726
1727
  file: clean(options?.file) || null,
1727
1728
  });
1729
+ return status === 'ready' ? withIgnoredChangeFiles(ownerSessionId, reply) : reply;
1730
+ }
1731
+
1732
+ // The gitignored files of each answered record revision (build output a file
1733
+ // tool wrote): flagged `ignored` so a viewer can leave them out of its turn
1734
+ // previews. Asked of Git once per revision; bounded.
1735
+ const IGNORED_CHANGE_CACHE_LIMIT = 200;
1736
+ const _ignoredChangeFiles = new Map();
1737
+
1738
+ async function ignoredChangePaths(key, root, paths) {
1739
+ const cached = _ignoredChangeFiles.get(key);
1740
+ if (cached) return cached;
1741
+ let ignored = new Set();
1742
+ try {
1743
+ const result = await runGitOffThread(['check-ignore', '-z', '--stdin'], {
1744
+ cwd: root,
1745
+ input: `${paths.join('\0')}\0`,
1746
+ timeoutMs: 10_000,
1747
+ maxBytes: 4 * 1024 * 1024,
1748
+ });
1749
+ // 0: some are ignored, 1: none; anything else (no repository) flags none.
1750
+ if (result.code === 0) ignored = new Set(result.stdout.split('\0').filter(Boolean));
1751
+ } catch {
1752
+ // No Git to ask: nothing is flagged.
1753
+ }
1754
+ _ignoredChangeFiles.set(key, ignored);
1755
+ while (_ignoredChangeFiles.size > IGNORED_CHANGE_CACHE_LIMIT) {
1756
+ _ignoredChangeFiles.delete(_ignoredChangeFiles.keys().next().value);
1757
+ }
1758
+ return ignored;
1759
+ }
1760
+
1761
+ async function withIgnoredChangeFiles(ownerSessionId, reply) {
1762
+ const paths = reply.files.map((file) => file.path).filter((path) => path && !isAbsolute(path));
1763
+ if (!reply.root || paths.length === 0) return reply;
1764
+ const key = `${ownerSessionId}\0${reply.checkpointId}\0${reply.revision}\0${reply.root}`;
1765
+ const ignored = await ignoredChangePaths(key, reply.root, paths);
1766
+ if (ignored.size === 0) return reply;
1767
+ return { ...reply, files: reply.files.map((file) => (ignored.has(file.path) ? { ...file, ignored: true } : file)) };
1728
1768
  }
1729
1769
 
1730
1770
  function serializeChangeMutation(ownerSessionId, task) {
@@ -1871,11 +1911,13 @@ async function mutateTurnChanges(sessionId, checkpointIdValue, fileValue, revisi
1871
1911
  if (targets.length === 0) throw new Error(`this turn has no changes to be ${verb}`);
1872
1912
  }
1873
1913
  const updated = await mutateRecordFiles(ownerSessionId, record, targets, reverse);
1874
- return turnChangesReply({
1914
+ const reply = turnChangesReply({
1875
1915
  status: updated ? 'ready' : 'missing',
1876
1916
  checkpointId,
1877
1917
  record: withUnknownChangeStates(ownerSessionId, updated),
1878
1918
  });
1919
+ // The viewer caches this reply as it caches a read: the same `ignored` flags.
1920
+ return updated ? withIgnoredChangeFiles(ownerSessionId, reply) : reply;
1879
1921
  });
1880
1922
  }
1881
1923
 
@@ -6,13 +6,15 @@
6
6
  // - cache hit = cacheRead / (input + cacheRead + cacheWrite) over every call
7
7
  // whose prompt was measured. A call that could not measure its prompt
8
8
  // (promptKnown === false) has unknown zeros, so it adds nothing.
9
- // - speed = output tokens over generation ms (first visible token → end of the
10
- // response) of the calls long enough to time. A reply too short says nothing
11
- // about the model's speed, so each call is held to the floors below BEFORE it
12
- // is summed.
9
+ // - speed = output tokens over request ms (provider request start → end of the
10
+ // response, the wait for the first token included: the same wall time as the
11
+ // usage ledger's `duration_ms`) of every call with positive output and
12
+ // positive duration: the same eligibility as the usage ledger and the usage
13
+ // dialog, so the readout and the dialog agree. Records saved before this
14
+ // definition carry `speedOutputTokens` / `speedMs` (first visible token →
15
+ // end); they are kept as they are and read only while no request-timed call
16
+ // has landed.
13
17
  // - total tokens = input + cacheRead + cacheWrite + output, from the same sums.
14
- export const MIN_SPEED_OUTPUT_TOKENS = 20;
15
- export const MIN_SPEED_MS = 500;
16
18
 
17
19
  function num(value) {
18
20
  const n = Number(value);
@@ -21,16 +23,16 @@ function num(value) {
21
23
 
22
24
  /** The zeroed totals record. */
23
25
  export function emptyUsageReadout() {
24
- return { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, speedOutputTokens: 0, speedMs: 0 };
26
+ return { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, requestOutputTokens: 0, requestMs: 0 };
25
27
  }
26
28
 
27
29
  /**
28
- * Fold one `onUsageDelta` event (and the call's generation ms, 0 when it was
30
+ * Fold one `onUsageDelta` event (and the call's request ms, 0 when it was
29
31
  * not timed) into the totals; returns a new record, never mutates `totals`.
30
32
  * Providers differ on whether input counts cache; the prompt total does not,
31
33
  * so fresh input is the prompt less its cache read/write.
32
34
  */
33
- export function foldUsageReadout(totals, delta = {}, generationMs = 0) {
35
+ export function foldUsageReadout(totals, delta = {}, requestMs = 0) {
34
36
  const base = totals || emptyUsageReadout();
35
37
  const next = { ...base };
36
38
  if (delta.promptKnown !== false) {
@@ -47,10 +49,10 @@ export function foldUsageReadout(totals, delta = {}, generationMs = 0) {
47
49
  // partial one would pass for the whole: it stays without until the session
48
50
  // starts over.
49
51
  if (Number.isFinite(base.output)) next.output = base.output + output;
50
- const ms = num(generationMs);
51
- if (ms >= MIN_SPEED_MS && output >= MIN_SPEED_OUTPUT_TOKENS) {
52
- next.speedOutputTokens = num(base.speedOutputTokens) + output;
53
- next.speedMs = num(base.speedMs) + ms;
52
+ const ms = num(requestMs);
53
+ if (ms > 0 && output > 0) {
54
+ next.requestOutputTokens = num(base.requestOutputTokens) + output;
55
+ next.requestMs = num(base.requestMs) + ms;
54
56
  }
55
57
  return next;
56
58
  }