mixdog 1.2.6 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +71 -9
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +14 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +178 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/output-styles/common.md +13 -8
  17. package/src/rules/agent/AGENT.md +4 -0
  18. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  19. package/src/rules/agent-mode/MIXY.md +28 -0
  20. package/src/rules/agent-mode/ROUTINE.md +6 -0
  21. package/src/rules/lead/LEAD.md +7 -3
  22. package/src/rules/shared/70-delivery.md +3 -0
  23. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  24. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  25. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
  26. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  27. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
  28. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  29. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  30. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  31. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  34. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  35. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  36. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  37. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  44. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  46. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  49. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  51. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  52. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  53. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  54. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
  58. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  61. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
  63. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
  64. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  66. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  67. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  68. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  69. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  70. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
  71. package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
  72. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  73. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  75. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
  76. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  77. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  78. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  79. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  80. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  81. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  82. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  83. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  84. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
  85. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  86. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  87. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  88. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  89. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
  90. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  91. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  92. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  93. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  94. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  95. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  96. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  97. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  98. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  99. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  100. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
  101. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
  102. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  103. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  105. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  106. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
  108. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  109. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
  110. package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
  111. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  112. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  114. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  115. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  116. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
  118. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  119. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  121. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  123. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  124. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  125. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  127. package/src/runtime/attachments/store.mjs +6 -0
  128. package/src/runtime/browser-bridge/client.mjs +24 -0
  129. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  130. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  131. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  132. package/src/runtime/channels/lib/webhook.mjs +0 -1
  133. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  134. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  135. package/src/runtime/computer-bridge/client.mjs +28 -1
  136. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  137. package/src/runtime/connectors/google-mcp.mjs +446 -0
  138. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  139. package/src/runtime/effort-judge/model-manifest.json +7 -7
  140. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  141. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  142. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  143. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  144. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  145. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  146. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  147. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  148. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  149. package/src/runtime/memory/lib/memory.mjs +7 -1
  150. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  151. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  152. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  153. package/src/runtime/memory/tool-defs.mjs +5 -0
  154. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  155. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  156. package/src/runtime/shared/agent-avatar.mjs +320 -0
  157. package/src/runtime/shared/agent-kind.mjs +152 -0
  158. package/src/runtime/shared/agent-route-config.d.mts +4 -0
  159. package/src/runtime/shared/agent-route-config.mjs +30 -0
  160. package/src/runtime/shared/agent-store.mjs +132 -0
  161. package/src/runtime/shared/atomic-file.mjs +14 -1
  162. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  163. package/src/runtime/shared/config-patch.mjs +20 -1
  164. package/src/runtime/shared/config.mjs +8 -1
  165. package/src/runtime/shared/connector-accounts.mjs +15 -0
  166. package/src/runtime/shared/connector-catalog.d.mts +51 -0
  167. package/src/runtime/shared/connector-catalog.mjs +228 -0
  168. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  169. package/src/runtime/shared/connector-triggers.mjs +45 -0
  170. package/src/runtime/shared/err-text.mjs +3 -1
  171. package/src/runtime/shared/error-presentation.mjs +91 -6
  172. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  173. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  174. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  175. package/src/runtime/shared/held-request-host.d.mts +12 -0
  176. package/src/runtime/shared/held-request-host.mjs +71 -0
  177. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  178. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  179. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  180. package/src/runtime/shared/plain-preview.d.mts +1 -0
  181. package/src/runtime/shared/plain-preview.mjs +33 -0
  182. package/src/runtime/shared/routine-limits.d.mts +11 -0
  183. package/src/runtime/shared/routine-limits.mjs +21 -0
  184. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  185. package/src/runtime/shared/routine-trigger.mjs +44 -0
  186. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  187. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  188. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  189. package/src/runtime/shared/slow-span.mjs +4 -12
  190. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  191. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  192. package/src/runtime/shared/turn-changes.mjs +0 -8
  193. package/src/runtime/shared/usage-readout-totals.mjs +58 -0
  194. package/src/runtime/shared/user-data-guard.mjs +119 -38
  195. package/src/session-runtime/agent-home-delivery.mjs +826 -0
  196. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  197. package/src/session-runtime/agent-room-delivery.mjs +133 -0
  198. package/src/session-runtime/agent-task-sessions.mjs +460 -0
  199. package/src/session-runtime/boot/apis.mjs +63 -18
  200. package/src/session-runtime/boot/facade.mjs +5 -8
  201. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  202. package/src/session-runtime/boot/providers.mjs +14 -4
  203. package/src/session-runtime/boot/tools.mjs +3 -0
  204. package/src/session-runtime/channel-config-api.mjs +53 -5
  205. package/src/session-runtime/config-lifecycle.mjs +43 -2
  206. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  207. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  208. package/src/session-runtime/handoff-attachments.mjs +593 -0
  209. package/src/session-runtime/held-requests.mjs +228 -0
  210. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  211. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  212. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  213. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
  214. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  215. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  216. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  217. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  218. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  219. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  220. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  221. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  222. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  223. package/src/session-runtime/model-route/set-route.mjs +35 -4
  224. package/src/session-runtime/native-web-search.mjs +2 -2
  225. package/src/session-runtime/new-session-config.mjs +6 -1
  226. package/src/session-runtime/owned-automation-run.mjs +229 -0
  227. package/src/session-runtime/resource-api.mjs +5 -0
  228. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  229. package/src/session-runtime/resource-skills-api.mjs +12 -0
  230. package/src/session-runtime/room-member-gate.mjs +48 -0
  231. package/src/session-runtime/route-resolution.mjs +5 -11
  232. package/src/session-runtime/routine-home-turn.mjs +156 -0
  233. package/src/session-runtime/routine-owner.mjs +48 -0
  234. package/src/session-runtime/schedule-session-run.mjs +30 -64
  235. package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
  236. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  237. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  238. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  239. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  240. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  241. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  242. package/src/session-runtime/services/channel-admin.mjs +93 -24
  243. package/src/session-runtime/services/memory-access.mjs +37 -0
  244. package/src/session-runtime/services/provider-admin.mjs +74 -39
  245. package/src/session-runtime/services/schedule-access.mjs +91 -0
  246. package/src/session-runtime/session-create/session-options.mjs +34 -7
  247. package/src/session-runtime/session-hooks.mjs +16 -8
  248. package/src/session-runtime/setup-tool/executor.mjs +79 -6
  249. package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
  250. package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
  251. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  252. package/src/session-runtime/skills-api.mjs +16 -1
  253. package/src/session-runtime/tool-policy-refresh.mjs +249 -29
  254. package/src/session-runtime/turn/session-ops.mjs +8 -1
  255. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  256. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
  257. package/src/session-runtime/webhook-session-run.mjs +49 -67
  258. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  259. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
  260. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  261. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  262. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  263. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  264. package/src/standalone/daemon-agent-control.mjs +1 -8
  265. package/src/standalone/daemon-session-control.mjs +21 -4
  266. package/src/standalone/daemon.mjs +31 -5
  267. package/src/standalone/session-protocol.mjs +27 -0
  268. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  269. package/src/standalone/session-service/agent-tree.mjs +4 -3
  270. package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
  271. package/src/standalone/session-service.mjs +19 -0
  272. package/src/tui/app/slash-commands.mjs +20 -0
  273. package/src/tui/dist/index.mjs +234 -95
  274. package/src/tui/session/context-state.mjs +6 -1
  275. package/src/tui/session/notice-surface.mjs +34 -4
  276. package/src/tui/session/queue-helpers.mjs +2 -1
  277. package/src/tui/session/session-api/extensions.mjs +4 -0
  278. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  279. package/src/tui/session/session-api/integrations.mjs +8 -0
  280. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  281. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  282. package/src/tui/session/session-api/routes.mjs +61 -5
  283. package/src/tui/session/session-flow/drain.mjs +12 -3
  284. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  285. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  286. package/src/tui/session/session-flow/steering.mjs +58 -24
  287. package/src/tui/session/tool-approval.mjs +26 -5
  288. package/src/tui/session/transcript-intake.mjs +8 -2
  289. package/src/tui/session/transcript-spill.mjs +5 -1
  290. package/src/tui/session/tui-steering-persist.mjs +65 -42
  291. package/src/tui/session/turn-live-call.mjs +64 -0
  292. package/src/tui/session/turn.mjs +34 -8
  293. package/src/session-runtime/automation-agents.mjs +0 -94
  294. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -1,10 +1,84 @@
1
- import { providerErrorKind } from '../agent/orchestrator/providers/provider-error-kind.mjs';
1
+ import {
2
+ boundedCauseChain,
3
+ providerErrorKind,
4
+ typedCodeKind,
5
+ } from '../agent/orchestrator/providers/provider-error-kind.mjs';
2
6
  import { readStreamOutcome } from '../agent/orchestrator/providers/lib/stream-outcome.mjs';
7
+ import { namesUnavailableModel } from './error-presentation.mjs';
3
8
 
4
- const ELIGIBLE = new Set(['rate_limited', 'quota_exhausted', 'overloaded', 'transient']);
9
+ // When a route's model fails, does the next model in its fallback chain get
10
+ // the request? Only for a refusal scoped to THIS provider, account or model —
11
+ // another provider may serve the identical request:
12
+ // quota_exhausted / entitlement / billing — the plan or account is spent;
13
+ // rate_limited / overloaded / service_tier_unavailable — this provider is
14
+ // saturated once its own retries are spent;
15
+ // auth / permission — the provider's own sign-in refresh already failed, or
16
+ // the account, region or tier is refused;
17
+ // transient — only when the server answered (a typed status or code): its
18
+ // retry ladder is spent. A bare network failure would fail on every
19
+ // provider, so it stays with the transport recovery;
20
+ // invalid_request on 404/410, a typed model code, or provider wording with
21
+ // the model as the refused subject (namesUnavailableModel) — the model is
22
+ // unknown, retired or not offered to this account.
23
+ // A request that fails the same way on any model never hops: request too
24
+ // large, a refused tool schema or other invalid request, a policy refusal,
25
+ // a cancel, a stall and any untyped failure. A context overflow hops only to
26
+ // a model with a larger window (`contextGrows`); compaction runs first.
27
+ // Whatever the kind, nothing may have been produced: no visible or buffered
28
+ // output, no tool call started or dispatched.
29
+ const PROVIDER_SCOPED_KINDS = new Set([
30
+ 'quota_exhausted',
31
+ 'entitlement',
32
+ 'billing',
33
+ 'rate_limited',
34
+ 'overloaded',
35
+ 'service_tier_unavailable',
36
+ 'auth',
37
+ 'permission',
38
+ ]);
39
+ const MODEL_UNAVAILABLE_CODES = new Set(['model_not_found', 'model_not_supported']);
5
40
 
6
- export function canFallbackRoute(error, { signal, outcome = readStreamOutcome(error), committed = false } = {}) {
7
- if (signal?.aborted || committed || outcome.userAbort || outcome.stallObserved) return false;
41
+ /** 'hop', 'context' (a context overflow: hop only to a larger window) or 'stay'. */
42
+ // A recovery layer may wrap the provider failure (a context overflow it could
43
+ // not compact away): what the original request exposed lives on the cause.
44
+ // Every node of the chain counts, so a wrapper never launders exposure.
45
+ function chainOutcome(error) {
46
+ return readStreamOutcome(...(error && typeof error === 'object' ? boundedCauseChain(error) : [error]));
47
+ }
48
+
49
+ export function routeFallbackVerdict(error, { signal, outcome = chainOutcome(error), committed = false } = {}) {
50
+ if (signal?.aborted || committed || outcome.userAbort || outcome.stallObserved) return 'stay';
51
+ if (!outcome.replaySafe || outcome.observedOutput || outcome.toolCallsStarted || outcome.pendingToolInput) {
52
+ return 'stay';
53
+ }
54
+ const { kind, status, code } = providerErrorKind(error);
55
+ if (PROVIDER_SCOPED_KINDS.has(kind)) return 'hop';
56
+ if (kind === 'transient') return status >= 408 || typedCodeKind(error) === 'transient' ? 'hop' : 'stay';
57
+ if (kind === 'invalid_request') {
58
+ return status === 404 || status === 410 || MODEL_UNAVAILABLE_CODES.has(code) || namesUnavailableModel(error)
59
+ ? 'hop'
60
+ : 'stay';
61
+ }
62
+ return kind === 'context_overflow' ? 'context' : 'stay';
63
+ }
64
+
65
+ export function canFallbackRoute(error, { contextGrows = false, ...options } = {}) {
66
+ const verdict = routeFallbackVerdict(error, options);
67
+ return verdict === 'hop' || (verdict === 'context' && contextGrows === true);
68
+ }
69
+
70
+ const CAPACITY_KINDS = new Set(['rate_limited', 'quota_exhausted', 'overloaded', 'transient']);
71
+
72
+ /**
73
+ * The narrower rule a stateless tool route (native web search) keeps: only a
74
+ * capacity refusal the server stated (429 / 503, or an account-pool quota
75
+ * refusal) moves to the next provider; a denied or failed credential
76
+ * surfaces so the user fixes that route.
77
+ */
78
+ // It also keeps its stricter stream guards: any started or terminal stream
79
+ // stays on its route, since a tool call cannot prove nothing was generated.
80
+ export function canFallbackCapacityRoute(error, { signal, outcome = readStreamOutcome(error) } = {}) {
81
+ if (signal?.aborted || outcome.userAbort || outcome.stallObserved) return false;
8
82
  if (
9
83
  !outcome.replaySafe ||
10
84
  outcome.observedOutput ||
@@ -15,8 +89,6 @@ export function canFallbackRoute(error, { signal, outcome = readStreamOutcome(er
15
89
  )
16
90
  return false;
17
91
  const { kind, status } = providerErrorKind(error);
18
- if (!ELIGIBLE.has(kind)) return false;
19
- // A rejected HTTP request or a provider account-pool refusal proves no
20
- // generation started. A transport disconnect alone does not.
92
+ if (!CAPACITY_KINDS.has(kind)) return false;
21
93
  return status === 429 || status === 503 || (kind === 'quota_exhausted' && error?.quotaExhausted != null);
22
94
  }
@@ -0,0 +1 @@
1
+ export function plainPreviewText(text: unknown): string;
@@ -0,0 +1,33 @@
1
+ // A messenger preview line reads as plain text, as KakaoTalk shows it: the
2
+ // markdown a reply was written in (bold, headings, list markers, quotes,
3
+ // code fences and spans, links, images) is dropped, its words kept.
4
+
5
+ /** `text` without markdown syntax, whitespace folded to single spaces. */
6
+ export function plainPreviewText(text) {
7
+ // HTML comments never show in markdown (a greeting's suggestions ride one).
8
+ return (
9
+ String(text ?? '')
10
+ .replace(/<!--[\s\S]*?(?:-->|$)/g, ' ')
11
+ .replace(/^[ \t]*(`{3,}|~{3,})[^\n]*$/gm, ' ')
12
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
13
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
14
+ .replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '')
15
+ .replace(/^[ \t]*>[ \t]?/gm, '')
16
+ .replace(/^[ \t]*(?:[-*+]|\d+[.)])[ \t]+(?:\[[ xX]\][ \t]+)?/gm, '')
17
+ .replace(/^[ \t]*(?:[-*_][ \t]*){3,}$/gm, ' ')
18
+ .replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2')
19
+ .replace(/(^|[^\w*])\*(?=\S)([^*\n]*?\S)\*(?!\w)/g, '$1$2')
20
+ .replace(/(^|[^\w_])_(?=\S)([^_\n]*?\S)_(?!\w)/g, '$1$2')
21
+ .replace(/~~(?=\S)([\s\S]*?\S)~~/g, '$1')
22
+ .replace(/`+([^`\n]*)`+/g, '$1')
23
+ .replace(/\s+/g, ' ')
24
+ // A summary stored before newlines were kept already folded its list: a
25
+ // dash or bullet right after a sentence end is a marker, not a hyphen.
26
+ .replace(/([.!?。…::])\s[-*+•]\s+(?=\S)/g, '$1 ')
27
+ .trim()
28
+ .slice(0, PREVIEW_CAP)
29
+ );
30
+ }
31
+
32
+ /** A safety cap only: the row clamps the shown text to two lines. */
33
+ const PREVIEW_CAP = 200;
@@ -0,0 +1,11 @@
1
+ export declare const ROUTINE_LIMITS: Readonly<{
2
+ backoffBaseMs: number;
3
+ pauseAfterFailures: number;
4
+ homeQueueLimit: number;
5
+ }>;
6
+
7
+ export declare function routineBackoffRemaining(
8
+ state: { consecutiveFailures?: unknown; lastFailedAt?: unknown } | null | undefined,
9
+ now?: number,
10
+ limits?: typeof ROUTINE_LIMITS
11
+ ): number;
@@ -0,0 +1,21 @@
1
+ // Safeguards of owned routines (schedules and webhook endpoints, see
2
+ // session-runtime/owned-automation-run.mjs): after a failure the next
3
+ // unattended fire waits `backoffBaseMs * 2^(n-1)` (n consecutive failures)
4
+ // and the routine switches itself off at `pauseAfterFailures`; a home thread
5
+ // holds at most `homeQueueLimit` routine turns waiting (and as many queued
6
+ // rows posted from outside its turns). Runs themselves are
7
+ // unlimited: no concurrency cap and no deadline.
8
+ export const ROUTINE_LIMITS = Object.freeze({
9
+ backoffBaseMs: 60_000,
10
+ pauseAfterFailures: 5,
11
+ homeQueueLimit: 200,
12
+ });
13
+
14
+ /** How long (ms) an unattended fire still waits after the routine's
15
+ * `consecutiveFailures`, the latest at `lastFailedAt`; 0 when it may run. */
16
+ export function routineBackoffRemaining(state, now = Date.now(), limits = ROUTINE_LIMITS) {
17
+ const failures = Number(state?.consecutiveFailures) || 0;
18
+ const at = state?.lastFailedAt ? new Date(state.lastFailedAt).getTime() : Number.NaN;
19
+ if (failures < 1 || !Number.isFinite(at)) return 0;
20
+ return Math.max(0, at + limits.backoffBaseMs * 2 ** (failures - 1) - now);
21
+ }
@@ -0,0 +1,10 @@
1
+ export interface RoutineTrigger {
2
+ kind: 'schedule' | 'webhook';
3
+ name: string;
4
+ event?: string;
5
+ }
6
+
7
+ export declare function routineTriggerHeader(input: { kind: string; name: string; project?: string }): string;
8
+ export declare function stripRoutineTriggerHeader(text: unknown): string;
9
+ export declare function normalizeRoutineTrigger(value: unknown): RoutineTrigger | null;
10
+ export declare function routineTriggerField(value: unknown): { routineTrigger?: RoutineTrigger };
@@ -0,0 +1,44 @@
1
+ // A routine fire delivered into its bot's home thread as a turn
2
+ // (session-runtime/routine-home-turn.mjs): the model-visible header line that
3
+ // opens the turn's prompt, and the `routineTrigger` marker its user row
4
+ // carries (live, persisted in meta.transcript, restored) so a chat paints it
5
+ // as a routine marker instead of a user bubble.
6
+
7
+ const HEADER_PREFIX = '[Routine trigger]';
8
+ const KINDS = new Set(['schedule', 'webhook']);
9
+
10
+ /** The first line of a routine turn's prompt: which routine fired and how to
11
+ * treat the turn. Only the turn's content carries it, never the system
12
+ * prompt, so the thread's prompt cache is untouched. */
13
+ export function routineTriggerHeader({ kind, name, project = '' }) {
14
+ const what = kind === 'webhook' ? 'webhook' : 'schedule';
15
+ return [
16
+ `${HEADER_PREFIX} Your ${what} "${String(name)}" fired.`,
17
+ 'Carry out its instructions below now, yourself or by delegating; nobody is watching this turn, so approvals wait for the user.',
18
+ 'Reply in this chat with the result.',
19
+ ...(project ? [`Project folder: ${project}`] : []),
20
+ ].join(' ');
21
+ }
22
+
23
+ /** `text` without the header line a routine turn opens with. */
24
+ export function stripRoutineTriggerHeader(text) {
25
+ const value = String(text ?? '');
26
+ if (!value.startsWith(HEADER_PREFIX)) return value;
27
+ const end = value.indexOf('\n');
28
+ return end < 0 ? '' : value.slice(end + 1).replace(/^\n+/, '');
29
+ }
30
+
31
+ /** A well-formed `{ kind, name, event? }` marker, or null. */
32
+ export function normalizeRoutineTrigger(value) {
33
+ if (!value || typeof value !== 'object') return null;
34
+ const name = typeof value.name === 'string' ? value.name.trim().slice(0, 200) : '';
35
+ if (!name || !KINDS.has(value.kind)) return null;
36
+ const event = typeof value.event === 'string' ? value.event.trim().slice(0, 200) : '';
37
+ return { kind: value.kind, name, ...(event ? { event } : {}) };
38
+ }
39
+
40
+ /** The `routineTrigger` field of a transcript row (absent when none). */
41
+ export function routineTriggerField(value) {
42
+ const trigger = normalizeRoutineTrigger(value?.routineTrigger);
43
+ return trigger ? { routineTrigger: trigger } : {};
44
+ }
@@ -1,13 +1,12 @@
1
- // schedule.model wire format shared by the channels-worker scheduler and the
2
- // engine-side run-now dispatch: either a config.presets id/name (legacy) or a
3
- // direct "provider/model[@effort][+fast][+noauto][?parameter=value]" route string
4
- // written by the desktop schedule editor (`+noauto`: Auto effort off for the
5
- // route; absent means on). Slash-form values become direct route
6
- // objects, which agent-dispatch consumes without a presets lookup.
7
- // The optional route fields an automation run carries, from a preset, a
8
- // maintenance slot or a parsed route ref. One shape so schedule and webhook
9
- // runs cannot drift when a field is added. modelParameters is copied so a
10
- // stored preset can never be mutated through the run.
1
+ // Model route-ref wire format (task sessions, provider model caches): either a
2
+ // config.presets id/name (legacy) or a direct
3
+ // "provider/model[@effort][+fast][+noauto][?parameter=value]" route string
4
+ // (`+noauto`: Auto effort off for the route; absent means on). Slash-form
5
+ // values become direct route objects, which agent-dispatch consumes without a
6
+ // presets lookup.
7
+ // The optional route fields a run carries, from a preset, a maintenance slot
8
+ // or a parsed route ref. modelParameters is copied so a stored preset can
9
+ // never be mutated through the run.
11
10
  export function modelRouteFields(slot = {}) {
12
11
  return {
13
12
  provider: slot.provider,
@@ -0,0 +1,15 @@
1
+ export interface SensitiveScope {
2
+ root: string[];
3
+ /** Subfolders of the root that stay reachable. */
4
+ except: readonly string[];
5
+ /** Whole tree (a registered private root) or Mixdog's secret subset. */
6
+ whole: boolean;
7
+ }
8
+ export function sensitivePathSegments(path: string): string[];
9
+ export function matchesSensitivePath(
10
+ path: string,
11
+ within?: readonly SensitiveScope[],
12
+ options?: { envTemplates?: boolean }
13
+ ): boolean;
14
+ export function mixdogSensitiveScopes(extraData?: readonly string[]): SensitiveScope[];
15
+ export function isSensitiveLocalPath(path: string, options?: { extraData?: readonly string[] }): boolean;
@@ -0,0 +1,256 @@
1
+ // The credential / secret file deny list, shared by every lane that hands a
2
+ // local file to someone else: the desktop's remote (paired-device) file lanes
3
+ // (apps/desktop/src/main/remote-sensitive-paths.ts adds its registered private
4
+ // roots and async canonicalization) and the runtime's attachment hand-offs
5
+ // between bots and agents (session-runtime/handoff-attachments.mjs).
6
+ //
7
+ // Matching is on SEGMENTS of a path, lower-cased on every platform, so a
8
+ // denied name cannot hide behind letter case, trailing dots/spaces or an NTFS
9
+ // stream suffix. Callers pass the canonical (realpath) form as well as the
10
+ // lexical one, so a link cannot reach a denied place under an innocent name.
11
+ import { realpathSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { dirname, join, resolve } from 'node:path';
14
+
15
+ /** A directory with this name anywhere on the path holds credentials. `.git`
16
+ * is one of them: its config carries remotes with embedded tokens and
17
+ * credential helpers, and its objects hold every secret ever committed. */
18
+ const SECRET_DIRECTORIES = new Set([
19
+ '.ssh',
20
+ '.gnupg',
21
+ '.aws',
22
+ '.azure',
23
+ '.kube',
24
+ '.mozilla',
25
+ '.password-store',
26
+ '.git',
27
+ ]);
28
+
29
+ /** Consecutive segments anywhere on the path that name a credential store:
30
+ * CLI configs, browser profiles and OS credential stores. */
31
+ const SECRET_SEQUENCES = [
32
+ // Tool credentials in the user's home.
33
+ ['.docker', 'config.json'],
34
+ ['.config', 'gcloud'],
35
+ ['appdata', 'roaming', 'gcloud'],
36
+ ['.config', 'gh'],
37
+ ['appdata', 'roaming', 'github cli'],
38
+ ['.config', 'git', 'credentials'],
39
+ ['.cargo', 'credentials'],
40
+ ['.cargo', 'credentials.toml'],
41
+ ['.terraform.d', 'credentials.tfrc.json'],
42
+ ['.claude', '.credentials.json'],
43
+ ['.codex', 'auth.json'],
44
+ ['.gemini', 'oauth_creds.json'],
45
+ // Browser profiles: cookies, saved logins, session tokens.
46
+ ['google', 'chrome', 'user data'],
47
+ ['microsoft', 'edge', 'user data'],
48
+ ['bravesoftware', 'brave-browser'],
49
+ ['chromium', 'user data'],
50
+ ['vivaldi', 'user data'],
51
+ ['opera software'],
52
+ ['mozilla', 'firefox'],
53
+ ['application support', 'google', 'chrome'],
54
+ ['application support', 'microsoft edge'],
55
+ ['application support', 'firefox'],
56
+ ['application support', 'chromium'],
57
+ ['application support', 'bravesoftware'],
58
+ ['application support', 'vivaldi'],
59
+ ['library', 'safari'],
60
+ ['library', 'cookies'],
61
+ ['.config', 'google-chrome'],
62
+ ['.config', 'chromium'],
63
+ ['.config', 'microsoft-edge'],
64
+ ['.config', 'bravesoftware'],
65
+ ['.config', 'vivaldi'],
66
+ ['snap', 'firefox'],
67
+ ['snap', 'chromium'],
68
+ ['.var', 'app', 'org.mozilla.firefox'],
69
+ ['.var', 'app', 'com.google.chrome'],
70
+ // OS credential stores.
71
+ ['microsoft', 'credentials'],
72
+ ['microsoft', 'protect'],
73
+ ['microsoft', 'vault'],
74
+ ['windows', 'system32', 'config'],
75
+ ['library', 'keychains'],
76
+ ['.local', 'share', 'keyrings'],
77
+ ['.local', 'share', 'kwalletd'],
78
+ ['etc', 'shadow'],
79
+ ['etc', 'gshadow'],
80
+ ['etc', 'ssh'],
81
+ ['etc', 'ssl', 'private'],
82
+ ];
83
+
84
+ const KEY_EXTENSIONS = new Set([
85
+ '.pem',
86
+ '.key',
87
+ '.p12',
88
+ '.pfx',
89
+ '.p8',
90
+ '.ppk',
91
+ '.jks',
92
+ '.keystore',
93
+ '.kdbx',
94
+ '.keychain',
95
+ '.keychain-db',
96
+ ]);
97
+ const CREDENTIAL_FILES = new Set([
98
+ '.netrc',
99
+ '_netrc',
100
+ '.npmrc',
101
+ '.pypirc',
102
+ '.git-credentials',
103
+ '.pgpass',
104
+ '.vault-token',
105
+ '.htpasswd',
106
+ '.dockercfg',
107
+ '.envrc',
108
+ '.s3cfg',
109
+ 'kubeconfig',
110
+ ]);
111
+ /** Environment files: every `.env*` name (`.env`, `.env.local`, `.envrc`,
112
+ * `.environment`, and the `.env.example` templates too — a template is
113
+ * checked in often enough with real values that no name is exempt) and
114
+ * `<stage>.env`. */
115
+ const ENV_FILE = /^(?:\.env.*|[^.]{1,64}(?:\.[^.]{1,64}){0,3}\.env)$/;
116
+ /** The dotenv templates a person may still open on purpose from a paired
117
+ * device (the desktop's remote file lanes, `envTemplates: true`); automatic
118
+ * transfers between bots and agents never get this allowance. */
119
+ const ENV_TEMPLATES = new Set(['.env.example', '.env.sample', '.env.template', '.env.dist']);
120
+ /** Credential JSON a cloud or OAuth client writes or downloads:
121
+ * `credentials.json`, `*credentials*.json`, `service-account*.json`,
122
+ * `client_secret*.json`. */
123
+ const CREDENTIAL_JSON = /^(?:.*credential.*|service[-_ ]?account.*|client[-_]?secret.*)\.json$/;
124
+ /** OpenSSH private keys (`id_ed25519`, `id_rsa_work`, `id_ecdsa_sk`, any
125
+ * `id_<type>`): no extension, so the public `.pub` half and documents named
126
+ * after a key type (`id_rsa_notes.md`) are not keys. */
127
+ const SSH_PRIVATE_KEY = /^id_[a-z0-9]{2,16}(?:[_-][^._-]{1,32}){0,3}$/;
128
+ /** A copy an editor or tool leaves beside a file (`x.bak`, `x.old`, `x~`,
129
+ * `x.1`): it holds what the original holds. */
130
+ const BACKUP_SUFFIX = /(?:\.(?:bak|backup|old|orig|prev|save|sav|swp|swo|tmp|copy|\d{1,3})|~)$/;
131
+ const MAX_BACKUP_SUFFIXES = 3;
132
+
133
+ /** The name a backup copy was made of (`.npmrc.bak.1` → `.npmrc`). */
134
+ function backupOriginal(name) {
135
+ let original = name;
136
+ for (let strip = 0; strip < MAX_BACKUP_SUFFIXES; strip += 1) {
137
+ const next = original.replace(BACKUP_SUFFIX, '');
138
+ if (next === original || !next) break;
139
+ original = next;
140
+ }
141
+ return original;
142
+ }
143
+
144
+ /** A file whose own name says it holds a secret, wherever it lives: a
145
+ * credential file, a dotenv file, a key or keystore, or a backup of one. */
146
+ function secretFileName(name, { envTemplates = false } = {}) {
147
+ const original = backupOriginal(name);
148
+ if (CREDENTIAL_FILES.has(original) || CREDENTIAL_JSON.test(original)) return true;
149
+ if (ENV_FILE.test(original)) return !(envTemplates && ENV_TEMPLATES.has(original));
150
+ const dot = original.lastIndexOf('.');
151
+ if (dot >= 0 && KEY_EXTENSIONS.has(original.slice(dot))) return true;
152
+ return SSH_PRIVATE_KEY.test(original);
153
+ }
154
+
155
+ // Mixdog's own state. Its home and data folders also hold what tools write
156
+ // (tool results, shell output, media), which stay reachable; only the files
157
+ // and folders that carry provider tokens, API keys, pairing and bridge secrets
158
+ // are denied.
159
+ const MIXDOG_SECRET_DIRECTORIES = new Set([
160
+ 'secrets',
161
+ 'mcp-oauth',
162
+ 'provider-accounts',
163
+ 'telegram',
164
+ 'relay-data',
165
+ 'dev-profile',
166
+ ]);
167
+ const MIXDOG_SECRET_FILE =
168
+ /^\.?(mixdog-config|provider-accounts|relay-hook-device|auth\.json|[a-z0-9-]+-oauth(-credentials)?\.json|[a-z0-9-]+-bridge\.json)/;
169
+ const MIXDOG_SECRET_EXTENSIONS = new Set(['.dpapi', '.token']);
170
+
171
+ function mixdogSecret(rest) {
172
+ if (rest.some((segment) => MIXDOG_SECRET_DIRECTORIES.has(segment))) return true;
173
+ const name = rest[rest.length - 1] ?? '';
174
+ const dot = name.lastIndexOf('.');
175
+ return MIXDOG_SECRET_FILE.test(name) || MIXDOG_SECRET_EXTENSIONS.has(dot >= 0 ? name.slice(dot) : '');
176
+ }
177
+
178
+ /** The segments of a path as the rules compare them: no extended-path prefix,
179
+ * no NTFS stream suffix, no trailing dots or spaces, lower case. */
180
+ export function sensitivePathSegments(path) {
181
+ return String(path)
182
+ .replace(/^[\\/]{2}[?.][\\/]/, '')
183
+ .split(/[\\/]+/)
184
+ .map((segment) =>
185
+ segment
186
+ .split(':')[0]
187
+ .replace(/[. ]+$/, '')
188
+ .toLowerCase()
189
+ )
190
+ .filter(Boolean);
191
+ }
192
+
193
+ function hasRun(segments, run) {
194
+ for (let start = 0; start + run.length <= segments.length; start += 1) {
195
+ if (run.every((segment, offset) => segments[start + offset] === segment)) return true;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ /**
201
+ * Whether `path` is denied under `within`: scopes `{ root: segments, except:
202
+ * subfolder names, whole }` — Mixdog's home and data folders (`whole: false`,
203
+ * only their secret subset is denied) and registered private roots (`whole:
204
+ * true`, everything but `except`). Pass both the lexical and canonical form.
205
+ * `envTemplates`: a user-initiated remote open may read the dotenv
206
+ * templates; nothing else gets that allowance.
207
+ */
208
+ export function matchesSensitivePath(path, within = [], { envTemplates = false } = {}) {
209
+ const segments = sensitivePathSegments(resolve(path));
210
+ const name = segments[segments.length - 1] ?? '';
211
+ if (segments.some((segment) => SECRET_DIRECTORIES.has(segment))) return true;
212
+ if (secretFileName(name, { envTemplates }) || SECRET_SEQUENCES.some((run) => hasRun(segments, run))) return true;
213
+ const containing = within.filter(
214
+ ({ root }) => root.length > 0 && root.every((segment, index) => segments[index] === segment)
215
+ );
216
+ // Mixdog's home and data folders follow Mixdog's own rule even inside a
217
+ // registered private root: a dev or isolated profile keeps them in the app
218
+ // profile, and the tool output and media there stay reachable.
219
+ const own = containing.filter(({ whole }) => !whole);
220
+ if (own.length) return own.some(({ root }) => mixdogSecret(segments.slice(root.length)));
221
+ return containing.some(({ root, except }) => {
222
+ const rest = segments.slice(root.length);
223
+ return rest.length > 0 && !except.includes(rest[0]);
224
+ });
225
+ }
226
+
227
+ function realOrSelf(path) {
228
+ try {
229
+ return realpathSync.native(path);
230
+ } catch {
231
+ const parent = dirname(path);
232
+ return parent === path ? path : join(realOrSelf(parent), path.slice(parent.length).replace(/^[\\/]+/, ''));
233
+ }
234
+ }
235
+
236
+ /** Mixdog's home and data folders (MIXDOG_HOME / MIXDOG_DATA_DIR, plus any
237
+ * extra data folder the caller names) as `matchesSensitivePath` scopes. */
238
+ export function mixdogSensitiveScopes(extraData = []) {
239
+ const home = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
240
+ const roots = [home, process.env.MIXDOG_DATA_DIR || join(home, 'data'), ...extraData].filter(Boolean);
241
+ const scopes = [];
242
+ for (const root of roots) {
243
+ for (const form of new Set([resolve(root), realOrSelf(resolve(root))])) {
244
+ scopes.push({ root: sensitivePathSegments(form), except: [], whole: false });
245
+ }
246
+ }
247
+ return scopes;
248
+ }
249
+
250
+ /** Whether `path` — its own name or what its links lead to — is a
251
+ * credential/secret file under the shared deny list and Mixdog's own scopes. */
252
+ export function isSensitiveLocalPath(path, { extraData = [] } = {}) {
253
+ const within = mixdogSensitiveScopes(extraData);
254
+ const absolute = resolve(path);
255
+ return matchesSensitivePath(absolute, within) || matchesSensitivePath(realOrSelf(absolute), within);
256
+ }
@@ -3,8 +3,8 @@
3
3
  // monotonic clock the owner heartbeat uses (performance.now), completed spans
4
4
  // over `minMs` land in a bounded ring, and spans over `slowMs` are logged as
5
5
  // `[slow-op] <label> <ms>`. When the event loop stalls, `describe(from, to)`
6
- // names the spans that overlapped the stall window. Spans carry labels, phase
7
- // durations and sizes only — never payloads.
6
+ // names the spans that overlapped the stall window. Spans carry labels and
7
+ // phase durations only — never payloads.
8
8
 
9
9
  import { AsyncLocalStorage } from 'node:async_hooks';
10
10
  import { performance } from 'node:perf_hooks';
@@ -36,18 +36,11 @@ export function markPhase(name) {
36
36
  span.mark = t;
37
37
  }
38
38
 
39
- /** Records a size (count/bytes) on the caller's span. */
40
- export function noteSpanSize(size) {
41
- const span = current.getStore();
42
- if (span && Number.isFinite(size)) span.size = size;
43
- }
44
-
45
39
  const round = (ms) => Math.round(ms);
46
40
 
47
41
  function formatSpan(span) {
48
42
  const phases = span.phases.map(([name, ms]) => `${name}=${round(ms)}`).join(' ');
49
- const size = span.size === undefined ? '' : ` size=${span.size}`;
50
- return `${span.label} ${round(span.ms)}ms${phases ? ` ${phases}` : ''}${size}`;
43
+ return `${span.label} ${round(span.ms)}ms${phases ? ` ${phases}` : ''}`;
51
44
  }
52
45
 
53
46
  export function createSlowSpans({
@@ -70,7 +63,6 @@ export function createSlowSpans({
70
63
  end: span.end,
71
64
  ms: span.ms,
72
65
  phases: span.phases,
73
- size: span.size,
74
66
  });
75
67
  if (ring.length > capacity) ring.shift();
76
68
  }
@@ -90,7 +82,7 @@ export function createSlowSpans({
90
82
  /** Runs `fn` inside a span; markPhase/annotateSpan inside it attach to this span. */
91
83
  async trace(label, fn) {
92
84
  const start = now();
93
- const span = { label: String(label), start, mark: start, now, phases: [], size: undefined };
85
+ const span = { label: String(label), start, mark: start, now, phases: [] };
94
86
  try {
95
87
  return await current.run(span, fn);
96
88
  } finally {
@@ -200,7 +200,8 @@ async function refreshShellJobsStatus(ownerPid) {
200
200
  // its owner retires the record. Its root pid may be gone while a
201
201
  // descendant lives, so the owner's liveness decides, not the root's.
202
202
  const unconfirmed = detail?.terminationUnconfirmed === true;
203
- if (unconfirmed ? !isPidAlive(ownerByJob.get(id) || 0) : !(await isShellJobAlive(detail, p, dir, id))) continue;
203
+ if (unconfirmed ? !isHeldShellJobAlive(detail, ownerByJob.get(id)) : !(await isShellJobAlive(detail, p, dir, id)))
204
+ continue;
204
205
  // A foreground command is awaited in its own tool card; listing it here
205
206
  // made every short command flash in and out of the shell list. Only
206
207
  // backgrounded jobs (including promoted ones) are background work.
@@ -326,6 +327,17 @@ function summarizeShellJobs({ count, oldestMs, jobs, bySession }) {
326
327
  return { count, elapsedLabel, jobs, sessions };
327
328
  }
328
329
 
330
+ // A held record lives while its owner process does. Once its owner session
331
+ // has closed, it also needs a process left to hold: the root or a survivor
332
+ // its termination watch last reported. Without one it is an orphan whose
333
+ // processes are gone, and it resolves instead of staying unconfirmed.
334
+ function isHeldShellJobAlive(detail, ownerPid) {
335
+ if (!isPidAlive(ownerPid || 0)) return false;
336
+ if (detail?.ownerSessionClosed !== true) return true;
337
+ const survivors = Array.isArray(detail?.survivorPids) ? detail.survivorPids : [];
338
+ return [detail?.pid, ...survivors].some((pid) => isPidAlive(positiveInt(pid)));
339
+ }
340
+
329
341
  async function isShellJobAlive(detail, detailPath, dir, id) {
330
342
  const pid = positiveInt(detail?.pid);
331
343
  if (!pid) return false;
@@ -2,6 +2,7 @@
2
2
  // These pure helpers keep live rows, persisted messages and restored rows on
3
3
  // the same field contract without rewriting older display-only transcripts.
4
4
  import { displayModelName } from './model-display.mjs';
5
+ import { routineTriggerField } from './routine-trigger.mjs';
5
6
 
6
7
  export function transcriptRouteMetadataFields(value) {
7
8
  if (!value || typeof value !== 'object') return {};
@@ -76,10 +77,19 @@ export function persistedUserTranscriptMetadata(value) {
76
77
  // The turn's change-record key (the outer prompt id), so a restored user
77
78
  // row still addresses its turn's Undo/Reapply.
78
79
  ...(typeof value.checkpointId === 'string' && value.checkpointId ? { checkpointId: value.checkpointId } : {}),
80
+ // Every submission a merged queued batch delivered as this turn.
81
+ ...submissionIdsField(value.submissionIds),
79
82
  ...attachmentsField(value.attachments),
83
+ // A routine fire delivered as this turn (routine-trigger.mjs).
84
+ ...routineTriggerField(value),
80
85
  };
81
86
  }
82
87
 
88
+ function submissionIdsField(value) {
89
+ const ids = Array.isArray(value) ? value.filter((id) => typeof id === 'string' && id) : [];
90
+ return ids.length ? { submissionIds: ids } : {};
91
+ }
92
+
83
93
  function attachmentsField(value) {
84
94
  const attachments = normalizeMessageAttachments(value);
85
95
  return attachments.length ? { attachments } : {};
@@ -27,7 +27,6 @@ import {
27
27
  export const TURN_CHANGE_FILE_PATCH_MAX_BYTES = 8 * 1024 * 1024;
28
28
  const APPLY_TIMEOUT_MS = 60_000;
29
29
  const APPLY_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
30
- const STATUSES = new Set(['A', 'M', 'D', 'R']);
31
30
  // Characters a patch header cannot carry unquoted; such a path is recorded but
32
31
  // never replayed.
33
32
  const UNSAFE_PATH_CHARACTERS = /[\t\n\r"\0]/;
@@ -37,13 +36,6 @@ export function changePathKey(value) {
37
36
  return process.platform === 'win32' ? text.toLowerCase() : text;
38
37
  }
39
38
 
40
- /** git status letter → record status (copies create a new file). */
41
- export function normalizeChangeStatus(code) {
42
- const letter = String(code || 'M').charAt(0);
43
- if (STATUSES.has(letter)) return letter;
44
- return letter === 'C' ? 'A' : 'M';
45
- }
46
-
47
39
  /** A root-relative forward-slash path that a patch may name, or ''. */
48
40
  function safeChangePath(value) {
49
41
  const text = clean(value).replace(/\\/g, '/');