mixdog 1.2.6 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +71 -9
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +14 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +178 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent/AGENT.md +4 -0
  18. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  19. package/src/rules/agent-mode/MIXY.md +28 -0
  20. package/src/rules/agent-mode/ROUTINE.md +6 -0
  21. package/src/rules/lead/LEAD.md +7 -3
  22. package/src/rules/shared/70-delivery.md +3 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  24. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  25. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
  26. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  27. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
  28. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  29. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  31. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  34. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  35. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  36. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  37. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  44. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  46. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  52. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  53. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  54. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
  58. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  61. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
  63. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
  64. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  66. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  67. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  68. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  69. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  70. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
  71. package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
  72. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  73. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  75. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
  76. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  77. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  78. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  79. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  80. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  81. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  82. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  83. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  84. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
  85. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  86. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  87. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  88. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  89. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
  90. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  91. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  92. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  93. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  94. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  96. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  97. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  98. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  99. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  100. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
  101. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
  102. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  103. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  105. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  106. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
  108. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  109. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
  110. package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
  111. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  112. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  114. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  115. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  116. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
  118. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  119. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  121. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  123. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  124. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  125. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  127. package/src/runtime/attachments/store.mjs +6 -0
  128. package/src/runtime/browser-bridge/client.mjs +24 -0
  129. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  130. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  131. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  132. package/src/runtime/channels/lib/webhook.mjs +0 -1
  133. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  134. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  135. package/src/runtime/computer-bridge/client.mjs +28 -1
  136. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  137. package/src/runtime/connectors/google-mcp.mjs +446 -0
  138. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  139. package/src/runtime/effort-judge/model-manifest.json +7 -7
  140. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  141. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  142. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  143. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  144. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  145. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  146. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  147. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  148. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  149. package/src/runtime/memory/lib/memory.mjs +7 -1
  150. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  151. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  152. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  153. package/src/runtime/memory/tool-defs.mjs +5 -0
  154. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  155. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  156. package/src/runtime/shared/agent-avatar.mjs +320 -0
  157. package/src/runtime/shared/agent-kind.mjs +152 -0
  158. package/src/runtime/shared/agent-route-config.d.mts +4 -0
  159. package/src/runtime/shared/agent-route-config.mjs +30 -0
  160. package/src/runtime/shared/agent-store.mjs +132 -0
  161. package/src/runtime/shared/atomic-file.mjs +14 -1
  162. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  163. package/src/runtime/shared/config-patch.mjs +20 -1
  164. package/src/runtime/shared/config.mjs +8 -1
  165. package/src/runtime/shared/connector-accounts.mjs +15 -0
  166. package/src/runtime/shared/connector-catalog.d.mts +51 -0
  167. package/src/runtime/shared/connector-catalog.mjs +228 -0
  168. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  169. package/src/runtime/shared/connector-triggers.mjs +45 -0
  170. package/src/runtime/shared/err-text.mjs +3 -1
  171. package/src/runtime/shared/error-presentation.mjs +91 -6
  172. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  173. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  174. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  175. package/src/runtime/shared/held-request-host.d.mts +12 -0
  176. package/src/runtime/shared/held-request-host.mjs +71 -0
  177. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  178. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  179. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  180. package/src/runtime/shared/plain-preview.d.mts +1 -0
  181. package/src/runtime/shared/plain-preview.mjs +33 -0
  182. package/src/runtime/shared/routine-limits.d.mts +11 -0
  183. package/src/runtime/shared/routine-limits.mjs +21 -0
  184. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  185. package/src/runtime/shared/routine-trigger.mjs +44 -0
  186. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  187. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  188. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  189. package/src/runtime/shared/slow-span.mjs +4 -12
  190. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  191. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  192. package/src/runtime/shared/turn-changes.mjs +0 -8
  193. package/src/runtime/shared/usage-readout-totals.mjs +58 -0
  194. package/src/runtime/shared/user-data-guard.mjs +119 -38
  195. package/src/session-runtime/agent-home-delivery.mjs +826 -0
  196. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  197. package/src/session-runtime/agent-room-delivery.mjs +133 -0
  198. package/src/session-runtime/agent-task-sessions.mjs +460 -0
  199. package/src/session-runtime/boot/apis.mjs +63 -18
  200. package/src/session-runtime/boot/facade.mjs +5 -8
  201. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  202. package/src/session-runtime/boot/providers.mjs +14 -4
  203. package/src/session-runtime/boot/tools.mjs +3 -0
  204. package/src/session-runtime/channel-config-api.mjs +53 -5
  205. package/src/session-runtime/config-lifecycle.mjs +43 -2
  206. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  207. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  208. package/src/session-runtime/handoff-attachments.mjs +593 -0
  209. package/src/session-runtime/held-requests.mjs +228 -0
  210. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  211. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  212. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  213. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
  214. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  215. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  216. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  217. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  218. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  219. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  220. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  221. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  222. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  223. package/src/session-runtime/model-route/set-route.mjs +35 -4
  224. package/src/session-runtime/native-web-search.mjs +2 -2
  225. package/src/session-runtime/new-session-config.mjs +6 -1
  226. package/src/session-runtime/owned-automation-run.mjs +229 -0
  227. package/src/session-runtime/resource-api.mjs +5 -0
  228. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  229. package/src/session-runtime/resource-skills-api.mjs +12 -0
  230. package/src/session-runtime/room-member-gate.mjs +48 -0
  231. package/src/session-runtime/route-resolution.mjs +5 -11
  232. package/src/session-runtime/routine-home-turn.mjs +156 -0
  233. package/src/session-runtime/routine-owner.mjs +48 -0
  234. package/src/session-runtime/schedule-session-run.mjs +30 -64
  235. package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
  236. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  237. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  238. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  239. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  240. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  241. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  242. package/src/session-runtime/services/channel-admin.mjs +93 -24
  243. package/src/session-runtime/services/memory-access.mjs +37 -0
  244. package/src/session-runtime/services/provider-admin.mjs +74 -39
  245. package/src/session-runtime/services/schedule-access.mjs +91 -0
  246. package/src/session-runtime/session-create/session-options.mjs +34 -7
  247. package/src/session-runtime/session-hooks.mjs +16 -8
  248. package/src/session-runtime/setup-tool/executor.mjs +79 -6
  249. package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
  250. package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
  251. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  252. package/src/session-runtime/skills-api.mjs +16 -1
  253. package/src/session-runtime/tool-policy-refresh.mjs +249 -29
  254. package/src/session-runtime/turn/session-ops.mjs +8 -1
  255. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  256. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
  257. package/src/session-runtime/webhook-session-run.mjs +49 -67
  258. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  259. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
  260. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  261. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  262. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  263. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  264. package/src/standalone/daemon-agent-control.mjs +1 -8
  265. package/src/standalone/daemon-session-control.mjs +21 -4
  266. package/src/standalone/daemon.mjs +31 -5
  267. package/src/standalone/session-protocol.mjs +27 -0
  268. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  269. package/src/standalone/session-service/agent-tree.mjs +4 -3
  270. package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
  271. package/src/standalone/session-service.mjs +19 -0
  272. package/src/tui/app/slash-commands.mjs +20 -0
  273. package/src/tui/dist/index.mjs +234 -95
  274. package/src/tui/session/context-state.mjs +6 -1
  275. package/src/tui/session/notice-surface.mjs +34 -4
  276. package/src/tui/session/queue-helpers.mjs +2 -1
  277. package/src/tui/session/session-api/extensions.mjs +4 -0
  278. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  279. package/src/tui/session/session-api/integrations.mjs +8 -0
  280. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  281. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  282. package/src/tui/session/session-api/routes.mjs +61 -5
  283. package/src/tui/session/session-flow/drain.mjs +12 -3
  284. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  285. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  286. package/src/tui/session/session-flow/steering.mjs +58 -24
  287. package/src/tui/session/tool-approval.mjs +26 -5
  288. package/src/tui/session/transcript-intake.mjs +8 -2
  289. package/src/tui/session/transcript-spill.mjs +5 -1
  290. package/src/tui/session/tui-steering-persist.mjs +65 -42
  291. package/src/tui/session/turn-live-call.mjs +64 -0
  292. package/src/tui/session/turn.mjs +34 -8
  293. package/src/session-runtime/automation-agents.mjs +0 -94
  294. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The summary's last-message line (a messenger row's preview): the newest
3
+ * user or assistant message with displayable text, and that line's routine
4
+ * provenance. Leaf module, shared by the summary index (store saves) and the
5
+ * catalog's strict file scan so both derive the same line.
6
+ */
7
+ import { cleanSessionPreview, isSessionPreviewNoise, sessionMessageText } from '../runtime-core/session-text.mjs';
8
+ import { plainPreviewText } from '../../../shared/plain-preview.mjs';
9
+ import { isRoutineResultMessage, routineProvenance, routineResultNotice } from './routine-result.mjs';
10
+
11
+ export const LAST_MESSAGE_PREVIEW_MAX = 160;
12
+
13
+ /** cleanSessionPreview cut back to a word boundary within `max`. */
14
+ export function cleanSummaryPreview(text, max) {
15
+ const value = cleanSessionPreview(text, max);
16
+ return value.length > max
17
+ ? value
18
+ .slice(0, max)
19
+ .replace(/\s+\S*$/, '')
20
+ .trim()
21
+ : value;
22
+ }
23
+
24
+ /** The raw text a conversational message contributes as the last line (a
25
+ * routine result: its notice line); null for any other message. */
26
+ export function lastMessageRawText(message) {
27
+ if (message?.role !== 'user' && message?.role !== 'assistant') return null;
28
+ return isRoutineResultMessage(message) ? routineResultNotice(message).text : sessionMessageText(message.content);
29
+ }
30
+
31
+ /** The message's last-message line, '' when it has none (tool-only, synthetic). */
32
+ export function lastMessageLine(message) {
33
+ const raw = lastMessageRawText(message);
34
+ if (raw === null || isSessionPreviewNoise(raw)) return '';
35
+ return cleanSummaryPreview(plainPreviewText(raw), LAST_MESSAGE_PREVIEW_MAX);
36
+ }
37
+
38
+ /** `{ lastMessagePreview, lastMessageRoutine }` of a transcript. */
39
+ export function lastMessageSummary(messages) {
40
+ const list = Array.isArray(messages) ? messages : [];
41
+ for (let index = list.length - 1; index >= 0; index -= 1) {
42
+ const line = lastMessageLine(list[index]);
43
+ if (line) return { lastMessagePreview: line, lastMessageRoutine: routineProvenance(list[index]) };
44
+ }
45
+ return { lastMessagePreview: '', lastMessageRoutine: null };
46
+ }
@@ -49,7 +49,3 @@ export function sessionVisibility(value) {
49
49
  export function isAgentOnlySession(value) {
50
50
  return sessionVisibility(value) === SESSION_VISIBILITY_AGENT_ONLY;
51
51
  }
52
-
53
- export function isOrdinarySession(value) {
54
- return !isAgentOnlySession(value);
55
- }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { contextMeasurementStats } from '../../../../shared/context-measurement.mjs';
8
8
  import { toolResultText } from './tool-result-text.mjs';
9
- import { restoredUserImageRefs, restoredUserText } from './user-text.mjs';
9
+ import { createUserTextRestore } from './user-text.mjs';
10
10
  import {
11
11
  completionCardFromExecution,
12
12
  parseModelVisibleCompletionWrapper,
@@ -38,8 +38,12 @@ import {
38
38
  } from '../../../../shared/tool-execution-contract.mjs';
39
39
  import { toolResultTerminalStatus } from '../../../../shared/tool-status.mjs';
40
40
  import { transcriptAttachmentFields, transcriptRouteMetadataFields } from '../../../../shared/transcript-metadata.mjs';
41
+ import { routineTriggerField } from '../../../../shared/routine-trigger.mjs';
41
42
  import { isImportedMessage } from '../imported-message.mjs';
42
43
  import { isContextAckMessage } from '../compact/user-message-kind.mjs';
44
+ import { isRoutineResultMessage, routineResultNotice } from '../routine-result.mjs';
45
+ import { heldRequestNotice, isHeldRequestMessage } from '../human-gate/held-message.mjs';
46
+ import { isRoomRowMessage, roomRowItem } from '../agent-room.mjs';
43
47
 
44
48
  /** A failed turn's recorded end: its route, reason and quota stop, the same
45
49
  * fields the live turndone carries. */
@@ -86,6 +90,8 @@ function restoredTranscriptMetadata(message) {
86
90
  // The attachments this prompt carried, by content hash: a restored row
87
91
  // opens/downloads them by id from the attachment store.
88
92
  ...transcriptAttachmentFields(value),
93
+ // A routine fire this turn delivered: painted as its marker row.
94
+ ...routineTriggerField(value),
89
95
  // Skills this user turn explicitly loaded (header selection or `$name`).
90
96
  ...(Array.isArray(value.appliedSkills) && value.appliedSkills.length
91
97
  ? { appliedSkills: value.appliedSkills.filter((name) => typeof name === 'string' && name) }
@@ -332,7 +338,8 @@ function mergeRestoredToolItems(items) {
332
338
  function restoredMessageItemUpperBound(message) {
333
339
  const boundaries =
334
340
  (Array.isArray(message?.meta?.sessionInheritances) ? message.meta.sessionInheritances.length : 0) +
335
- (Array.isArray(message?.meta?.modelSwitches) ? message.meta.modelSwitches.length : 0);
341
+ (Array.isArray(message?.meta?.modelSwitches) ? message.meta.modelSwitches.length : 0) +
342
+ (Array.isArray(message?.meta?.workflowSwitches) ? message.meta.workflowSwitches.length : 0);
336
343
  const completionCount =
337
344
  Number(Boolean(message?.meta?.transcript?.completion)) +
338
345
  (Array.isArray(message?.meta?.transcript?.previousCompletions)
@@ -372,17 +379,31 @@ function restoredNotificationToolItem(parsed, text, message, nextId, errorLabel)
372
379
  };
373
380
  }
374
381
 
375
- function restoredUserTranscriptItems(message, nextId) {
382
+ function restoredUserTranscriptItems(message, nextId, userText) {
376
383
  // Injected model-context payloads are model-visible but never user-authored:
377
384
  // skill bodies (meta:'skill'), hook/system reminders, and tag-wrapped context
378
385
  // blocks must not restore as user bubbles in any client.
379
386
  if (message?.meta === 'skill' || message?.meta === 'hook') return [];
387
+ // A schedule's result in its owner's home thread: a notice row.
388
+ // A held request of an unattended run (human-gate/held-message.mjs) likewise.
389
+ if (isRoutineResultMessage(message) || isHeldRequestMessage(message)) {
390
+ const { messageId, at } = restoredTranscriptMetadata(message);
391
+ return [
392
+ {
393
+ kind: 'notice',
394
+ id: nextId(),
395
+ ...(isHeldRequestMessage(message) ? heldRequestNotice(message) : routineResultNotice(message)),
396
+ ...(messageId ? { messageId } : {}),
397
+ ...(at ? { at } : {}),
398
+ },
399
+ ];
400
+ }
380
401
  // Imported prose renders verbatim: never a control row, card or display rewrite.
381
402
  if (isImportedMessage(message)) {
382
403
  const text = (typeof message.content === 'string' ? message.content : toolResultText(message.content)).trim();
383
404
  return text ? [{ kind: 'user', id: nextId(), text, ...restoredTranscriptMetadata(message) }] : [];
384
405
  }
385
- const text = restoredUserText(message?.content).trim();
406
+ const text = userText.text(message?.content).trim();
386
407
  // Persisted async-completion wrappers ("Async shell task ... finished. Result:
387
408
  // > ...") are the only durable record of a background completion — the live
388
409
  // Response card is an event-time push that does not survive a transcript
@@ -413,7 +434,7 @@ function restoredUserTranscriptItems(message, nextId) {
413
434
  }
414
435
  const synthetic = parseSyntheticAgentMessage(text);
415
436
  if (!synthetic) {
416
- const imageRefs = restoredUserImageRefs(message?.content);
437
+ const imageRefs = userText.imageRefs(message?.content);
417
438
  return [
418
439
  {
419
440
  kind: 'user',
@@ -421,6 +442,8 @@ function restoredUserTranscriptItems(message, nextId) {
421
442
  text,
422
443
  ...restoredTranscriptMetadata(message),
423
444
  ...(imageRefs.length ? { imageRefs } : {}),
445
+ // Read by the model inside its running turn, as the live row says.
446
+ ...(message?.meta?.source === 'steering' ? { steered: true } : {}),
424
447
  },
425
448
  ];
426
449
  }
@@ -440,7 +463,20 @@ export function modelSwitchItem(id, boundary) {
440
463
  };
441
464
  }
442
465
 
443
- function restoreTranscriptRange(messages, start, sessionId) {
466
+ /** The transcript row of a mid-conversation workflow switch (live and restored). */
467
+ export function workflowSwitchItem(id, boundary) {
468
+ return {
469
+ kind: 'statusdone',
470
+ id,
471
+ status: 'workflow-switched',
472
+ label: 'Workflow switched',
473
+ from: boundary.from,
474
+ to: boundary.to,
475
+ at: boundary.at,
476
+ };
477
+ }
478
+
479
+ function restoreTranscriptRange(messages, start, sessionId, userText) {
444
480
  const items = [];
445
481
  const pendingToolCalls = new Map();
446
482
  for (let index = start; index < messages.length; index += 1) {
@@ -450,8 +486,13 @@ function restoreTranscriptRange(messages, start, sessionId) {
450
486
  // let a tail-only restore skip the prefix without first counting every old
451
487
  // projected item.
452
488
  const restoredId = () => `hist_${sessionId}_${index}_${++part}`;
489
+ // A group room's posted row (agent-room.mjs): one bubble or notice row.
490
+ if (isRoomRowMessage(message)) {
491
+ items.push(roomRowItem(message, restoredId()));
492
+ continue;
493
+ }
453
494
  if (message?.role === 'user') {
454
- items.push(...restoredUserTranscriptItems(message, restoredId));
495
+ items.push(...restoredUserTranscriptItems(message, restoredId, userText));
455
496
  } else if (message?.role === 'assistant' && !isContextAckMessage(message, messages[index - 1])) {
456
497
  items.push(...restoredAssistantTranscriptItems(message, restoredId));
457
498
  items.push(...restoredToolCallItems(message, restoredId, pendingToolCalls));
@@ -477,6 +518,10 @@ function restoreTranscriptRange(messages, start, sessionId) {
477
518
  if (!boundary?.provider || !boundary?.modelId) continue;
478
519
  items.push(modelSwitchItem(restoredId(), boundary));
479
520
  }
521
+ for (const boundary of Array.isArray(message?.meta?.workflowSwitches) ? message.meta.workflowSwitches : []) {
522
+ if (!boundary?.to) continue;
523
+ items.push(workflowSwitchItem(restoredId(), boundary));
524
+ }
480
525
  if (message?.meta?.transcript?.previousCompletions?.length) {
481
526
  const events = items.slice(eventStart);
482
527
  if (events.every((item) => Number.isFinite(item.at))) {
@@ -490,9 +535,12 @@ function restoreTranscriptRange(messages, start, sessionId) {
490
535
 
491
536
  export function restoreTranscriptItems(messages, { sessionId = 'session', itemLimit = Number.POSITIVE_INFINITY } = {}) {
492
537
  const source = Array.isArray(messages) ? messages : [];
538
+ // One projection per load: a widened range reuses each prompt's parts and
539
+ // the paste-line reads share one budget.
540
+ const userText = createUserTextRestore();
493
541
  const numericLimit = Number(itemLimit);
494
542
  const limited = Number.isFinite(numericLimit) && numericLimit > 0;
495
- if (!limited) return restoreTranscriptRange(source, 0, sessionId);
543
+ if (!limited) return restoreTranscriptRange(source, 0, sessionId, userText);
496
544
 
497
545
  const limit = Math.max(1, Math.floor(numericLimit));
498
546
  // Restore beyond the visible cap so a boundary tool run can merge exactly
@@ -506,7 +554,7 @@ export function restoreTranscriptItems(messages, { sessionId = 'session', itemLi
506
554
  upperBound += restoredMessageItemUpperBound(source[start]);
507
555
  }
508
556
 
509
- let restored = restoreTranscriptRange(source, start, sessionId);
557
+ let restored = restoreTranscriptRange(source, start, sessionId, userText);
510
558
  // Hidden/context messages and aggregate tool runs can make the cheap upper
511
559
  // bound optimistic. Expand backward exponentially only when necessary.
512
560
  let expansionTarget = Math.max(64, limit - restored.length);
@@ -516,7 +564,7 @@ export function restoreTranscriptItems(messages, { sessionId = 'session', itemLi
516
564
  start -= 1;
517
565
  expansion += restoredMessageItemUpperBound(source[start]);
518
566
  }
519
- restored = restoreTranscriptRange(source, start, sessionId);
567
+ restored = restoreTranscriptRange(source, start, sessionId, userText);
520
568
  expansionTarget *= 2;
521
569
  }
522
570
  return restored.length > limit ? restored.slice(-limit) : restored;
@@ -116,14 +116,64 @@ function referencedFeedbackText(part) {
116
116
  }
117
117
  }
118
118
 
119
+ const PASTED_LINES = /· (\d+) lines?$/;
120
+ const PASTED_TEXT_MAX_BYTES = 1024 * 1024;
121
+ /** Blob bytes one transcript load may read to count the lines of pastes
122
+ * whose stored name does not carry the count (pastes stored before it did). */
123
+ export const PASTED_LINE_READ_BUDGET_BYTES = 4 * 1024 * 1024;
124
+
125
+ /** Visible lines of a stored paste (as `pastedTextLineCount` counts them), or
126
+ * null when the count would take a read past the load's budget. */
127
+ function storedPasteLines(part, budget) {
128
+ const named = PASTED_LINES.exec(String(part.filename || ''));
129
+ if (named) return Number(named[1]);
130
+ const allowance = Math.min(PASTED_TEXT_MAX_BYTES, budget.remaining);
131
+ if (allowance <= 0) return null;
132
+ try {
133
+ const read = readAttachmentTextWithin(part, allowance);
134
+ if (read == null) return null;
135
+ budget.remaining -= Buffer.byteLength(read, 'utf8');
136
+ const value = read.replace(/\r\n?/g, '\n').replace(/\n+$/, '');
137
+ return value ? value.split('\n').length : 0;
138
+ } catch (error) {
139
+ // A blob that failed its integrity check was read before it failed: the
140
+ // read counts against the cap (at most the allowance). A missing blob
141
+ // read nothing.
142
+ if (error?.code !== 'ENOENT') budget.remaining -= allowance;
143
+ return null;
144
+ }
145
+ }
146
+
147
+ /** The chip token a stored pasted / attached text stood for in the live
148
+ * bubble (intake moved its body into the store and dropped the token). A
149
+ * paste whose line count is unknown shows as the plain `[Pasted text #n]`. */
150
+ function storedTextToken(part, number, budget) {
151
+ if (part?.type !== 'text') return '';
152
+ if (part.textKind === 'paste') {
153
+ const lines = storedPasteLines(part, budget);
154
+ return lines == null ? `[Pasted text #${number}]` : `[Pasted text #${number} +${lines} lines]`;
155
+ }
156
+ if (part.textKind === 'file') return `[File #${number}: ${part.filename || 'attachment.txt'}]`;
157
+ return '';
158
+ }
159
+
119
160
  /** The display lines of a stored prompt's parts, and for each image part the
120
161
  * line that draws it: the composer's metadata line right before it, else a
121
- * bare line of its own (a bare line never stands for the next image). */
122
- function restoredUserParts(content) {
162
+ * bare line of its own (a bare line never stands for the next image). A
163
+ * stored pasted or attached text shows as its chip token. */
164
+ function restoredUserParts(content, budget) {
123
165
  const texts = [];
124
166
  const images = [];
125
167
  let pendingMetadata = '';
168
+ let storedTexts = 0;
126
169
  for (const part of content) {
170
+ const token = isAttachmentReference(part) ? storedTextToken(part, storedTexts + 1, budget) : '';
171
+ if (token) {
172
+ storedTexts += 1;
173
+ texts.push(token);
174
+ pendingMetadata = '';
175
+ continue;
176
+ }
127
177
  if (part?.type === 'image' || part?.type === 'input_image') {
128
178
  let line = pendingMetadata;
129
179
  if (!line) {
@@ -141,21 +191,48 @@ function restoredUserParts(content) {
141
191
  return { texts, images };
142
192
  }
143
193
 
194
+ /**
195
+ * The user-bubble projection of one transcript load: each stored prompt's
196
+ * parts are worked out once (its text and its image refs share them, and a
197
+ * load that widens its range sees the same messages again), and the blob
198
+ * reads that count paste lines share one byte budget.
199
+ */
200
+ export function createUserTextRestore({ readBudgetBytes = PASTED_LINE_READ_BUDGET_BYTES } = {}) {
201
+ const budget = { remaining: readBudgetBytes };
202
+ const parts = new WeakMap();
203
+ const partsOf = (content) => {
204
+ let found = parts.get(content);
205
+ if (!found) {
206
+ found = restoredUserParts(content, budget);
207
+ parts.set(content, found);
208
+ }
209
+ return found;
210
+ };
211
+ return {
212
+ text(content) {
213
+ if (!Array.isArray(content)) {
214
+ return withVisualizeFeedbackDisplay(typeof content === 'string' ? content : toolResultText(content));
215
+ }
216
+ return withVisualizeFeedbackDisplay(partsOf(content).texts.join('\n'));
217
+ },
218
+ /** The stored original of each image the restored bubble draws, by the
219
+ * `[Image: …]` line that draws it. Every image keeps its slot (one
220
+ * without a stored original has no `ref`) so later images do not shift
221
+ * onto it; empty when no image has a stored original. */
222
+ imageRefs(content) {
223
+ if (!Array.isArray(content)) return [];
224
+ const slots = partsOf(content).images.map(({ part, line }) =>
225
+ isAttachmentReference(part) ? { line, ref: part.attachmentRef } : { line }
226
+ );
227
+ return slots.some((slot) => slot.ref) ? slots : [];
228
+ },
229
+ };
230
+ }
231
+
144
232
  export function restoredUserText(content) {
145
- if (!Array.isArray(content)) {
146
- return withVisualizeFeedbackDisplay(typeof content === 'string' ? content : toolResultText(content));
147
- }
148
- return withVisualizeFeedbackDisplay(restoredUserParts(content).texts.join('\n'));
233
+ return createUserTextRestore().text(content);
149
234
  }
150
235
 
151
- /** The stored original of each image the restored bubble draws, by the
152
- * `[Image: …]` line that draws it. Every image keeps its slot (one without a
153
- * stored original has no `ref`) so later images do not shift onto it; empty
154
- * when no image has a stored original. */
155
236
  export function restoredUserImageRefs(content) {
156
- if (!Array.isArray(content)) return [];
157
- const slots = restoredUserParts(content).images.map(({ part, line }) =>
158
- isAttachmentReference(part) ? { line, ref: part.attachmentRef } : { line }
159
- );
160
- return slots.some((slot) => slot.ref) ? slots : [];
237
+ return createUserTextRestore().imageRefs(content);
161
238
  }
@@ -339,11 +339,23 @@ export function releaseShellJobWorkspace(jobId) {
339
339
  if (workspaceLeases.get(key) === leave) workspaceLeases.delete(key);
340
340
  }
341
341
 
342
+ /** True once `sessionId` closed in this runtime. */
343
+ export function isShellOwnerSessionClosed(sessionId) {
344
+ return closedOwnerSessions.has(String(sessionId || '').trim());
345
+ }
346
+
342
347
  /** Flag a still-open job record as "termination unconfirmed" for the readouts. */
343
348
  export function markShellJobTerminationUnconfirmed(jobId) {
344
349
  return patchRunningShellJobRecord(jobId, { terminationUnconfirmed: true });
345
350
  }
346
351
 
352
+ /** Record the pids a held job's termination watch last found alive, so a
353
+ * reader in another process can tell a live hold from a stale one. */
354
+ export function noteShellJobSurvivors(jobId, pids) {
355
+ const survivorPids = (pids || []).map(Number).filter((pid) => Number.isInteger(pid) && pid > 0);
356
+ return patchRunningShellJobRecord(jobId, { survivorPids });
357
+ }
358
+
347
359
  function patchRunningShellJobRecord(jobId, patch) {
348
360
  const key = validJobId(jobId);
349
361
  if (!key) return Promise.resolve(false);
@@ -8,7 +8,8 @@
8
8
  //
9
9
  // MIXDOG_SEARCH_SERVER=0 disables; MIXDOG_SEARCH_SERVER_BIN overrides the
10
10
  // executable.
11
- import { existsSync } from 'node:fs';
11
+ import { existsSync, lstatSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
12
+ import { basename, dirname, isAbsolute, join, resolve as resolvePath, sep } from 'node:path';
12
13
  import { bindChildLifecycle, createNativeSearchTransport } from './native-search-transport.mjs';
13
14
  import { invalidateBuiltinResultCache } from './cache-layers.mjs';
14
15
  import { getPluginData } from '../../config.mjs';
@@ -254,6 +255,32 @@ export async function shutdownNativeSearchServer(reason = 'process-exit', timeou
254
255
 
255
256
  export { bindChildLifecycle as _bindNativeSearchServerLifecycle };
256
257
 
258
+ // Inventories persisted before search roots were canonicalized (see
259
+ // canonicalPath) can hold another spelling of a folder's files under the
260
+ // folder's canonical key; loading one would keep serving every file twice.
261
+ // Drop that snapshot once per data dir, before a server of this process loads
262
+ // it. The marker records that the remaining snapshot was written by servers
263
+ // that only ever saw canonical roots.
264
+ const INVENTORY_SNAPSHOT_FILE = 'file-inventories-v1.bin';
265
+ const CANONICAL_ROOTS_MARKER = 'file-inventories-v1.canonical-roots';
266
+ let _inventorySnapshotChecked = false;
267
+
268
+ function dropAliasSpelledInventorySnapshot() {
269
+ if (_inventorySnapshotChecked) return;
270
+ _inventorySnapshotChecked = true;
271
+ const dir = join(getPluginData(), 'search-index');
272
+ const marker = join(dir, CANONICAL_ROOTS_MARKER);
273
+ if (existsSync(marker)) return;
274
+ try {
275
+ rmSync(join(dir, INVENTORY_SNAPSHOT_FILE), { force: true });
276
+ mkdirSync(dir, { recursive: true });
277
+ writeFileSync(marker, '');
278
+ } catch {
279
+ // A snapshot that cannot be removed now (held open by another server) is
280
+ // retried by the next process; search itself does not depend on it.
281
+ }
282
+ }
283
+
257
284
  function _ensureServer() {
258
285
  if (_server) return _server;
259
286
  if (
@@ -261,6 +288,7 @@ function _ensureServer() {
261
288
  Date.now() - _lastFailureAt < RESTART_BACKOFF_MS
262
289
  )
263
290
  return null;
291
+ dropAliasSpelledInventorySnapshot();
264
292
  let transport;
265
293
  try {
266
294
  transport = createNativeSearchTransport({
@@ -633,15 +661,93 @@ function consumedDeadline(response, deadlineMs) {
633
661
  );
634
662
  }
635
663
 
664
+ // The server walks a root as the request spells it, but its file-change
665
+ // updates carry the resolved path. Any other spelling of the same folder —
666
+ // letter case on Windows, a junction or symlink, a `..` segment — therefore
667
+ // listed every file twice (one copy per spelling). Roots are resolved once,
668
+ // here, so the first walk, later updates and the inventory key agree.
669
+ function canonicalPath(path) {
670
+ try {
671
+ return realpathSync.native(path);
672
+ } catch {
673
+ return path;
674
+ }
675
+ }
676
+
677
+ // One positional operand resolved against the canonical cwd. An operand that
678
+ // is already canonical is sent unchanged. An exact file that is itself a
679
+ // symlink keeps its own name: --glob/--type filter the name the caller asked
680
+ // for. `file` tells restoreOperandSpelling how the server prints the operand.
681
+ function canonicalOperand(cwd, operand) {
682
+ const joined = isAbsolute(operand) ? operand : `${cwd}${sep}${operand}`;
683
+ let canonical;
684
+ let file;
685
+ try {
686
+ file = statSync(joined).isFile();
687
+ canonical =
688
+ file && lstatSync(joined).isSymbolicLink()
689
+ ? join(realpathSync.native(dirname(joined)), basename(joined))
690
+ : realpathSync.native(joined);
691
+ } catch {
692
+ return null;
693
+ }
694
+ if (canonical === (isAbsolute(operand) ? operand : resolvePath(cwd, operand))) return null;
695
+ // The server prints results with `/` when the operand only uses `/`.
696
+ const sent = operand.includes('/') && !operand.includes('\\') ? canonical.replaceAll('\\', '/') : canonical;
697
+ if (sent === operand) return null;
698
+ // A directory is printed with its trailing separators trimmed.
699
+ const printed = (path) => (file ? path : path.replace(/[\\/]+$/, ''));
700
+ return { sent, from: printed(sent), to: printed(operand), file };
701
+ }
702
+
703
+ // Canonical cwd and operands for a search request, plus the operands whose
704
+ // spelling restoreOperandSpelling puts back into the result lines. Positional
705
+ // operands follow the last `--` (search-builders emits it before them; a `--`
706
+ // pattern is always an `-e` value before it).
707
+ function canonicalSearchRequest(cwd, argsList) {
708
+ const root = canonicalPath(cwd);
709
+ const args = argsList.map(String);
710
+ const remaps = [];
711
+ const operandsAt = args.lastIndexOf('--') + 1;
712
+ if (operandsAt > 0) {
713
+ for (let i = operandsAt; i < args.length; i += 1) {
714
+ const remap = canonicalOperand(root, args[i]);
715
+ if (!remap) continue;
716
+ args[i] = remap.sent;
717
+ remaps.push(remap);
718
+ }
719
+ }
720
+ remaps.sort((a, b) => b.from.length - a.from.length);
721
+ return { cwd: root, args, remaps };
722
+ }
723
+
724
+ // Results under a canonicalized operand are printed by the server as
725
+ // `<operand><sep><relative>` (a directory) or as the operand itself followed
726
+ // by `:`/`-` fields (an exact file); show them under the caller's spelling.
727
+ function restoreOperandSpelling(lines, remaps) {
728
+ if (remaps.length === 0) return lines;
729
+ return lines.map((line) => {
730
+ if (typeof line !== 'string') return line;
731
+ for (const { from, to, file } of remaps) {
732
+ if (!line.startsWith(from)) continue;
733
+ const rest = line.slice(from.length);
734
+ if (rest !== '' && !(file ? /^[:-]/ : /^[\\/]/).test(rest)) continue;
735
+ return `${to}${rest}`;
736
+ }
737
+ return line;
738
+ });
739
+ }
740
+
636
741
  /** Returns a runRgWindowedLines-shaped result, or null when the native server
637
742
  * is disabled or the response carries no line list. */
638
743
  export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
639
744
  if (searchServerDisabled()) return null;
640
745
  const deadlineMs = callerDeadlineMs(execOptions, REQUEST_TIMEOUT_MS);
746
+ const canonical = canonicalSearchRequest(String(execOptions.cwd || process.cwd()), argsList);
641
747
  const buildRequest = (server, remaining) => ({
642
748
  id: ++server.sequence,
643
- cwd: String(execOptions.cwd || process.cwd()),
644
- args: argsList.map(String),
749
+ cwd: canonical.cwd,
750
+ args: [...canonical.args],
645
751
  offset: Math.max(0, Math.floor(Number(opts.offset) || 0)),
646
752
  limit: Number.isFinite(Number(opts.limit)) && Number(opts.limit) > 0 ? Math.floor(Number(opts.limit)) : 0,
647
753
  deadlineMs: softDeadlineMs(remaining),
@@ -701,7 +807,7 @@ export async function tryServeSearch(argsList, execOptions = {}, opts = {}) {
701
807
  ? response.walkErrorDetails.map(String).slice(0, 8)
702
808
  : [];
703
809
  return {
704
- lines: response.lines,
810
+ lines: restoreOperandSpelling(response.lines, canonical.remaps),
705
811
  complete: response.complete === true,
706
812
  totalSeen: Math.max(0, Math.floor(Number(response.totalSeen) || 0)),
707
813
  partial: response.partial === true,
@@ -731,7 +837,8 @@ export async function tryServeFuzzySearch(args, execOptions = {}) {
731
837
  const response = await requestNativeWithRestart(
732
838
  (server, remaining) => ({
733
839
  id: ++server.sequence,
734
- cwd: String(args?.cwd || execOptions.cwd || process.cwd()),
840
+ // Matches are relative to cwd, so only the root needs canonicalizing.
841
+ cwd: canonicalPath(String(args?.cwd || execOptions.cwd || process.cwd())),
735
842
  fuzzy: String(args?.query || ''),
736
843
  limit: Math.max(1, Math.min(1_000, Math.floor(Number(args?.limit) || 25))),
737
844
  hidden: args?.hidden !== false,
@@ -813,4 +920,5 @@ export function _resetNativeSearchClientForTest() {
813
920
  _binaryResolveStarted = false;
814
921
  _warmPromise = null;
815
922
  _lastTimeoutRecycleAt = 0;
923
+ _inventorySnapshotChecked = false;
816
924
  }
@@ -28,8 +28,10 @@ import {
28
28
  adoptShellWorkspaceAdmission,
29
29
  clearTerminationHold,
30
30
  completeShellJobRecord,
31
+ isShellOwnerSessionClosed,
31
32
  listShellJobRecords,
32
33
  markShellJobTerminationUnconfirmed,
34
+ noteShellJobSurvivors,
33
35
  publishShellJobRecord,
34
36
  registerTerminationHold,
35
37
  retireShellJobRecord,
@@ -194,6 +196,8 @@ function trackShellJobRecord(task, options) {
194
196
  sweep: killed,
195
197
  onTerminated: finish,
196
198
  releaseRequested: () => takeReleaseRequest(jobId),
199
+ ownerClosed: () => isShellOwnerSessionClosed(options?.ownerSessionId),
200
+ onSurvivors: (pids) => void noteShellJobSurvivors(jobId, pids),
197
201
  onUnconfirmed: () => {
198
202
  void markShellJobTerminationUnconfirmed(jobId);
199
203
  registerTerminationHold(jobId, {
@@ -195,12 +195,22 @@ function sweepLiveIdentities(tracker) {
195
195
  *
196
196
  * `sweep` (a killed tree): a pass that finds captured identities alive
197
197
  * terminates them and observes again in the same pass, so a cancellation
198
- * ends with the tree gone instead of a held folder over live processes. */
198
+ * ends with the tree gone instead of a held folder over live processes.
199
+ *
200
+ * `ownerClosed`: once the owning session has closed and the root has
201
+ * exited, a tree without a complete pre-kill snapshot is observed after
202
+ * exit (what is still linked to the root) instead of answering 'unknown'
203
+ * forever — no one is left to release it, and the close must end in a
204
+ * definitive state. `onSurvivors(pids)` reports the pids a pass that is
205
+ * not 'gone' found alive, whenever that set changes, so out-of-process
206
+ * readers can tell a live hold from a stale one. */
199
207
  export function watchTermination({
200
208
  tracker,
201
209
  rootExited = () => true,
202
210
  onTerminated,
203
211
  onUnconfirmed = null,
212
+ onSurvivors = null,
213
+ ownerClosed = null,
204
214
  releaseRequested = null,
205
215
  intervalMs = TERMINATION_RECHECK_MS,
206
216
  sweep = false,
@@ -208,6 +218,7 @@ export function watchTermination({
208
218
  let timer = null;
209
219
  let stopped = false;
210
220
  let reportedUnconfirmed = false;
221
+ let survivorKey = null;
211
222
  const stop = () => {
212
223
  stopped = true;
213
224
  if (timer) clearTimeout(timer);
@@ -223,6 +234,7 @@ export function watchTermination({
223
234
  if (releaseRequested?.()) state = 'gone';
224
235
  else
225
236
  try {
237
+ if (!tracker.complete && rootExited() && ownerClosed?.()) tracker.acceptExitObservation();
226
238
  observation = tracker.observe({ rootExited: rootExited() });
227
239
  observation.catch(() => {});
228
240
  // A pass that has not answered by the next cadence tick is unknown.
@@ -250,6 +262,18 @@ export function watchTermination({
250
262
  /* presentation only */
251
263
  }
252
264
  }
265
+ if (state !== 'gone' && onSurvivors) {
266
+ const pids = tracker.live.map((entry) => entry.pid);
267
+ const key = pids.join(',');
268
+ if (key !== survivorKey) {
269
+ survivorKey = key;
270
+ try {
271
+ onSurvivors(pids);
272
+ } catch {
273
+ /* presentation only */
274
+ }
275
+ }
276
+ }
253
277
  if (state === 'gone') {
254
278
  try {
255
279
  await onTerminated();
@@ -60,6 +60,14 @@ export async function promoteWithRetry(
60
60
  */
61
61
  export function killAfterFailedPromotion(run, reason, promotionFailure) {
62
62
  run.autoBackgrounded = false;
63
+ // The command finished while it was being promoted (the spawn server
64
+ // releases a foreground task's record at its exit, so the promotion found
65
+ // none): report that completion. Killing the exited root would have no
66
+ // pre-kill snapshot and hold the workspace as unconfirmed for good.
67
+ if (run.child.exitCode != null || run.child.signalCode != null) {
68
+ if (run.promotionClose) void run.settle(...run.promotionClose);
69
+ return;
70
+ }
63
71
  if (reason === 'timeout') {
64
72
  run.timedOut = true;
65
73
  treeKillForceSettle(run, 'timeout');
@@ -8,6 +8,8 @@ import { unregisterOwnedShellTree } from './owned-shell-trees.mjs';
8
8
  import {
9
9
  adoptShellWorkspaceAdmission,
10
10
  clearTerminationHold,
11
+ isShellOwnerSessionClosed,
12
+ noteShellJobSurvivors,
11
13
  registerTerminationHold,
12
14
  takeReleaseRequest,
13
15
  } from '../builtin/lib/shell-job-records.mjs';
@@ -60,6 +62,8 @@ export function releaseAdmissionOnConfirmedTermination(run, { markNow = true } =
60
62
  sweep: run.killed === true,
61
63
  onTerminated: release,
62
64
  onUnconfirmed: markNow ? null : () => keepTerminationUnconfirmedRecord(run),
65
+ onSurvivors: (pids) => void noteShellJobSurvivors(run.foregroundRecordId, pids),
66
+ ownerClosed: () => isShellOwnerSessionClosed(run.foregroundRecordInput?.ownerSessionId),
63
67
  releaseRequested: () => takeReleaseRequest(run.foregroundRecordId),
64
68
  });
65
69
  }
@@ -164,6 +168,8 @@ export function createSettle(run, releasePromotedCapture) {
164
168
  if (run.settled) return;
165
169
  if (run.ownedTree) unregisterOwnedShellTree(run.foregroundRecordId, run.ownedTree);
166
170
  if (run.autoBackgrounded) {
171
+ // A failed promotion settles the run with this close instead.
172
+ run.promotionClose = [exitCode, signal];
167
173
  releasePromotedCapture();
168
174
  return;
169
175
  }