mixdog 1.2.6 → 1.3.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 (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  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 +7 -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 +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -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/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -21,6 +21,8 @@ import { readStrictRecordFields } from './store-record-strict-scan.mjs';
21
21
  import { isAgentOnlySession, isRootLeadSession, sessionVisibility } from './store-summary-visibility.mjs';
22
22
  import { applySummaryLogText, summaryLogPath } from './store-summary-log.mjs';
23
23
  import { dataDir } from './store-summary-locations.mjs';
24
+ import { normalizeRoutineProvenance } from './routine-result.mjs';
25
+ import { lastMessageLine, lastMessageSummary } from './store-summary-last-message.mjs';
24
26
  import {
25
27
  desktopSession,
26
28
  importKey,
@@ -31,6 +33,7 @@ import {
31
33
  workspaceSummary,
32
34
  } from './store-summary-fields.mjs';
33
35
  import { isRealTurnMessage } from './compact/user-message-kind.mjs';
36
+ import { sessionModeOf } from './agent-mode.mjs';
34
37
  import { byActivityThenId } from './catalog-db/schema.mjs';
35
38
  import {
36
39
  sessionStampOf,
@@ -191,6 +194,7 @@ export function normalizedRow(row) {
191
194
  status: String(row.status || (row.closed === true ? 'closed' : 'idle')),
192
195
  owner: row.owner || 'user',
193
196
  agent: row.agent || null,
197
+ sessionMode: sessionModeOf(row),
194
198
  sourceType: row.sourceType || null,
195
199
  sourceName: row.sourceName || null,
196
200
  scopeKey: row.scopeKey || null,
@@ -210,6 +214,10 @@ export function normalizedRow(row) {
210
214
  realTurnCount: realTurnCount(row),
211
215
  title: cleanText(row.title, 100),
212
216
  preview: cleanText(row.preview),
217
+ lastMessagePreview: cleanText(row.lastMessagePreview),
218
+ // Null for any line that is not a routine result, and on rows written
219
+ // before the marker existed (they show their plain text).
220
+ lastMessageRoutine: normalizeRoutineProvenance(row.lastMessageRoutine),
213
221
  generation: typeof row.generation === 'number' ? row.generation : 0,
214
222
  storageMtimeMs: positiveNumber(row.storageMtimeMs, 0),
215
223
  storageSize: positiveNumber(row.storageSize, 0),
@@ -224,8 +232,9 @@ function leadRows(rows) {
224
232
  }
225
233
 
226
234
  // What a row derivation decodes of a record (store-record-strict-scan.mjs):
227
- // rowFromSession reads its user messages whole and only the role of the rest,
228
- // and never these top-level fields — together ~10% of a record, mostly tool
235
+ // rowFromSession reads its user messages whole, the newest messages back to
236
+ // the one with a last-message line whole too, only the role of the rest, and
237
+ // never these top-level fields — together ~10% of a record, mostly tool
229
238
  // catalogs. Every other top-level field is decoded, so a field a row starts
230
239
  // to read is there without touching this list.
231
240
  const CATALOG_RECORD_READ = {
@@ -239,6 +248,7 @@ const CATALOG_RECORD_READ = {
239
248
  'pendingDeferredToolDelta',
240
249
  ]),
241
250
  fullMessageRoles: new Set(['user']),
251
+ fullTailUntil: (message) => Boolean(lastMessageLine(message)),
242
252
  };
243
253
 
244
254
  function rowFromSession(session) {
@@ -256,11 +266,15 @@ function rowFromSession(session) {
256
266
  )
257
267
  .map((message) => cleanText(messageText(message.content)))
258
268
  .find(Boolean) || '';
269
+ // The last-message line comes from the transcript like the summary index
270
+ // derives it; without it a session saved after the index was written lost
271
+ // its contact-row preview whenever its row came from this file read.
259
272
  return normalizedRow({
260
273
  ...session,
261
274
  messageCount: messages.filter((message) => message?.role === 'user' || message?.role === 'assistant').length,
262
275
  realTurnCount: messages.filter(isRealTurnMessage).length,
263
276
  preview,
277
+ ...lastMessageSummary(messages),
264
278
  });
265
279
  }
266
280
 
@@ -16,8 +16,9 @@ import {
16
16
  writeJsonAtomicSync,
17
17
  } from '../../../shared/atomic-file.mjs';
18
18
  import { applySummaryLogText, encodeSummaryOps, summaryLogPath } from './store-summary-log.mjs';
19
- import { cleanSessionPreview, isSessionPreviewNoise, sessionMessageText } from '../runtime-core/session-text.mjs';
19
+ import { isSessionPreviewNoise, sessionMessageText } from '../runtime-core/session-text.mjs';
20
20
  import { sessionVisibility } from './store-summary-visibility.mjs';
21
+ import { sessionModeOf } from './agent-mode.mjs';
21
22
  import {
22
23
  importKey,
23
24
  inheritedFromSessionId,
@@ -27,6 +28,8 @@ import {
27
28
  workspaceSummary,
28
29
  } from './store-summary-fields.mjs';
29
30
  import { isRealTurnMessage } from './compact/user-message-kind.mjs';
31
+ import { isRoutineResultMessage, normalizeRoutineProvenance, routineProvenance } from './routine-result.mjs';
32
+ import { LAST_MESSAGE_PREVIEW_MAX, cleanSummaryPreview, lastMessageRawText } from './store-summary-last-message.mjs';
30
33
 
31
34
  export const SESSION_SUMMARY_INDEX_VERSION = 2;
32
35
 
@@ -37,15 +40,7 @@ export function summaryIndexPath() {
37
40
  return join(getPluginData(), 'session-summaries.json');
38
41
  }
39
42
 
40
- function _computeCleanPreview(text, max) {
41
- const value = cleanSessionPreview(text, max);
42
- return value.length > max
43
- ? value
44
- .slice(0, max)
45
- .replace(/\s+\S*$/, '')
46
- .trim()
47
- : value;
48
- }
43
+ const _computeCleanPreview = cleanSummaryPreview;
49
44
 
50
45
  // Every index flush re-normalizes EVERY row's stored title and preview, and
51
46
  // cleaning runs a chain of regex replaces that each allocate a copy. Those
@@ -79,8 +74,13 @@ const sessionMessageProjectionMemo = new WeakMap();
79
74
  // re-cleaned.
80
75
  const messagePreviewMemo = new WeakMap();
81
76
 
77
+ // Routine results (a schedule's output in its owner's home thread) follow ONE
78
+ // rule here: each is exactly one message of `messageCount` (so a thread's
79
+ // unread count, messageCount minus the read cursor, rises by one per result),
80
+ // never a real turn (`realTurnCount`), never the first-prompt `preview` the
81
+ // title falls back to, and shown as its notice line in `lastMessagePreview`.
82
82
  function _previewFromMessage(message) {
83
- if (message?.role !== 'user') return '';
83
+ if (message?.role !== 'user' || isRoutineResultMessage(message)) return '';
84
84
  const raw = sessionMessageText(message.content);
85
85
  const memo = messagePreviewMemo.get(message);
86
86
  if (memo && memo.raw === raw) return memo.preview;
@@ -89,6 +89,20 @@ function _previewFromMessage(message) {
89
89
  return preview;
90
90
  }
91
91
 
92
+ // message → { raw, preview } for the LAST conversational message, user or
93
+ // assistant alike (a messenger row's last-message line).
94
+ const lastMessagePreviewMemo = new WeakMap();
95
+
96
+ function _lastPreviewFromMessage(message) {
97
+ const raw = lastMessageRawText(message);
98
+ if (raw === null) return '';
99
+ const memo = lastMessagePreviewMemo.get(message);
100
+ if (memo && memo.raw === raw) return memo.preview;
101
+ const preview = isSessionPreviewNoise(raw) ? '' : _cleanPreview(raw, LAST_MESSAGE_PREVIEW_MAX);
102
+ lastMessagePreviewMemo.set(message, { raw, preview });
103
+ return preview;
104
+ }
105
+
92
106
  function _sessionMessageProjection(session) {
93
107
  const messages = Array.isArray(session?.messages) ? session.messages : [];
94
108
  const cached = session && typeof session === 'object' ? sessionMessageProjectionMemo.get(session) : null;
@@ -134,6 +148,17 @@ function _sessionMessageProjection(session) {
134
148
  }
135
149
  }
136
150
  }
151
+ // The newest line, read back from the tail every time: a streamed reply
152
+ // fills its message in place. Tool-only and synthetic rows are skipped, and
153
+ // each message's text is memoized, so this stays a few lookups per save.
154
+ // The line's routine provenance rides along, so a client labels a routine
155
+ // result by this marker and never by the line's wording.
156
+ let last = '';
157
+ let lastRoutine = null;
158
+ for (let index = messages.length - 1; index >= 0 && !last; index -= 1) {
159
+ last = _lastPreviewFromMessage(messages[index]);
160
+ if (last) lastRoutine = routineProvenance(messages[index]);
161
+ }
137
162
  if (session && typeof session === 'object') {
138
163
  sessionMessageProjectionMemo.set(session, {
139
164
  length: messages.length,
@@ -144,7 +169,7 @@ function _sessionMessageProjection(session) {
144
169
  previewMessage,
145
170
  });
146
171
  }
147
- return { count, real, preview };
172
+ return { count, real, preview, last, lastRoutine };
148
173
  }
149
174
 
150
175
  function _desktopSessionSummary(value, cwd = null) {
@@ -178,6 +203,8 @@ export function _sessionSummary(session) {
178
203
  status: String(session.status || (session.closed === true ? 'closed' : 'idle')),
179
204
  owner: session.owner || 'user',
180
205
  agent: session.agent || null,
206
+ // 'agent' | 'coding' (agent-mode.mjs); older records resolve it.
207
+ sessionMode: sessionModeOf(session),
181
208
  sourceType: session.sourceType || null,
182
209
  sourceName: session.sourceName || null,
183
210
  scopeKey: session.scopeKey || null,
@@ -209,6 +236,8 @@ export function _sessionSummary(session) {
209
236
  realTurnCount: messageProjection.real,
210
237
  title: _cleanPreview(session.title || '', 100),
211
238
  preview: messageProjection.preview,
239
+ lastMessagePreview: messageProjection.last,
240
+ lastMessageRoutine: messageProjection.lastRoutine,
212
241
  generation: typeof session.generation === 'number' ? session.generation : 0,
213
242
  storageMtimeMs: _positiveNumber(session.storageMtimeMs, 0),
214
243
  storageSize: _positiveNumber(session.storageSize, 0),
@@ -234,6 +263,7 @@ function _normalizeSummaryRow(row) {
234
263
  status: String(row.status || (row.closed === true ? 'closed' : 'idle')),
235
264
  owner: row.owner || 'user',
236
265
  agent: row.agent || null,
266
+ sessionMode: sessionModeOf(row),
237
267
  sourceType: row.sourceType || null,
238
268
  sourceName: row.sourceName || null,
239
269
  scopeKey: row.scopeKey || null,
@@ -264,6 +294,8 @@ function _normalizeSummaryRow(row) {
264
294
  realTurnCount: realTurnCount(row),
265
295
  title: _cleanPreview(row.title || '', 100),
266
296
  preview: _cleanPreview(row.preview || ''),
297
+ lastMessagePreview: _cleanPreview(row.lastMessagePreview || '', 160),
298
+ lastMessageRoutine: normalizeRoutineProvenance(row.lastMessageRoutine),
267
299
  generation: typeof row.generation === 'number' ? row.generation : 0,
268
300
  storageMtimeMs: _positiveNumber(row.storageMtimeMs, 0),
269
301
  storageSize: _positiveNumber(row.storageSize, 0),
@@ -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
- }
@@ -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)
@@ -377,6 +384,20 @@ function restoredUserTranscriptItems(message, nextId) {
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();
@@ -440,6 +461,19 @@ export function modelSwitchItem(id, boundary) {
440
461
  };
441
462
  }
442
463
 
464
+ /** The transcript row of a mid-conversation workflow switch (live and restored). */
465
+ export function workflowSwitchItem(id, boundary) {
466
+ return {
467
+ kind: 'statusdone',
468
+ id,
469
+ status: 'workflow-switched',
470
+ label: 'Workflow switched',
471
+ from: boundary.from,
472
+ to: boundary.to,
473
+ at: boundary.at,
474
+ };
475
+ }
476
+
443
477
  function restoreTranscriptRange(messages, start, sessionId) {
444
478
  const items = [];
445
479
  const pendingToolCalls = new Map();
@@ -450,6 +484,11 @@ function restoreTranscriptRange(messages, start, sessionId) {
450
484
  // let a tail-only restore skip the prefix without first counting every old
451
485
  // projected item.
452
486
  const restoredId = () => `hist_${sessionId}_${index}_${++part}`;
487
+ // A group room's posted row (agent-room.mjs): one bubble or notice row.
488
+ if (isRoomRowMessage(message)) {
489
+ items.push(roomRowItem(message, restoredId()));
490
+ continue;
491
+ }
453
492
  if (message?.role === 'user') {
454
493
  items.push(...restoredUserTranscriptItems(message, restoredId));
455
494
  } else if (message?.role === 'assistant' && !isContextAckMessage(message, messages[index - 1])) {
@@ -477,6 +516,10 @@ function restoreTranscriptRange(messages, start, sessionId) {
477
516
  if (!boundary?.provider || !boundary?.modelId) continue;
478
517
  items.push(modelSwitchItem(restoredId(), boundary));
479
518
  }
519
+ for (const boundary of Array.isArray(message?.meta?.workflowSwitches) ? message.meta.workflowSwitches : []) {
520
+ if (!boundary?.to) continue;
521
+ items.push(workflowSwitchItem(restoredId(), boundary));
522
+ }
480
523
  if (message?.meta?.transcript?.previousCompletions?.length) {
481
524
  const events = items.slice(eventStart);
482
525
  if (events.every((item) => Number.isFinite(item.at))) {
@@ -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
  }