mixdog 1.4.0 → 1.4.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 (236) hide show
  1. package/package.json +1 -1
  2. package/scripts/check-voice-runtime-inputs.mjs +25 -0
  3. package/scripts/gate-local.mjs +12 -3
  4. package/scripts/latest-guard.mjs +21 -9
  5. package/scripts/lib/isolated-test-env.mjs +10 -5
  6. package/scripts/lib/run-node-tests.mjs +37 -37
  7. package/scripts/lib/scoped-temp.d.mts +16 -0
  8. package/scripts/lib/scoped-temp.mjs +123 -0
  9. package/scripts/lib/stale-temp.mjs +44 -0
  10. package/scripts/post-release-check.mjs +1 -6
  11. package/scripts/pptx-kit-index.mjs +17 -4
  12. package/scripts/suite-health.mjs +10 -1
  13. package/scripts/test-data-dir.mjs +10 -7
  14. package/src/app.mjs +4 -0
  15. package/src/defaults/shipped-history.json +5 -0
  16. package/src/defaults/skills/browser-use/SKILL.md +6 -4
  17. package/src/defaults/skills/computer-use/SKILL.md +4 -2
  18. package/src/lib/rules-builder.cjs +29 -12
  19. package/src/output-styles/common.md +2 -2
  20. package/src/output-styles/simple.md +1 -1
  21. package/src/rules/agent/AGENT.md +5 -4
  22. package/src/rules/lead/LEAD.md +10 -5
  23. package/src/rules/shared/10-tool-workflow.md +3 -1
  24. package/src/rules/shared/45-destructive.md +0 -2
  25. package/src/rules/shared/50-execution.md +2 -0
  26. package/src/rules/shared/70-delivery.md +7 -9
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
  28. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
  36. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
  37. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
  47. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
  49. package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
  50. package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
  51. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
  62. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
  63. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
  64. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
  65. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
  66. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
  68. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
  76. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
  77. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
  78. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  79. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
  80. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
  81. package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
  82. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
  83. package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
  84. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
  85. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
  86. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
  87. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
  88. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
  89. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
  90. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
  92. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
  93. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
  97. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
  99. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
  102. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
  103. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
  104. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
  105. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
  106. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
  107. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
  108. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
  110. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
  111. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
  112. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
  115. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
  116. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
  118. package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
  121. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
  123. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
  125. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
  126. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  136. package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
  137. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
  138. package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
  139. package/src/runtime/channels/lib/whisper-port.mjs +10 -1
  140. package/src/runtime/computer-bridge/action-schema.mjs +2 -2
  141. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  142. package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
  143. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
  144. package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
  145. package/src/runtime/local-provider/server-process.mjs +2 -1
  146. package/src/runtime/media/adapters/gemini-video.mjs +4 -1
  147. package/src/runtime/media/adapters/xai-media.mjs +2 -1
  148. package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
  149. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
  150. package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
  151. package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
  152. package/src/runtime/office/design/design-art-direction.mjs +29 -27
  153. package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
  154. package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
  155. package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
  156. package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
  157. package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
  158. package/src/runtime/office/quality/design-review.mjs +1 -5
  159. package/src/runtime/office/quality/document-brief.mjs +1 -5
  160. package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
  161. package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
  162. package/src/runtime/shared/bot-owned-work.mjs +56 -0
  163. package/src/runtime/shared/defaults-separation.mjs +1 -4
  164. package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
  165. package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
  166. package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
  167. package/src/runtime/shared/handoff-artifacts.mjs +18 -8
  168. package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
  169. package/src/runtime/shared/llm/cost.mjs +5 -1
  170. package/src/runtime/shared/loopback-listener.mjs +25 -2
  171. package/src/runtime/shared/model-route-chain.d.mts +7 -2
  172. package/src/runtime/shared/model-route-chain.mjs +22 -4
  173. package/src/runtime/shared/open-url.mjs +10 -0
  174. package/src/runtime/shared/session-activity-signal.mjs +14 -0
  175. package/src/runtime/shared/session-scratch.mjs +37 -0
  176. package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
  177. package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
  178. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  179. package/src/runtime/shared/tool-surface.mjs +3 -0
  180. package/src/runtime/shared/tool-work-units.mjs +2 -0
  181. package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
  182. package/src/runtime/shared/worktrees/contract.mjs +4 -1
  183. package/src/runtime/shared/worktrees/git.mjs +15 -0
  184. package/src/runtime/shared/worktrees/manager.mjs +87 -38
  185. package/src/runtime/shared/worktrees/test-support.mjs +6 -10
  186. package/src/runtime/visualize-bridge/client.mjs +2 -0
  187. package/src/session-runtime/agent-home-delivery.mjs +41 -0
  188. package/src/session-runtime/agent-task-sessions.mjs +152 -63
  189. package/src/session-runtime/agent-tool-routing.mjs +8 -41
  190. package/src/session-runtime/boot/tools.mjs +5 -1
  191. package/src/session-runtime/bot-status.mjs +12 -6
  192. package/src/session-runtime/bot-target.mjs +38 -0
  193. package/src/session-runtime/channel-config-api.mjs +6 -1
  194. package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
  195. package/src/session-runtime/conversation-import/discovery.mjs +271 -25
  196. package/src/session-runtime/conversation-import/service.mjs +26 -5
  197. package/src/session-runtime/handoff-attachments.mjs +82 -31
  198. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
  199. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
  200. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
  201. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
  202. package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
  203. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
  204. package/src/session-runtime/model-route/set-route.mjs +11 -2
  205. package/src/session-runtime/provider-catalog-cache.mjs +82 -36
  206. package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
  207. package/src/session-runtime/provider-models.mjs +4 -2
  208. package/src/session-runtime/resource-recall-api.mjs +6 -1
  209. package/src/session-runtime/runtime-feature-gates.mjs +5 -1
  210. package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
  211. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
  212. package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
  213. package/src/session-runtime/services/agent-tool/render.mjs +71 -17
  214. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
  215. package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
  216. package/src/session-runtime/services/provider-admin.mjs +2 -1
  217. package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
  218. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
  219. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
  220. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
  221. package/src/session-runtime/workspace/transitions.mjs +25 -0
  222. package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
  223. package/src/standalone/daemon-session-control.mjs +18 -2
  224. package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
  225. package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
  226. package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
  227. package/src/tui/app/app-view/floating-panel.jsx +7 -4
  228. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
  229. package/src/tui/app/model-picker/route-selection.mjs +5 -6
  230. package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
  231. package/src/tui/app/provider-setup/provider-items.mjs +8 -2
  232. package/src/tui/app/provider-setup-picker.mjs +9 -5
  233. package/src/tui/components/QuestionPanel.jsx +13 -4
  234. package/src/tui/dist/index.mjs +75 -28
  235. package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
  236. package/src/ui/statusline-agents.mjs +1 -3
@@ -15,6 +15,12 @@
15
15
  * answer or approval posts one faint line instead; the card stays in the task
16
16
  * session for the user.
17
17
  *
18
+ * Whether a turn reports is apart from whose work it is: every accepted send
19
+ * of an agent is the agent's work in that session (bot-owned-work.mjs, shown
20
+ * as the bot working) until its chain ends, the point a report would be
21
+ * final, and with `notify: false` or an unstored watch it reports nothing.
22
+ * A turn the user types there is owned by no agent.
23
+ *
18
24
  * An agent's send to any other session reports the same way, by an origin for
19
25
  * that send only (reportOrigin). Every reporting turn is also watched durably
20
26
  * (agent-home-delivery.mjs `watches`) until its end is queued, so a daemon
@@ -41,6 +47,7 @@ import { wrapAgentMessage } from '../runtime/shared/agent-message-envelope.mjs';
41
47
  import { hasPendingAgentWork, sessionsWithPendingWork } from '../runtime/shared/pending-agent-work.mjs';
42
48
  import { agentTaskIdOfExecution } from './services/agent-tool/restart-notify.mjs';
43
49
  import { botChainOf } from '../runtime/shared/bot-hops.mjs';
50
+ import { endBotWork, noteBotWork } from '../runtime/shared/bot-owned-work.mjs';
44
51
  import { worktreeError, worktreeNameError } from '../runtime/shared/worktrees/contract.mjs';
45
52
  import { samePath } from '../runtime/shared/worktrees/paths.mjs';
46
53
  import { readModelCatalog, routeByDisplayName, withEffort } from './task-session-route.mjs';
@@ -813,17 +820,24 @@ async function sendReportOrigin({ loadSession, agentName }, sessionId, agentId,
813
820
  }
814
821
 
815
822
  /** Re-attach one watch a stopped daemon left: report the turn's end once
816
- * from the target's record, or watch it until it ends. */
823
+ * from the target's record, or watch it until it ends. `quiet`: `watch` is
824
+ * the record of work that reports nothing (no home, no title): the same
825
+ * follow, but its end is only the end of the agent's work, dropped from
826
+ * `disown`, and no progress update or report goes out. */
817
827
  async function rearmTaskWatch(
818
- { deliver, loadSession, materializeSession, queuedSubmissionIds, artifactFields, followToFinal, log },
819
- watch
828
+ { deliver, disown, loadSession, materializeSession, queuedSubmissionIds, artifactFields, followToFinal, log },
829
+ watch,
830
+ { quiet = false } = {}
820
831
  ) {
821
832
  const sessionId = watch.taskSessionId;
822
833
  // The turn that answers: the watched submission's own, or the one it
823
834
  // follows (a room member's turn under another submission id).
824
835
  const turnId = watch.follows || watch.runId;
825
- const finish = async (status, text = '', answerId = '') =>
826
- deliver(watch.agentId, {
836
+ // The work is the agent's until its end is reported (or cannot be).
837
+ const finish = async (status, text = '', answerId = '') => {
838
+ endBotWork(watch.runId);
839
+ if (quiet) return disown(watch.runId);
840
+ return deliver(watch.agentId, {
827
841
  runId: `${watch.runId}_end`,
828
842
  taskSessionId: sessionId,
829
843
  title: watch.title,
@@ -836,6 +850,7 @@ async function rearmTaskWatch(
836
850
  ...progressOf(watch.runId, answerId),
837
851
  ...(watch.textOnly || text === GONE_TEXT || text === LOST_TEXT ? {} : await artifactFields(sessionId, turnId)),
838
852
  });
853
+ };
839
854
  if (!(await loadSession(sessionId))) return finish('cancelled', GONE_TEXT);
840
855
  let runtime;
841
856
  try {
@@ -845,6 +860,7 @@ async function rearmTaskWatch(
845
860
  log(`${sessionId} could not be loaded after the restart: ${error?.message || error}`);
846
861
  return finish('failed', RESUME_FAILED_TEXT);
847
862
  }
863
+ noteBotWork({ runId: watch.runId, sessionId, agentId: watch.agentId });
848
864
  let done = false;
849
865
  let checking = false;
850
866
  let again = false;
@@ -878,22 +894,26 @@ async function rearmTaskWatch(
878
894
  sessionId,
879
895
  runId: turnId,
880
896
  runtime,
881
- wakeOwed: true,
897
+ // Only a report wakes the session for the results the restart left
898
+ // it; work that reports nothing is only observed, never driven.
899
+ wakeOwed: !quiet,
882
900
  // The turns an earlier process already reported as progress.
883
901
  progressedThrough: watch.progressed || '',
884
- interim: (answer) =>
885
- deliver(watch.agentId, {
886
- runId: progressRunId(watch.runId, answer.id),
887
- taskSessionId: sessionId,
888
- title: watch.title,
889
- status: 'working',
890
- text: answer.text,
891
- progressWatchId: watch.runId,
892
- answerId: answer.id,
893
- ...(watch.target === 'session' ? { target: 'session' } : {}),
894
- ...(watch.hop > 0 ? { hop: watch.hop } : {}),
895
- ...roomFrom(watch.homeSessionId),
896
- }),
902
+ interim: quiet
903
+ ? null
904
+ : (answer) =>
905
+ deliver(watch.agentId, {
906
+ runId: progressRunId(watch.runId, answer.id),
907
+ taskSessionId: sessionId,
908
+ title: watch.title,
909
+ status: 'working',
910
+ text: answer.text,
911
+ progressWatchId: watch.runId,
912
+ answerId: answer.id,
913
+ ...(watch.target === 'session' ? { target: 'session' } : {}),
914
+ ...(watch.hop > 0 ? { hop: watch.hop } : {}),
915
+ ...roomFrom(watch.homeSessionId),
916
+ }),
897
917
  finish: async (status, text, answerId) => {
898
918
  done = true;
899
919
  unsubscribe();
@@ -925,13 +945,16 @@ function createSendSettlement(log) {
925
945
  const settledTurns = new WeakMap();
926
946
  let sendCount = 0;
927
947
 
928
- /** Add send `member` (`{ seq, id, unwatch, settle }`) to the settlement
948
+ /** Add send `member` (`{ seq, id, notify, unwatch, settle }`) to the settlement
929
949
  * `detail` of its turn, per reporting conversation. The sends of one turn
930
950
  * settle in the same tick; after it the earliest (the turn's own, whose
931
951
  * record holds the whole turn) settles with all their ids and the others
932
952
  * stand down. */
933
953
  function join(detail, origin, member) {
934
- const key = `${origin.agentId}\0${origin.sessionId}`;
954
+ // Sends that report and sends that do not settle apart: the group's first
955
+ // member settles for the group, so a quiet one must never stand for a
956
+ // send that owes a report.
957
+ const key = `${origin.agentId}\0${origin.sessionId}\0${member.notify ? 'report' : 'quiet'}`;
935
958
  let groups = settledTurns.get(detail);
936
959
  if (!groups) {
937
960
  groups = new Map();
@@ -1074,13 +1097,19 @@ function startedGate() {
1074
1097
  export function createAgentTaskSessions({
1075
1098
  getSessionService,
1076
1099
  deliver = async (agentId, report) => (await import('./agent-home-delivery.mjs')).deliverAgentTask(agentId, report),
1077
- // The durable watch of each reporting turn until its end is queued.
1100
+ // The durable watch of each reporting turn until its end is queued. A bot's
1101
+ // turn that reports nothing (reports off, or a watch that could not be
1102
+ // stored) keeps a record of its work instead (`own`, `owned`, `disown`),
1103
+ // which no report and no hand-off follows: a restart re-attaches both.
1078
1104
  watches = {
1079
1105
  add: async (watch) => (await import('./agent-home-delivery.mjs')).watchAgentTask(watch),
1080
1106
  list: async () => (await import('./agent-home-delivery.mjs')).agentTaskWatches(),
1081
1107
  remove: async (runId) => (await import('./agent-home-delivery.mjs')).unwatchAgentTask(runId),
1082
1108
  follow: async (runId, submissionId) =>
1083
1109
  (await import('./agent-home-delivery.mjs')).followAgentTask(runId, submissionId),
1110
+ own: async (work) => (await import('./agent-home-delivery.mjs')).ownAgentTask(work),
1111
+ owned: async () => (await import('./agent-home-delivery.mjs')).agentOwnedTasks(),
1112
+ disown: async (runId) => (await import('./agent-home-delivery.mjs')).disownAgentTask(runId),
1084
1113
  },
1085
1114
  // Submissions still queued (durably) for a session after a restart.
1086
1115
  queuedSubmissionIds = async (sessionId) =>
@@ -1209,7 +1238,35 @@ export function createAgentTaskSessions({
1209
1238
  log(`report of ${sessionId} failed: ${error?.message || error}`)
1210
1239
  );
1211
1240
  let unwatch = () => {};
1212
- // Set once the turn ended completed: the report waits for the final answer.
1241
+ // The agent's work in `sessionId`: owned from the send's acceptance until
1242
+ // it settles or is cancelled, whether or not it reports (`notify`). A send
1243
+ // that holds no report watch keeps a durable record of the work instead
1244
+ // (`watches.own`), so a restart re-attaches it (recover); the writes of
1245
+ // one send go in order, so a record never outlives its work.
1246
+ let owning = true;
1247
+ let recorded = false;
1248
+ let recording = Promise.resolve();
1249
+ const record = (write) => {
1250
+ recording = recording
1251
+ .then(write)
1252
+ .catch((error) => log(`recording the work of ${sessionId} failed: ${error?.message || error}`));
1253
+ return recording;
1254
+ };
1255
+ // The work of every send `ids` this turn answered ends together: one
1256
+ // write drops all their records (a send standing down for the turn's
1257
+ // first leaves its record to that one write).
1258
+ const disown = (ids = [id]) => {
1259
+ owning = false;
1260
+ for (const done of ids) endBotWork(done);
1261
+ if (recorded || ids.length > 1) void record(() => watches.disown?.(ids));
1262
+ };
1263
+ const standDown = () => {
1264
+ unwatch();
1265
+ owning = false;
1266
+ endBotWork(id);
1267
+ };
1268
+ // Set once the turn ended completed: the work and any report wait for the
1269
+ // final answer.
1213
1270
  let follow = null;
1214
1271
  let result;
1215
1272
  /** This send ends the turn `detail` for the sends `answered` (createSendSettlement). */
@@ -1217,11 +1274,12 @@ export function createAgentTaskSessions({
1217
1274
  sendIds = answered;
1218
1275
  const status = turnStatus(detail);
1219
1276
  const text = detail?.result?.content || detail?.error || '';
1220
- // A failed or cancelled turn reports at once; a completed one
1221
- // reports the task's final answer (followToFinal).
1277
+ // A failed or cancelled turn ends at once; a completed one ends with
1278
+ // the task's final answer (followToFinal).
1222
1279
  if (status !== 'completed') {
1223
1280
  unwatch();
1224
- void report(status, text);
1281
+ disown(sendIds);
1282
+ if (notify) void report(status, text);
1225
1283
  return;
1226
1284
  }
1227
1285
  follow = followToFinal({
@@ -1231,23 +1289,23 @@ export function createAgentTaskSessions({
1231
1289
  immediate: { status, text },
1232
1290
  finish: async (endStatus, endText, answerId) => {
1233
1291
  unwatch();
1234
- await report(endStatus, endText, { answerId });
1292
+ disown(sendIds);
1293
+ if (notify) await report(endStatus, endText, { answerId });
1235
1294
  },
1236
- // Each other answer of its chain: a progress update that wakes nobody.
1237
- interim: (answer) => deliverReport('working', answer.text, { answerId: answer.id }),
1295
+ // Each other answer of its chain: a progress update that wakes nobody
1296
+ // (only a send that reports gives them).
1297
+ ...(notify ? { interim: (answer) => deliverReport('working', answer.text, { answerId: answer.id }) } : {}),
1238
1298
  });
1239
1299
  follow.check();
1240
1300
  };
1241
1301
  try {
1242
- if (notify) {
1243
- runtime = await sessions.materializeSession(sessionId);
1244
- const onState = approvalWaitReporter(id, report);
1245
- unwatch =
1246
- runtime?.subscribe?.(() => {
1247
- follow?.check();
1248
- onState(runtime.getState?.() || {});
1249
- }) || unwatch;
1250
- }
1302
+ runtime = await sessions.materializeSession(sessionId);
1303
+ const onState = notify ? approvalWaitReporter(id, report) : null;
1304
+ unwatch =
1305
+ runtime?.subscribe?.(() => {
1306
+ follow?.check();
1307
+ onState?.(runtime.getState?.() || {});
1308
+ }) || unwatch;
1251
1309
  // A bot's send to a session that is not its own task session reaches the
1252
1310
  // model inside the sender envelope (`envelopeName`, set by reportOrigin).
1253
1311
  const sent = origin.envelopeName
@@ -1259,39 +1317,55 @@ export function createAgentTaskSessions({
1259
1317
  id,
1260
1318
  mode: 'prompt',
1261
1319
  transcriptMeta: sendTranscriptMeta(origin),
1262
- ...(notify
1263
- ? {
1264
- // A room answers this send in a turn under another submission
1265
- // id (one turn for several sends, or a fresh turn for one cut
1266
- // short): the watch follows that turn, recorded before it runs.
1267
- follow: (submissionId) => watches.follow?.(id, submissionId),
1268
- onSettled: (detail) =>
1269
- settlement.join(detail, origin, {
1270
- seq,
1271
- id,
1272
- unwatch: () => unwatch(),
1273
- settle: (answered) => settleTurn(detail, answered),
1274
- }),
1275
- }
1276
- : {}),
1320
+ // A room answers this send in a turn under another submission id (one
1321
+ // turn for several sends, or a fresh turn for one cut short): the
1322
+ // watch follows that turn, recorded before it runs.
1323
+ ...(notify ? { follow: (submissionId) => watches.follow?.(id, submissionId) } : {}),
1324
+ onSettled: (detail) =>
1325
+ settlement.join(detail, origin, {
1326
+ seq,
1327
+ id,
1328
+ notify,
1329
+ // Standing down for the turn's first send, which owns the same work.
1330
+ unwatch: standDown,
1331
+ settle: (answered) => settleTurn(detail, answered),
1332
+ }),
1277
1333
  });
1278
1334
  } catch (error) {
1279
1335
  unwatch();
1336
+ disown();
1280
1337
  if (notify) await dropWatch();
1281
1338
  throw error;
1282
1339
  }
1283
1340
  const accepted = result?.accepted === true;
1284
1341
  if (!accepted) {
1285
1342
  unwatch();
1343
+ disown();
1286
1344
  if (notify) await dropWatch();
1345
+ } else if (owning) {
1346
+ // Unless it already settled: nothing then is left to end the work.
1347
+ noteBotWork({ runId: id, sessionId, agentId: origin.agentId });
1348
+ if (!notify) {
1349
+ recorded = true;
1350
+ await record(() =>
1351
+ watches.own?.({
1352
+ runId: id,
1353
+ agentId: origin.agentId,
1354
+ taskSessionId: sessionId,
1355
+ owner: processToken,
1356
+ at: Date.now(),
1357
+ })
1358
+ );
1359
+ }
1287
1360
  }
1288
1361
  return sendResult(accepted, notify, id, noReport);
1289
1362
  }
1290
1363
 
1291
- const rearm = (watch) =>
1364
+ const rearm = (watch, options) =>
1292
1365
  rearmTaskWatch(
1293
1366
  {
1294
1367
  deliver,
1368
+ disown: async (runId) => watches.disown?.(runId),
1295
1369
  loadSession,
1296
1370
  materializeSession: (sessionId) => service().materializeSession(sessionId),
1297
1371
  queuedSubmissionIds,
@@ -1299,13 +1373,25 @@ export function createAgentTaskSessions({
1299
1373
  followToFinal,
1300
1374
  log,
1301
1375
  },
1302
- watch
1376
+ watch,
1377
+ options
1303
1378
  );
1304
1379
 
1305
- /** Daemon start: re-attach every reporting turn an earlier process watched.
1380
+ /** Daemon start: re-attach every reporting turn an earlier process watched,
1381
+ * and every turn of a bot's that reported nothing but whose work it owned
1382
+ * (nothing is reported for those, they are only the bot's work again).
1306
1383
  * Resolves once each is reported or watched. */
1307
1384
  async function recover() {
1308
- const pending = (await watches.list()).filter((watch) => watch?.owner !== processToken);
1385
+ const earlier = (list) => list.filter((entry) => entry?.owner !== processToken);
1386
+ const pending = earlier(await watches.list());
1387
+ const quiet = earlier(
1388
+ await Promise.resolve(watches.owned?.() ?? [])
1389
+ .then((list) => (Array.isArray(list) ? list : []))
1390
+ .catch((error) => {
1391
+ log(`reading the records of bot work failed: ${error?.message || error}`);
1392
+ return [];
1393
+ })
1394
+ );
1309
1395
  // The agents the restart killed become queued failure results first, so
1310
1396
  // each re-attached task session has them to take (followToFinal wakeOwed).
1311
1397
  if (pending.length) {
@@ -1313,12 +1399,15 @@ export function createAgentTaskSessions({
1313
1399
  .then(settleAgentResults)
1314
1400
  .catch((error) => log(`settling the agents of the previous process failed: ${error?.message || error}`));
1315
1401
  }
1316
- await Promise.all(
1317
- pending.map((watch) =>
1318
- rearm(watch).catch((error) => log(`re-attaching ${watch?.taskSessionId} failed: ${error?.message || error}`))
1319
- )
1320
- );
1321
- return pending.length;
1402
+ const reattach = (work, options) =>
1403
+ rearm(work, options).catch((error) =>
1404
+ log(`re-attaching ${work?.taskSessionId} failed: ${error?.message || error}`)
1405
+ );
1406
+ await Promise.all([
1407
+ ...pending.map((watch) => reattach(watch)),
1408
+ ...quiet.map((work) => reattach(work, { quiet: true })),
1409
+ ]);
1410
+ return pending.length + quiet.length;
1322
1411
  }
1323
1412
 
1324
1413
  /** Create the task session for `brief` and start it; returns at once.
@@ -5,55 +5,22 @@
5
5
  */
6
6
 
7
7
  import { clean } from '../runtime/shared/clean.mjs';
8
-
9
- const lower = (value) => clean(value).toLowerCase();
10
-
11
- // A pinned model named by its catalog display name ("GPT-6.1-Sol") is not a
12
- // wire id. Canonicalize it only on an unambiguous EXACT match: the id itself,
13
- // or a display name / label, case-insensitively, within the requested provider
14
- // (or across providers when none is given and exactly one row matches).
15
- // Anything else — no match, several matches, an empty or partial catalog —
16
- // returns the args unchanged: the catalog never refuses a model and never
17
- // substitutes a near one.
18
- export function canonicalizePinnedModel(args, rows) {
19
- const model = clean(args?.model);
20
- if (!model || !Array.isArray(rows)) return args;
21
- const provider = lower(args.provider);
22
- const pool = rows.filter((row) => row && (!provider || lower(row.provider) === provider));
23
- const wanted = model.toLowerCase();
24
- const exactId = pool.filter((row) => row.id === model);
25
- // A partial catalog may lack the real id a display name collides with, so
26
- // only an exact id can pass there; aliases wait for a complete catalog.
27
- const partial = pool.some((row) => row.catalogPartial === true);
28
- const hits = exactId.length
29
- ? exactId
30
- : partial
31
- ? []
32
- : pool.filter((row) => lower(row.id) === wanted || lower(row.display) === wanted || lower(row.label) === wanted);
33
- const distinct = [...new Map(hits.map((row) => [`${row.provider}\n${row.id}`, row])).values()];
34
- if (distinct.length !== 1) return args;
35
- const [hit] = distinct;
36
- if (hit.id === model && hit.provider === args.provider) return args;
37
- return { ...args, provider: hit.provider, model: hit.id };
38
- }
8
+ import { resolveRequestedModel } from './services/agent-tool/model-select.mjs';
39
9
 
40
10
  // `listProviderModels` is this runtime's provider catalog. The agent control
41
11
  // behind `executeAgentControl` (the daemon's canonical agent) has none, so a
42
- // display-name model is canonicalized here before it is forwarded. Only the
43
- // cached/quick catalog is read, so a spawn never waits on provider listings.
12
+ // pinned model is resolved here, by the same rule the in-process agent tool
13
+ // applies (exact id, display name, then every query word; ambiguous or
14
+ // unknown names fail with candidates), and forwarded as the exact route.
44
15
  export function createRoutedAgentTool({ rt, agentTool, executeAgentControl = null, listProviderModels = null }) {
45
16
  const routedAgentTool = {
46
17
  ...agentTool,
47
18
  async execute(args, context = {}) {
48
19
  if (typeof executeAgentControl !== 'function') return agentTool.execute(args, context);
49
- let routedArgs = args;
50
- if (typeof listProviderModels === 'function' && clean(args?.model) && (clean(args.type) || 'spawn') === 'spawn') {
51
- try {
52
- routedArgs = canonicalizePinnedModel(args, await listProviderModels({ quick: true }));
53
- } catch {
54
- routedArgs = args;
55
- }
56
- }
20
+ const routedArgs =
21
+ typeof listProviderModels === 'function' && clean(args?.model) && (clean(args.type) || 'spawn') === 'spawn'
22
+ ? await resolveRequestedModel(args, listProviderModels)
23
+ : args;
57
24
  return executeAgentControl(routedArgs, context);
58
25
  },
59
26
  closeAll(reason, scope = {}) {
@@ -22,7 +22,10 @@ import { TOOL_DEFS as OFFICE_TOOL_DEFS } from '../../runtime/office/tool-defs.mj
22
22
  import { TOOL_DEFS as MEDIA_TOOL_DEFS } from '../../runtime/media/tool-defs.mjs';
23
23
  import { TOOL_DEFS as TIDY_TOOL_DEFS } from '../../runtime/tidy/tool-defs.mjs';
24
24
  import { TOOL_DEFS as VISUALIZE_BRIDGE_TOOL_DEFS } from '../../runtime/visualize-bridge/tool-defs.mjs';
25
- import { ASK_USER_TOOL_DEFS } from '../../runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs';
25
+ import {
26
+ ASK_USER_TOOL_DEFS,
27
+ HANDOFF_USER_TOOL_DEFS,
28
+ } from '../../runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs';
26
29
  import { REACTION_TOOL_DEFS } from '../../runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs';
27
30
  import { SETUP_TOOL_DEFS } from '../setup-tool/tool-defs.mjs';
28
31
  import { createSetupToolExecutor } from '../setup-tool/executor.mjs';
@@ -171,6 +174,7 @@ function wireToolSurface(boot) {
171
174
  visualizeToolDefs: VISUALIZE_BRIDGE_TOOL_DEFS,
172
175
  setupToolDefs: SETUP_TOOL_DEFS,
173
176
  askUserToolDefs: ASK_USER_TOOL_DEFS,
177
+ handoffUserToolDefs: HANDOFF_USER_TOOL_DEFS,
174
178
  reactionToolDefs: REACTION_TOOL_DEFS,
175
179
  goalTools: goalRuntime.tools,
176
180
  agentTools: agentTool.tools,
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * `session bots`: the state of the user's bots, read on demand (never injected
3
3
  * into a turn). One line per bot: its home chat (`sess_home_<id>`), the last
4
- * activity there and its latest line, whether it works now (its chat, or a
5
- * task session it runs), approvals waiting on the user, its routines and what
4
+ * activity there and its latest line, whether it works now (its chat, or work
5
+ * it sent running in a task session; a turn the user typed there is the
6
+ * user's), approvals waiting on the user, its routines and what
6
7
  * needs attention. With one bot (`session_id`: its id or chat id): its
7
8
  * routines, its held requests and its recent sessions.
8
9
  *
@@ -143,7 +144,7 @@ function span(ms) {
143
144
  const ago = (at, now) => `${span(now - at)} ago`;
144
145
  const until = (at, now) => (at <= now ? 'due now' : `in ${span(at - now)}`);
145
146
 
146
- async function defaultListBots() {
147
+ export async function defaultListBots() {
147
148
  const [{ workflowHelpers }, { getPluginData, loadConfig }, { isAgentDisabled, isContactAgentId }] = await Promise.all(
148
149
  [
149
150
  import('./boot/shared.mjs'),
@@ -264,7 +265,12 @@ export function createBotStatus({
264
265
  const homeId = agentHomeSessionId(bot.id);
265
266
  const owned = ctx.owned.get(bot.id) || [];
266
267
  const home = ctx.rows.get(homeId) || null;
267
- // Running task sessions: a turn whose report is still owed, or one working now.
268
+ // Work this bot sent that is running now: a session working with the bot's
269
+ // send in flight (`handedBy`, bot-owned-work.mjs). A turn the user typed
270
+ // in the bot's task session is the user's, not the bot working.
271
+ const sentWork = (row) => isWorking(row) && row.handedBy?.includes(bot.id) === true;
272
+ // Running task sessions: a turn whose report is still owed, or the bot's
273
+ // work running now.
268
274
  const tasks = new Map();
269
275
  const addTask = (id, title, at) => {
270
276
  const known = tasks.get(id);
@@ -276,7 +282,7 @@ export function createBotStatus({
276
282
  addTask(watch.taskSessionId, watch.title, Number(watch.at) || 0);
277
283
  }
278
284
  for (const row of owned) {
279
- if (row.id === homeId || roomMemberOfSession(row.id) || !isWorking(row)) continue;
285
+ if (row.id === homeId || roomMemberOfSession(row.id) || !sentWork(row)) continue;
280
286
  addTask(row.id, row.title, Number(row.createdAt) || 0);
281
287
  }
282
288
  const running = [...tasks.values()].sort((left, right) => left.at - right.at || (left.id < right.id ? -1 : 1));
@@ -302,7 +308,7 @@ export function createBotStatus({
302
308
  approvals,
303
309
  routines,
304
310
  flags,
305
- working: isWorking(home) || running.some((task) => isWorking(ctx.rows.get(task.id))),
311
+ working: isWorking(home) || running.some((task) => sentWork(ctx.rows.get(task.id))),
306
312
  lastAt: activityOf(home),
307
313
  };
308
314
  }
@@ -0,0 +1,38 @@
1
+ // A bot id as the `session_id` of read, send and status: a bot's chat is a
2
+ // session of its own (`sess_home_<id>`), but the schema also lets a model name
3
+ // the bot (`bots` takes either), so the id resolves to that home chat — before
4
+ // any check of the target, so the bot-chain, hop and task-origin checks see
5
+ // the chat they would for `sess_home_<id>`. A minted session id always has an
6
+ // underscore, a normalized bot id never does (agent-home.mjs), so only ids
7
+ // shaped like a bot id are looked up.
8
+ import { MIXY_AGENT_ID, agentHomeSessionId } from '../runtime/agent/orchestrator/session/agent-home.mjs';
9
+ import { defaultListBots, quotedText } from './bot-status.mjs';
10
+
11
+ const MAX_NAMED_BOTS = 30;
12
+ /** The actions whose `session_id` may be a bot id. */
13
+ export const BOT_TARGET_ACTIONS = new Set(['read', 'send', 'status']);
14
+
15
+ /** The ids of the bots a send can reach: Mixy and every contact agent. */
16
+ async function botIds(listBots) {
17
+ return [MIXY_AGENT_ID, ...(await listBots()).map((bot) => bot.id)];
18
+ }
19
+
20
+ /** `target`'s home chat when it is a known bot id, else `target` unchanged. */
21
+ export async function resolveBotTarget(target, listBots = defaultListBots) {
22
+ const home = agentHomeSessionId(target);
23
+ return home && (await botIds(listBots)).includes(target) ? home : target;
24
+ }
25
+
26
+ /** Whether `target` is shaped like a bot id (so, with no session, a mistyped one). */
27
+ export const isBotShaped = (target) => agentHomeSessionId(target) !== '';
28
+
29
+ /** The error for a bot-shaped `target` that is neither a known bot nor a session. */
30
+ export async function unknownBotError(target, listBots = defaultListBots, cause) {
31
+ const ids = await botIds(listBots);
32
+ const named = ids.slice(0, MAX_NAMED_BOTS).join(', ');
33
+ const more = ids.length > MAX_NAMED_BOTS ? `, … (${ids.length - MAX_NAMED_BOTS} more)` : '';
34
+ return new Error(
35
+ `session: "${quotedText(target, 64)}" is neither a session nor a known bot id; bot ids: ${named}${more}; session action=list finds session ids`,
36
+ cause ? { cause } : undefined
37
+ );
38
+ }
@@ -12,7 +12,12 @@ import {
12
12
  testWebhook,
13
13
  withScheduleRenameReserved,
14
14
  } from './services/channel-admin.mjs';
15
- import { getSchedule, markScheduleFailure, markScheduleSkipped, markScheduleSuccess } from '../runtime/channels/lib/schedules-db.mjs';
15
+ import {
16
+ getSchedule,
17
+ markScheduleFailure,
18
+ markScheduleSkipped,
19
+ markScheduleSuccess,
20
+ } from '../runtime/channels/lib/schedules-db.mjs';
16
21
  import { scheduleRenameUncertainError } from '../runtime/channels/lib/schedule-errors.mjs';
17
22
  import { loadEndpointConfig, readEndpointLastPayload } from '../runtime/channels/lib/webhooks-db.mjs';
18
23
  import { webhookEventSummary, webhookRunPrompt } from '../runtime/channels/lib/webhook/session-dispatch.mjs';
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The thread titles Codex itself stores, by thread id (read-only):
3
+ *
4
+ * state_<N>.sqlite `threads.title`, overridden by `threads.name` (a rename)
5
+ * session_index.jsonl append-only `{ id, thread_name }`; the latest
6
+ * non-empty name of an id wins and overrides the DB
7
+ *
8
+ * Anything unreadable is skipped: a missing or locked store simply leaves the
9
+ * conversation to the title derived from its messages.
10
+ */
11
+ import { DatabaseSync } from 'node:sqlite';
12
+ import { readFile, readdir } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { titleFromText } from '../../runtime/agent/orchestrator/session/conversation-import/turns.mjs';
15
+
16
+ const STATE_DB_RE = /^state_(\d+)\.sqlite$/i;
17
+
18
+ async function newestStateDb(home) {
19
+ let newest = null;
20
+ for (const name of await readdir(home).catch(() => [])) {
21
+ const version = STATE_DB_RE.exec(name)?.[1];
22
+ if (version !== undefined && (!newest || Number(version) > newest.version)) {
23
+ newest = { name, version: Number(version) };
24
+ }
25
+ }
26
+ return newest ? join(home, newest.name) : null;
27
+ }
28
+
29
+ function readStateRows(path) {
30
+ const db = new DatabaseSync(path, { readOnly: true });
31
+ try {
32
+ const columns = new Set(
33
+ db
34
+ .prepare('PRAGMA table_info(threads)')
35
+ .all()
36
+ .map((column) => column.name)
37
+ );
38
+ const wanted = ['title', 'name'].filter((column) => columns.has(column));
39
+ if (!wanted.length) return [];
40
+ return db.prepare(`SELECT id, ${wanted.join(', ')} FROM threads`).all();
41
+ } finally {
42
+ db.close();
43
+ }
44
+ }
45
+
46
+ async function readStateTitles(home, titles) {
47
+ try {
48
+ const path = await newestStateDb(home);
49
+ if (!path) return;
50
+ const rows = readStateRows(path);
51
+ for (const column of ['title', 'name']) {
52
+ for (const row of rows) {
53
+ const title = titleFromText(row[column]);
54
+ if (title && typeof row.id === 'string') titles.set(row.id, title);
55
+ }
56
+ }
57
+ } catch {
58
+ /* unreadable store: fall back to the session index and derived titles */
59
+ }
60
+ }
61
+
62
+ async function readIndexTitles(home, titles) {
63
+ let text;
64
+ try {
65
+ text = await readFile(join(home, 'session_index.jsonl'), 'utf8');
66
+ } catch {
67
+ return;
68
+ }
69
+ for (const line of text.split('\n')) {
70
+ if (!line.trim()) continue;
71
+ let entry;
72
+ try {
73
+ entry = JSON.parse(line);
74
+ } catch {
75
+ continue;
76
+ }
77
+ const title = titleFromText(entry?.thread_name);
78
+ if (title && typeof entry.id === 'string') titles.set(entry.id, title);
79
+ }
80
+ }
81
+
82
+ /** Map of Codex thread id → its stored title (whitespace-normalized, ≤100 chars). */
83
+ export async function loadCodexThreadTitles(home) {
84
+ const titles = new Map();
85
+ await readStateTitles(home, titles);
86
+ await readIndexTitles(home, titles);
87
+ return titles;
88
+ }