mixdog 1.2.2 → 1.2.3

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 (415) hide show
  1. package/README.md +18 -0
  2. package/package.json +2 -1
  3. package/scripts/provider-pingpong-live.mjs +509 -0
  4. package/src/agents/advisor/AGENT.md +1 -1
  5. package/src/agents/advisor/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +5 -0
  7. package/src/defaults/skills/setup/SKILL.md +6 -1
  8. package/src/defaults/skills/setup/references/actions.md +44 -22
  9. package/src/defaults/skills/setup/references/surfaces.md +10 -8
  10. package/src/headless-exec.mjs +15 -8
  11. package/src/lib/keychain-cjs.cjs +7 -4
  12. package/src/output-styles/common.md +11 -10
  13. package/src/rules/agent/AGENT.md +5 -5
  14. package/src/rules/lead/LEAD.md +5 -5
  15. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +92 -61
  16. package/src/runtime/agent/orchestrator/cache-mtime.mjs +10 -1
  17. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +7 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +5 -0
  19. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +26 -7
  21. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +112 -7
  22. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +48 -20
  23. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +2 -0
  24. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +24 -17
  25. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +7 -6
  26. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +29 -10
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-blocks.mjs +20 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-delta.mjs +15 -2
  30. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +11 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/message-events.mjs +3 -0
  32. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +12 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-outcome.mjs +1 -0
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-state.mjs +14 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +9 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -0
  38. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +58 -7
  39. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +29 -10
  40. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +16 -3
  41. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +10 -61
  42. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +12 -6
  43. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +114 -72
  45. package/src/runtime/agent/orchestrator/providers/compat-request-policy.mjs +78 -2
  46. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +37 -5
  47. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-messages.mjs +43 -2
  50. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +3 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor.mjs +9 -2
  52. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +64 -1
  53. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +70 -6
  54. package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +34 -7
  55. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +32 -6
  56. package/src/runtime/agent/orchestrator/providers/deepseek.mjs +67 -0
  57. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +11 -4
  59. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +25 -8
  60. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +87 -5
  61. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +41 -5
  62. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +13 -13
  63. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +355 -37
  64. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -2
  65. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +93 -47
  66. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +55 -10
  67. package/src/runtime/agent/orchestrator/providers/gemini.mjs +26 -8
  68. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +60 -0
  71. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +1 -0
  72. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +246 -42
  73. package/src/runtime/agent/orchestrator/providers/lib/anthropic-server-tool-usage.mjs +18 -0
  74. package/src/runtime/agent/orchestrator/providers/lib/anthropic-system-blocks.mjs +27 -6
  75. package/src/runtime/agent/orchestrator/providers/lib/claude-bridge-schema.mjs +388 -0
  76. package/src/runtime/agent/orchestrator/providers/lib/inclusive-usage.mjs +54 -0
  77. package/src/runtime/agent/orchestrator/providers/lib/origin-bound-fetch.mjs +16 -0
  78. package/src/runtime/agent/orchestrator/providers/lib/responses-terminal-fields.mjs +11 -0
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +8 -0
  80. package/src/runtime/agent/orchestrator/providers/lib/wire-pairing.mjs +48 -22
  81. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +119 -18
  82. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +13 -15
  83. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  85. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +33 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +121 -61
  89. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +35 -14
  90. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +5 -2
  91. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +37 -7
  92. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/tool-call-acc.mjs +2 -9
  93. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +47 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +47 -25
  96. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +5 -1
  97. package/src/runtime/agent/orchestrator/providers/openai-compat-reasoning-field.mjs +151 -0
  98. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +42 -24
  99. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +63 -25
  100. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-head-parity.json +2901 -0
  101. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-parity.fixtures.mjs +172 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +53 -5
  103. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -1
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +2 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +103 -26
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +23 -4
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +11 -6
  108. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  109. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +15 -23
  110. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +23 -10
  111. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +25 -4
  112. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +10 -0
  113. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-items.mjs +18 -7
  114. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +4 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +5 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/text-relay.mjs +9 -1
  117. package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +80 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +3 -1
  119. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -0
  120. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +7 -1
  121. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +71 -24
  122. package/src/runtime/agent/orchestrator/providers/openai-responses-payload-parity.fixture.json +323 -0
  123. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +44 -16
  124. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +7 -16
  125. package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/refusals.mjs +60 -0
  126. package/src/runtime/agent/orchestrator/providers/openai-ws-response-state/tool-calls.mjs +51 -35
  127. package/src/runtime/agent/orchestrator/providers/openai-ws-response-state.mjs +30 -0
  128. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +12 -0
  129. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/terminal-frames.mjs +25 -6
  130. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +11 -3
  131. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +170 -0
  132. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +75 -70
  133. package/src/runtime/agent/orchestrator/providers/openrouter.mjs +75 -0
  134. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +147 -8
  135. package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -1
  136. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +5 -0
  137. package/src/runtime/agent/orchestrator/providers/transport-scope.d.mts +6 -0
  138. package/src/runtime/agent/orchestrator/providers/transport-scope.mjs +28 -0
  139. package/src/runtime/agent/orchestrator/providers/wire-gateway.mjs +157 -0
  140. package/src/runtime/agent/orchestrator/providers/wire-route.mjs +512 -0
  141. package/src/runtime/agent/orchestrator/providers/xai-tool-search-head-parity.json +1131 -0
  142. package/src/runtime/agent/orchestrator/providers/xai-tool-search.fixtures.mjs +183 -0
  143. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +100 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-availability.mjs +21 -0
  145. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -0
  146. package/src/runtime/agent/orchestrator/runtime-core/provider-request-snapshot.mjs +7 -3
  147. package/src/runtime/agent/orchestrator/runtime-core/session-title.mjs +21 -1
  148. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +14 -3
  149. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +526 -0
  150. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  151. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +10 -7
  152. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +176 -71
  153. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +62 -10
  154. package/src/runtime/agent/orchestrator/runtime-core/tool-exposure.mjs +96 -0
  155. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  156. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +3 -2
  157. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +14 -93
  158. package/src/runtime/agent/orchestrator/session/compact/summary-anchor.mjs +5 -0
  159. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +120 -0
  160. package/src/runtime/agent/orchestrator/session/context-tool-schema.mjs +17 -2
  161. package/src/runtime/agent/orchestrator/session/context-utils.mjs +8 -4
  162. package/src/runtime/agent/orchestrator/session/eager-dispatch/admission.mjs +2 -1
  163. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +6 -1
  164. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +91 -1
  165. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +1 -1
  166. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +56 -0
  167. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +18 -8
  168. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -0
  169. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +21 -10
  170. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +132 -0
  171. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +30 -0
  172. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +63 -20
  173. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +2 -1
  174. package/src/runtime/agent/orchestrator/session/loop/tool-call-resolution.mjs +95 -0
  175. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +30 -3
  176. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +15 -4
  177. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  178. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +150 -0
  179. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +4 -1
  180. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -1
  181. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +34 -12
  182. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +16 -0
  183. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +57 -25
  184. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  185. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
  186. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
  187. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +208 -14
  188. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +15 -3
  189. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +25 -3
  190. package/src/runtime/agent/orchestrator/session/manager/session-fallback-title.mjs +82 -0
  191. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -2
  192. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
  193. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
  194. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -1
  195. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +6 -0
  196. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +13 -0
  197. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +96 -0
  198. package/src/runtime/agent/orchestrator/session/side-question.mjs +192 -0
  199. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +17 -3
  200. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +23 -0
  201. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +58 -2
  202. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +41 -28
  203. package/src/runtime/agent/orchestrator/session/store/session-observers.mjs +4 -2
  204. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +12 -1
  205. package/src/runtime/agent/orchestrator/session/store/write-admission.mjs +3 -1
  206. package/src/runtime/agent/orchestrator/session/store-summary-archived-agent.mjs +34 -15
  207. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  208. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +8 -0
  209. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +10 -1
  210. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
  211. package/src/runtime/agent/orchestrator/session/store.mjs +19 -3
  212. package/src/runtime/agent/orchestrator/session/tool-batch/execute-call.mjs +1 -0
  213. package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +1 -1
  214. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +1 -1
  215. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +7 -1
  216. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -0
  217. package/src/runtime/agent/orchestrator/session/tool-schema-rejection.mjs +77 -0
  218. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +8 -2
  219. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +3 -3
  220. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
  221. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +14 -10
  222. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +49 -34
  223. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +23 -1
  224. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +1 -1
  225. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1 -1
  226. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +73 -39
  227. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +18 -1
  228. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -4
  229. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +88 -12
  230. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +2 -0
  231. package/src/runtime/computer-bridge/action-schema.mjs +44 -21
  232. package/src/runtime/github/tool.mjs +92 -2
  233. package/src/runtime/memory/index.mjs +1 -0
  234. package/src/runtime/memory/lib/http-router/ingest-routes.mjs +22 -1
  235. package/src/runtime/memory/lib/ingest/transcript-offsets.mjs +2 -0
  236. package/src/runtime/memory/lib/pg/adapter.mjs +98 -1
  237. package/src/runtime/memory/lib/pg/process.mjs +31 -26
  238. package/src/runtime/memory/lib/pg/run-child.mjs +49 -0
  239. package/src/runtime/memory/lib/pg/supervisor.mjs +108 -3
  240. package/src/runtime/memory/lib/runtime-fetcher.mjs +12 -13
  241. package/src/runtime/memory/lib/transcript-ingest.mjs +14 -0
  242. package/src/runtime/office/README.md +1 -1
  243. package/src/runtime/office/authoring/html-document-author.mjs +2 -0
  244. package/src/runtime/office/authoring/pptx-contact-sheet.mjs +2 -0
  245. package/src/runtime/office/authoring/pptx-html-measure.mjs +2 -0
  246. package/src/runtime/office/authoring/pptx-html-runner.mjs +2 -0
  247. package/src/runtime/office/authoring/xlsx-html-runner.mjs +2 -0
  248. package/src/runtime/office/core/office-actions-read.mjs +3 -0
  249. package/src/runtime/office/core/office-render-preview.mjs +2 -0
  250. package/src/runtime/office/core/office-session-dispatch.mjs +7 -0
  251. package/src/runtime/office/core/office-sessionless-actions.mjs +8 -0
  252. package/src/runtime/office/core/office-sessions.mjs +5 -0
  253. package/src/runtime/office/core/office-transactions.mjs +3 -0
  254. package/src/runtime/office/core/office-write-ledger.mjs +98 -0
  255. package/src/runtime/office/design/library/templates/mixdog-executive.mixdog-edit.pptx +0 -0
  256. package/src/runtime/office/index.mjs +30 -0
  257. package/src/runtime/office/pdf/pdf-edit-document.mjs +2 -0
  258. package/src/runtime/office/pdf/pdf-render.mjs +23 -7
  259. package/src/runtime/office/pdf/pdf-security.mjs +2 -0
  260. package/src/runtime/office/portable/portable-opc.mjs +4 -1
  261. package/src/runtime/office/quality/visual-diff.mjs +2 -0
  262. package/src/runtime/shared/bounded-file-read.mjs +13 -0
  263. package/src/runtime/shared/bounded-line-diff.mjs +182 -0
  264. package/src/runtime/shared/buffered-appender.mjs +18 -0
  265. package/src/runtime/shared/config.mjs +78 -18
  266. package/src/runtime/shared/err-text.mjs +6 -0
  267. package/src/runtime/shared/file-permissions.d.mts +6 -1
  268. package/src/runtime/shared/file-permissions.mjs +54 -6
  269. package/src/runtime/shared/llm/anthropic-effort.mjs +6 -0
  270. package/src/runtime/shared/llm/usage-accounting.mjs +5 -2
  271. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +3 -0
  272. package/src/runtime/shared/loopback-listener.mjs +9 -0
  273. package/src/runtime/shared/provider-api-key.mjs +2 -0
  274. package/src/runtime/shared/review-diff.mjs +52 -0
  275. package/src/runtime/shared/session-sidebar-host.d.mts +4 -0
  276. package/src/runtime/shared/session-sidebar-host.mjs +38 -0
  277. package/src/runtime/shared/slow-span.mjs +114 -0
  278. package/src/runtime/shared/statusline/statusline-segments.mjs +28 -4
  279. package/src/runtime/shared/tool-card-model/card-header.mjs +6 -1
  280. package/src/runtime/shared/tool-card-model/generic-detail.mjs +39 -5
  281. package/src/runtime/shared/tool-card-model.mjs +5 -2
  282. package/src/runtime/shared/tool-output-limit.mjs +4 -0
  283. package/src/runtime/shared/tool-primitives.mjs +13 -0
  284. package/src/runtime/shared/tool-result-summary.mjs +8 -1
  285. package/src/runtime/shared/tool-surface.mjs +18 -3
  286. package/src/runtime/shared/tool-work-units.mjs +40 -4
  287. package/src/runtime/shared/transcript-writer/purge.mjs +74 -0
  288. package/src/runtime/shared/transcript-writer.mjs +10 -2
  289. package/src/runtime/shared/turn-changes.mjs +268 -100
  290. package/src/runtime/shared/turn-review-reply.mjs +8 -39
  291. package/src/runtime/shared/turn-snapshot-store.mjs +19 -108
  292. package/src/runtime/shared/turn-snapshot.mjs +857 -683
  293. package/src/runtime/shared/turn-worktree-snapshot.mjs +183 -255
  294. package/src/runtime/shared/user-data-guard.mjs +262 -49
  295. package/src/runtime/shared/worktrees/carry.mjs +531 -0
  296. package/src/runtime/shared/worktrees/contract.mjs +34 -0
  297. package/src/runtime/shared/worktrees/file-recovery.mjs +68 -0
  298. package/src/runtime/shared/worktrees/git-head.mjs +205 -0
  299. package/src/runtime/shared/worktrees/git.mjs +8 -3
  300. package/src/runtime/shared/worktrees/maintenance.mjs +1 -1
  301. package/src/runtime/shared/worktrees/manager.mjs +156 -8
  302. package/src/runtime/shared/worktrees/merge.mjs +31 -21
  303. package/src/runtime/shared/worktrees/paths.d.mts +6 -0
  304. package/src/runtime/terminal-bridge/client.mjs +44 -11
  305. package/src/runtime/terminal-bridge/tool-defs.mjs +13 -6
  306. package/src/runtime/tidy/apply.mjs +47 -2
  307. package/src/runtime/tidy/run-engines.mjs +11 -5
  308. package/src/session-runtime/agent-tool-routing.mjs +19 -13
  309. package/src/session-runtime/boot/apis.mjs +13 -5
  310. package/src/session-runtime/boot/facade.mjs +1 -0
  311. package/src/session-runtime/boot/infrastructure.mjs +6 -1
  312. package/src/session-runtime/boot/session.mjs +0 -1
  313. package/src/session-runtime/boot/tools.mjs +4 -1
  314. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +12 -5
  315. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +65 -0
  316. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +3 -1
  317. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +11 -4
  318. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +115 -18
  319. package/src/session-runtime/internal-tool-executor.mjs +6 -2
  320. package/src/session-runtime/lifecycle/cold-heir.mjs +188 -0
  321. package/src/session-runtime/lifecycle/inheritance.mjs +71 -27
  322. package/src/session-runtime/lifecycle/session-catalog.mjs +12 -3
  323. package/src/session-runtime/lifecycle/session-switch/resume.mjs +6 -0
  324. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +29 -6
  325. package/src/session-runtime/lifecycle/teardown.mjs +72 -7
  326. package/src/session-runtime/lifecycle-api.mjs +1 -0
  327. package/src/session-runtime/mcp-glue/connect.mjs +17 -5
  328. package/src/session-runtime/model-route/set-route.mjs +2 -2
  329. package/src/session-runtime/native-web-search-usage.mjs +51 -0
  330. package/src/session-runtime/native-web-search.mjs +10 -1
  331. package/src/session-runtime/new-session-config.mjs +4 -2
  332. package/src/session-runtime/provider-auth/login.mjs +4 -0
  333. package/src/session-runtime/resource-scope-decorate.mjs +10 -3
  334. package/src/session-runtime/runtime-review-api.mjs +8 -2
  335. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +81 -20
  336. package/src/session-runtime/services/agent-tool/lead-worker-index/forgotten-leads.mjs +29 -0
  337. package/src/session-runtime/services/agent-tool/lead-worker-index/index-file.mjs +1 -1
  338. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +73 -7
  339. package/src/session-runtime/services/agent-tool/tag-registry.mjs +15 -1
  340. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +10 -1
  341. package/src/session-runtime/services/agent-tool/worker-index.mjs +46 -7
  342. package/src/session-runtime/services/agent-tool.mjs +3 -0
  343. package/src/session-runtime/services/channel-admin.mjs +19 -3
  344. package/src/session-runtime/services/memory-runtime-proxy/calls.mjs +4 -0
  345. package/src/session-runtime/services/memory-runtime-proxy.mjs +1 -0
  346. package/src/session-runtime/services/provider-admin.mjs +81 -15
  347. package/src/session-runtime/session-transcript-purge.mjs +32 -0
  348. package/src/session-runtime/session-turn-api.mjs +14 -5
  349. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -0
  350. package/src/session-runtime/setup-tool/executor.mjs +36 -6
  351. package/src/session-runtime/setup-tool/extended-actions.mjs +86 -6
  352. package/src/session-runtime/setup-tool/settings-contract.mjs +13 -3
  353. package/src/session-runtime/setup-tool/tool-defs.mjs +13 -4
  354. package/src/session-runtime/skills-api.mjs +3 -5
  355. package/src/session-runtime/tool-surface.mjs +5 -1
  356. package/src/session-runtime/turn/compaction-gate.mjs +25 -0
  357. package/src/session-runtime/turn/session-ops.mjs +7 -5
  358. package/src/session-runtime/turn/side-question-ops.mjs +22 -0
  359. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +5 -1
  360. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +8 -3
  361. package/src/session-runtime/turn/turn-run/turn-settle.mjs +10 -0
  362. package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
  363. package/src/session-runtime/turn/turn-run.mjs +25 -9
  364. package/src/session-runtime/usage-stats-api.mjs +43 -11
  365. package/src/session-runtime/workspace/project-branches.mjs +7 -5
  366. package/src/session-runtime/workspace/transition-steps.mjs +129 -31
  367. package/src/session-runtime/workspace/transitions.mjs +190 -15
  368. package/src/standalone/daemon-agent-control.mjs +9 -1
  369. package/src/standalone/daemon-local-session-bridge.mjs +14 -1
  370. package/src/standalone/daemon-owner.mjs +17 -4
  371. package/src/standalone/daemon-pg-stop.mjs +51 -0
  372. package/src/standalone/daemon-stop-budget.d.mts +2 -0
  373. package/src/standalone/daemon-stop-budget.mjs +18 -0
  374. package/src/standalone/daemon-stored-session-views.mjs +2 -0
  375. package/src/standalone/daemon-telemetry.mjs +19 -0
  376. package/src/standalone/daemon.mjs +61 -1
  377. package/src/standalone/desktop-service-registry.mjs +5 -0
  378. package/src/standalone/local-session-runtime.mjs +70 -19
  379. package/src/standalone/session-client.mjs +42 -0
  380. package/src/standalone/session-protocol.mjs +7 -0
  381. package/src/standalone/session-proxy/projection.mjs +41 -4
  382. package/src/standalone/session-runtime-inline-host/boot-loaders.mjs +15 -1
  383. package/src/standalone/session-runtime-inline-host.mjs +1 -0
  384. package/src/standalone/session-service/session-calls/session-action.mjs +18 -3
  385. package/src/standalone/session-service-api.mjs +6 -1
  386. package/src/standalone/session-state-patch.d.mts +2 -1
  387. package/src/standalone/session-state-patch.mjs +41 -1
  388. package/src/tui/App.jsx +25 -0
  389. package/src/tui/app/app-view/floating-panel.jsx +26 -1
  390. package/src/tui/app/btw-controller.mjs +118 -0
  391. package/src/tui/app/create-app-pickers.mjs +2 -0
  392. package/src/tui/app/shell-layout/row-budgets.mjs +11 -1
  393. package/src/tui/app/shell-layout/surfaces.mjs +3 -1
  394. package/src/tui/app/slash-commands.mjs +13 -3
  395. package/src/tui/app/slash-dispatch/session-commands.mjs +19 -6
  396. package/src/tui/app/use-global-key-input/approval-shortcut.mjs +12 -0
  397. package/src/tui/app/use-global-key-input.mjs +5 -12
  398. package/src/tui/app/use-prompt-handlers.mjs +11 -0
  399. package/src/tui/components/BtwPanel.jsx +53 -0
  400. package/src/tui/components/QuestionPanel.jsx +275 -0
  401. package/src/tui/components/question-panel-model.mjs +156 -0
  402. package/src/tui/dist/index.mjs +2413 -1465
  403. package/src/tui/session/context-state.mjs +4 -2
  404. package/src/tui/session/session-api/commands.mjs +10 -0
  405. package/src/tui/session/session-api/controls.mjs +2 -2
  406. package/src/tui/session/session-api/integrations.mjs +9 -2
  407. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  408. package/src/tui/session/session-api/lifecycle.mjs +2 -1
  409. package/src/tui/session/session-api/settings.mjs +6 -0
  410. package/src/tui/session/session-api.mjs +1 -0
  411. package/src/tui/session/tool-approval.mjs +10 -7
  412. package/src/tui/session/transcript-spill.mjs +14 -7
  413. package/src/tui/session/turn-stream.mjs +174 -31
  414. package/src/tui/session/turn.mjs +12 -5
  415. package/src/workflows/default/WORKFLOW.md +3 -1
@@ -0,0 +1,96 @@
1
+ // side-question-store.mjs — the in-memory state behind /btw side questions:
2
+ // per session, the latest provider request the main loop sent (the request a
3
+ // side question forks) and the side question in flight. It holds live
4
+ // provider/tool objects, so it never reaches the session record or disk.
5
+ import { isAgentOwner } from '../agent-owner.mjs';
6
+
7
+ // sessionId -> { provider, model, messages, tools, opts, response }
8
+ const requests = new Map();
9
+ // sessionId -> AbortController of the side question in flight.
10
+ const inFlight = new Map();
11
+
12
+ // Send options that steer the live loop rather than shape the request: its
13
+ // abort signal, a one-shot cache-break trace tag and the per-request stall
14
+ // budget. A forced tool choice is dropped too (a side question must be able
15
+ // to answer in text); the hard-cap `none` choice is kept.
16
+ const LOOP_ONLY_OPTIONS = new Set(['signal', 'cacheBreakIntent', '_stallRetryBudget', 'toolChoice']);
17
+
18
+ function requestOptions(opts) {
19
+ const out = {};
20
+ for (const [key, value] of Object.entries(opts || {})) {
21
+ if (typeof value === 'function' || LOOP_ONLY_OPTIONS.has(key)) continue;
22
+ out[key] = value;
23
+ }
24
+ if (opts?.toolChoice === 'none') out.toolChoice = 'none';
25
+ return out;
26
+ }
27
+
28
+ /** A request in the stored shape: its own message list and only the send
29
+ * options a fork may reuse. `response` is null until the reply settles. */
30
+ export function sideQuestionRequestSnapshot({ provider, model, messages, tools, opts }) {
31
+ return {
32
+ provider,
33
+ model,
34
+ messages: [...messages],
35
+ tools: Array.isArray(tools) ? tools : [],
36
+ opts: requestOptions(opts),
37
+ response: null,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Record a provider request the main loop is sending as the session's latest
43
+ * one. Agent sessions record nothing (/btw is a Lead command). The handle
44
+ * attaches the reply once the send proceeds, or puts the previous request
45
+ * back when this one fails, so only a successfully sent request is kept.
46
+ */
47
+ export function captureSideQuestionRequest({ sessionId, sessionRef, provider, model, messages, tools, opts }) {
48
+ if (!sessionId || !provider || isAgentOwner(sessionRef)) return null;
49
+ const previous = requests.get(sessionId);
50
+ const request = sideQuestionRequestSnapshot({ provider, model, messages, tools, opts });
51
+ requests.set(sessionId, request);
52
+ return {
53
+ settle(response) {
54
+ request.response = response || null;
55
+ },
56
+ discard() {
57
+ if (requests.get(sessionId) !== request) return;
58
+ if (previous) requests.set(sessionId, previous);
59
+ else requests.delete(sessionId);
60
+ },
61
+ };
62
+ }
63
+
64
+ export function sideQuestionRequest(sessionId) {
65
+ return (sessionId && requests.get(sessionId)) || null;
66
+ }
67
+
68
+ /** Start the session's side question, aborting the one still in flight. */
69
+ export function beginSideQuestion(sessionId) {
70
+ cancelSideQuestion(sessionId);
71
+ const controller = new AbortController();
72
+ inFlight.set(sessionId, controller);
73
+ return {
74
+ signal: controller.signal,
75
+ end() {
76
+ if (inFlight.get(sessionId) === controller) inFlight.delete(sessionId);
77
+ },
78
+ };
79
+ }
80
+
81
+ export function cancelSideQuestion(sessionId) {
82
+ const controller = sessionId ? inFlight.get(sessionId) : null;
83
+ if (!controller) return false;
84
+ inFlight.delete(sessionId);
85
+ controller.abort(new Error('Side question canceled.'));
86
+ return true;
87
+ }
88
+
89
+ /** The transcript the saved request carried is gone (clear, rewind): a later
90
+ * side question waits for the next request. `cancel` also aborts the side
91
+ * question in flight (session close / runtime unload). */
92
+ export function forgetSideQuestion(sessionId, { cancel = false } = {}) {
93
+ if (!sessionId) return;
94
+ requests.delete(sessionId);
95
+ if (cancel) cancelSideQuestion(sessionId);
96
+ }
@@ -0,0 +1,192 @@
1
+ // side-question.mjs — /btw: a quick question answered while the main turn
2
+ // keeps working. The answer comes from a cache-sharing fork of the session's
3
+ // latest provider request (side-question-store.mjs): the same model, system
4
+ // prompt, tools and cache-relevant options and the same message prefix, so
5
+ // the provider's prompt cache serves the prefix. Only the reply that request
6
+ // produced and one question are appended. One provider call, no tool loop;
7
+ // the answer is returned to the caller and never enters the transcript.
8
+ import { collectAssistantToolCallIds } from './context-tool-pairs.mjs';
9
+ import { buildIntermediateAssistantMessage, buildToolCallAssistantMessage } from './loop/assistant-commit.mjs';
10
+ import { logSessionLlmUsage } from './manager/ask-turn-commit.mjs';
11
+ import { disposeTransportScope, forkTransportScope } from '../providers/transport-scope.mjs';
12
+ import { isAgentOwner } from '../agent-owner.mjs';
13
+ import { getProvider } from '../providers/registry.mjs';
14
+ import { prepareTurnEffortConfiguration } from '../providers/effort-configuration.mjs';
15
+ import { providerNativeToolPrefixCount } from '../runtime-core/provider-request-tools.mjs';
16
+ import { snapshotProviderRequestTools } from '../runtime-core/tool-catalog.mjs';
17
+ import { repairTranscriptBeforeProviderSend } from './loop/transcript-repair.mjs';
18
+ import { buildRequestSendOptions } from './manager/ask-turn-loop-options.mjs';
19
+ import { filterModelVisibleSessionMessages } from './manager/message-sanitize.mjs';
20
+ import { hasUserConversationMessage } from './manager/prompt-utils.mjs';
21
+ import { loadSession } from './store.mjs';
22
+ import { projectSyntheticUserEnvelopes } from './synthetic-user-envelope.mjs';
23
+ import { beginSideQuestion, sideQuestionRequest, sideQuestionRequestSnapshot } from './side-question-store.mjs';
24
+
25
+ export { cancelSideQuestion } from './side-question-store.mjs';
26
+
27
+ export const SIDE_QUESTION_NO_REQUEST = 'Send a message first, then try /btw.';
28
+ export const SIDE_QUESTION_TOOL_ATTEMPT =
29
+ 'The model tried to use a tool instead of answering. A side question cannot run tools; try asking it differently.';
30
+ const SIDE_QUESTION_EMPTY_ANSWER = 'The model returned no answer.';
31
+
32
+ // The reply's tool calls run in the main session; the fork only needs each
33
+ // call answered so the request stays well-formed.
34
+ const SIDE_QUESTION_TOOL_STUB =
35
+ 'This tool call belongs to the main task and is handled there; its result is not part of this side question.';
36
+
37
+ function sideQuestionPrompt(question) {
38
+ return [
39
+ '<side-question>',
40
+ 'The user is asking you a quick side question. The main agent keeps working on the task on its own;',
41
+ 'this exchange does not reach it and does not change that work.',
42
+ 'Answer directly, in this one response, from what this conversation already contains.',
43
+ 'No tools are available here: do not call any, and do not offer or promise to do anything.',
44
+ 'If the conversation does not tell you the answer, say that you do not know.',
45
+ '',
46
+ question,
47
+ '</side-question>',
48
+ ].join('\n');
49
+ }
50
+
51
+ // The assistant turn the saved request produced, built the way the loop
52
+ // commits it (without the turn's one-shot transcript metadata), plus a stub
53
+ // result per tool call.
54
+ function replyMessages(response) {
55
+ if (!response) return [];
56
+ const reply = response.toolCalls?.length
57
+ ? buildToolCallAssistantMessage(response, { calls: response.toolCalls, suppressMidTurnText: false, opts: {} })
58
+ : buildIntermediateAssistantMessage(response, {});
59
+ if (!reply) return [];
60
+ const stubs = collectAssistantToolCallIds(reply).map((id) => ({
61
+ role: 'tool',
62
+ toolCallId: id,
63
+ content: SIDE_QUESTION_TOOL_STUB,
64
+ }));
65
+ return [reply, ...stubs];
66
+ }
67
+
68
+ /**
69
+ * The fork of a saved request: its messages unchanged, then the reply and
70
+ * the question. The send options are the saved ones; the provider
71
+ * continuation state follows the reply when it is appended, exactly as the
72
+ * main loop's next request would carry it. `sharedCachePrefixCount` (the
73
+ * shared prefix's message count, which survives model-proxy serialization)
74
+ * keeps an Anthropic message breakpoint on the shared prefix, so the fork reads the
75
+ * saved request's cache entry and writes none for its suffix.
76
+ */
77
+ export function buildSideQuestionRequest(request, question) {
78
+ const reply = replyMessages(request.response);
79
+ const opts = { ...request.opts };
80
+ if (reply.length && Object.hasOwn(request.response, 'providerState')) {
81
+ opts.providerState = request.response.providerState;
82
+ }
83
+ if (request.messages.length) opts.sharedCachePrefixCount = request.messages.length;
84
+ const messages = [
85
+ ...request.messages,
86
+ ...reply,
87
+ // A user interjection keeps its own turn (never folded into a preceding
88
+ // tool result), so the prefix's last block reaches the provider unchanged.
89
+ { role: 'user', content: sideQuestionPrompt(question), meta: { source: 'steering' } },
90
+ ];
91
+ return { messages, opts };
92
+ }
93
+
94
+ /**
95
+ * The request the main loop would send next for a session that has none saved
96
+ * (the runtime restarted, or the saved one was forgotten): the session's own
97
+ * provider, model, tools, send options and transcript, assembled by the same
98
+ * pieces the loop uses, without the loop's side effects (no compaction, no
99
+ * transcript repair in place, nothing written to the session). Null when the
100
+ * session has no conversation, no resolvable provider, or is an agent's.
101
+ */
102
+ function nextMainRequest(sessionId) {
103
+ const session = sessionId ? loadSession(sessionId) : null;
104
+ if (!session || session.closed === true || isAgentOwner(session)) return null;
105
+ if (!hasUserConversationMessage(session.messages)) return null;
106
+ const provider = getProvider(session.provider);
107
+ if (!provider) return null;
108
+ const transcript = filterModelVisibleSessionMessages(session.messages);
109
+ repairTranscriptBeforeProviderSend(transcript, sessionId);
110
+ const opts = buildRequestSendOptions({
111
+ sessionId,
112
+ session,
113
+ turnEffort: session.effort || null,
114
+ effortConfiguration: prepareTurnEffortConfiguration(session, provider),
115
+ stepAutoEffort: null,
116
+ codexTurnId: null,
117
+ startedAtMs: Date.now(),
118
+ signal: undefined,
119
+ });
120
+ const sendTools = snapshotProviderRequestTools({
121
+ provider: session.provider || provider.name,
122
+ tools: session.tools,
123
+ nativeTools: opts.nativeTools,
124
+ messages: transcript,
125
+ session,
126
+ });
127
+ opts.iteration = 1;
128
+ opts.providerToolSnapshotAuthoritative = true;
129
+ opts.providerNativeToolPrefixCount = providerNativeToolPrefixCount(sendTools);
130
+ return sideQuestionRequestSnapshot({
131
+ provider,
132
+ model: session.model,
133
+ messages: projectSyntheticUserEnvelopes(transcript).messages,
134
+ tools: sendTools,
135
+ opts,
136
+ });
137
+ }
138
+
139
+ function answerText(response) {
140
+ if (response?.toolCalls?.length) return SIDE_QUESTION_TOOL_ATTEMPT;
141
+ const text = typeof response?.content === 'string' ? response.content.trim() : '';
142
+ return text || SIDE_QUESTION_EMPTY_ANSWER;
143
+ }
144
+
145
+ /**
146
+ * Answer `question` from the session's latest provider request. One side
147
+ * question runs per session: a new one aborts the previous, and
148
+ * cancelSideQuestion aborts it. A session without a saved request forks the
149
+ * request its next turn would send. Rejects when the session has no
150
+ * conversation or provider, with the provider's error, or with the cancel
151
+ * reason.
152
+ */
153
+ export async function askSideQuestion(sessionId, question) {
154
+ const text = typeof question === 'string' ? question.trim() : '';
155
+ if (!text) throw new TypeError('Side question is empty.');
156
+ const request = sideQuestionRequest(sessionId) || nextMainRequest(sessionId);
157
+ if (!request) throw new Error(SIDE_QUESTION_NO_REQUEST);
158
+ const run = beginSideQuestion(sessionId);
159
+ const startedAt = Date.now();
160
+ // The fork keeps the session id (prompt cache routing) but runs on its own
161
+ // transport scope: no pooled socket, response-chain anchor, turn-state
162
+ // token or provider run of the main session is waited on, read or replaced.
163
+ const transportScope = forkTransportScope(sessionId, 'side');
164
+ try {
165
+ const { messages, opts } = buildSideQuestionRequest(request, text);
166
+ let response;
167
+ try {
168
+ response = await request.provider.send(messages, request.model, request.tools.length ? request.tools : undefined, {
169
+ ...opts,
170
+ transportScope,
171
+ admissionOwner: sessionId,
172
+ signal: run.signal,
173
+ });
174
+ } catch (error) {
175
+ throw run.signal.aborted ? run.signal.reason : error;
176
+ }
177
+ if (run.signal.aborted) throw run.signal.reason;
178
+ const session = request.opts.session;
179
+ if (response?.usage && session?.id) {
180
+ void logSessionLlmUsage({
181
+ session: { ...session, model: request.model },
182
+ usage: response.usage,
183
+ startedAt,
184
+ mode: 'side-question',
185
+ }).catch(() => {});
186
+ }
187
+ return { text: answerText(response) };
188
+ } finally {
189
+ run.end();
190
+ disposeTransportScope(transportScope, request.provider);
191
+ }
192
+ }
@@ -158,9 +158,23 @@ function _logSessionCloseMetric(id, existing, closedAt, reason) {
158
158
  }
159
159
  }
160
160
 
161
+ /**
162
+ * A barrier that found no record at all — nothing on disk and no live or
163
+ * pending copy in memory — fenced nothing because there is nothing to fence:
164
+ * its pending writes were already cancelled under the commit lock. It still
165
+ * returns null (no generation exists), but a caller passing `options.outcome`
166
+ * learns that the record was legitimately gone rather than vetoed, contended
167
+ * or failed.
168
+ */
169
+ function _recordAbsent(options) {
170
+ if (options?.outcome && typeof options.outcome === 'object') options.outcome.absent = true;
171
+ return null;
172
+ }
173
+
161
174
  /**
162
175
  * Atomically mark a session closed on disk with a bumped generation.
163
176
  * Returns the new generation, or null if the session file doesn't exist.
177
+ * `options.outcome` (an object) gets `absent: true` when no record existed.
164
178
  * Used by closeSession() to plant a tombstone that races against in-flight
165
179
  * saveSession() calls.
166
180
  */
@@ -187,7 +201,7 @@ export function markSessionClosed(id, reason = 'manual', options = {}) {
187
201
  _cancelSessionWrites(id);
188
202
  _uncacheSessionSummary(id);
189
203
  const existing = loadSession(id);
190
- if (!existing) return null;
204
+ if (!existing) return _recordAbsent(options);
191
205
  // Re-close idempotence: a session that is ALREADY tombstoned keeps its
192
206
  // ORIGINAL close time (updatedAt) and generation. The old code refreshed
193
207
  // updatedAt=Date.now() on every call, so the 5-min idle sweep re-closing a
@@ -253,7 +267,7 @@ export function markSessionClosed(id, reason = 'manual', options = {}) {
253
267
  * of clobbering whatever the resumed session writes next.
254
268
  * Returns the new generation, or null if the session file doesn't exist.
255
269
  */
256
- export function bumpSessionGeneration(id, reason = 'detach') {
270
+ export function bumpSessionGeneration(id, reason = 'detach', options = {}) {
257
271
  clearSessionLifecycleCommitError(id);
258
272
  // The detach barrier is a canonical write and MUST take the same commit
259
273
  // lock as markSessionClosed. Cancellation alone is not a barrier: a writer
@@ -278,7 +292,7 @@ export function bumpSessionGeneration(id, reason = 'detach') {
278
292
  _cancelSessionWrites(id);
279
293
  _uncacheSessionSummary(id);
280
294
  const existing = loadSession(id);
281
- if (!existing) return null;
295
+ if (!existing) return _recordAbsent(options);
282
296
  const newGen = (typeof existing.generation === 'number' ? existing.generation : 0) + 1;
283
297
  const detached = { ...existing, generation: newGen, updatedAt: Date.now(), detachedReason: reason };
284
298
  if (!_writeLifecycleRecord(id, detached, existing, reason)) return null;
@@ -124,6 +124,29 @@ export function _retireSessionIncarnation(id) {
124
124
  if (id) _sessionIncarnations.delete(id);
125
125
  }
126
126
 
127
+ // ── Delete fence ────────────────────────────────────────────────────────────
128
+ // An explicitly deleted session may still own a running turn whose later saves
129
+ // carry a brand-new write guard and incarnation, so neither the cancellation
130
+ // counter nor the retired incarnation stops them: an absent file reads as
131
+ // "creatable". The fence is the id-level refusal every write-admission check
132
+ // consults. Only explicit user deletes fence (ids are never reused); the cap
133
+ // only ever forgets the oldest, long-settled entries.
134
+ const SESSION_DELETE_FENCE_MAX = 4096;
135
+ const _deleteFence = new Set();
136
+
137
+ export function fenceSessionWrites(id) {
138
+ if (!id) return;
139
+ _deleteFence.delete(id);
140
+ _deleteFence.add(id);
141
+ if (_deleteFence.size > SESSION_DELETE_FENCE_MAX) {
142
+ _deleteFence.delete(_deleteFence.values().next().value);
143
+ }
144
+ }
145
+
146
+ export function isSessionWriteFenced(id) {
147
+ return _deleteFence.has(id);
148
+ }
149
+
127
150
  /** Clear every save marker for an id (successful hard delete). */
128
151
  export function _clearSessionSaveState(id) {
129
152
  if (!id) return;
@@ -83,6 +83,13 @@ function _readSidecarOwnerPid(path) {
83
83
  }
84
84
  }
85
85
 
86
+ /** True only when the sidecar recorded an owner pid that no longer exists
87
+ * (pid-less legacy sidecars prove nothing and read false). */
88
+ export function isSidecarOwnerDead(path) {
89
+ const pid = _readSidecarOwnerPid(path);
90
+ return pid > 0 && !isProcessAlive(pid);
91
+ }
92
+
86
93
  // ── Heartbeat publish ─────────────────────────────────────
87
94
  // Lightweight per-session timestamp file (`<id>.hb`) consumed by the
88
95
  // status aggregator for fresh-session detection. Decoupled from the
@@ -193,25 +200,74 @@ export function isSessionHeartbeatOwnerDead(id) {
193
200
  // writer on the same session file (dual-writer last-save-wins clobbering).
194
201
  // Refreshed by the engine share tick (~3s call, throttled here) and cleared
195
202
  // on session switch/dispose; staleness covers crashes.
203
+ //
204
+ // Presence is a sidecar OF a session record and never outlives it: it is
205
+ // only published while the record exists, and a publish that loses a race
206
+ // with a deletion (this process or another one unlinked the record and its
207
+ // sidecar while the write was in flight) removes what it just wrote.
196
208
  const _PRESENCE_THROTTLE_MS = 20_000;
197
209
  const _presenceLastAt = new Map();
210
+ // id -> in-flight publish; deleteSessionPresence cancels it so the write that
211
+ // lands after the unlink is removed again instead of resurrecting the file.
212
+ const _presenceInFlight = new Map();
198
213
 
199
214
  function _presencePath(id) {
200
215
  return _sessionFilePath(id, '.own');
201
216
  }
202
217
 
218
+ async function _sessionRecordExists(id) {
219
+ try {
220
+ await fsp.stat(sessionPath(id));
221
+ return true;
222
+ } catch (error) {
223
+ // Only a proven absence stops the beacon; an unreadable stat keeps it.
224
+ return error?.code !== 'ENOENT';
225
+ }
226
+ }
227
+
228
+ async function _publishPresence(id, now, flight) {
229
+ try {
230
+ // A record that does not exist (never saved yet, or deleted) gets no
231
+ // beacon; the throttle stays open so the next tick retries.
232
+ if (!(await _sessionRecordExists(id)) || flight.cancelled) {
233
+ if (_presenceLastAt.get(id) === now) _presenceLastAt.delete(id);
234
+ return;
235
+ }
236
+ const target = _presencePath(id);
237
+ await fsp.writeFile(target, `${now}\n${process.pid}\n`, 'utf8');
238
+ if (flight.cancelled || !(await _sessionRecordExists(id))) {
239
+ await fsp.unlink(target).catch(() => {});
240
+ if (_presenceLastAt.get(id) === now) _presenceLastAt.delete(id);
241
+ }
242
+ } catch {
243
+ /* best-effort beacon */
244
+ } finally {
245
+ if (_presenceInFlight.get(id) === flight) _presenceInFlight.delete(id);
246
+ }
247
+ }
248
+
203
249
  export function publishSessionPresence(id, ts) {
204
250
  if (!id) return;
205
251
  const now = ts || Date.now();
206
252
  const last = _presenceLastAt.get(id) || 0;
207
253
  if (now - last < _PRESENCE_THROTTLE_MS) return;
208
- const target = _presencePath(id);
254
+ if (_presenceInFlight.has(id)) return;
255
+ _assertSessionId(id);
209
256
  _presenceLastAt.set(id, now);
210
- void fsp.writeFile(target, `${now}\n${process.pid}\n`, 'utf8').catch(() => {});
257
+ const flight = { cancelled: false, done: null };
258
+ _presenceInFlight.set(id, flight);
259
+ flight.done = _publishPresence(id, now, flight);
260
+ }
261
+
262
+ /** Test/drain seam: resolves once the id's in-flight presence publish settled. */
263
+ export function settleSessionPresence(id) {
264
+ return _presenceInFlight.get(id)?.done || Promise.resolve();
211
265
  }
212
266
 
213
267
  export function deleteSessionPresence(id) {
214
268
  if (!id) return;
269
+ const flight = _presenceInFlight.get(id);
270
+ if (flight) flight.cancelled = true;
215
271
  try {
216
272
  unlinkSync(_presencePath(id));
217
273
  } catch {
@@ -4,10 +4,11 @@
4
4
  * edge plus the full local-state purge that must happen under the same lock
5
5
  * (incarnation retirement, pending slots, purge hooks, markers, summary row).
6
6
  */
7
- import { readFileSync, readdirSync, unlinkSync } from 'node:fs';
8
- import { join } from 'node:path';
7
+ import { unlinkSync } from 'node:fs';
9
8
  import { isAgentOwner } from '../../agent-owner.mjs';
10
- import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from '../lifecycle-scan.mjs';
9
+ import { listStoredAgentWorkerLinks } from '../store-agent-worker-rows.mjs';
10
+ import { listStoredSessionSummaries } from './listing.mjs';
11
+ import { STORED_SESSION_UNREADABLE, _storedSessionFromFile } from './serialize.mjs';
11
12
  import { getStoreDir, sessionPath, deleteSessionPresence } from './paths-heartbeat.mjs';
12
13
  import {
13
14
  readCanonicalSessionRecord as _readCanonicalRecord,
@@ -22,49 +23,58 @@ import {
22
23
  } from './write-guards.mjs';
23
24
  import {
24
25
  _clearLiveSession,
26
+ _liveSessions,
25
27
  _retireSessionIncarnation,
26
28
  _clearSessionSaveState,
27
29
  clearSessionLifecycleCommitError,
30
+ fenceSessionWrites,
28
31
  } from './live-state.mjs';
29
32
  import { purgeSessionSaveBookkeeping as _purgeSessionSaveBookkeeping } from './save-worker.mjs';
30
- import { _uncacheSessionSummary, _queueSessionSummaryRemoval } from './summary-cache.mjs';
33
+ import { _uncacheSessionSummary, _queueSessionSummaryRemoval, _cachedSummaryRows } from './summary-cache.mjs';
31
34
  import { _savePending, _clearDebounce, _releasePendingSlot } from './pending-saves.mjs';
32
35
  import { _runtimeLivenessVeto, _heartbeatLivenessVeto, _deleteHeartbeatUnlessNewer } from './liveness-veto.mjs';
33
36
  import { _runSessionPurgeHooks } from './session-observers.mjs';
34
37
  import { deleteCompactionArchive } from '../compaction-archive.mjs';
35
38
 
36
- /** Strictly enumerate child-agent session files linked to one visible parent.
37
- * Used only by explicit parent deletion; ordinary close/context switches keep
38
- * the relationship intact. */
39
+ /** Child-agent session ids linked to one visible parent. Candidates come from
40
+ * metadata only (the summary rows and the agent-worker links — no folder
41
+ * scan); each candidate's own file is then read strictly and must still be an
42
+ * agent-owned record linked to this parent, so a stale/foreign metadata row
43
+ * can never name a deletion target. Used only by explicit parent deletion;
44
+ * ordinary close/context switches keep the relationship intact. */
39
45
  export function listOwnedAgentSessionIds(ownerSessionId) {
40
46
  const ownerId = String(ownerSessionId || '').trim();
41
47
  if (!/^[A-Za-z0-9_-]+$/.test(ownerId)) return [];
42
- const dir = getStoreDir();
43
- if (probePath(dir).state !== PROBE_PRESENT) return [];
44
- let files;
45
- try {
46
- files = readdirSync(dir).filter((file) => file.endsWith('.json'));
47
- } catch {
48
- return [];
48
+ const linksTo = (row) => row?.ownerSessionId === ownerId || row?.parentSessionId === ownerId;
49
+ const candidates = new Set();
50
+ for (const row of [...listStoredSessionSummaries(), ...listStoredAgentWorkerLinks()]) {
51
+ const candidateId = String(row?.id || row?.sessionId || '').trim();
52
+ if (linksTo(row) && candidateId !== ownerId && /^[A-Za-z0-9_-]+$/.test(candidateId)) {
53
+ candidates.add(candidateId);
54
+ }
49
55
  }
56
+ const dir = getStoreDir();
50
57
  const ids = [];
51
- for (const file of files) {
52
- const candidateId = file.slice(0, -5);
53
- if (!candidateId || candidateId === ownerId || !/^[A-Za-z0-9_-]+$/.test(candidateId)) continue;
54
- try {
55
- const record = readTopLevelLifecycleRecord(readFileSync(join(dir, file), 'utf8'));
56
- if (isLifecycleUnreadable(record) || record.id !== candidateId) continue;
57
- const session = record.doc;
58
- if (!isAgentOwner(session)) continue;
59
- const linkedOwner = String(session.ownerSessionId || session.parentSessionId || '').trim();
60
- if (linkedOwner === ownerId) ids.push(candidateId);
61
- } catch {
62
- /* unreadable/vanished records are never deletion targets */
63
- }
58
+ for (const candidateId of candidates) {
59
+ const session = _storedSessionFromFile(dir, `${candidateId}.json`, false);
60
+ // Unreadable, vanished, corrupt or foreign records are never deletion targets.
61
+ if (!session || session === STORED_SESSION_UNREADABLE || !isAgentOwner(session)) continue;
62
+ const linkedOwner = String(session.ownerSessionId || session.parentSessionId || '').trim();
63
+ if (linkedOwner === ownerId) ids.push(candidateId);
64
64
  }
65
65
  return ids;
66
66
  }
67
67
 
68
+ /** Working folders the session is known to have used (its cached summary row
69
+ * and live snapshot — no file read, no scan); the purge hooks derive
70
+ * per-folder artifacts such as the transcript path from them. */
71
+ function knownSessionCwds(id) {
72
+ const row = _cachedSummaryRows()?.find((candidate) => candidate.id === id);
73
+ const live = _liveSessions.get(id);
74
+ const cwds = [row?.cwd, row?.workspace?.cwd, row?.workspace?.projectPath, live?.cwd];
75
+ return [...new Set(cwds.filter((cwd) => typeof cwd === 'string' && cwd))];
76
+ }
77
+
68
78
  export function deleteSession(id, options = {}) {
69
79
  // Keep caller probes and all vetoes ahead of the non-reentrant lock and
70
80
  // ahead of pending-save disruption, matching markSessionClosed().
@@ -122,12 +132,15 @@ export function deleteSession(id, options = {}) {
122
132
  // its settlement moves no marker and touches no bookkeeping — and a
123
133
  // re-created id gets a brand new incarnation of its own.
124
134
  _retireSessionIncarnation(id);
135
+ // An explicit user delete also fences the id: a still-running turn's later
136
+ // saves get fresh guards/incarnations and would otherwise recreate the file.
137
+ if (options.fenceWrites === true) fenceSessionWrites(id);
125
138
  _cancelSessionWrites(id);
126
139
  _clearDebounce(id);
127
140
  _releasePendingSlot(_savePending.get(id));
128
141
  _savePending.delete(id);
129
142
  _purgeSessionSaveBookkeeping(id);
130
- _runSessionPurgeHooks(id);
143
+ _runSessionPurgeHooks(id, { cwds: knownSessionCwds(id) });
131
144
  // Preserve a sidecar published strictly after the sweep's scan snapshot.
132
145
  _deleteHeartbeatUnlessNewer(id, options);
133
146
  deleteSessionPresence(id);
@@ -44,10 +44,12 @@ export function _publishLiveSession(session) {
44
44
 
45
45
  _setLiveSessionPublisher(_publishLiveSession);
46
46
 
47
- export function _runSessionPurgeHooks(id) {
47
+ /** `context` (optional, e.g. `{ cwds }`: working folders the session is known
48
+ * to have used) is passed to every hook as its second argument. */
49
+ export function _runSessionPurgeHooks(id, context) {
48
50
  for (const hook of _sessionPurgeHooks) {
49
51
  try {
50
- hook(id);
52
+ hook(id, context);
51
53
  } catch {
52
54
  /* a cleanup hook never breaks delete */
53
55
  }
@@ -7,6 +7,11 @@ import { probePath, PROBE_PRESENT, PROBE_ABSENT } from '../fs-probe.mjs';
7
7
  import { _queueSummaryIndexPrune } from '../summary-cache.mjs';
8
8
  import { deleteSession } from '../session-delete.mjs';
9
9
  import { COMPACTION_ARCHIVE_SUFFIX } from '../../compaction-archive.mjs';
10
+ import { isSidecarOwnerDead } from '../paths-heartbeat.mjs';
11
+
12
+ // A live presence owner rewrites `.own` every ~20s; one untouched for the
13
+ // attach guard's whole freshness window holds nothing open.
14
+ const PRESENCE_ORPHAN_AGE_MS = 2 * 60 * 1000;
10
15
 
11
16
  /** Retention cap: prune resumable open (non-tombstone) sessions newest-first
12
17
  * — keep the most recent openMaxCount, prune anything older than openMaxAgeMs
@@ -68,7 +73,13 @@ export function* reapOrphanSidecars({ plan, dir, now, tally }) {
68
73
  if (probePath(join(dir, recordOf(h))).state !== PROBE_ABSENT) continue;
69
74
  const sidecarProbe = probePath(join(dir, h));
70
75
  if (sidecarProbe.state !== PROBE_PRESENT) continue;
71
- if (now - sidecarProbe.mtimeMs > plan.maxAge) {
76
+ // A presence sidecar is only ever published while its record exists,
77
+ // so one without a record whose recorded owner is dead (or which no
78
+ // live owner refreshed within the presence window) is a leftover of a
79
+ // deleted session, not a session mid-create.
80
+ const orphanPresence =
81
+ h.endsWith('.own') && (now - sidecarProbe.mtimeMs > PRESENCE_ORPHAN_AGE_MS || isSidecarOwnerDead(join(dir, h)));
82
+ if (orphanPresence || now - sidecarProbe.mtimeMs > plan.maxAge) {
72
83
  try {
73
84
  unlinkSync(join(dir, h));
74
85
  tally.cleaned++;
@@ -14,6 +14,7 @@ import {
14
14
  } from './canonical-reader.mjs';
15
15
  import { isCancelledWrite as _isCancelledWrite } from './write-guards.mjs';
16
16
  import { _setSessionWriteAuthorityCheck } from './save-worker.mjs';
17
+ import { isSessionWriteFenced } from './live-state.mjs';
17
18
 
18
19
  const _observeStamp = (target) => {
19
20
  try {
@@ -71,7 +72,7 @@ function _writeAuthorityRecord(target, attempt) {
71
72
  * under their own authority read).
72
73
  */
73
74
  export function _shouldDrop(id, opts, attempt = null) {
74
- if (_isCancelledWrite(opts)) return true;
75
+ if (_isCancelledWrite(opts) || isSessionWriteFenced(id)) return true;
75
76
  const expected = typeof opts?.expectedGeneration === 'number' ? opts.expectedGeneration : null;
76
77
  const target = sessionPath(id);
77
78
  let record;
@@ -126,6 +127,7 @@ export function _shouldDrop(id, opts, attempt = null) {
126
127
  // under the lock by _shouldDrop.
127
128
  export function _sessionWriteAuthorityRefusal(id, attempt = null) {
128
129
  if (!id) return null;
130
+ if (isSessionWriteFenced(id)) return 'deleted';
129
131
  let authority;
130
132
  try {
131
133
  // A synchronous save passes its `attempt`: its pre-admission is then the