mixdog 1.3.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +2 -2
  3. package/package.json +2 -11
  4. package/scripts/daemon-stub.mjs +1 -1
  5. package/scripts/gate-local.mjs +4 -2
  6. package/scripts/native-release-status.mjs +2 -2
  7. package/scripts/recall-history-cases.json +1 -1
  8. package/scripts/suite-health.mjs +10 -1
  9. package/scripts/test.mjs +7 -11
  10. package/src/agents/reviewer/AGENT.md +7 -11
  11. package/src/agents/reviewer/agent.json +1 -1
  12. package/src/defaults/shipped-history.json +17 -1
  13. package/src/defaults/skills/browser-use/SKILL.md +59 -11
  14. package/src/defaults/skills/code-tidy/SKILL.md +142 -270
  15. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
  16. package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
  17. package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
  18. package/src/defaults/skills/computer-use/SKILL.md +33 -9
  19. package/src/defaults/skills/setup/SKILL.md +22 -0
  20. package/src/defaults/skills/setup/references/actions.md +39 -5
  21. package/src/defaults/skills/setup/references/surfaces.md +11 -2
  22. package/src/defaults/skills/visualize/SKILL.md +2 -1
  23. package/src/lib/rules-builder.cjs +10 -8
  24. package/src/rules/agent/AGENT.md +6 -6
  25. package/src/rules/lead/LEAD.md +18 -20
  26. package/src/rules/shared/05-tool-calls.md +5 -6
  27. package/src/rules/shared/10-tool-workflow.md +2 -0
  28. package/src/rules/shared/60-verification.md +7 -5
  29. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  32. package/src/runtime/agent/orchestrator/config.mjs +17 -1
  33. package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
  34. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
  35. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
  36. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +176 -72
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
  39. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
  40. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
  41. package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
  42. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
  44. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
  45. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +145 -66
  46. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
  47. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
  48. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
  49. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +123 -102
  50. package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
  52. package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
  53. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
  54. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
  55. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
  56. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
  57. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
  58. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +2 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
  64. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
  65. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
  66. package/src/runtime/agent/orchestrator/providers/registry.mjs +26 -0
  67. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
  68. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
  69. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
  72. package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
  75. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
  76. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
  77. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
  78. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
  79. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -1
  81. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
  82. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
  83. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
  84. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
  86. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
  87. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +3 -1
  88. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
  90. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
  92. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +3 -2
  93. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
  94. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
  97. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +12 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
  99. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
  101. package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
  102. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
  103. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
  104. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
  105. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
  106. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
  107. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  109. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
  110. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
  112. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
  113. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
  114. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
  115. package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
  116. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
  117. package/src/runtime/agent/orchestrator/session/routine-result.mjs +36 -9
  118. package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
  120. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
  121. package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
  122. package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
  123. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
  124. package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
  125. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
  126. package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
  127. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +8 -6
  128. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
  129. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
  130. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
  131. package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
  132. package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
  133. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
  135. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +18 -1
  136. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
  138. package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
  141. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
  143. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
  144. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
  145. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
  146. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
  147. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
  148. package/src/runtime/browser-bridge/action-schema.mjs +13 -2
  149. package/src/runtime/browser-bridge/client.mjs +73 -8
  150. package/src/runtime/browser-bridge/input-fields.mjs +8 -9
  151. package/src/runtime/browser-bridge/mail-links.mjs +107 -0
  152. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  153. package/src/runtime/channels/lib/config.mjs +6 -61
  154. package/src/runtime/channels/lib/crash-log.mjs +1 -1
  155. package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
  156. package/src/runtime/channels/lib/event-queue.mjs +3 -18
  157. package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
  158. package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
  159. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
  160. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
  161. package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
  162. package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
  163. package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
  164. package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
  165. package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
  166. package/src/runtime/channels/lib/scheduler.mjs +33 -147
  167. package/src/runtime/channels/lib/schedules-db.mjs +34 -11
  168. package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
  169. package/src/runtime/channels/lib/tool-dispatch.mjs +1 -2
  170. package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
  171. package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
  172. package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
  173. package/src/runtime/channels/lib/worker-main.mjs +10 -34
  174. package/src/runtime/computer-bridge/action-schema.mjs +42 -1
  175. package/src/runtime/computer-bridge/actions.mjs +9 -1
  176. package/src/runtime/computer-bridge/client.mjs +180 -8
  177. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  178. package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
  179. package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
  180. package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
  181. package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
  182. package/src/runtime/connectors/gmail-app-password.mjs +172 -39
  183. package/src/runtime/connectors/google-mcp.mjs +10 -12
  184. package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
  185. package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
  186. package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
  187. package/src/runtime/session-search/session-search-db.mjs +73 -9
  188. package/src/runtime/session-search/session-search-ingest.mjs +110 -20
  189. package/src/runtime/shared/abort-race.mjs +89 -0
  190. package/src/runtime/shared/agent-avatar.d.mts +0 -1
  191. package/src/runtime/shared/agent-kind.mjs +4 -3
  192. package/src/runtime/shared/bounded-download.mjs +8 -3
  193. package/src/runtime/shared/config.mjs +2 -13
  194. package/src/runtime/shared/connector-accounts.mjs +20 -10
  195. package/src/runtime/shared/connector-catalog.d.mts +29 -0
  196. package/src/runtime/shared/connector-catalog.mjs +150 -0
  197. package/src/runtime/shared/connector-triggers.mjs +22 -19
  198. package/src/runtime/shared/defaults-separation.mjs +2 -14
  199. package/src/runtime/shared/delegated-approval-host.mjs +103 -11
  200. package/src/runtime/shared/err-text.mjs +24 -15
  201. package/src/runtime/shared/file-permissions.d.mts +0 -8
  202. package/src/runtime/shared/file-permissions.mjs +0 -11
  203. package/src/runtime/shared/git-direct.test-support.mjs +19 -0
  204. package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
  205. package/src/runtime/shared/heartbeat-interval.mjs +19 -0
  206. package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
  207. package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
  208. package/src/runtime/shared/mixy-presence.mjs +22 -0
  209. package/src/runtime/shared/orchestration.d.mts +2 -0
  210. package/src/runtime/shared/pg-schema-db.mjs +27 -16
  211. package/src/runtime/shared/profile-config.cjs +1 -1
  212. package/src/runtime/shared/prompt-origin.d.mts +4 -0
  213. package/src/runtime/shared/prompt-origin.mjs +21 -0
  214. package/src/runtime/shared/provider-accounts.mjs +7 -1
  215. package/src/runtime/shared/provider-display-name.mjs +29 -0
  216. package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
  217. package/src/runtime/shared/routine-trigger.d.mts +2 -0
  218. package/src/runtime/shared/routine-trigger.mjs +10 -0
  219. package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
  220. package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
  221. package/src/runtime/shared/session-activity-signal.mjs +34 -1
  222. package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
  223. package/src/runtime/shared/session-sidebar-host.mjs +7 -3
  224. package/src/runtime/shared/session-turn-prelude.mjs +18 -0
  225. package/src/runtime/shared/tool-result-summary.mjs +1 -14
  226. package/src/runtime/shared/worktrees/test-support.mjs +9 -4
  227. package/src/runtime/terminal-bridge/client.mjs +10 -1
  228. package/src/runtime/visualize-bridge/client.mjs +27 -8
  229. package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
  230. package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
  231. package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
  232. package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
  233. package/src/session-runtime/agent-home-delivery.mjs +448 -185
  234. package/src/session-runtime/agent-task-sessions.mjs +329 -180
  235. package/src/session-runtime/boot/apis.mjs +9 -0
  236. package/src/session-runtime/boot/infrastructure.mjs +4 -0
  237. package/src/session-runtime/boot/providers.mjs +3 -0
  238. package/src/session-runtime/boot/tools.mjs +6 -1
  239. package/src/session-runtime/config-lifecycle.mjs +6 -0
  240. package/src/session-runtime/connector-merge-migration.mjs +305 -0
  241. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
  242. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
  243. package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
  244. package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
  245. package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
  246. package/src/session-runtime/lifecycle/teardown.mjs +23 -5
  247. package/src/session-runtime/mcp-glue/server-catalog.mjs +55 -3
  248. package/src/session-runtime/mcp-oauth-login.mjs +5 -3
  249. package/src/session-runtime/mixy-heartbeat.mjs +153 -33
  250. package/src/session-runtime/notification-bus.mjs +31 -117
  251. package/src/session-runtime/notification-completion-queue.mjs +54 -0
  252. package/src/session-runtime/notification-listeners.mjs +76 -0
  253. package/src/session-runtime/owned-automation-run.mjs +2 -1
  254. package/src/session-runtime/prewarm/channel-start.mjs +3 -7
  255. package/src/session-runtime/resource-mcp-api.mjs +143 -224
  256. package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
  257. package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
  258. package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
  259. package/src/session-runtime/resource-mcp-servers.mjs +78 -0
  260. package/src/session-runtime/resource-mcp-store.mjs +49 -0
  261. package/src/session-runtime/routine-owner.mjs +40 -9
  262. package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
  263. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
  264. package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
  265. package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
  266. package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
  267. package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
  268. package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
  269. package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
  270. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
  271. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
  272. package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
  273. package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
  274. package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
  275. package/src/session-runtime/services/channel-admin.mjs +7 -13
  276. package/src/session-runtime/services/channel-session-router.mjs +4 -2
  277. package/src/session-runtime/services/seeds.mjs +0 -28
  278. package/src/session-runtime/session-compaction-sync.mjs +27 -0
  279. package/src/session-runtime/session-create/session-options.mjs +1 -0
  280. package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
  281. package/src/session-runtime/settings-compaction-api.mjs +2 -40
  282. package/src/session-runtime/settings-system-api.mjs +13 -1
  283. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
  284. package/src/session-runtime/setup-tool/executor.mjs +145 -22
  285. package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
  286. package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
  287. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
  288. package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
  289. package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
  290. package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
  291. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  292. package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
  293. package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
  294. package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
  295. package/src/standalone/channel-transport/client-registry.mjs +24 -163
  296. package/src/standalone/channel-transport.mjs +1 -1
  297. package/src/standalone/daemon-channels-loader.mjs +10 -8
  298. package/src/standalone/daemon-session-control/list-page.mjs +72 -0
  299. package/src/standalone/daemon-session-control/request.mjs +51 -0
  300. package/src/standalone/daemon-session-control/room-host.mjs +57 -0
  301. package/src/standalone/daemon-session-control/route-control.mjs +48 -0
  302. package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
  303. package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
  304. package/src/standalone/daemon-session-control.mjs +124 -188
  305. package/src/standalone/daemon.mjs +26 -3
  306. package/src/standalone/session-client.mjs +7 -3
  307. package/src/standalone/session-protocol.mjs +5 -0
  308. package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
  309. package/src/standalone/session-service/quota-resume.mjs +3 -0
  310. package/src/standalone/session-service/retention/busy.mjs +65 -1
  311. package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
  312. package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
  313. package/src/standalone/session-tool-output.mjs +15 -7
  314. package/src/tui/app/model-options.mjs +3 -28
  315. package/src/tui/dist/index.mjs +58 -64
  316. package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
  317. package/src/tui/session/initial-state.mjs +3 -0
  318. package/src/tui/session/notice-surface.mjs +7 -1
  319. package/src/tui/session/queue-helpers.mjs +3 -42
  320. package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
  321. package/src/tui/session/session-api/routes-write.mjs +5 -1
  322. package/src/tui/session/session-api/routes.mjs +1 -0
  323. package/src/tui/session/session-api/settings.mjs +17 -1
  324. package/src/tui/session/session-flow/auto-clear.mjs +4 -33
  325. package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
  326. package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
  327. package/src/tui/session/session-flow/drain-retry.mjs +35 -0
  328. package/src/tui/session/session-flow/drain.mjs +118 -149
  329. package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
  330. package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
  331. package/src/tui/session/tool-approval-delegation.mjs +64 -0
  332. package/src/tui/session/tool-approval-request.mjs +39 -0
  333. package/src/tui/session/tool-approval-settle.mjs +62 -0
  334. package/src/tui/session/tool-approval.mjs +69 -115
  335. package/src/tui/session/tui-steering-persist.mjs +8 -1
  336. package/src/tui/session/turn-stream.mjs +19 -2
  337. package/src/tui/session/turn.mjs +1 -0
  338. package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
  339. package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
  340. package/scripts/code-graph-description-contract.mjs +0 -188
  341. package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
@@ -3,6 +3,7 @@
3
3
  // side question or a compaction summary forks) and the side question in flight. It holds live
4
4
  // provider/tool objects, so it never reaches the session record or disk.
5
5
  import { isAgentOwner } from '../agent-owner.mjs';
6
+ import { armCacheWarm, cancelCacheWarm, forgetCacheWarm, noteCacheWarmActivity } from './cache-warm.mjs';
6
7
 
7
8
  // sessionId -> { provider, model, messages, tools, opts, response }
8
9
  const requests = new Map();
@@ -42,21 +43,27 @@ export function sideQuestionRequestSnapshot({ provider, model, messages, tools,
42
43
 
43
44
  /**
44
45
  * Record a provider request the main loop is sending as the session's latest
45
- * one. Agent sessions record nothing (/btw is a Lead command). The handle
46
- * attaches the reply once the send proceeds, or puts the previous request
47
- * back when this one fails, so only a successfully sent request is kept.
46
+ * one. Agent sessions keep nothing here (/btw is a Lead command); their
47
+ * request only reaches the cache warmer. The handle attaches the reply once
48
+ * the send proceeds, or puts the previous request back when this one fails,
49
+ * so only a successfully sent request is kept.
48
50
  */
49
51
  export function captureSideQuestionRequest({ sessionId, sessionRef, provider, model, messages, tools, opts }) {
50
- if (!sessionId || !provider || isAgentOwner(sessionRef)) return null;
52
+ if (!sessionId || !provider) return null;
53
+ const kept = !isAgentOwner(sessionRef);
51
54
  const previous = requests.get(sessionId);
52
55
  const request = sideQuestionRequestSnapshot({ provider, model, messages, tools, opts });
53
- requests.set(sessionId, request);
56
+ if (kept) requests.set(sessionId, request);
57
+ // A real request is session activity: it closes the session's idle gap and
58
+ // replaces any cache warm (cache-warm.mjs); a settled one arms the next.
59
+ noteCacheWarmActivity(sessionId, request);
54
60
  return {
55
61
  settle(response) {
56
62
  request.response = response || null;
63
+ armCacheWarm(sessionId, request, response);
57
64
  },
58
65
  discard() {
59
- if (requests.get(sessionId) !== request) return;
66
+ if (!kept || requests.get(sessionId) !== request) return;
60
67
  if (previous) requests.set(sessionId, previous);
61
68
  else requests.delete(sessionId);
62
69
  },
@@ -80,6 +87,7 @@ export function compactionForkRequest(sessionId) {
80
87
  export function retireCompactionFork(sessionId) {
81
88
  const request = sideQuestionRequest(sessionId);
82
89
  if (request) request.compacted = true;
90
+ forgetCacheWarm(sessionId);
83
91
  }
84
92
 
85
93
  /** Start the session's side question, aborting the one still in flight. */
@@ -109,5 +117,10 @@ export function cancelSideQuestion(sessionId) {
109
117
  export function forgetSideQuestion(sessionId, { cancel = false } = {}) {
110
118
  if (!sessionId) return;
111
119
  requests.delete(sessionId);
112
- if (cancel) cancelSideQuestion(sessionId);
120
+ // Clear / rewind drop the cached prefix; a close or runtime unload (daemon
121
+ // shutdown) keeps its durable touch for the restart re-arm.
122
+ if (cancel) {
123
+ cancelCacheWarm(sessionId);
124
+ cancelSideQuestion(sessionId);
125
+ } else forgetCacheWarm(sessionId);
113
126
  }
@@ -98,6 +98,24 @@ function nextMainRequest(sessionId) {
98
98
  });
99
99
  }
100
100
 
101
+ /**
102
+ * The session's last provider request as its next turn would rebuild it from
103
+ * the record (a runtime restart lost the in-memory one): the next request
104
+ * without the reply that last request produced, stamped with the session's
105
+ * last send time. Null when the transcript does not end in that reply or the
106
+ * session has no rebuildable request (nextMainRequest).
107
+ */
108
+ export function lastSentMainRequest(sessionId) {
109
+ const request = nextMainRequest(sessionId);
110
+ if (!request) return null;
111
+ let end = request.messages.length;
112
+ while (end > 0 && request.messages[end - 1]?.role === 'assistant') end -= 1;
113
+ if (end === 0 || end === request.messages.length) return null;
114
+ const sentAt = Number(request.opts.session?.lastProviderSendAt);
115
+ if (!(sentAt > 0)) return null;
116
+ return { ...request, messages: request.messages.slice(0, end), sentAt };
117
+ }
118
+
101
119
  function answerText(response) {
102
120
  if (response?.toolCalls?.length) return SIDE_QUESTION_TOOL_ATTEMPT;
103
121
  const text = typeof response?.content === 'string' ? response.content.trim() : '';
@@ -0,0 +1,60 @@
1
+ // A thread can live for days while its loaded skills change underneath it (an
2
+ // app update ships a new SKILL.md, the user edits or switches one off). Compact
3
+ // restores every skill from its current source; between Compacts, each turn
4
+ // tells the model once that a skill it holds instructions for changed, as a
5
+ // new runtime message after the history, never by rewriting what was sent.
6
+ import { createHash } from 'node:crypto';
7
+ import { loadConfig } from '../config.mjs';
8
+ import { collectSkillsCached, currentSkillDelivery } from '../context/collect.mjs';
9
+ import { latestSkillBodies, skillMessageText } from '../context/skill-state.mjs';
10
+
11
+ export const SKILL_UPDATE_SOURCE = 'skill-update-notice';
12
+
13
+ const UNAVAILABLE = 'unavailable';
14
+ // The loaded body is what the Skill tool delivers again (after an
15
+ // unavailable notice): its instructions are back in force.
16
+ const CURRENT = 'current';
17
+
18
+ function skillUpdateNotice(name, version) {
19
+ let text = `[mixdog-runtime] Skill "${name}" was updated since it was loaded. Reload it with the Skill tool before relying on it.`;
20
+ if (version === UNAVAILABLE) {
21
+ text = `[mixdog-runtime] Skill "${name}" is no longer available in this session (switched off, removed, or outside this project's scope). Do not rely on its instructions.`;
22
+ } else if (version === CURRENT) {
23
+ text = `[mixdog-runtime] Skill "${name}" is available again, unchanged; its loaded instructions apply again.`;
24
+ }
25
+ return { role: 'user', content: text, meta: { source: SKILL_UPDATE_SOURCE, synthetic: true, skill: name, version } };
26
+ }
27
+
28
+ /**
29
+ * For every skill whose latest body is in `messages`, compare it with what the
30
+ * Skill tool would deliver at `cwd` now. Its state — the loaded body still
31
+ * current, a changed version, or unavailable — is announced whenever it
32
+ * differs from the last state told after that body (none told: current).
33
+ * So each transition notifies once: changed, switched off, and back again
34
+ * unchanged. A reload replaces the body and starts over. Returns the
35
+ * appended notices.
36
+ */
37
+ export function appendSkillUpdateNotices(messages, cwd) {
38
+ const bodies = latestSkillBodies(messages);
39
+ if (!bodies.length) return [];
40
+ const config = loadConfig({ secrets: false });
41
+ const skills = collectSkillsCached(cwd);
42
+ const notices = [];
43
+ for (const { name, message } of bodies) {
44
+ const delivery = currentSkillDelivery(name, cwd, config, skills);
45
+ let version = UNAVAILABLE;
46
+ if (delivery) {
47
+ version =
48
+ skillMessageText(message.content).trimStart() === delivery.body
49
+ ? CURRENT
50
+ : createHash('sha256').update(delivery.body).digest('hex').slice(0, 16);
51
+ }
52
+ const told =
53
+ messages
54
+ .slice(messages.lastIndexOf(message) + 1)
55
+ .findLast((m) => m?.meta?.source === SKILL_UPDATE_SOURCE && m.meta.skill === name)?.meta.version || CURRENT;
56
+ if (told !== version) notices.push(skillUpdateNotice(name, version));
57
+ }
58
+ messages.push(...notices);
59
+ return notices;
60
+ }
@@ -1,4 +1,5 @@
1
1
  import { contentCarriesLiveMedia } from '../../providers/media-normalization.mjs';
2
+ import { publishSessionPersistence } from './session-persistence-events.mjs';
2
3
 
3
4
  const _lastSaveError = new Map(); // id -> { message, at }
4
5
 
@@ -287,9 +288,11 @@ export function _recordSaveDrop(id, epoch = null) {
287
288
  export function _clearSaveStateIfCurrent(id, epoch = null) {
288
289
  if (!id) return false;
289
290
  const marked = _failureEpochs.get(id);
290
- if (marked !== undefined && Number.isFinite(epoch) && epoch < marked) return false;
291
- _clearSessionSaveState(id);
292
- return true;
291
+ const cleared = !(marked !== undefined && Number.isFinite(epoch) && epoch < marked);
292
+ if (cleared) _clearSessionSaveState(id);
293
+ // A landed write put the record on disk whether or not it clears the markers.
294
+ publishSessionPersistence(id, 'landed');
295
+ return cleared;
293
296
  }
294
297
 
295
298
  /** True while this process's last save attempt for `id` failed. */
@@ -1,5 +1,6 @@
1
1
  import { hash } from 'node:crypto';
2
2
  import { _messagesForDisk, _sessionForDisk } from './serialize.mjs';
3
+ import { sensitiveValuesVersion } from '../sensitive-values.mjs';
3
4
 
4
5
  // ── Delta handoff to the save worker ─────────────────────────────────────────
5
6
  // Worker.postMessage structured-clones on the CALLER's thread, so a full-
@@ -93,7 +94,7 @@ function _attemptRecord(header, refs, prints) {
93
94
  */
94
95
  export function _attemptEvidence(attempt) {
95
96
  if (!attempt) return null;
96
- const messages = _messagesForDisk(attempt.refs);
97
+ const messages = _messagesForDisk(attempt.refs, attempt.header.id);
97
98
  for (let index = 0; index < messages.length; index += 1) {
98
99
  const print = attempt.prints[index];
99
100
  if (print === null || _messagePrint(messages[index]) !== print) return null;
@@ -112,14 +113,17 @@ function _isPureAppend(refs, live) {
112
113
  export function _buildWirePayload(id, session, forceFull = false) {
113
114
  const live = Array.isArray(session.messages) ? session.messages : [];
114
115
  const base = forceFull ? null : _deltaBaseline.get(id);
115
- const resyncDue = !base || base.deltasSinceFull >= DELTA_FULL_RESYNC_EVERY;
116
+ // A value marked sensitive since the baseline was sent must leave the
117
+ // messages the worker already holds too: write the whole transcript again.
118
+ const sensitive = sensitiveValuesVersion(id);
119
+ const resyncDue = !base || base.deltasSinceFull >= DELTA_FULL_RESYNC_EVERY || base.sensitive !== sensitive;
116
120
  if (!resyncDue && _isPureAppend(base.refs, live)) {
117
121
  // Header: full disk projection with an empty message list (strips
118
122
  // transient aliases); tail: per-message projection of the appended
119
123
  // suffix only. Both use the same idempotent per-message projection
120
124
  // as a full send, so worker-side reconstruction is byte-identical.
121
125
  const header = _sessionForDisk({ ...session, messages: [] });
122
- const tailMessages = _messagesForDisk(live.slice(base.refs.length));
126
+ const tailMessages = _messagesForDisk(live.slice(base.refs.length), id);
123
127
  // The EXACT payload the worker will reconstruct and write is the sent
124
128
  // baseline + this delta. Its evidence is fingerprint-checked against
125
129
  // what was sent, never a fresh projection of the mutable live session
@@ -127,7 +131,7 @@ export function _buildWirePayload(id, session, forceFull = false) {
127
131
  // reference-identity delta). Only the tail is fingerprinted.
128
132
  const prints = base.prints ? base.prints.concat(tailMessages.map(_messagePrint)) : null;
129
133
  const refs = live.slice();
130
- _deltaBaseline.set(id, { refs, deltasSinceFull: base.deltasSinceFull + 1, prints });
134
+ _deltaBaseline.set(id, { refs, deltasSinceFull: base.deltasSinceFull + 1, prints, sensitive });
131
135
  return {
132
136
  delta: { baseCount: base.refs.length, header, tailMessages },
133
137
  attempt: _attemptRecord(header, refs, prints),
@@ -142,7 +146,7 @@ export function _buildWirePayload(id, session, forceFull = false) {
142
146
  // DELTA_FULL_RESYNC_EVERY saves.
143
147
  const refs = live.slice();
144
148
  const prints = _printMessages(live, diskMessages, resyncDue ? null : base);
145
- _deltaBaseline.set(id, { refs, deltasSinceFull: 0, prints });
149
+ _deltaBaseline.set(id, { refs, deltasSinceFull: 0, prints, sensitive });
146
150
  // (saveSessionAsync normalizes `messages` to an array before any post.)
147
151
  return { session: full, attempt: _attemptRecord(full, refs, prints) };
148
152
  }
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { renameWithRetrySync } from '../../../../shared/atomic-file.mjs';
4
4
  import { sanitizeContentForStoredHistory } from '../../providers/media-normalization.mjs';
5
5
  import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from '../lifecycle-scan.mjs';
6
+ import { redactSensitiveMessage } from '../sensitive-projection.mjs';
6
7
 
7
8
  // Inline base64 images (tool screenshots included) are written as
8
9
  // content-addressed refs, like prompt media, so session JSON stays small while
@@ -13,17 +14,17 @@ import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from '../lifecycle
13
14
  // Per-message disk projection shared by _sessionForDisk and the save-worker
14
15
  // delta path (tail-only projection). Returns the SAME array reference when no
15
16
  // message changed. Idempotent: already-sanitized content passes through
16
- // unchanged, so projecting a projected message is a no-op.
17
- export function _messagesForDisk(messages) {
17
+ // unchanged, so projecting a projected message is a no-op. A value session
18
+ // `sessionId` marked sensitive (sensitive-values.mjs) is stored as a marker in
19
+ // the message's text (sensitive-projection.mjs).
20
+ export function _messagesForDisk(messages, sessionId) {
18
21
  let changed = false;
19
22
  const out = messages.map((m) => {
20
23
  if (!m || typeof m !== 'object') return m;
21
24
  const content = sanitizeContentForStoredHistory(m.content);
22
- if (content !== m.content) {
23
- changed = true;
24
- return { ...m, content };
25
- }
26
- return m;
25
+ const projected = redactSensitiveMessage(sessionId, content === m.content ? m : { ...m, content });
26
+ if (projected !== m) changed = true;
27
+ return projected;
27
28
  });
28
29
  return changed ? out : messages;
29
30
  }
@@ -46,7 +47,7 @@ export function _sessionForDisk(session) {
46
47
  Object.hasOwn(session, 'toolApprovalHook') ||
47
48
  Object.hasOwn(session, '_providerPrefixGuardState'));
48
49
  const messages = Array.isArray(session?.messages) ? session.messages : null;
49
- const out = messages?.length ? _messagesForDisk(messages) : messages;
50
+ const out = messages?.length ? _messagesForDisk(messages, session.id) : messages;
50
51
  if (out === messages) {
51
52
  if (!hasTransient) return session;
52
53
  return _withMidTurnContextAnchor(_withoutTransientFields(session), session);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Read-only notifications about a session's persistence lifecycle, for owners
3
+ * of derived sidecars (the compaction archive) that must wait for the session
4
+ * record instead of polling for it. A pure registry: it persists nothing and
5
+ * imports no write path.
6
+ *
7
+ * 'landed' — a save of the session reached disk (its record exists);
8
+ * 'closed' — the session was closed (no further save is to be expected).
9
+ *
10
+ * A hard delete is announced through the existing purge hooks
11
+ * (session-observers.mjs registerSessionPurgeHook).
12
+ */
13
+ const listeners = new Set();
14
+
15
+ export function subscribeSessionPersistence(listener) {
16
+ if (typeof listener !== 'function') return () => {};
17
+ listeners.add(listener);
18
+ return () => listeners.delete(listener);
19
+ }
20
+
21
+ export function publishSessionPersistence(id, event) {
22
+ for (const listener of [...listeners]) {
23
+ try {
24
+ listener(id, event);
25
+ } catch {
26
+ /* observers never affect persistence */
27
+ }
28
+ }
29
+ }
@@ -161,12 +161,12 @@ function cancelState(source) {
161
161
  function effectiveCancel(row, session) {
162
162
  const rowCancel = cancelState(row);
163
163
  const sessionCancel = cancelState(session);
164
- const cancel =
165
- rowCancel && sessionCancel && CANCEL_UNCONFIRMED_AGENT_STATUS.test(sessionCancel.status)
166
- ? CANCEL_UNCONFIRMED_AGENT_STATUS.test(rowCancel.status)
167
- ? rowCancel
168
- : sessionCancel
169
- : rowCancel || sessionCancel;
164
+ let cancel;
165
+ if (rowCancel && sessionCancel && CANCEL_UNCONFIRMED_AGENT_STATUS.test(sessionCancel.status)) {
166
+ cancel = CANCEL_UNCONFIRMED_AGENT_STATUS.test(rowCancel.status) ? rowCancel : sessionCancel;
167
+ } else {
168
+ cancel = rowCancel || sessionCancel;
169
+ }
170
170
  if (!cancel) return null;
171
171
  const newerWork = cancel.at > 0 && Math.max(workStampMs(row), workStampMs(session)) > cancel.at;
172
172
  return newerWork ? null : cancel;
@@ -633,6 +633,8 @@ function projectChildWorkerRow(row, context) {
633
633
  // A live turn blocked on the user's approval (persisted by the agent
634
634
  // tool's worker index); never carried by a row that is no longer working.
635
635
  ...(working && row.awaitingUser === 'approval' ? { awaitingUser: 'approval' } : {}),
636
+ // The owner closed this agent (status unchanged); reused work clears it.
637
+ ...(!working && row.released === true ? { released: true } : {}),
636
638
  // Why a settled failed row failed; a row working again has no failure.
637
639
  ...(!working && failedAgentWorker(row) && cleanValue(row.failureReason)
638
640
  ? { failureReason: cleanValue(row.failureReason) }
@@ -131,8 +131,9 @@ export function isLeadVisibleRow(row) {
131
131
  if (!agentSession && !isRootLeadSession(row) && owner && !LEAD_OWNERS.has(owner)) return false;
132
132
  // Mirror listLeadSessions: a previewless zero-message row is an unusable
133
133
  // scratch (desktop boot leftovers, crashed first turns) — resuming it
134
- // shows an empty conversation, so the catalog hides it.
135
- if (!row.preview && row.messageCount === 0) return false;
134
+ // shows an empty conversation, so the catalog hides it. A titled one is a
135
+ // real conversation whose prompts were rewound away: it stays listed.
136
+ if (!row.preview && row.messageCount === 0 && !row.title) return false;
136
137
  const sourceType = String(row.sourceType || '')
137
138
  .trim()
138
139
  .toLowerCase();
@@ -282,6 +283,15 @@ function rowFromSession(session) {
282
283
  });
283
284
  }
284
285
 
286
+ /** The catalog row of one session read off `<directory>/<sessionId>.json` by
287
+ * id — the same strict, stamp-cached derivation a listing uses — or null
288
+ * when it is absent, unreadable, foreign or not one the catalog lists. */
289
+ export function storedSessionCatalogRow(directory, sessionId) {
290
+ if (!isStoredSessionId(sessionId)) return null;
291
+ const entry = reconcileSessionFile(directory, sessionId, null, false);
292
+ return entry?.kind === 'row' && isLeadVisibleRow(entry.row) ? entry.row : null;
293
+ }
294
+
285
295
  // ── Session file reconcile ──────────────────────────────────────────────────
286
296
  // What one `sessions/<id>.json` name contributes to the catalog, with the full
287
297
  // stamp { dev, ino, size, mtimeNs, ctimeNs } it was judged at (and the
@@ -391,31 +391,58 @@ const _pendingRemovals = new Set(); // ids to drop (upsert/removal are mutually
391
391
  let _summaryRetryTimer = null;
392
392
  let _summaryFlushScheduled = false;
393
393
  let _summaryFlushInflight = 0;
394
- const _summarySettleWaiters = new Set();
394
+ // Ids whose ops the async flush in flight took (they are requeued if it fails).
395
+ const _inflightSummaryIds = new Set();
396
+ const _summarySettleWaiters = new Set(); // { ids: string[]|null, resolve }
395
397
 
396
398
  export function _hasUnsettledSummaryOps() {
397
399
  return _pendingUpserts.size > 0 || _pendingRemovals.size > 0 || _summaryFlushScheduled || _summaryFlushInflight > 0;
398
400
  }
399
401
 
402
+ /** The ids among `ids` whose summary ops are queued or being written. */
403
+ export function pendingSummaryIndexIds(ids) {
404
+ return ids.filter((id) => _pendingUpserts.has(id) || _pendingRemovals.has(id) || _inflightSummaryIds.has(id));
405
+ }
406
+
407
+ function _waiterSettled({ ids }) {
408
+ return ids ? pendingSummaryIndexIds(ids).length === 0 : !_hasUnsettledSummaryOps();
409
+ }
410
+
400
411
  function _notifySummarySettled() {
401
- if (_hasUnsettledSummaryOps()) return;
402
- for (const resolve of _summarySettleWaiters) resolve();
403
- _summarySettleWaiters.clear();
412
+ for (const waiter of _summarySettleWaiters) {
413
+ if (_waiterSettled(waiter)) waiter.settle(true);
414
+ }
404
415
  }
405
416
 
406
417
  /**
407
- * Resolves once every queued summary-index mutation has landed and no flush
408
- * is scheduled or in flight — from then on this process no longer touches
409
- * the index, its lock or its temp files. Callers are told a save/sweep is
410
- * done before its deferred index flush runs, so an owner that removes or
411
- * switches the data dir awaits this first. A flush refused by a held lock
412
- * (an overlapping flush of this process, or another process) stays queued
413
- * and settles when its retry lands.
418
+ * Resolves true once the queued summary-index mutations have landed — of
419
+ * `ids` only when given, else every one with no flush scheduled or in flight;
420
+ * from then on this process no longer touches the index for them. Callers
421
+ * are told a save/sweep is done before its deferred index flush runs, so an
422
+ * owner that removes or switches the data dir awaits this first. A flush
423
+ * refused by a held lock (an overlapping flush of this process, or another
424
+ * process) stays queued and settles when its retry lands. When `signal`
425
+ * aborts first it resolves false and stops waiting; the ops stay queued.
414
426
  */
415
- export function settleSummaryIndexWrites() {
416
- if (!_hasUnsettledSummaryOps()) return Promise.resolve();
427
+ export function settleSummaryIndexWrites({ ids = null, signal = null } = {}) {
428
+ const waiter = { ids, settle: null };
429
+ if (_waiterSettled(waiter)) return Promise.resolve(true);
430
+ if (signal?.aborted) return Promise.resolve(false);
417
431
  _summaryRetryTimer?.ref?.();
418
- return new Promise((resolve) => _summarySettleWaiters.add(resolve));
432
+ return new Promise((resolve) => {
433
+ const onAbort = () => waiter.settle(false);
434
+ // The one exit, landed or abandoned: the waiter and its abort listener
435
+ // both go, so a long-lived signal collects nothing per settle.
436
+ waiter.settle = (landed) => {
437
+ if (!_summarySettleWaiters.delete(waiter)) return;
438
+ signal?.removeEventListener('abort', onAbort);
439
+ // Only an awaited settlement keeps the process alive for the retry.
440
+ if (_summarySettleWaiters.size === 0) _summaryRetryTimer?.unref?.();
441
+ resolve(landed);
442
+ };
443
+ _summarySettleWaiters.add(waiter);
444
+ signal?.addEventListener('abort', onAbort, { once: true });
445
+ });
419
446
  }
420
447
 
421
448
  function _scheduleSummaryRetry() {
@@ -498,11 +525,14 @@ export function _flushPendingSummaryOps({ sync = false } = {}) {
498
525
  return;
499
526
  }
500
527
  _summaryFlushInflight++;
528
+ const taken = [...upserts.keys(), ...removals];
529
+ for (const id of taken) _inflightSummaryIds.add(id);
501
530
  withFileLock(`${path}.lock`, () => _appendSummaryOpsLocked(path, text), { timeoutMs: SUMMARY_LOCK_TIMEOUT_MS })
502
531
  .then(() => _sweepStaleSummaryTempOnce(path))
503
532
  .catch(requeue)
504
533
  .finally(() => {
505
534
  _summaryFlushInflight--;
535
+ for (const id of taken) _inflightSummaryIds.delete(id);
506
536
  if (_pendingUpserts.size > 0 || _pendingRemovals.size > 0) _scheduleSummaryFlush();
507
537
  _notifySummarySettled();
508
538
  });
@@ -22,6 +22,7 @@ import {
22
22
  scanSessionFiles,
23
23
  scanSessionFilesAsync,
24
24
  stampedDerivation,
25
+ storedSessionCatalogRow,
25
26
  } from './store-summary-catalog.mjs';
26
27
  import { createStoredTranscriptCache, nextProjectionStamp } from './store-transcript-cache.mjs';
27
28
  import {
@@ -34,10 +35,17 @@ import { projectStoredTranscript } from './store-transcript-projection.mjs';
34
35
  import { dataDir } from './store-summary-locations.mjs';
35
36
  import { isStoredSessionId, workspaceSummary } from './store-summary-fields.mjs';
36
37
  import { readArchivedAgentResult } from './store-summary-archived-agent.mjs';
37
- import { compactionArchivePath, readCompactionArchivePage } from './compaction-archive.mjs';
38
+ import {
39
+ compactionArchivePath,
40
+ compactionArchiveRange,
41
+ readCompactionArchivePage,
42
+ visitCompactionArchive,
43
+ } from './compaction-archive.mjs';
44
+ import { attachmentHistoryRows } from './stored-attachment-history.mjs';
38
45
  import { currentSessionCatalogDb, onSessionCatalogDbChange, sessionCatalogDb } from './catalog-db/index.mjs';
39
46
  import { decodeCatalogCursor, encodeCatalogCursor, isAfterCursor } from './catalog-db/schema.mjs';
40
- import { onSessionActivityChange } from '../../../shared/session-activity-signal.mjs';
47
+ import { onSessionActivityChange, onSessionApprovalCardChange } from '../../../shared/session-activity-signal.mjs';
48
+ import { inFlightSessionActivity } from './manager/runtime-state.mjs';
41
49
  import {
42
50
  readCanonicalSessionRecord as readCanonicalLifecycle,
43
51
  offerStrictSessionRecord,
@@ -59,6 +67,11 @@ export { storedAgentWorkerIndexPath } from './store-summary-locations.mjs';
59
67
  // the desktop service worker keeps its own for the deep-history prefetch.
60
68
  const storedTranscriptCache = createStoredTranscriptCache();
61
69
 
70
+ // The attachment rows of whole stored conversations: the same cache mechanism
71
+ // and byte budget as the windows above, as an instance of its own, which a
72
+ // lookup can neither fill nor evict.
73
+ const attachmentHistoryCache = createStoredTranscriptCache();
74
+
62
75
  /** Test seam: forget every cached projection. */
63
76
  export function clearStoredTranscriptCache() {
64
77
  storedTranscriptCache.clear();
@@ -106,7 +119,23 @@ onSessionCatalogDbChange(() => notifyCatalogListeners());
106
119
  // Runtime activity transitions (turn start/settle, task wait, background
107
120
  // hand-off/finish) change only the Agents pool projection: `agent-activity`
108
121
  // lets the reader refresh just that, at once.
109
- onSessionActivityChange(() => notifyCatalogListeners('agent-activity'));
122
+ // A transition that starts or settles a turn also changes the catalog rows'
123
+ // `working` / `agentWorking` overlay. The listing that a turn's final save
124
+ // triggers can run before the turn leaves the runtime map, and nothing else
125
+ // re-lists: a bot home (no title write after its turn) kept `working` — a
126
+ // spinner and no completion dot — until its next file change.
127
+ let catalogActivity = '';
128
+ onSessionActivityChange(() => {
129
+ notifyCatalogListeners('agent-activity');
130
+ const { working, agentWorking } = inFlightSessionActivity();
131
+ const next = `${[...working].sort().join(',')}|${[...agentWorking].sort().join(',')}`;
132
+ if (next === catalogActivity) return;
133
+ catalogActivity = next;
134
+ notifyCatalogListeners('turn-activity');
135
+ });
136
+ // A session started or stopped waiting on the user's approval: its catalog row
137
+ // (`awaitingApproval`) changed, though no store file did.
138
+ onSessionApprovalCardChange(() => notifyCatalogListeners('approval'));
110
139
 
111
140
  /** `listener()` runs when a background pass finds a session file that changed
112
141
  * without a directory or index change, i.e. when an ordinary listing would
@@ -232,6 +261,12 @@ export async function listStoredSessionSummaryPage(options = {}) {
232
261
  };
233
262
  }
234
263
 
264
+ /** One session's catalog row by id, read off its record (not the paged
265
+ * listing): null when the catalog would not list it. */
266
+ export function readStoredSessionCatalogRow(id) {
267
+ return storedSessionCatalogRow(join(dataDir(), 'sessions'), String(id || '').trim());
268
+ }
269
+
235
270
  /** Exact, fail-closed existence check for a durable session address.
236
271
  * Summary indexes and desktop metadata are presentation caches; neither may
237
272
  * make a missing sessions/<id>.json record addressable again. */
@@ -269,6 +304,7 @@ function storedSessionMetadata(sessionId, session) {
269
304
  ownerSessionId: session.ownerSessionId || session.parentSessionId || null,
270
305
  visibility: sessionVisibility(session),
271
306
  agentTag: session.agentTag || null,
307
+ title: session.title || null,
272
308
  cwd: session.cwd || '',
273
309
  ...(workspaceSummary(session.workspace) ? { workspace: workspaceSummary(session.workspace) } : {}),
274
310
  provider: session.provider || null,
@@ -405,6 +441,49 @@ async function produceStoredTranscript(text, stamp, context) {
405
441
  }
406
442
  }
407
443
 
444
+ /** The rows of one session's whole stored conversation that name attachments
445
+ * (see attachmentHistoryRows): its record's messages and every visible
446
+ * segment of its compaction archive (a cleared or capped archive stays
447
+ * excluded, as for a window). Read from the files alone, under the same
448
+ * strict record authority as a transcript read, and cached by content
449
+ * identity in a cache of its own: the transcript windows are untouched.
450
+ * Null when the record is absent, unreadable or not this session's. */
451
+ async function readStoredAttachmentHistory(sessionId, recordPath, archivePath) {
452
+ const recordStat = probePath(recordPath);
453
+ if (recordStat.state !== PROBE_PRESENT) return null;
454
+ const archive = probePath(archivePath);
455
+ const key = `${sessionId}|attachments`;
456
+ let complete = true;
457
+ const { value } = await attachmentHistoryCache.read({
458
+ key,
459
+ fingerprint: [archive.state, archive.mtimeMs, archive.ctimeMs, archive.size, archive.ino, archive.dev].join(':'),
460
+ fileStat: recordStat,
461
+ loadText: () => {
462
+ const body = readTextFile(recordPath);
463
+ return body.state === PROBE_PRESENT ? body.text : null;
464
+ },
465
+ produce: async (text) => {
466
+ const record = readTopLevelLifecycleRecord(text);
467
+ if (isLifecycleUnreadable(record) || record.id !== sessionId) return null;
468
+ const { restoreTranscriptItems } = await import('./transcript-restore/restore.mjs');
469
+ const items = [];
470
+ const range = compactionArchiveRange(record.doc);
471
+ // An archive that exists but could not be walked is a transient miss,
472
+ // not a state worth keeping.
473
+ const walked = range
474
+ ? await visitCompactionArchive(join(dataDir(), 'sessions'), sessionId, range, (segment) =>
475
+ items.push(...attachmentHistoryRows(segment))
476
+ )
477
+ : true;
478
+ complete = walked || archive.state !== PROBE_PRESENT;
479
+ items.push(...attachmentHistoryRows(restoreTranscriptItems(record.doc.messages, { sessionId })));
480
+ return { sessionId, items };
481
+ },
482
+ });
483
+ if (!complete) attachmentHistoryCache.forget(key);
484
+ return value ?? null;
485
+ }
486
+
408
487
  /** Read exactly one persisted session for a visible desktop pane. This never
409
488
  * enumerates siblings. Normal reads stay independent of runtime ownership;
410
489
  * interrupted turns conditionally use the same durable reconnect recovery as
@@ -416,6 +495,8 @@ export async function readStoredSessionTranscript(id, options = {}) {
416
495
  // absent, unreadable, ambiguous or foreign record yields no transcript.
417
496
  const { recordPath, checkpointPath, archivePath } = storedTranscriptInputPaths(sessionId);
418
497
  if (options.metadataOnly === true) return readStoredSessionMetadata(recordPath, sessionId);
498
+ // The attachments the whole conversation names, apart from any window.
499
+ if (options.attachmentHistory === true) return readStoredAttachmentHistory(sessionId, recordPath, archivePath);
419
500
  // One page of the display-only compaction archive (the live head loader
420
501
  // pages it once a runtime's in-memory rows are exhausted).
421
502
  if (options.compactionArchive && typeof options.compactionArchive === 'object') {
@@ -0,0 +1,32 @@
1
+ // The attachments a session's whole stored conversation names (its record and
2
+ // its compaction archive), reduced to the metadata that says which rows name
3
+ // which attachment. Whether an attachment id belongs to a session is decided
4
+ // against this, not against a transcript window (a tail plus the pages paged
5
+ // in), which no longer holds rows a compaction moved into the archive.
6
+
7
+ const pick = (entry, keys) =>
8
+ Object.fromEntries(keys.filter((key) => entry?.[key] != null).map((key) => [key, entry[key]]));
9
+ const picked = (list, keys) => (Array.isArray(list) ? list.map((entry) => pick(entry, keys)) : []);
10
+
11
+ /** The rows of `items` that name attachments, reduced to the fields the
12
+ * desktop reads to attribute an id to its row: the row kind (and a notice's
13
+ * routine / room kind) and each attachment's id, name, type and size. No
14
+ * text, tool output or other row content is kept. */
15
+ export function attachmentHistoryRows(items) {
16
+ const rows = [];
17
+ for (const item of Array.isArray(items) ? items : []) {
18
+ const attachments = picked(item?.attachments, ['id', 'name', 'mimeType', 'kind', 'size']);
19
+ const images = picked(item?.images, ['attachmentRef', 'name', 'mimeType', 'bytes']);
20
+ const imageRefs = picked(item?.imageRefs, ['ref']);
21
+ if (!attachments.length && !images.length && !imageRefs.length) continue;
22
+ rows.push({
23
+ kind: item.kind,
24
+ ...(item.routine?.kind ? { routine: { kind: item.routine.kind } } : {}),
25
+ ...(item.room?.kind ? { room: { kind: item.room.kind } } : {}),
26
+ ...(attachments.length ? { attachments } : {}),
27
+ ...(images.length ? { images } : {}),
28
+ ...(imageRefs.length ? { imageRefs } : {}),
29
+ });
30
+ }
31
+ return rows;
32
+ }