synabun 2.0.9 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +25 -2
- package/THIRD-PARTY-LICENSES.md +68 -4
- package/bin/synabun.js +71 -24
- package/docs/whatsapp.md +2 -2
- package/hooks/claude-code/pre-websearch.mjs +12 -1
- package/hooks/claude-code/shared.mjs +5 -0
- package/hooks/claude-code/stop.mjs +1 -1
- package/launcher.mjs +9 -3
- package/lib/claude-hooks.js +172 -18
- package/lib/client-config-repair.js +17 -8
- package/lib/desktop-app.js +7 -1
- package/lib/external-tools.js +1 -1
- package/lib/first-launch-protection.js +57 -12
- package/lib/mcp-dist.js +176 -0
- package/lib/release-npm-ci.mjs +138 -0
- package/lib/release-publish.mjs +91 -0
- package/lib/release-scan.mjs +50 -0
- package/lib/server-crash-record.js +123 -0
- package/lib/server-exit.js +318 -0
- package/lib/server-supervisor.js +508 -0
- package/lib/start-launcher.js +43 -5
- package/lib/tools-cli.js +45 -0
- package/lib/update-snapshot.js +228 -0
- package/lib/virustotal.mjs +192 -0
- package/lib/windows-crash-event.js +269 -0
- package/mcp-server/dist/http.d.ts +20 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +59 -12
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts +33 -27
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +128 -143
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/preload.js +1 -1
- package/mcp-server/dist/preload.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +81 -28
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +16 -10
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +67 -11
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts +7 -2
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +20 -62
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
- package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-catalog.js +339 -0
- package/mcp-server/dist/services/tool-catalog.js.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
- package/mcp-server/dist/services/tool-list.d.ts +108 -0
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list.js +334 -0
- package/mcp-server/dist/services/tool-list.js.map +1 -0
- package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-runtime.js +400 -0
- package/mcp-server/dist/services/tool-runtime.js.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -10
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +22 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill-tools.js +639 -0
- package/mcp-server/dist/tools/skill-tools.js.map +1 -0
- package/mcp-server/dist/tools/skill.d.ts +7 -0
- package/mcp-server/dist/tools/skill.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill.js +11 -0
- package/mcp-server/dist/tools/skill.js.map +1 -0
- package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
- package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/tool-groups.js +147 -0
- package/mcp-server/dist/tools/tool-groups.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-format.js +7 -4
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/http.ts +75 -12
- package/mcp-server/src/index.ts +147 -154
- package/mcp-server/src/preload.ts +12 -12
- package/mcp-server/src/services/codex-config-heal.ts +80 -29
- package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
- package/mcp-server/src/services/neural-interface.ts +79 -20
- package/mcp-server/src/services/server-instructions.ts +27 -63
- package/mcp-server/src/services/tool-catalog.ts +403 -0
- package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
- package/mcp-server/src/services/tool-list.ts +410 -0
- package/mcp-server/src/services/tool-runtime.ts +457 -0
- package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
- package/mcp-server/src/tools/agents.ts +22 -14
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/skill-tools.ts +589 -0
- package/mcp-server/src/tools/skill.ts +13 -0
- package/mcp-server/src/tools/tool-groups.ts +164 -0
- package/mcp-server/src/tools/whiteboard-format.ts +9 -3
- package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
- package/neural-interface/i18n/en.json +3680 -206
- package/neural-interface/i18n/pt-BR.json +3628 -157
- package/neural-interface/lib/account-usage.js +5 -3
- package/neural-interface/lib/app-controls-api.js +161 -0
- package/neural-interface/lib/assistant-api.js +148 -35
- package/neural-interface/lib/assistant-brains/claude.js +4 -3
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
- package/neural-interface/lib/assistant-brains/codex.js +9 -5
- package/neural-interface/lib/assistant-brains/opencode.js +122 -44
- package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +59 -0
- package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +97 -0
- package/neural-interface/lib/assistant-catalog.js +23 -9
- package/neural-interface/lib/assistant-dispatch.js +874 -156
- package/neural-interface/lib/assistant-envelope.js +22 -2
- package/neural-interface/lib/assistant-persona.js +80 -11
- package/neural-interface/lib/assistant-plan-permissions.js +10 -9
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
- package/neural-interface/lib/assistant-route-gate.js +6 -4
- package/neural-interface/lib/assistant-router.js +13 -8
- package/neural-interface/lib/assistant-runtime.js +183 -26
- package/neural-interface/lib/assistant-task-prompt.js +78 -14
- package/neural-interface/lib/assistant-tools.js +51 -0
- package/neural-interface/lib/backup-service.js +620 -3
- package/neural-interface/lib/backup-zip-reader.js +10 -5
- package/neural-interface/lib/backup-zip-writer.js +257 -0
- package/neural-interface/lib/browser-execution.js +10 -2
- package/neural-interface/lib/browser-guard-cdp.js +83 -0
- package/neural-interface/lib/browser-network-guard.js +284 -0
- package/neural-interface/lib/browser-policy-roots.js +64 -0
- package/neural-interface/lib/browser-tool-policy.js +186 -31
- package/neural-interface/lib/claude-agent-bridge.js +4 -2
- package/neural-interface/lib/claude-hooks-status.js +103 -0
- package/neural-interface/lib/claude-model-catalog.js +64 -14
- package/neural-interface/lib/claude-temporary.js +5 -11
- package/neural-interface/lib/cli-install.js +18 -138
- package/neural-interface/lib/cli-resolver.js +4 -1
- package/neural-interface/lib/cli-update-planner.js +32 -31
- package/neural-interface/lib/cloudflared/tunnel.js +42 -2
- package/neural-interface/lib/codex-mcp-approvals.js +11 -0
- package/neural-interface/lib/codex-settings-schema.js +109 -47
- package/neural-interface/lib/codex-worker-app-server.js +172 -0
- package/neural-interface/lib/confined-fs.js +30 -3
- package/neural-interface/lib/crash-notice-api.js +98 -0
- package/neural-interface/lib/editor-completion-opencode.js +164 -67
- package/neural-interface/lib/exec-loop-command.js +4 -1
- package/neural-interface/lib/http-guards.js +8 -2
- package/neural-interface/lib/install-details.js +4 -1
- package/neural-interface/lib/launch-cache.js +293 -0
- package/neural-interface/lib/login-start.js +524 -0
- package/neural-interface/lib/mcp-agent-servers.js +67 -0
- package/neural-interface/lib/mcp-client-config.js +0 -29
- package/neural-interface/lib/mcp-external-servers.js +323 -0
- package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
- package/neural-interface/lib/morelogin.js +30 -0
- package/neural-interface/lib/native-loop-providers.js +565 -50
- package/neural-interface/lib/native-loop-runtime.js +287 -41
- package/neural-interface/lib/opencode-config.js +557 -0
- package/neural-interface/lib/opencode-db-schema.js +58 -0
- package/neural-interface/lib/opencode-history.js +49 -204
- package/neural-interface/lib/opencode-mcp-ready.js +108 -0
- package/neural-interface/lib/opencode-native-boundary.js +44 -0
- package/neural-interface/lib/opencode-protocol.js +310 -0
- package/neural-interface/lib/opencode-provider-view.js +81 -0
- package/neural-interface/lib/opencode-runtime-config.js +32 -15
- package/neural-interface/lib/opencode-serve-records.js +624 -0
- package/neural-interface/lib/opencode-v2-client.js +53 -131
- package/neural-interface/lib/opencode-v2-ws-requests.js +35 -195
- package/neural-interface/lib/opencode-worker-activity.js +229 -0
- package/neural-interface/lib/opencode-worker-data.js +27 -0
- package/neural-interface/lib/opencode2-client.js +796 -0
- package/neural-interface/lib/opencode2-proxy.js +76 -0
- package/neural-interface/lib/opencode2-translate.js +1057 -0
- package/neural-interface/lib/owner-authority.js +249 -0
- package/neural-interface/lib/page-session.js +275 -0
- package/neural-interface/lib/peer-identity.js +763 -0
- package/neural-interface/lib/process-identity.js +361 -0
- package/neural-interface/lib/request-gate.js +17 -2
- package/neural-interface/lib/rulesets/api.js +1 -1
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +92 -27
- package/neural-interface/lib/rulesets/json-edit.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +18 -10
- package/neural-interface/lib/session-title-generator.js +58 -18
- package/neural-interface/lib/skills/api-v2.js +495 -0
- package/neural-interface/lib/skills/bundle.js +418 -0
- package/neural-interface/lib/skills/catalog.js +294 -0
- package/neural-interface/lib/skills/creation-api.js +191 -0
- package/neural-interface/lib/skills/creation-brief.js +247 -0
- package/neural-interface/lib/skills/creation-sources.js +267 -0
- package/neural-interface/lib/skills/creation.js +843 -0
- package/neural-interface/lib/skills/deploy.js +564 -0
- package/neural-interface/lib/skills/digest.js +119 -0
- package/neural-interface/lib/skills/discover.js +111 -0
- package/neural-interface/lib/skills/draft-confinement.js +214 -0
- package/neural-interface/lib/skills/eval-confinement.js +397 -0
- package/neural-interface/lib/skills/eval-sandbox.js +280 -0
- package/neural-interface/lib/skills/eval-trace.js +119 -0
- package/neural-interface/lib/skills/evals-api.js +212 -0
- package/neural-interface/lib/skills/evals.js +1573 -0
- package/neural-interface/lib/skills/export.js +200 -0
- package/neural-interface/lib/skills/frontmatter.js +64 -0
- package/neural-interface/lib/skills/health.js +75 -0
- package/neural-interface/lib/skills/json-edit.js +165 -0
- package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
- package/neural-interface/lib/skills/known-bridges.js +96 -0
- package/neural-interface/lib/skills/library.js +823 -0
- package/neural-interface/lib/skills/local-store.js +21 -0
- package/neural-interface/lib/skills/migration.js +433 -0
- package/neural-interface/lib/skills/operations.js +45 -0
- package/neural-interface/lib/skills/ownership.js +427 -0
- package/neural-interface/lib/skills/packages.js +88 -0
- package/neural-interface/lib/skills/reviews.js +212 -0
- package/neural-interface/lib/skills/revision.js +80 -0
- package/neural-interface/lib/skills/roots.js +262 -0
- package/neural-interface/lib/skills/runtime-checks.js +355 -0
- package/neural-interface/lib/skills/scan-assets.js +252 -0
- package/neural-interface/lib/skills/scan-context.js +26 -0
- package/neural-interface/lib/skills/scan-shell.js +324 -0
- package/neural-interface/lib/skills/scan.js +364 -0
- package/neural-interface/lib/skills/scanners.js +54 -0
- package/neural-interface/lib/skills/service.js +522 -0
- package/neural-interface/lib/skills/telemetry.js +113 -0
- package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
- package/neural-interface/lib/style-guide/api.js +4 -3
- package/neural-interface/lib/tool-lists.js +124 -0
- package/neural-interface/lib/untrusted-file-headers.js +27 -0
- package/neural-interface/lib/whatsapp/bridge.js +6 -3
- package/neural-interface/lib/worker-confinement.js +1192 -0
- package/neural-interface/lib/worker-guard-setting.js +56 -0
- package/neural-interface/lib/worker-mcp-channel.js +175 -0
- package/neural-interface/lib/worker-relay-ports.js +227 -0
- package/neural-interface/lib/worker-resume.js +229 -0
- package/neural-interface/lib/worker-sandbox.js +299 -0
- package/neural-interface/package-lock.json +1354 -50
- package/neural-interface/package.json +6 -2
- package/neural-interface/public/index.html +5 -3
- package/neural-interface/public/index2d.html +19 -11
- package/neural-interface/public/onboarding.html +2 -2
- package/neural-interface/public/shared/account-menu.js +8 -5
- package/neural-interface/public/shared/agent-runtime-options.js +76 -16
- package/neural-interface/public/shared/api.js +1527 -1336
- package/neural-interface/public/shared/app-frame.js +192 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +205 -19
- package/neural-interface/public/shared/assistant/asst-api.js +11 -3
- package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
- package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
- package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +241 -112
- package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
- package/neural-interface/public/shared/assistant/asst-control.js +157 -26
- package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
- package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
- package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
- package/neural-interface/public/shared/assistant/asst-menu.js +84 -12
- package/neural-interface/public/shared/assistant/asst-panel.js +303 -44
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +93 -58
- package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
- package/neural-interface/public/shared/assistant/asst-render.js +770 -100
- package/neural-interface/public/shared/assistant/asst-route.js +83 -12
- package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
- package/neural-interface/public/shared/assistant/asst-scenes.js +530 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
- package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
- package/neural-interface/public/shared/assistant/asst-state.js +25 -15
- package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +102 -21
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +165 -13
- package/neural-interface/public/shared/automation-context.js +5 -2
- package/neural-interface/public/shared/cc-dropdown.js +550 -0
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
- package/neural-interface/public/shared/cdx/cdx-panel.js +58 -150
- package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
- package/neural-interface/public/shared/cdx/cdx-render.js +62 -32
- package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
- package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
- package/neural-interface/public/shared/cdx/cdx-tabs.js +201 -74
- package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
- package/neural-interface/public/shared/cli-install-view.js +4 -8
- package/neural-interface/public/shared/cli-status.js +2 -2
- package/neural-interface/public/shared/constants.js +2 -2
- package/neural-interface/public/shared/context-menu-areas.js +34 -0
- package/neural-interface/public/shared/context-menu-logic.js +243 -0
- package/neural-interface/public/shared/context-menu-model.js +366 -0
- package/neural-interface/public/shared/context-menu.js +372 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
- package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
- package/neural-interface/public/shared/crash-notice-view.js +168 -0
- package/neural-interface/public/shared/data-cache.js +223 -0
- package/neural-interface/public/shared/desktop-bridge.js +11 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
- package/neural-interface/public/shared/desktop-titlebar.js +214 -28
- package/neural-interface/public/shared/desktop-update-view.js +35 -5
- package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
- package/neural-interface/public/shared/file-links.js +73 -8
- package/neural-interface/public/shared/html-shell.js +78 -86
- package/neural-interface/public/shared/i18n.js +2 -0
- package/neural-interface/public/shared/launch-prefetch.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +9 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +140 -254
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +97 -203
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -22
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +20 -13
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +53 -82
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -40
- package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +205 -62
- package/neural-interface/public/shared/opencode-providers.js +39 -0
- package/neural-interface/public/shared/page-session.js +96 -0
- package/neural-interface/public/shared/registry.js +5 -3
- package/neural-interface/public/shared/retired-2d.js +3 -0
- package/neural-interface/public/shared/schedules-view.js +452 -0
- package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
- package/neural-interface/public/shared/settings/settings-app.js +243 -0
- package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
- package/neural-interface/public/shared/settings/settings-data.js +90 -0
- package/neural-interface/public/shared/settings/settings-ia.js +23 -6
- package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
- package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
- package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
- package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
- package/neural-interface/public/shared/skills/ss-args.js +96 -0
- package/neural-interface/public/shared/skills/ss-creation.js +219 -0
- package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
- package/neural-interface/public/shared/skills/ss-doc.js +173 -0
- package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
- package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
- package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
- package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
- package/neural-interface/public/shared/skills/ss-evals.js +417 -0
- package/neural-interface/public/shared/skills/ss-form.js +159 -0
- package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
- package/neural-interface/public/shared/skills/ss-ids.js +32 -0
- package/neural-interface/public/shared/skills/ss-lint.js +141 -0
- package/neural-interface/public/shared/skills/ss-presets.js +117 -0
- package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
- package/neural-interface/public/shared/skills/ss-resources.js +165 -0
- package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
- package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
- package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
- package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
- package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
- package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
- package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
- package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
- package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
- package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
- package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
- package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
- package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
- package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
- package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
- package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
- package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
- package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
- package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
- package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
- package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
- package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
- package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
- package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
- package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
- package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
- package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
- package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
- package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
- package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
- package/neural-interface/public/shared/styles.css +2065 -2150
- package/neural-interface/public/shared/synabun-mascot.js +1115 -9
- package/neural-interface/public/shared/tool-list-picker.js +55 -0
- package/neural-interface/public/shared/tool-list-storage.js +24 -0
- package/neural-interface/public/shared/tool-list-view.js +112 -0
- package/neural-interface/public/shared/tour/tour-actor.js +268 -0
- package/neural-interface/public/shared/tour/tour-beats.js +336 -0
- package/neural-interface/public/shared/tour/tour-engine.js +884 -0
- package/neural-interface/public/shared/tour/tour-ink.js +390 -0
- package/neural-interface/public/shared/tour/tour-stage.js +440 -0
- package/neural-interface/public/shared/tour/tour-state.js +40 -0
- package/neural-interface/public/shared/tour/tour-tips.js +99 -0
- package/neural-interface/public/shared/tour/tour.css +144 -0
- package/neural-interface/public/shared/ui-assistant.js +27 -5
- package/neural-interface/public/shared/ui-automation-studio.js +155 -152
- package/neural-interface/public/shared/ui-bookmarks.js +2 -1
- package/neural-interface/public/shared/ui-claude-panel.js +153 -13
- package/neural-interface/public/shared/ui-codex-panel.js +1 -0
- package/neural-interface/public/shared/ui-command-runner.js +6 -3
- package/neural-interface/public/shared/ui-context-menu.js +295 -0
- package/neural-interface/public/shared/ui-cost-widget.js +57 -59
- package/neural-interface/public/shared/ui-crash-notice.js +187 -0
- package/neural-interface/public/shared/ui-detail.js +21 -9
- package/neural-interface/public/shared/ui-file-links.js +1 -0
- package/neural-interface/public/shared/ui-image-gallery.js +56 -52
- package/neural-interface/public/shared/ui-invite.js +18 -2
- package/neural-interface/public/shared/ui-keybinds.js +26 -1
- package/neural-interface/public/shared/ui-link.js +9 -9
- package/neural-interface/public/shared/ui-menubar.js +229 -40
- package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +9 -1
- package/neural-interface/public/shared/ui-navbar.js +4 -24
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
- package/neural-interface/public/shared/ui-panels.js +96 -65
- package/neural-interface/public/shared/ui-schedules-studio.js +2267 -953
- package/neural-interface/public/shared/ui-sessions.js +20 -7
- package/neural-interface/public/shared/ui-settings.js +679 -496
- package/neural-interface/public/shared/ui-sidebar.js +4 -4
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +56 -5
- package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
- package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
- package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
- package/neural-interface/public/shared/ui-skills.js +42 -2781
- package/neural-interface/public/shared/ui-styleguide.js +5 -2
- package/neural-interface/public/shared/ui-sync.js +22 -5
- package/neural-interface/public/shared/ui-terminal.js +159 -199
- package/neural-interface/public/shared/ui-tool-updates.js +3 -1
- package/neural-interface/public/shared/ui-tooltip.js +10 -5
- package/neural-interface/public/shared/ui-trash.js +14 -5
- package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
- package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +3 -61
- package/neural-interface/public/shared/ui-tutorial.css +47 -100
- package/neural-interface/public/shared/ui-tutorial.js +285 -499
- package/neural-interface/public/shared/ui-update.js +121 -16
- package/neural-interface/public/shared/ui-whiteboard.js +313 -59
- package/neural-interface/public/shared/ui-worker-end.js +4 -0
- package/neural-interface/public/shared/ui-workspaces.js +3 -2
- package/neural-interface/public/shared/view-variant.js +66 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
- package/neural-interface/public/shared/whiteboard-list.js +172 -0
- package/neural-interface/public/shared/worker-end.js +10 -3
- package/neural-interface/public/shared/worker-restart.js +12 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- package/neural-interface/public/variant/2d/background.js +2 -1
- package/neural-interface/public/variant/2d/context-menu.js +8 -2
- package/neural-interface/public/variant/2d/graph.js +42 -20
- package/neural-interface/public/variant/2d/hulls.js +14 -9
- package/neural-interface/public/variant/2d/lasso.js +11 -7
- package/neural-interface/public/variant/2d/main.js +36 -2
- package/neural-interface/public/variant/2d/minimap.js +11 -13
- package/neural-interface/public/variant/3d/main.js +72 -6
- package/neural-interface/public/variant/3d/map-controls.js +24 -9
- package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat.css +17 -0
- package/neural-interface/public/vendor/caveat/manifest.json +12 -0
- package/neural-interface/public/vendor/highlight/LICENSE +29 -0
- package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
- package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
- package/neural-interface/public/vendor/highlight/manifest.json +5 -0
- package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
- package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
- package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
- package/neural-interface/public/vendor/marked/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
- package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
- package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
- package/neural-interface/public/vendor/yaml/LICENSE +13 -0
- package/neural-interface/public/vendor/yaml/manifest.json +4 -0
- package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
- package/neural-interface/scripts/build-vendor.mjs +116 -3
- package/neural-interface/server.js +4834 -2541
- package/neural-interface/templates/rulesets/core.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
- package/neural-interface/templates/rulesets/manifest.json +8 -8
- package/npm-shrinkwrap.json +18 -12
- package/package.json +16 -3
- package/postinstall.js +9 -4
- package/preuninstall.js +1 -1
- package/setup.js +97 -322
- package/skills/skill-forge/SKILL.md +177 -0
- package/skills/skill-forge/evals/evals.json +54 -0
- package/skills/skill-forge/references/package.md +105 -0
- package/skills/skill-forge/references/review-and-recovery.md +63 -0
- package/skills/skill-forge/references/runtime-profiles.md +20 -0
- package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
- package/skills/synabun/SKILL.md +10 -7
- package/skills/synabun/modules/audit.md +1 -5
- package/skills/synabun/modules/changelog.md +1 -5
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/memorize.md +6 -9
- package/skills/synabun/modules/schedule.md +5 -5
- package/mcp-server/dist/services/profiles.d.ts +0 -120
- package/mcp-server/dist/services/profiles.d.ts.map +0 -1
- package/mcp-server/dist/services/profiles.js +0 -387
- package/mcp-server/dist/services/profiles.js.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
- package/mcp-server/dist/tools/leonardo.d.ts +0 -13
- package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo.js +0 -21
- package/mcp-server/dist/tools/leonardo.js.map +0 -1
- package/mcp-server/dist/tools/profile.d.ts +0 -17
- package/mcp-server/dist/tools/profile.d.ts.map +0 -1
- package/mcp-server/dist/tools/profile.js +0 -114
- package/mcp-server/dist/tools/profile.js.map +0 -1
- package/mcp-server/src/services/profiles.ts +0 -428
- package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
- package/mcp-server/src/tools/leonardo.ts +0 -30
- package/mcp-server/src/tools/profile.ts +0 -122
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
- package/neural-interface/lib/mcp-profile-servers.js +0 -120
- package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
- package/skills/leonardo/SKILL.md +0 -168
- package/skills/leonardo/icon.svg +0 -1
- package/skills/leonardo/modules/browser-guide.md +0 -378
- package/skills/leonardo/modules/image-prompter.md +0 -283
- package/skills/leonardo/modules/model-advisor.md +0 -100
- package/skills/leonardo/modules/prompt-library.md +0 -110
- package/skills/leonardo/modules/style-guide.md +0 -186
- package/skills/leonardo/modules/video-prompter.md +0 -317
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { retainExternalRefs } from './mcp-external-servers.js';
|
|
2
|
+
import { readStoredToolListRecord } from './tool-lists.js';
|
|
1
3
|
// ═══════════════════════════════════════════
|
|
2
4
|
// SynaBun — Assistant dispatcher (task-mode runs on NativeLoopRuntime)
|
|
3
5
|
// ═══════════════════════════════════════════
|
|
@@ -18,14 +20,22 @@
|
|
|
18
20
|
// It never touches server.js internals directly — every server dependency is
|
|
19
21
|
// injected.
|
|
20
22
|
|
|
23
|
+
import { isBrowserGroup } from '../../mcp-server/dist/services/tool-catalog.js';
|
|
24
|
+
import { browserToolsConflict, dispatchTools, launchTools, toolsView } from './assistant-tools.js';
|
|
25
|
+
import { storedToolList, toolListRecord } from './tool-lists.js';
|
|
21
26
|
import { randomBytes } from 'node:crypto';
|
|
22
27
|
import { chooseUsageAccount } from './account-usage.js';
|
|
23
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from 'node:fs';
|
|
24
29
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
25
30
|
import {
|
|
26
|
-
RESULT_STATUSES, buildTaskPrompt, buildFollowUpPrompt, buildResultRetryPrompt, parseResultContract,
|
|
31
|
+
RESULT_STATUSES, buildTaskPrompt, buildFollowUpPrompt, buildResultRetryPrompt, buildResumePrompt, parseResultContract,
|
|
27
32
|
} from './assistant-task-prompt.js';
|
|
33
|
+
import { MAX_RESUMES, clockTime, endOrphanProcess, endProcessTree, findClaudeTranscript, isInside, readCostState, resumeRefusal } from './worker-resume.js';
|
|
28
34
|
import { normalizeCapability, normalizePermissionPolicy } from './native-loop-providers.js';
|
|
35
|
+
import { WORKER_GUARD_DEFAULT, describeConfinementEvent, normalizeWorkerGuardMode } from './worker-confinement.js';
|
|
36
|
+
import { draftConfinementSupport } from './skills/draft-confinement.js';
|
|
37
|
+
import { EVAL_CONFINEMENT, EVAL_ROLES, evalConfinementSupport } from './skills/eval-confinement.js';
|
|
38
|
+
import { initOf as evalInitOf, toolCallsOf as evalToolCallsOf } from './skills/eval-trace.js';
|
|
29
39
|
import { contextVariantOf, defaultIsHidden, defaultModelRow, findModel, hiddenModelId, providerAllHidden, providerEmpty, rowMakes } from './assistant-catalog.js';
|
|
30
40
|
import { TASK_CLASS_META, classPlaybook, collectedMedia, describeTarget, designFit, requiredOutput, requiresVision, runContract } from './assistant-router.js';
|
|
31
41
|
import { MAX_MEDIA_BYTES, MAX_RUN_MEDIA_BYTES, copyRunMedia, mediaType, mediaUrl, servableRunMedia } from './assistant-media.js';
|
|
@@ -40,25 +50,31 @@ import { createBudgetPlanner } from './assistant-budget-planner.js';
|
|
|
40
50
|
import { CLAUDE_ESTIMATE_SOURCE, CODEX_USAGE_KEYS, TOKEN_KEYS, addTokens, codexUsageTokens, createClaudeMeter, createCodexMeter, createOpenCodeMeter, totalTokens, withTotal, zeroTokens } from './assistant-usage.js';
|
|
41
51
|
import { claudeTokensCostUsd } from './assistant-pricing.js';
|
|
42
52
|
import { RemotePolicyError, clampDispatchSpec, getSessionPolicy, readRegisteredProjects } from './remote-policy.js';
|
|
53
|
+
import { limitFromText } from '../public/shared/assistant/asst-limit.js';
|
|
43
54
|
|
|
44
55
|
export const NATIVE_PROVIDERS = new Set(['claude-code', 'codex', 'opencode']);
|
|
45
56
|
// awaiting_route: an un-routed dispatch held until the user picks a model on
|
|
46
57
|
// the route card (ask modes). It is active but consumes no rails.
|
|
47
|
-
|
|
58
|
+
// suspended: a native worker the server suspended as it went down (its CLI
|
|
59
|
+
// ended); resuming: the next start continues it (resumeRun). Neither is an end.
|
|
60
|
+
export const ACTIVE_STATES = new Set(['queued', 'awaiting_route', 'starting', 'running', 'idle', 'awaiting_permission', 'suspended', 'resuming']);
|
|
48
61
|
export const TERMINAL_STATES = new Set(['completed', 'failed', 'stopped', 'interrupted']);
|
|
49
62
|
const WAITING_STATES = new Set(['queued', 'awaiting_route']);
|
|
63
|
+
// What a run can be suspended from at a shutdown: its worker is up, in a turn or between turns.
|
|
64
|
+
const SUSPENDABLE_STATES = new Set(['starting', 'running', 'idle', 'awaiting_permission']);
|
|
50
65
|
|
|
51
66
|
/**
|
|
52
67
|
* The run status a view reports beside its state (the agent_status contract:
|
|
53
68
|
* queued, starting, running, completed, failed, stopped, interrupted). A warm
|
|
54
|
-
* worker between turns (idle, awaiting_permission) is still running
|
|
55
|
-
* held for the user's model
|
|
69
|
+
* worker between turns (idle, awaiting_permission) is still running, and so is
|
|
70
|
+
* one the server suspended while it restarts; a run held for the user's model
|
|
71
|
+
* choice is still queued; one being resumed after a restart is starting.
|
|
56
72
|
*/
|
|
57
73
|
export function runStatusFromState(state) {
|
|
58
74
|
const value = String(state || '');
|
|
59
75
|
if (TERMINAL_STATES.has(value)) return value;
|
|
60
76
|
if (WAITING_STATES.has(value)) return 'queued';
|
|
61
|
-
if (value === 'starting') return 'starting';
|
|
77
|
+
if (value === 'starting' || value === 'resuming') return 'starting';
|
|
62
78
|
return ACTIVE_STATES.has(value) ? 'running' : null;
|
|
63
79
|
}
|
|
64
80
|
|
|
@@ -105,47 +121,13 @@ export const DEFAULT_LIMITS = Object.freeze({
|
|
|
105
121
|
mediaRunBytes: MAX_RUN_MEDIA_BYTES,
|
|
106
122
|
});
|
|
107
123
|
|
|
108
|
-
/**
|
|
109
|
-
* What a class's run gets when the dispatch leaves a field out (undefined, null
|
|
110
|
-
* or ''): a design run works in its project with the SynaBun browser for an hour.
|
|
111
|
-
* Explicit values win. The browser profile comes with the browser: an explicit
|
|
112
|
-
* uses_browser:false keeps the run off it (no `browser` profile, which would turn
|
|
113
|
-
* browser use back on, and no tab), on the normal default profile. The browser
|
|
114
|
-
* rule for capability "full" (QUARANTINE_VIOLATION) still applies. Any class's
|
|
115
|
-
* browser run on Codex / OpenCode without an mcp_profile gets BROWSER_PROFILE
|
|
116
|
-
* (normalizeSpec): the stock default profiles have no browser tools.
|
|
117
|
-
*/
|
|
124
|
+
/** Class defaults; the shared resolver owns automatic groups. */
|
|
118
125
|
export const CLASS_DEFAULTS = Object.freeze({
|
|
119
|
-
design: Object.freeze({ capability: 'workspace', usesBrowser: true,
|
|
126
|
+
design: Object.freeze({ capability: 'workspace', usesBrowser: true, maxMinutes: 60 }),
|
|
120
127
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// and the stock presets with no browser tools (profiles.ts: core, standard).
|
|
125
|
-
const BROWSER_PROFILE_RE = /^(browser|twitter|facebook|tiktok|whatsapp|instagram|linkedin|bluesky|gsc|youtube)$/i;
|
|
126
|
-
const NO_BROWSER_PRESETS = new Set(['core', 'standard']);
|
|
127
|
-
function turnsBrowserOn(profile) {
|
|
128
|
-
const name = String(profile || '');
|
|
129
|
-
return BROWSER_PROFILE_RE.test(name) || /browser/i.test(name);
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Browser settings of a run (any class) that cannot work together, after CLASS_DEFAULTS (so a
|
|
133
|
-
* uses_browser false and any mcp_profile are the brain's own): the text of a
|
|
134
|
-
* BROWSER_SETTINGS_CONFLICT (400) that says what to change, else null. An explicit browser
|
|
135
|
-
* profile would turn an explicit uses_browser:false back on; a stock profile without the browser
|
|
136
|
-
* tools leaves a Codex / OpenCode worker unable to use the browser the run gets (Claude workers
|
|
137
|
-
* always get the full catalog). `provider` null: only the first check (it does not depend on it).
|
|
138
|
-
*/
|
|
139
|
-
export function browserSettingsConflict(spec, provider = null) {
|
|
140
|
-
const profile = String(spec?.mcpProfile || '').trim();
|
|
141
|
-
if (!profile) return null;
|
|
142
|
-
if (spec.usesBrowser === false && turnsBrowserOn(profile)) {
|
|
143
|
-
return `uses_browser is false, but mcp_profile "${profile}" is a browser profile, which turns browser use back on (the run would take a browser tab). Drop mcp_profile or pick one without the browser (such as "standard"), or set uses_browser to true.`;
|
|
144
|
-
}
|
|
145
|
-
if (provider && provider !== 'claude-code' && spec.usesBrowser === true && NO_BROWSER_PRESETS.has(profile.toLowerCase())) {
|
|
146
|
-
return `This run uses the SynaBun browser (uses_browser is on), but mcp_profile "${profile}" has no browser tools, so the ${provider} worker could not use it. Use mcp_profile "browser" or drop mcp_profile, or set uses_browser to false.`;
|
|
147
|
-
}
|
|
148
|
-
return null;
|
|
128
|
+
export function browserSettingsConflict(spec) {
|
|
129
|
+
try { return browserToolsConflict(spec); }
|
|
130
|
+
catch (error) { throw new DispatchError(error.code, error.message, { status: error.status || 400, problems: error.problems }); }
|
|
149
131
|
}
|
|
150
132
|
/** `spec` with its class's defaults filled in where it leaves a field out. */
|
|
151
133
|
export function withClassDefaults(spec, taskClass) {
|
|
@@ -160,7 +142,6 @@ export function withClassDefaults(spec, taskClass) {
|
|
|
160
142
|
if (!defaults) return out;
|
|
161
143
|
for (const [key, value] of Object.entries(defaults)) {
|
|
162
144
|
if (!(out[key] === undefined || out[key] === null || out[key] === '')) continue;
|
|
163
|
-
if (key === 'mcpProfile' && out.usesBrowser === false) continue;
|
|
164
145
|
out[key] = value;
|
|
165
146
|
}
|
|
166
147
|
return out;
|
|
@@ -218,6 +199,43 @@ function clip(value, max = 160) {
|
|
|
218
199
|
}
|
|
219
200
|
function isDirectory(path) { try { return statSync(path).isDirectory(); } catch { return false; } }
|
|
220
201
|
|
|
202
|
+
/** Internal Skills workers own their runtime selection and confinement. */
|
|
203
|
+
function isSkillsRun(entry) { return ['skill-eval', 'skill-draft'].includes(entry?.confinement?.kind); }
|
|
204
|
+
|
|
205
|
+
function requireConfinementProvider(entry) {
|
|
206
|
+
if (entry.confinement && entry.confinement.provider !== entry.provider) {
|
|
207
|
+
throw new DispatchError('CONFINEMENT_PROVIDER_MISMATCH', `Confinement was built for ${entry.confinement.provider || 'an unknown provider'}, but the run uses ${entry.provider}. The run was refused.`, { status: 409 });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A skill-evaluation run: its own prompt, result, memory and escalation rules. */
|
|
212
|
+
export function isEvalRun(entry) { return entry?.confinement?.kind === EVAL_CONFINEMENT; }
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Keep an evaluation run's tool calls (one entry per call id: a later update of the same call replaces its earlier
|
|
216
|
+
* one, so a command's "done" with its exit code wins over its "started") and the catalog its init named. Bounded.
|
|
217
|
+
*/
|
|
218
|
+
export function recordEvalCalls(liveState, payload = {}) {
|
|
219
|
+
const init = evalInitOf(payload);
|
|
220
|
+
if (init && !liveState.evalInit) liveState.evalInit = init;
|
|
221
|
+
const calls = evalToolCallsOf(payload);
|
|
222
|
+
if (!calls.length) return;
|
|
223
|
+
if (!liveState.evalCalls) { liveState.evalCalls = []; liveState.evalCallIndex = new Map(); }
|
|
224
|
+
for (const call of calls) {
|
|
225
|
+
const key = call.id ? `${call.tool}:${call.id}` : null;
|
|
226
|
+
const at = key ? liveState.evalCallIndex.get(key) : undefined;
|
|
227
|
+
if (at !== undefined) {
|
|
228
|
+
const prev = liveState.evalCalls[at];
|
|
229
|
+
// An update without the input (an OpenCode part's later state) keeps the input seen first.
|
|
230
|
+
liveState.evalCalls[at] = { ...prev, ...call, input: call.input && Object.keys(call.input).length ? call.input : prev.input };
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (liveState.evalCalls.length >= 300) return;
|
|
234
|
+
if (key) liveState.evalCallIndex.set(key, liveState.evalCalls.length);
|
|
235
|
+
liveState.evalCalls.push(call);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
221
239
|
/** One-line human summary of a provider stream event (used for transcript tails). */
|
|
222
240
|
export function summarizeProviderEvent(payload = {}) {
|
|
223
241
|
const event = payload.event || {};
|
|
@@ -226,6 +244,7 @@ export function summarizeProviderEvent(payload = {}) {
|
|
|
226
244
|
if (eventType === 'synabun.permission_request') return `permission requested: ${event.toolName || event.kind || 'tool'} ${clip(JSON.stringify(event.input || {}), 120)}`;
|
|
227
245
|
if (eventType === 'synabun.permission_resolved') return `permission ${event.behavior || 'resolved'}: ${event.toolName || ''}`;
|
|
228
246
|
if (eventType === 'synabun.error') return `error: ${clip(event.message, 200)}`;
|
|
247
|
+
if (eventType === 'synabun.confinement') return describeConfinementEvent(event);
|
|
229
248
|
if (payload.provider === 'claude-code') {
|
|
230
249
|
if (event.type === 'assistant') {
|
|
231
250
|
const blocks = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
@@ -330,9 +349,12 @@ function parseJsonObject(text) {
|
|
|
330
349
|
*/
|
|
331
350
|
export function createAssistantDispatcher({
|
|
332
351
|
getRuntime,
|
|
352
|
+
resolveExternalTools = () => ({ names: [], disabled: [] }),
|
|
333
353
|
dataDir,
|
|
334
354
|
loopDir = null,
|
|
335
355
|
broadcastSync = () => {},
|
|
356
|
+
observeSkillEvent = () => {},
|
|
357
|
+
completeSkillUsage = () => {},
|
|
336
358
|
log = () => {},
|
|
337
359
|
PACKAGE_ROOT = process.cwd(),
|
|
338
360
|
findCodexAccount = () => null,
|
|
@@ -345,9 +367,13 @@ export function createAssistantDispatcher({
|
|
|
345
367
|
codexAccounts = null,
|
|
346
368
|
accountUsage = null,
|
|
347
369
|
codexWorkerAccount = () => null,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
370
|
+
// The Protect workers setting (lib/worker-guard-setting.js) as a run starts: 'enforce' | 'warn' | 'off'.
|
|
371
|
+
workerGuard = () => WORKER_GUARD_DEFAULT,
|
|
372
|
+
// SynaBun's own port: a guarded worker's shell may not call it (lib/worker-confinement.js).
|
|
373
|
+
serverPort = null,
|
|
374
|
+
// The ports SynaBun keeps for its OpenCode serves ({ from, to }): a guarded worker's shell calls none of them.
|
|
375
|
+
localPortRange = null,
|
|
376
|
+
toolListOptions = () => ({}),
|
|
351
377
|
acquireLoopBrowserAndTab = null,
|
|
352
378
|
releaseSharedBrowserTab = async () => {},
|
|
353
379
|
reconcileKeepAwake = () => {},
|
|
@@ -398,6 +424,9 @@ export function createAssistantDispatcher({
|
|
|
398
424
|
// The session's Test density (assistant-test-density.js): (assistantSessionId) => level | null.
|
|
399
425
|
// Read when a prompt is composed; null or an unknown value is the default level.
|
|
400
426
|
testDensity = () => null,
|
|
427
|
+
// After a restart, resolves once this server answers its workers (a resumed CLI connects to SynaBun's MCP as it
|
|
428
|
+
// starts): runs are resumed only then. null: at once.
|
|
429
|
+
serverReady = null,
|
|
401
430
|
} = {}) {
|
|
402
431
|
if (typeof getRuntime !== 'function') throw new Error('createAssistantDispatcher requires getRuntime()');
|
|
403
432
|
if (!dataDir) throw new Error('createAssistantDispatcher requires dataDir');
|
|
@@ -419,6 +448,8 @@ export function createAssistantDispatcher({
|
|
|
419
448
|
const registryPath = resolve(dataDir, 'assistant-dispatches.json');
|
|
420
449
|
// What image / video creation runs generated, and design runs' screenshots: <dataDir>/media/<runId>/<n>-<name>.
|
|
421
450
|
const mediaRoot = resolve(dataDir, 'media');
|
|
451
|
+
// A guarded worker's own scratch folder (lib/worker-confinement.js): <dataDir>/scratch/<runId>, removed when empty.
|
|
452
|
+
const scratchRoot = resolve(dataDir, 'scratch');
|
|
422
453
|
/** A known task class, else null. */
|
|
423
454
|
const knownClass = (value) => (Object.prototype.hasOwnProperty.call(TASK_CLASS_META, String(value || '')) ? String(value) : null);
|
|
424
455
|
const entries = new Map(); // runId → entry (persisted shape)
|
|
@@ -438,29 +469,93 @@ export function createAssistantDispatcher({
|
|
|
438
469
|
let persistTimer = null;
|
|
439
470
|
let stalledTimer = null;
|
|
440
471
|
let unsubscribeRuntime = null;
|
|
472
|
+
// A server restart: the runs this start resumes (resumeOnBoot), and the ones it ends whose end is said once the
|
|
473
|
+
// Assistant listens (both filled by loadRegistry). Follow-ups sent to a run while it is away wait here for it.
|
|
474
|
+
const bootResumes = [];
|
|
475
|
+
const bootInterrupts = [];
|
|
476
|
+
const awaitingResume = new Map(); // runId → follow-up items (sendTurn) for a suspended or resuming run
|
|
477
|
+
const resumeInProgress = new Set(); // runIds resumeRun works on (they hold their slot; the others waiting do not)
|
|
478
|
+
// The server is going down (suspendForShutdown ran): a resume under way stops where it is, for the next start.
|
|
479
|
+
let closing = false;
|
|
441
480
|
|
|
442
481
|
// ── persistence ────────────────────────────────────────────────────────────
|
|
482
|
+
/** Why the original provider account of a run cannot be used now ('' when it can). */
|
|
483
|
+
function providerAccountUnusable(accountId, provider = 'claude-code', entry = null) {
|
|
484
|
+
if (provider === 'codex') {
|
|
485
|
+
const home = codexAccounts?.homeFor?.(accountId);
|
|
486
|
+
if (home && entry?.codexHome && resolve(home) !== resolve(entry.codexHome)) return 'its CODEX_HOME changed';
|
|
487
|
+
return codexAccounts?.unusableReason?.(accountId) || '';
|
|
488
|
+
}
|
|
489
|
+
try { return typeof claudeAccounts?.unusableReason === 'function' ? String(claudeAccounts.unusableReason(accountId || 'default') || '') : ''; } catch (error) { return error?.message || 'it could not be read'; }
|
|
490
|
+
}
|
|
443
491
|
function loadRegistry() {
|
|
444
492
|
const rows = readJson(registryPath, []);
|
|
445
493
|
if (!Array.isArray(rows)) return;
|
|
446
494
|
for (const row of rows) {
|
|
447
495
|
if (!row?.runId) continue;
|
|
448
|
-
|
|
496
|
+
let entry = { ...row };
|
|
497
|
+
try {
|
|
498
|
+
entry = readStoredToolListRecord(row, toolListOptions());
|
|
499
|
+
entry.toolsRequested = row.toolsRequested !== undefined ? row.toolsRequested : storedToolList(row, toolListOptions());
|
|
500
|
+
dispatchTools({ tools: entry.toolsRequested ?? undefined });
|
|
501
|
+
if (row.rawSpec) entry.rawSpec = toolListRecord(row.rawSpec, toolListOptions());
|
|
502
|
+
if (!entry.tools) entry.tools = launchTools(dispatchTools({ ...entry, tools: entry.toolsRequested ?? undefined }));
|
|
503
|
+
} catch (error) {
|
|
504
|
+
entry.tools = []; entry.toolsRequested = [];
|
|
505
|
+
entry.toolListError = { code: error.code, message: error.message, original: row.tools ?? row.mcpProfile };
|
|
506
|
+
delete entry.mcpProfile; delete entry.rawSpec;
|
|
507
|
+
entry.state = 'failed'; entry.completionReason = 'tools_invalid';
|
|
508
|
+
}
|
|
449
509
|
// A failure cause being judged when the server went down is never coming.
|
|
450
510
|
delete entry.judging;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
entry.pending = [];
|
|
456
|
-
entry.turnState = 'terminal';
|
|
457
|
-
closeUsageState(entry);
|
|
458
|
-
}
|
|
459
|
-
// A turn cut off by the restart worked until the run was last updated (as far as anything shows).
|
|
511
|
+
// When the server was last seen with the run live: its last update, or the last beat() after it.
|
|
512
|
+
const lastSeen = Math.max(Date.parse(entry.updatedAt || '') || 0, Math.min(now(), Date.parse(entry.aliveAt || '') || 0));
|
|
513
|
+
delete entry.aliveAt;
|
|
514
|
+
// A turn cut off by the restart worked until then (as far as anything shows).
|
|
460
515
|
if (entry.workingSince) {
|
|
461
|
-
entry.workMs = workUsedMs(entry, Math.max(Date.parse(entry.workingSince) || 0,
|
|
516
|
+
entry.workMs = workUsedMs(entry, Math.max(Date.parse(entry.workingSince) || 0, lastSeen));
|
|
462
517
|
entry.workingSince = null;
|
|
463
518
|
}
|
|
519
|
+
// A run the last shutdown could not suspend ended then (suspendForShutdown): it is said now, as a refusal here is.
|
|
520
|
+
if (entry.refusedAtShutdown) {
|
|
521
|
+
delete entry.refusedAtShutdown;
|
|
522
|
+
if (entry.resumeRefused && TERMINAL_STATES.has(entry.state)) bootInterrupts.push(entry.runId);
|
|
523
|
+
}
|
|
524
|
+
if (ACTIVE_STATES.has(entry.state)) {
|
|
525
|
+
// What the restart cut off: a run the last shutdown suspended, one a crash left working (no shutdown ran), or
|
|
526
|
+
// one a restart caught while it resumed. A native worker that may continue is resumed (resumeOnBoot);
|
|
527
|
+
// everything else ends as interrupted, as every cut-off run did before, with why in its notes.
|
|
528
|
+
const candidate = entry.state === 'suspended' || entry.state === 'resuming' || SUSPENDABLE_STATES.has(entry.state);
|
|
529
|
+
// A crash suspended nothing: such a run was left when the server was last seen with it.
|
|
530
|
+
const crashed = candidate && entry.state !== 'suspended' && entry.state !== 'resuming';
|
|
531
|
+
const leftAt = crashed ? iso(lastSeen || now()) : null;
|
|
532
|
+
const why = candidate
|
|
533
|
+
? resumeRefusal(crashed ? { ...entry, suspendedAt: leftAt } : entry, { now: now(), workLeftMs: workLeftMs(entry), isDirectory, accountUnusable: providerAccountUnusable })
|
|
534
|
+
: null;
|
|
535
|
+
if (candidate && !why) {
|
|
536
|
+
if (crashed) {
|
|
537
|
+
entry.suspendedAt = leftAt;
|
|
538
|
+
entry.suspendReason = 'server_restart';
|
|
539
|
+
entry.crashed = true;
|
|
540
|
+
}
|
|
541
|
+
entry.state = 'resuming';
|
|
542
|
+
entry.turnState = 'resuming';
|
|
543
|
+
entry.pending = [];
|
|
544
|
+
bootResumes.push(entry.runId);
|
|
545
|
+
} else {
|
|
546
|
+
entry.state = 'interrupted';
|
|
547
|
+
entry.completionReason = entry.completionReason || 'server_restart';
|
|
548
|
+
entry.finishedAt = entry.finishedAt || iso(now());
|
|
549
|
+
entry.pending = [];
|
|
550
|
+
entry.turnState = 'terminal';
|
|
551
|
+
if (why) {
|
|
552
|
+
entry.notes = [...(Array.isArray(entry.notes) ? entry.notes : []), `not resumed: ${why}`];
|
|
553
|
+
entry.resumeRefused = why;
|
|
554
|
+
bootInterrupts.push(entry.runId);
|
|
555
|
+
}
|
|
556
|
+
closeUsageState(entry);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
464
559
|
entries.set(entry.runId, entry);
|
|
465
560
|
}
|
|
466
561
|
}
|
|
@@ -480,6 +575,21 @@ export function createAssistantDispatcher({
|
|
|
480
575
|
persistTimer = setTimeout(() => { persistTimer = null; write(); }, 200);
|
|
481
576
|
persistTimer.unref?.();
|
|
482
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Every minute, on each live run (a worker up or about to be): the server is alive with it now (aliveAt, saved, not an
|
|
580
|
+
* update). After a crash loadRegistry measures from it, which can be long after the run's last update (a warm worker
|
|
581
|
+
* idle for a follow-up, a long silent turn).
|
|
582
|
+
*/
|
|
583
|
+
function beat() {
|
|
584
|
+
const at = iso(now());
|
|
585
|
+
let alive = 0;
|
|
586
|
+
for (const entry of entries.values()) {
|
|
587
|
+
if (!ACTIVE_STATES.has(entry.state) || WAITING_STATES.has(entry.state)) continue;
|
|
588
|
+
entry.aliveAt = at;
|
|
589
|
+
alive += 1;
|
|
590
|
+
}
|
|
591
|
+
if (alive) persist();
|
|
592
|
+
}
|
|
483
593
|
|
|
484
594
|
// ── views + events ─────────────────────────────────────────────────────────
|
|
485
595
|
/** The task of a live run's turn in progress (or the last one it started); null for a run that ended or has not started one. */
|
|
@@ -508,10 +618,13 @@ export function createAssistantDispatcher({
|
|
|
508
618
|
const startedAt = entry.startedAt || entry.createdAt;
|
|
509
619
|
const end = entry.finishedAt ? new Date(entry.finishedAt).getTime() : at;
|
|
510
620
|
const elapsedMs = startedAt ? Math.max(0, end - new Date(startedAt).getTime()) : 0;
|
|
511
|
-
// The brief goes to the worker's prompt; views name it only. The meters' state stays inside
|
|
512
|
-
|
|
621
|
+
// The brief goes to the worker's prompt; views name it only. The meters' state stays inside, and so does what a
|
|
622
|
+
// resume after a restart needs (resumeInfo: the CLI's pid, the turn in progress); `resumes` counts them.
|
|
623
|
+
const { rawSpec: _rawSpec, brief: _brief, usageState: _usageState, resumeInfo: _resumeInfo, crashed: _crashed, refusedAtShutdown: _refusedAtShutdown, aliveAt: _aliveAt, ...rest } = entry;
|
|
513
624
|
return {
|
|
514
625
|
...rest,
|
|
626
|
+
tools: toolsView({ ...entry, tools: runtimeRun?.tools ?? entry.tools }),
|
|
627
|
+
resumes: Number(entry.resumes) || 0,
|
|
515
628
|
briefId: entry.brief?.briefId || null,
|
|
516
629
|
taskId: entry.taskId || null,
|
|
517
630
|
// The task of the turn in progress (or the last one started): a follow-up may work for a
|
|
@@ -594,6 +707,7 @@ export function createAssistantDispatcher({
|
|
|
594
707
|
touch(entry);
|
|
595
708
|
persist({ force: TERMINAL_STATES.has(entry.state) });
|
|
596
709
|
const payload = { type: 'assistant:dispatch', reason, run: view(entry), ...extra };
|
|
710
|
+
if (['turn_completed', 'idle', 'failed', 'stopped', 'completed'].includes(reason)) { try { completeSkillUsage(entry); } catch {} }
|
|
597
711
|
try { broadcastSync(payload); } catch {}
|
|
598
712
|
for (const listener of listeners) { try { listener(payload); } catch {} }
|
|
599
713
|
notifyWaiters(entry.runId, reason);
|
|
@@ -666,7 +780,7 @@ export function createAssistantDispatcher({
|
|
|
666
780
|
const cwd = String(spec.cwd || spec.project || PACKAGE_ROOT).trim();
|
|
667
781
|
if (!cwd || !isDirectory(cwd)) throw new DispatchError('CWD_INVALID', `cwd is not a directory: ${cwd}`);
|
|
668
782
|
let permissionPolicy = normalizePermissionPolicy(spec.permissionPolicy);
|
|
669
|
-
|
|
783
|
+
let capability = normalizeCapability(spec.capability || 'full');
|
|
670
784
|
const notes = [];
|
|
671
785
|
if (provider === 'codex' && permissionPolicy === 'ask') {
|
|
672
786
|
// A route card may move an 'ask' dispatch onto Codex: degrade instead of failing.
|
|
@@ -687,6 +801,7 @@ export function createAssistantDispatcher({
|
|
|
687
801
|
throw new DispatchError('PROVIDER_NOT_CONNECTED', `No ${provider} model is connected. Pick another provider from agent_catalog, or ask the user to connect one.`, { status: 400 });
|
|
688
802
|
}
|
|
689
803
|
if (!model && catalogValue && defaultIsHidden(catalogValue, provider)) {
|
|
804
|
+
if (isSkillsRun(spec)) throw new DispatchError('MODEL_DISABLED', `The ${provider} default model is disabled; the Skills run cannot substitute another model.`, { status: 400 });
|
|
690
805
|
// No model = the provider default, which the user hid: pin the first visible model.
|
|
691
806
|
const pinned = defaultModelRow(catalogValue, provider);
|
|
692
807
|
if (pinned) {
|
|
@@ -721,16 +836,46 @@ export function createAssistantDispatcher({
|
|
|
721
836
|
// an unsupported level becomes the nearest one it runs, recorded on the run.
|
|
722
837
|
const checked = normalizeEffort({ provider, model, effort: spec.effort, catalog: catalogValue });
|
|
723
838
|
const effortCorrection = checked.corrected || null;
|
|
839
|
+
if (isSkillsRun(spec) && effortCorrection) throw new DispatchError('EFFORT_UNSUPPORTED', `The Skills run requested effort ${spec.effort}, which ${provider}/${model || 'default'} cannot run.`, { status: 400 });
|
|
724
840
|
if (effortCorrection) notes.push(`effort "${effortCorrection.from}" is not supported by ${model || `the ${provider} default model`}; using ${effortCorrection.to ? `"${effortCorrection.to}"` : 'the model default'}`);
|
|
725
841
|
// The run's own browser settings must agree on the provider it gets, whatever its class.
|
|
726
842
|
const conflict = browserSettingsConflict(spec, provider);
|
|
727
843
|
if (conflict) throw new DispatchError('BROWSER_SETTINGS_CONFLICT', conflict, { status: 400 });
|
|
728
|
-
const
|
|
844
|
+
const selection = dispatchTools({ ...spec, taskClass, provider });
|
|
845
|
+
const externalServers = resolveExternalTools({ ...spec, provider }, selection.servers);
|
|
846
|
+
const usesBrowser = spec.usesBrowser === true || selection.groups.some(isBrowserGroup);
|
|
729
847
|
const usesComputer = spec.usesComputer === true || spec.usesComputer === 'true';
|
|
730
848
|
const tags = Array.isArray(spec.tags) ? spec.tags.map((tag) => String(tag).trim()).filter(Boolean).slice(0, 10) : [];
|
|
731
849
|
if ((usesBrowser || usesComputer) && capability === 'full' && !tags.includes('user-authorized-full')) {
|
|
732
850
|
throw new DispatchError('QUARANTINE_VIOLATION', `${usesComputer ? 'Computer-use' : 'Browser-reading'} runs must use capability "read-only" or "workspace" unless tagged "user-authorized-full" by explicit user request.`);
|
|
733
851
|
}
|
|
852
|
+
// A skill-drafting run (lib/skills/creation.js): held to its folder by lib/skills/draft-confinement.js, each
|
|
853
|
+
// provider the way it can (draftConfinementSupport); one that cannot is refused, never started unconfined.
|
|
854
|
+
// It reads no page, drives no computer and is never asked anything.
|
|
855
|
+
let confinement = null;
|
|
856
|
+
if (spec.confinement !== undefined && spec.confinement !== null) {
|
|
857
|
+
if (spec.confinement.provider) requireConfinementProvider({ provider, confinement: spec.confinement });
|
|
858
|
+
if (spec.confinement?.kind === EVAL_CONFINEMENT) {
|
|
859
|
+
// A skill-evaluation run (lib/skills/evals.js): held to its job folder by lib/skills/eval-confinement.js, the
|
|
860
|
+
// same way on every provider that can hold it; its task is sent as it is (the case's own words), and it has
|
|
861
|
+
// no Result contract, no Jev reading, no result retry and no escalation.
|
|
862
|
+
const support = evalConfinementSupport(provider);
|
|
863
|
+
if (!support.ok) throw new DispatchError('CONFINEMENT_UNSUPPORTED', `A skill-evaluation run cannot run on ${provider}: ${support.reason}`, { status: 400 });
|
|
864
|
+
if (usesBrowser || usesComputer) throw new DispatchError('CONFINEMENT_UNSUPPORTED', 'A skill-evaluation run reads no page and drives no computer.', { status: 400 });
|
|
865
|
+
const role = EVAL_ROLES.includes(spec.confinement.role) ? spec.confinement.role : null;
|
|
866
|
+
if (!role) throw new DispatchError('CONFINEMENT_INVALID', 'Unknown evaluation role.', { status: 400 });
|
|
867
|
+
const skill = typeof spec.confinement.skill === 'string' && /^[a-z0-9][a-z0-9._-]{0,63}$/.test(spec.confinement.skill) ? spec.confinement.skill : null;
|
|
868
|
+
confinement = { kind: EVAL_CONFINEMENT, provider, role, shell: role === 'trial' && spec.confinement.shell === true, skill };
|
|
869
|
+
} else {
|
|
870
|
+
if (spec.confinement?.kind !== 'skill-draft') throw new DispatchError('CONFINEMENT_INVALID', 'Unknown confinement.', { status: 400 });
|
|
871
|
+
const support = draftConfinementSupport(provider);
|
|
872
|
+
if (!support.ok) throw new DispatchError('CONFINEMENT_UNSUPPORTED', `A skill-drafting run cannot run on ${provider}: ${support.reason}`, { status: 400 });
|
|
873
|
+
if (usesBrowser || usesComputer) throw new DispatchError('CONFINEMENT_UNSUPPORTED', 'A skill-drafting run reads no page and drives no computer.', { status: 400 });
|
|
874
|
+
confinement = { kind: 'skill-draft', provider };
|
|
875
|
+
}
|
|
876
|
+
capability = 'workspace';
|
|
877
|
+
permissionPolicy = 'auto';
|
|
878
|
+
}
|
|
734
879
|
if (usesComputer) {
|
|
735
880
|
if (!desktop || !desktop.isSupported?.()) throw new DispatchError('COMPUTER_UNAVAILABLE', 'Computer use is not available on this machine (macOS only).', { status: 409 });
|
|
736
881
|
if (!desktop.isReady?.()) throw new DispatchError('COMPUTER_UNAVAILABLE', `Computer use is not set up yet (${desktop.setupState?.() || 'setup required'}). Ask the user to finish the one-time setup in the assistant panel.`, { status: 409 });
|
|
@@ -750,13 +895,6 @@ export function createAssistantDispatcher({
|
|
|
750
895
|
const sightRefusal = designRefusal(catalogValue, provider, model, taskClass);
|
|
751
896
|
if (sightRefusal) throw sightRefusal;
|
|
752
897
|
const agent = provider === 'opencode' && spec.agent ? String(spec.agent).trim().slice(0, 60) || null : null;
|
|
753
|
-
let mcpProfile = null;
|
|
754
|
-
if (provider === 'claude-code') mcpProfile = 'full';
|
|
755
|
-
else {
|
|
756
|
-
// A browser run needs the browser tools: the active profile (core by default) has none.
|
|
757
|
-
mcpProfile = normalizeMcpProfileName(spec.mcpProfile || (usesBrowser ? BROWSER_PROFILE : readActiveMcpProfile()));
|
|
758
|
-
if (mcpProfile && !isValidMcpProfileValue(mcpProfile)) throw new DispatchError('MCP_PROFILE_INVALID', `Invalid MCP profile: ${mcpProfile}`);
|
|
759
|
-
}
|
|
760
898
|
let codexAccount = null;
|
|
761
899
|
let claudeAccount = null;
|
|
762
900
|
const accountId = spec.accountId || spec.codexAccountId || spec.claudeAccountId || null;
|
|
@@ -809,7 +947,7 @@ export function createAssistantDispatcher({
|
|
|
809
947
|
model, modelContextWindow, pricingModel,
|
|
810
948
|
effort: checked.effort || null, effortCorrection,
|
|
811
949
|
agent, modelInfo, notes,
|
|
812
|
-
|
|
950
|
+
toolsRequested: selection.requested, tools: launchTools(selection), externalMcpVersion: 2, externalServers, permissionPolicy, capability, usesBrowser, usesComputer, confinement,
|
|
813
951
|
codexAccountId: codexAccount?.id || null,
|
|
814
952
|
codexHome: codexAccount ? (codexAccount.home || CODEX_DEFAULT_HOME) : null,
|
|
815
953
|
claudeAccountId: claudeAccount?.id || (provider === 'claude-code' ? 'default' : null),
|
|
@@ -826,21 +964,39 @@ export function createAssistantDispatcher({
|
|
|
826
964
|
}
|
|
827
965
|
|
|
828
966
|
// ── rails ──────────────────────────────────────────────────────────────────
|
|
967
|
+
// A suspended run holds no slot (its worker is gone with the server); one being resumed holds its own again.
|
|
829
968
|
function activeEntries(filter = () => true) {
|
|
830
|
-
return [...entries.values()].filter((entry) => ACTIVE_STATES.has(entry.state) && !WAITING_STATES.has(entry.state) && filter(entry));
|
|
969
|
+
return [...entries.values()].filter((entry) => ACTIVE_STATES.has(entry.state) && !WAITING_STATES.has(entry.state) && entry.state !== 'suspended' && filter(entry));
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* The runs that hold a provider's slots, as a queued note says them: every Assistant session's runs
|
|
973
|
+
* count (a warm worker waiting for a follow-up too; never a brain, which is no run), so a slot taken
|
|
974
|
+
* in another session is named as such.
|
|
975
|
+
*/
|
|
976
|
+
function slotHolders(holders, assistantSessionId) {
|
|
977
|
+
const named = holders.map((e) => `${String(e.runId).slice(0, 8)}${(e.assistantSessionId || null) !== (assistantSessionId || null) ? ' (another session)' : ''}${e.state === 'idle' ? ' (idle, waiting for a follow-up)' : ''}`);
|
|
978
|
+
return `held by ${named.join(', ')}`;
|
|
831
979
|
}
|
|
832
|
-
|
|
980
|
+
/**
|
|
981
|
+
* `except`: a run being resumed, which counts the others but not itself, nor the runs still waiting for their resume
|
|
982
|
+
* (resumeRun has not taken them up): runs that wait for a slot never hold each other back. A new dispatch counts them,
|
|
983
|
+
* so it waits behind them.
|
|
984
|
+
*/
|
|
985
|
+
function railBlock(spec, assistantSessionId, { except = null } = {}) {
|
|
986
|
+
const others = (filter) => activeEntries((e) => e.runId !== except && (!except || e.state !== 'resuming' || resumeInProgress.has(e.runId)) && filter(e));
|
|
833
987
|
// One desktop, one controller: computer-use runs never overlap.
|
|
834
|
-
if (spec.usesComputer &&
|
|
988
|
+
if (spec.usesComputer && others((e) => e.usesComputer).length >= 1) return 'another computer-use run is active (one at a time)';
|
|
835
989
|
const perProvider = limits.perProvider[spec.provider] ?? 3;
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
|
|
990
|
+
const providerRuns = others((e) => e.provider === spec.provider);
|
|
991
|
+
if (providerRuns.length >= perProvider) return `provider ${spec.provider} at its concurrency limit (${perProvider}), ${slotHolders(providerRuns, assistantSessionId)}`;
|
|
992
|
+
const accountRuns = spec.provider === 'codex' ? others((e) => e.provider === 'codex' && e.accountId === spec.accountId) : [];
|
|
993
|
+
if (spec.provider === 'codex' && accountRuns.length >= limits.perCodexAccount) {
|
|
994
|
+
return `Codex account ${spec.accountId} at its concurrency limit (${limits.perCodexAccount}), ${slotHolders(accountRuns, assistantSessionId)}`;
|
|
839
995
|
}
|
|
840
|
-
if (assistantSessionId &&
|
|
996
|
+
if (assistantSessionId && others((e) => e.assistantSessionId === assistantSessionId).length >= limits.perSession) {
|
|
841
997
|
return `assistant session at its concurrency limit (${limits.perSession})`;
|
|
842
998
|
}
|
|
843
|
-
if (spec.workflowId &&
|
|
999
|
+
if (spec.workflowId && others((e) => e.workflowId === spec.workflowId).length >= limits.maxFanOut) {
|
|
844
1000
|
return `workflow ${spec.workflowId} at its fan-out limit (${limits.maxFanOut})`;
|
|
845
1001
|
}
|
|
846
1002
|
return null;
|
|
@@ -989,16 +1145,29 @@ export function createAssistantDispatcher({
|
|
|
989
1145
|
/**
|
|
990
1146
|
* The Claude CLI's own stop (--max-budget-usd) for a Claude worker under a budget: the run's ceiling,
|
|
991
1147
|
* never more than the session has left when it starts. A backstop: the dispatcher stops the run at its
|
|
992
|
-
* provider's share between model calls. null under No limit (no CLI cap at all).
|
|
1148
|
+
* provider's share between model calls. null under No limit (no CLI cap at all). `spentUsd`: what the run
|
|
1149
|
+
* spent already (a resume after a restart): its ceiling less that is what it may still spend.
|
|
993
1150
|
*/
|
|
994
|
-
function claudeBackstop(entry) {
|
|
1151
|
+
function claudeBackstop(entry, { spentUsd = 0 } = {}) {
|
|
995
1152
|
if (!limitedNow()) return null;
|
|
996
1153
|
const cap = Number(entry.budgetUsd) || 0;
|
|
1154
|
+
const capLeft = cap > 0 ? Math.max(0, cap - Math.max(0, Number(spentUsd) || 0)) : Infinity;
|
|
997
1155
|
const left = entry.assistantSessionId ? (budget.coverage(entry.assistantSessionId)?.leftUsd ?? null) : null;
|
|
998
|
-
const value = Math.min(
|
|
1156
|
+
const value = Math.min(capLeft, left === null ? Infinity : left);
|
|
999
1157
|
if (!Number.isFinite(value)) return cap > 0 ? cap : null;
|
|
1000
1158
|
return Math.max(MIN_SPEND_USD, floorCents(value) / 100);
|
|
1001
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* The Claude CLI's stop for a run: claudeBackstop() under a budget; a skill-drafting run (confined) also stops at its
|
|
1162
|
+
* own ceiling whatever the session's mode, No limit included (its budget_usd, or what No limit set aside).
|
|
1163
|
+
*/
|
|
1164
|
+
function claudeRunCap(entry, options = {}) {
|
|
1165
|
+
const backstop = claudeBackstop(entry, options);
|
|
1166
|
+
if (!entry.confinement) return backstop;
|
|
1167
|
+
const own = Number(entry.budgetUsd) || Number(entry.budgetIgnoredUsd) || 0;
|
|
1168
|
+
const caps = [backstop, own > 0 ? own : null].filter((v) => Number(v) > 0);
|
|
1169
|
+
return caps.length ? Math.min(...caps) : null;
|
|
1170
|
+
}
|
|
1002
1171
|
/**
|
|
1003
1172
|
* After money moved in a session: one warning at 80 % of the budget (per budget amount), and at
|
|
1004
1173
|
* the budget every live, queued and held run of the session stops. Nothing under No limit.
|
|
@@ -1058,8 +1227,9 @@ export function createAssistantDispatcher({
|
|
|
1058
1227
|
return {
|
|
1059
1228
|
provider: spec.provider, model: spec.model, effort: spec.effort, effortCorrection: spec.effortCorrection || null, agent: spec.agent || null, accountId: spec.accountId,
|
|
1060
1229
|
codexAccountId: spec.codexAccountId, codexHome: spec.codexHome, claudeAccountId: spec.claudeAccountId, claudeConfigDir: spec.claudeConfigDir,
|
|
1061
|
-
cwd: spec.cwd, project: spec.project,
|
|
1230
|
+
cwd: spec.cwd, project: spec.project, toolsRequested: spec.toolsRequested, tools: spec.tools, externalMcpVersion: 2, externalServers: spec.externalServers,
|
|
1062
1231
|
permissionPolicy: spec.permissionPolicy, capability: spec.capability, usesBrowser: spec.usesBrowser, usesComputer: !!spec.usesComputer,
|
|
1232
|
+
confinement: spec.confinement || null,
|
|
1063
1233
|
title: spec.title, task: spec.task, context: spec.context, tags: spec.tags,
|
|
1064
1234
|
maxMinutes: spec.maxMinutes, budgetUsd: spec.budgetUsd ?? null, freeModel: spec.freeModel === true, idleTimeoutMs: spec.idleTimeoutMs, maxTurns: spec.maxTurns,
|
|
1065
1235
|
outputSchema: spec.outputSchema, focus: spec.focus, workflowId: spec.workflowId, parentRunId: spec.parentRunId,
|
|
@@ -1163,7 +1333,8 @@ export function createAssistantDispatcher({
|
|
|
1163
1333
|
const catalogValue = await catalogSnapshot();
|
|
1164
1334
|
// Routing (ask modes may hold the run until the user picks a model).
|
|
1165
1335
|
let routed = null;
|
|
1166
|
-
|
|
1336
|
+
// Skills owns the runtime selection and its provider-specific workspace. Never bind Assistant routes.
|
|
1337
|
+
if (router && !isSkillsRun(rawSpec) && origin !== 'ui' && sessionId) {
|
|
1167
1338
|
routed = await router.resolveDispatch({ sessionId, spec: rawSpec, origin });
|
|
1168
1339
|
// A hold names an open card, but an answer that was already in flight (it waited on the same
|
|
1169
1340
|
// catalog read) can decide that card before this continuation runs. Approved for this
|
|
@@ -1330,6 +1501,12 @@ export function createAssistantDispatcher({
|
|
|
1330
1501
|
};
|
|
1331
1502
|
const heldClass = knownClass(routed.route?.taskClass) || knownClass(input.taskClass);
|
|
1332
1503
|
if (heldClass) entry.taskClass = heldClass;
|
|
1504
|
+
const selection = dispatchTools({ ...input, taskClass: heldClass });
|
|
1505
|
+
entry.externalServers = resolveExternalTools({ ...entry, ...input }, selection.servers);
|
|
1506
|
+
entry.externalMcpVersion = 2;
|
|
1507
|
+
entry.toolsRequested = selection.requested;
|
|
1508
|
+
entry.tools = launchTools(selection);
|
|
1509
|
+
entry.usesBrowser = input.usesBrowser === true || selection.groups.some(isBrowserGroup);
|
|
1333
1510
|
if (brief) entry.brief = brief;
|
|
1334
1511
|
// A held run belongs to the task that asked for it, whenever the user picks its model.
|
|
1335
1512
|
entry.taskId = taskAtDispatch(sessionId);
|
|
@@ -1374,7 +1551,7 @@ export function createAssistantDispatcher({
|
|
|
1374
1551
|
const entry = entries.get(runId);
|
|
1375
1552
|
if (!entry || entry.state !== 'awaiting_route' || !target?.provider) return { ok: false, code: 'NOT_HELD' };
|
|
1376
1553
|
const catalogValue = await catalogSnapshot();
|
|
1377
|
-
let input = { ...(entry.rawSpec || {}), provider: target.provider, model: target.model || null, effort: target.effort ??
|
|
1554
|
+
let input = { ...(entry.rawSpec || {}), provider: target.provider, model: target.model || null, effort: target.effort ?? null, accountId: target.accountId || null, codexAccountId: null, claudeAccountId: null };
|
|
1378
1555
|
let spec;
|
|
1379
1556
|
// The picked provider may need another clamp (Codex under strict worker approvals), or the level fell meanwhile.
|
|
1380
1557
|
const remoteNotes = [];
|
|
@@ -1413,6 +1590,7 @@ export function createAssistantDispatcher({
|
|
|
1413
1590
|
/** Re-dispatch a failed/blocked run one tier up, or on the same model after a transient failure (the user pressed Escalate / Retry). */
|
|
1414
1591
|
async function escalate(runId, { target = null } = {}) {
|
|
1415
1592
|
const entry = requireEntry(runId);
|
|
1593
|
+
if (isSkillsRun(entry)) throw new DispatchError('NO_ESCALATION', 'Skills runs cannot change runtime or model through escalation.', { status: 409 });
|
|
1416
1594
|
const to = target?.provider ? target : entry.escalation?.to;
|
|
1417
1595
|
if (!to?.provider) throw new DispatchError('NO_ESCALATION', `Run ${runId} has no escalation target`, { status: 409 });
|
|
1418
1596
|
// A transient failure (timeout, rate limit, outage) is retried as it was, not escalated.
|
|
@@ -1426,7 +1604,7 @@ export function createAssistantDispatcher({
|
|
|
1426
1604
|
provider: to.provider, model: to.model || null, effort: to.effort || null, task: entry.task, cwd: entry.cwd, context,
|
|
1427
1605
|
accountId: to.accountId || (to.provider === entry.provider ? entry.accountId : null),
|
|
1428
1606
|
title: clip(`${entry.title} ${retry ? '↻' : '↑'}`, 80), tags: entry.tags, workflowId: entry.workflowId, parentRunId: runId,
|
|
1429
|
-
|
|
1607
|
+
tools: entry.tools?.some(item => item.startsWith('mcp:')) ? retainExternalRefs(entry.toolsRequested ?? dispatchTools({}).items, entry.tools) : entry.toolsRequested ?? undefined, capability: entry.capability,
|
|
1430
1608
|
permissionPolicy: to.provider === 'codex' && entry.permissionPolicy === 'ask' ? 'restricted' : entry.permissionPolicy,
|
|
1431
1609
|
usesBrowser: entry.usesBrowser, usesComputer: entry.usesComputer, maxMinutes: entry.maxMinutes, budgetUsd: entry.budgetUsd,
|
|
1432
1610
|
outputSchema: entry.outputSchema, taskClass: entry.taskClass || null,
|
|
@@ -1434,13 +1612,47 @@ export function createAssistantDispatcher({
|
|
|
1434
1612
|
const child = entries.get(result?.run?.runId);
|
|
1435
1613
|
if (child) { child.escalationDepth = (Number(entry.escalationDepth) || 0) + 1; child.escalatedFrom = runId; }
|
|
1436
1614
|
entry.notes.push(`${retry ? 'retried on' : 'escalated to'} ${to.provider}/${to.model || 'default'} as ${result?.run?.runId || '?'}`);
|
|
1615
|
+
if (entry.limit && typeof entry.limit === 'object') entry.limit = { ...entry.limit, retriedAt: iso(now()) };
|
|
1437
1616
|
emit(entry, 'escalated', { childRunId: result?.run?.runId || null });
|
|
1438
1617
|
return result;
|
|
1439
1618
|
}
|
|
1440
1619
|
function computeEscalation(entry, stateOverride = null) {
|
|
1441
|
-
if (!router?.escalationFor) return null;
|
|
1442
|
-
|
|
1620
|
+
if (isSkillsRun(entry) || !router?.escalationFor) return null;
|
|
1621
|
+
let found = null;
|
|
1622
|
+
try { found = router.escalationFor({ run: stateOverride ? { ...entry, state: stateOverride } : entry, depth: Number(entry.escalationDepth) || 0 }) || null; }
|
|
1443
1623
|
catch { return null; }
|
|
1624
|
+
return found;
|
|
1625
|
+
}
|
|
1626
|
+
/** The Protect workers setting now, or the default when the host cannot say. */
|
|
1627
|
+
function safeWorkerGuard() {
|
|
1628
|
+
try { return workerGuard?.() ?? WORKER_GUARD_DEFAULT; } catch { return WORKER_GUARD_DEFAULT; }
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* A run a provider's usage limit stopped (failed, or a turn that ended blocked or without a result):
|
|
1632
|
+
* entry.limit = { provider, accountId, resetsAt (ISO | null), source, detectedAt } for the panel's
|
|
1633
|
+
* waiting card and the brain's mailbox line, else null. The reset is the account's own usage window
|
|
1634
|
+
* when SynaBun reads one (account-usage.js), else the time the worker's message names (asst-limit.js).
|
|
1635
|
+
*/
|
|
1636
|
+
function noteLimit(entry) {
|
|
1637
|
+
const prev = entry.limit && typeof entry.limit === 'object' ? entry.limit : null;
|
|
1638
|
+
const text = [entry.error, entry.lastResult?.summary, entry.lastResult?.question, lastParagraph(entry.lastText), ...(entry.notes || []).slice(-3)]
|
|
1639
|
+
.filter(Boolean).map((line) => String(line)).join('\n');
|
|
1640
|
+
const read = limitFromText(text, { now: now() });
|
|
1641
|
+
let usage = null;
|
|
1642
|
+
try { usage = accountUsage?.peek?.(entry.provider, entry.accountId || 'default', { stale: true }) || null; } catch { usage = null; }
|
|
1643
|
+
const limited = usage?.limited === true;
|
|
1644
|
+
if (!read && !limited) { entry.limit = null; return null; }
|
|
1645
|
+
const accountReset = Date.parse(usage?.resetsAt || '');
|
|
1646
|
+
const fromAccount = Number.isFinite(accountReset) && accountReset > now() && (limited || read?.resetsAt == null);
|
|
1647
|
+
const resetsAt = fromAccount ? accountReset : read?.resetsAt ?? null;
|
|
1648
|
+
entry.limit = {
|
|
1649
|
+
provider: entry.provider || null,
|
|
1650
|
+
accountId: entry.accountId || 'default',
|
|
1651
|
+
resetsAt: Number.isFinite(resetsAt) ? new Date(resetsAt).toISOString() : null,
|
|
1652
|
+
source: fromAccount ? 'account' : resetsAt != null ? 'message' : null,
|
|
1653
|
+
detectedAt: prev?.detectedAt || iso(now()),
|
|
1654
|
+
};
|
|
1655
|
+
return entry.limit;
|
|
1444
1656
|
}
|
|
1445
1657
|
|
|
1446
1658
|
// ── Jev (worker-outcome, rider worker-claim) ────────────────────────────────
|
|
@@ -1518,8 +1730,8 @@ export function createAssistantDispatcher({
|
|
|
1518
1730
|
* for it — at most the surface timeout. Without Jev it is emitted at once.
|
|
1519
1731
|
*/
|
|
1520
1732
|
function emitFailed(entry, evidenceAcc = null) {
|
|
1521
|
-
const done = () => { delete entry.judging; entry.escalation = computeEscalation(entry); emit(entry, 'failed'); };
|
|
1522
|
-
if (!judge?.workerOutcome || budgetStopReason(entry.completionReason)) { done(); return Promise.resolve(); }
|
|
1733
|
+
const done = () => { delete entry.judging; noteLimit(entry); entry.escalation = computeEscalation(entry); emit(entry, 'failed'); };
|
|
1734
|
+
if (!judge?.workerOutcome || budgetStopReason(entry.completionReason) || isEvalRun(entry)) { done(); return Promise.resolve(); }
|
|
1523
1735
|
entry.judging = true;
|
|
1524
1736
|
const evidence = evidenceSnapshot(evidenceAcc);
|
|
1525
1737
|
const error = [entry.error, ...(entry.notes || []).slice(-3)].filter(Boolean).map((line) => String(line)).join('\n');
|
|
@@ -1541,6 +1753,7 @@ export function createAssistantDispatcher({
|
|
|
1541
1753
|
}
|
|
1542
1754
|
} catch { /* the failure stays unjudged */ }
|
|
1543
1755
|
delete entry.judging;
|
|
1756
|
+
noteLimit(entry);
|
|
1544
1757
|
entry.escalation = computeEscalation(entry);
|
|
1545
1758
|
if (judged?.logId) {
|
|
1546
1759
|
try { judge.annotate?.(judged.logId, { run: entry.runId, turn: null, source: 'failure', retried: false, status: 'failed', cause: judged.cause || null, claim: null, escalation: entry.escalation?.kind || null, judged: view, applied: { cause: !!judged.cause } }); } catch {}
|
|
@@ -1550,6 +1763,12 @@ export function createAssistantDispatcher({
|
|
|
1550
1763
|
}
|
|
1551
1764
|
|
|
1552
1765
|
async function start(entry) {
|
|
1766
|
+
try { requireConfinementProvider(entry); } catch (error) {
|
|
1767
|
+
entry.state = 'failed'; entry.error = error.message; entry.completionReason = 'confinement_provider_mismatch';
|
|
1768
|
+
entry.finishedAt = iso(now()); entry.turnState = 'terminal';
|
|
1769
|
+
emit(entry, 'failed');
|
|
1770
|
+
throw error;
|
|
1771
|
+
}
|
|
1553
1772
|
const runtime = safeRuntime();
|
|
1554
1773
|
if (!runtime) throw new DispatchError('RUNTIME_UNAVAILABLE', 'Native loop runtime is not available', { status: 503 });
|
|
1555
1774
|
// A queued or held run starts under the budget as it is now (its provider's share included).
|
|
@@ -1577,49 +1796,18 @@ export function createAssistantDispatcher({
|
|
|
1577
1796
|
}
|
|
1578
1797
|
entry.browserSessionId = browserSessionId;
|
|
1579
1798
|
entry.browserTabId = browserTabId;
|
|
1580
|
-
const liveState =
|
|
1581
|
-
entry, ctx: null, ring: [], ringChars: 0, followUps: [], waitResolve: null, idleTimer: null,
|
|
1582
|
-
pendingResolvers: new Map(), dead: false, lastEventAt: now(), stalledFlagged: false, turnCounter: 0, abortReason: null,
|
|
1583
|
-
evidence: null, // the current turn's commands / tool results / edited files (assistant-evidence.js)
|
|
1584
|
-
completed: false, // its turn loop ended on a completion (agent_complete, a closed finished tab, a removal)
|
|
1585
|
-
};
|
|
1799
|
+
const liveState = newLiveState(entry);
|
|
1586
1800
|
live.set(entry.runId, liveState);
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
}
|
|
1597
|
-
const state = {
|
|
1598
|
-
active: true, task: entry.task, context: entry.context, totalIterations: 1, currentIteration: 0,
|
|
1599
|
-
maxMinutes: entry.maxMinutes, startedAt: iso(now()), lastIterationAt: null, retries: 0, pending: false,
|
|
1600
|
-
terminalSessionId: entry.runId, usesBrowser: entry.usesBrowser, usesComputer: !!entry.usesComputer, browserSessionId, browserTabId,
|
|
1601
|
-
profile: entry.provider, provider: entry.provider, agent: entry.agent || null,
|
|
1602
|
-
model: entry.model, modelContextWindow: entry.modelContextWindow || null, effort: entry.effort, mcpProfile: entry.mcpProfile,
|
|
1603
|
-
codexAccountId: entry.codexAccountId, codexHome: entry.provider === 'codex' ? (entryCodexHome(entry)) : null,
|
|
1604
|
-
claudeAccountId: entry.claudeAccountId, claudeConfigDir: entry.claudeConfigDir,
|
|
1605
|
-
driverType: 'native', runtimeType: 'native', surface: 'sidepanel', runId: entry.runId, cwd: entry.cwd,
|
|
1606
|
-
runMode: 'task', assistantSessionId: entry.assistantSessionId, permissionPolicy: entry.permissionPolicy,
|
|
1607
|
-
// A Claude worker's CLI stop (--max-budget-usd): a backstop under a budget, none under No limit.
|
|
1608
|
-
capability: entry.capability, budgetUsd: entry.budgetUsd, maxBudgetUsd: entry.provider === 'claude-code' ? claudeBackstop(entry) : null,
|
|
1609
|
-
idleTimeoutMs: entry.idleTimeoutMs, maxTurns: entry.maxTurns, outputSchema: entry.outputSchema, project: entry.project,
|
|
1610
|
-
title: entry.title, tags: entry.tags, workflowId: entry.workflowId, parentRunId: entry.parentRunId,
|
|
1611
|
-
// The user's brief after a clarification (assistant-task-prompt.js briefBlock).
|
|
1612
|
-
brief: entry.brief || null,
|
|
1613
|
-
// Image / video creation: the prompt's media block and what the adapter collects ('image' | 'video');
|
|
1614
|
-
// design collects its screenshots and images the same way ('image').
|
|
1615
|
-
taskClass: entry.taskClass || null, collectMedia: collectedMedia(entry.taskClass),
|
|
1616
|
-
// extraEnv is a plain object, so it IS written to the loop state file;
|
|
1617
|
-
// keep secrets out of it. The desktop grant rides as a function instead.
|
|
1618
|
-
extraEnv,
|
|
1619
|
-
desktopGrantProvider: desktopGrant ? () => desktopGrant : null,
|
|
1620
|
-
// What the adapter reports while it starts (a Codex worker whose browser-policy hook is not trusted).
|
|
1621
|
-
onNote: (text) => { const note = String(text || '').trim(); if (note && !entry.notes.includes(note)) entry.notes.push(note); },
|
|
1622
|
-
};
|
|
1801
|
+
// The guard a read-only / workspace run starts under (never for full, never for a drafting run): the setting
|
|
1802
|
+
// now, and a scratch folder of its own. Recorded on the run so the panel and the brain can see it.
|
|
1803
|
+
entry.guardMode = normalizeWorkerGuardMode(safeWorkerGuard());
|
|
1804
|
+
if (entry.provider === 'opencode' && !entry.confinement) resumeInfoOf(entry).persistentOpenCode = true;
|
|
1805
|
+
if ((!entry.confinement || isEvalRun(entry)) && entry.guardMode !== 'off' && (entry.capability === 'read-only' || entry.capability === 'workspace')) {
|
|
1806
|
+
const scratch = resolve(scratchRoot, entry.runId);
|
|
1807
|
+
try { mkdirSync(scratch, { recursive: true }); entry.scratchDir = scratch; } catch (error) { entry.scratchDir = null; entry.notes.push(`scratch folder not made: ${error?.message || error}`); }
|
|
1808
|
+
} else entry.scratchDir = null;
|
|
1809
|
+
const desktopGrant = mintDesktopGrant(entry);
|
|
1810
|
+
const state = runState(entry, { browserSessionId, browserTabId, desktopGrant, startedAt: iso(now()) });
|
|
1623
1811
|
if (entry.permissionPolicy === 'ask') state.permissionBroker = createBroker(entry.runId);
|
|
1624
1812
|
// A class's rules (design), read now so an edited override applies without a restart;
|
|
1625
1813
|
// they go to the prompt only, not into the loop state file.
|
|
@@ -1637,7 +1825,9 @@ export function createAssistantDispatcher({
|
|
|
1637
1825
|
source: 'assistant',
|
|
1638
1826
|
focus: entry.focus === true,
|
|
1639
1827
|
title: entry.title,
|
|
1640
|
-
|
|
1828
|
+
// A skill-evaluation run gets its task as written (a case's request, a grader's brief): the worker preamble and
|
|
1829
|
+
// the Result contract would change what is measured.
|
|
1830
|
+
buildPrompt: () => (isEvalRun(entry) ? entry.task : buildTaskPrompt({ ...state, ...(playbook ? { playbook } : {}), ...(styleGuide ? { styleGuide } : {}), testDensity: briefTestDensity(entry) })),
|
|
1641
1831
|
wrapAdapter: (adapter, ctx) => createTaskAdapter(adapter, ctx, liveState, state),
|
|
1642
1832
|
});
|
|
1643
1833
|
entry.runtime = { status: descriptor.status };
|
|
@@ -1654,6 +1844,97 @@ export function createAssistantDispatcher({
|
|
|
1654
1844
|
throw error;
|
|
1655
1845
|
}
|
|
1656
1846
|
}
|
|
1847
|
+
/** What a run's worker keeps in memory while it lives (never saved): its turn loop, transcript tail, waits. */
|
|
1848
|
+
function newLiveState(entry) {
|
|
1849
|
+
return {
|
|
1850
|
+
entry, ctx: null, ring: [], ringChars: 0, followUps: [], waitResolve: null, idleTimer: null,
|
|
1851
|
+
pendingResolvers: new Map(), dead: false, lastEventAt: now(), stalledFlagged: false, turnCounter: 0, abortReason: null,
|
|
1852
|
+
evidence: null, // the current turn's commands / tool results / edited files (assistant-evidence.js)
|
|
1853
|
+
completed: false, // its turn loop ended on a completion (agent_complete, a closed finished tab, a removal)
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Computer use: an unforgeable per-run grant unlocks the `computer` MCP group for this worker only. Passed as a
|
|
1858
|
+
* function so the loop runtime's JSON state file never contains it. null for a run without computer use.
|
|
1859
|
+
*/
|
|
1860
|
+
function mintDesktopGrant(entry) {
|
|
1861
|
+
if (!entry.usesComputer || !desktop?.mintGrant) return null;
|
|
1862
|
+
try { return desktop.mintGrant({ kind: 'run', assistantSessionId: entry.assistantSessionId, runId: entry.runId, provider: entry.provider, model: entry.model, vision: entry.modelInfo?.vision ?? null }) || null; }
|
|
1863
|
+
catch (error) { entry.notes.push(`desktop grant failed: ${error?.message || error}`); return null; }
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* The loop state a run starts (or resumes, with `extra`) with: what its provider factory and its prompt read. Its
|
|
1867
|
+
* functions (notes, the CLI's process, the desktop grant) stay in memory: the runtime writes the rest to the run's
|
|
1868
|
+
* state file.
|
|
1869
|
+
*/
|
|
1870
|
+
function runState(entry, { browserSessionId = null, browserTabId = null, desktopGrant = null, startedAt = iso(now()), extra = {} } = {}) {
|
|
1871
|
+
const extraEnv = {};
|
|
1872
|
+
if (entry.claudeConfigDir) extraEnv.CLAUDE_CONFIG_DIR = entry.claudeConfigDir;
|
|
1873
|
+
return {
|
|
1874
|
+
active: true, task: entry.task, context: entry.context, totalIterations: 1, currentIteration: 0,
|
|
1875
|
+
maxMinutes: entry.maxMinutes, startedAt, lastIterationAt: null, retries: 0, pending: false,
|
|
1876
|
+
terminalSessionId: entry.runId, usesBrowser: entry.usesBrowser, usesComputer: !!entry.usesComputer, browserSessionId, browserTabId,
|
|
1877
|
+
profile: entry.provider, provider: entry.provider, agent: entry.agent || null,
|
|
1878
|
+
model: entry.model, modelContextWindow: entry.modelContextWindow || null, effort: entry.effort, tools: entry.tools,
|
|
1879
|
+
codexAccountId: entry.codexAccountId, codexHome: entry.provider === 'codex' ? (entryCodexHome(entry)) : null,
|
|
1880
|
+
claudeAccountId: entry.claudeAccountId, claudeConfigDir: entry.claudeConfigDir,
|
|
1881
|
+
driverType: 'native', runtimeType: 'native', surface: 'sidepanel', runId: entry.runId, cwd: entry.cwd,
|
|
1882
|
+
runMode: 'task', assistantSessionId: entry.assistantSessionId, permissionPolicy: entry.permissionPolicy,
|
|
1883
|
+
// A Claude worker's CLI stop (--max-budget-usd): a backstop under a budget, none under No limit.
|
|
1884
|
+
capability: entry.capability, budgetUsd: entry.budgetUsd, maxBudgetUsd: entry.provider === 'claude-code' ? claudeRunCap(entry) : null,
|
|
1885
|
+
// A skill-drafting run's confinement (lib/skills/draft-confinement.js): its adapter enforces it, or refuses to start.
|
|
1886
|
+
confinement: entry.confinement || null,
|
|
1887
|
+
// The worker confinement (lib/worker-confinement.js): the Protect workers setting as the run starts, kept for
|
|
1888
|
+
// its life, and the run's own scratch folder (writable beside the working directory and the temp folder).
|
|
1889
|
+
persistentOpenCode: !!entry.resumeInfo?.persistentOpenCode,
|
|
1890
|
+
sandboxPlan: entry.resumeInfo?.sandboxPlan || null,
|
|
1891
|
+
providerTurnBase: Number(entry.resumeInfo?.providerTurn) || 0,
|
|
1892
|
+
adminSandbox: entry.resumeInfo?.adminSandbox === true,
|
|
1893
|
+
onCodexSandbox: (value) => { resumeInfoOf(entry).adminSandbox = value === true; persist({ force: true }); },
|
|
1894
|
+
onSandboxPlan: (plan) => { resumeInfoOf(entry).sandboxPlan = plan; persist({ force: true }); },
|
|
1895
|
+
guardMode: entry.guardMode, scratchDir: entry.scratchDir || null, serverPort: Number(serverPort) > 0 ? Number(serverPort) : null,
|
|
1896
|
+
localPortRange: localPortRange && Number(localPortRange.from) > 0 && Number(localPortRange.to) >= Number(localPortRange.from) ? { from: Number(localPortRange.from), to: Number(localPortRange.to) } : null,
|
|
1897
|
+
idleTimeoutMs: entry.idleTimeoutMs, maxTurns: entry.maxTurns, outputSchema: entry.outputSchema, project: entry.project,
|
|
1898
|
+
title: entry.title, tags: entry.tags, workflowId: entry.workflowId, parentRunId: entry.parentRunId,
|
|
1899
|
+
// The user's brief after a clarification (assistant-task-prompt.js briefBlock).
|
|
1900
|
+
brief: entry.brief || null,
|
|
1901
|
+
// Image / video creation: the prompt's media block and what the adapter collects ('image' | 'video');
|
|
1902
|
+
// design collects its screenshots and images the same way ('image').
|
|
1903
|
+
taskClass: entry.taskClass || null, collectMedia: collectedMedia(entry.taskClass),
|
|
1904
|
+
// extraEnv is a plain object, so it IS written to the loop state file;
|
|
1905
|
+
// keep secrets out of it. The desktop grant rides as a function instead.
|
|
1906
|
+
extraEnv,
|
|
1907
|
+
desktopGrantProvider: desktopGrant ? () => desktopGrant : null,
|
|
1908
|
+
// What the adapter reports while it starts (a Codex worker whose browser-policy hook is not trusted).
|
|
1909
|
+
onNote: (text) => { const note = String(text || '').trim(); if (note && !entry.notes.includes(note)) entry.notes.push(note); },
|
|
1910
|
+
// A Claude Code worker's CLI (lib/native-loop-providers.js onProcess): saved with the run at once, so the next
|
|
1911
|
+
// start can end it should it outlive this server (a crash ends no process) before the session is resumed.
|
|
1912
|
+
onProcess: (info) => {
|
|
1913
|
+
const pid = Number(info?.pid);
|
|
1914
|
+
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
1915
|
+
resumeInfoOf(entry).pid = pid;
|
|
1916
|
+
entry.resumeInfo.pidStartedAt = info.startedAt || iso(now());
|
|
1917
|
+
persist({ force: true });
|
|
1918
|
+
},
|
|
1919
|
+
...extra,
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
/** What a resume after a restart needs that is saved with the run (the CLI's pid, the turn in progress). */
|
|
1923
|
+
function resumeInfoOf(entry) {
|
|
1924
|
+
if (!entry.resumeInfo || typeof entry.resumeInfo !== 'object') entry.resumeInfo = {};
|
|
1925
|
+
return entry.resumeInfo;
|
|
1926
|
+
}
|
|
1927
|
+
/** The number of the run's last recorded turn (0 before its first): a resumed run's turns go on from it. */
|
|
1928
|
+
function lastTurnNumber(entry) {
|
|
1929
|
+
return (Array.isArray(entry.turns) ? entry.turns : []).reduce((max, turn) => Math.max(max, Number(turn?.n) || 0), 0);
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* The turns a run away for a restart will have had when a follow-up reaches it (as turnCounter counts a turn in
|
|
1933
|
+
* progress): its recorded turns, the one the restart cut off (its resume continues it), or its first, not yet run.
|
|
1934
|
+
*/
|
|
1935
|
+
function awayTurns(entry) {
|
|
1936
|
+
return Math.max(lastTurnNumber(entry), Number(entry.resumeInfo?.inFlight?.turn) || 0, 1);
|
|
1937
|
+
}
|
|
1657
1938
|
/**
|
|
1658
1939
|
* What the run's prompt says about its project's Style Guide (lib/style-guide/store.js): the
|
|
1659
1940
|
* summary for the run's class, DESIGN.md and the token files. null when the project has no saved
|
|
@@ -1805,6 +2086,8 @@ export function createAssistantDispatcher({
|
|
|
1805
2086
|
// Saved with the run: a result that arrives after a restart belongs to the turn that was
|
|
1806
2087
|
// started, whether or not it was ever recorded.
|
|
1807
2088
|
usageStateOf(entry).turn = { n: turnNumber, taskId };
|
|
2089
|
+
// So is the turn in progress: a restart in it is resumed with the continuation prompt (resumeRun).
|
|
2090
|
+
resumeInfoOf(entry).inFlight = { turn: turnNumber, taskId, origin, startedAt };
|
|
1808
2091
|
liveState.turnUsage = null;
|
|
1809
2092
|
liveState.turnTokens = zeroTokens();
|
|
1810
2093
|
liveState.turnOpen = true;
|
|
@@ -1815,6 +2098,7 @@ export function createAssistantDispatcher({
|
|
|
1815
2098
|
startWork(entry, liveState);
|
|
1816
2099
|
ctx.setTurnState('running');
|
|
1817
2100
|
emit(entry, 'turn_started', { turn: turnNumber });
|
|
2101
|
+
persist({ force: true });
|
|
1818
2102
|
// What this turn runs (commands, tool results, edited files): the evidence behind a "done".
|
|
1819
2103
|
liveState.evidence = createTurnEvidence();
|
|
1820
2104
|
// Money booked while this turn runs (OpenCode steps, Codex rollout records).
|
|
@@ -1822,14 +2106,23 @@ export function createAssistantDispatcher({
|
|
|
1822
2106
|
let raw = await inner.runTurn(prompt, { ...meta, turn: turnNumber, outputSchema: entry.outputSchema || undefined });
|
|
1823
2107
|
if (entry.provider === 'codex') settleCodexTurn(entry, liveState, raw);
|
|
1824
2108
|
let text = typeof raw?.text === 'string' ? raw.text : '';
|
|
1825
|
-
|
|
2109
|
+
const evalRun = isEvalRun(entry);
|
|
2110
|
+
let parsed = evalRun ? { found: false } : parseResultContract(text);
|
|
1826
2111
|
parsed.source = parsed.found ? 'contract' : null;
|
|
1827
2112
|
// A run with an output schema answers with bare JSON: that object is its result, no retry turn.
|
|
1828
2113
|
if (!parsed.found) parsed = structuredResult(entry, raw, text) || parsed;
|
|
2114
|
+
// A skill-evaluation run's answer is what is measured: it is its result as it is (no contract to find).
|
|
2115
|
+
if (evalRun && !parsed.found) parsed = { found: true, parseFailed: false, status: 'done', summary: clip(text, 2000), changes: [], files: [], follow_ups: [], media: [], question: '', json: null, jsonError: null, raw: text, source: 'eval' };
|
|
2116
|
+
// The server is going down (suspendForShutdown) as the turn ended without its ## Result block: no retry turn can
|
|
2117
|
+
// run now. The turn stays in flight, so the resumed worker is asked to finish it; what it spent is booked.
|
|
2118
|
+
if (liveState.suspending && !parsed.found) {
|
|
2119
|
+
accumulateCost(entry, raw);
|
|
2120
|
+
throw Object.assign(new Error('suspended for a server restart'), { name: 'AbortError' });
|
|
2121
|
+
}
|
|
1829
2122
|
const noBlock = !parsed.found && strictResult && !!text.trim() && !liveState.dead;
|
|
1830
2123
|
const evidence = evidenceSnapshot(liveState.evidence);
|
|
1831
|
-
// One Jev request before any retry turn (null without Jev: today's behaviour).
|
|
1832
|
-
const judged = await judgeTurn(entry, { text, parsed, noBlock, evidence, followUp });
|
|
2124
|
+
// One Jev request before any retry turn (null without Jev: today's behaviour). Never for an evaluation run.
|
|
2125
|
+
const judged = evalRun ? null : await judgeTurn(entry, { text, parsed, noBlock, evidence, followUp });
|
|
1833
2126
|
let retried = false;
|
|
1834
2127
|
let retryCost = 0;
|
|
1835
2128
|
if (judged?.status) {
|
|
@@ -1852,7 +2145,8 @@ export function createAssistantDispatcher({
|
|
|
1852
2145
|
} catch (error) {
|
|
1853
2146
|
// Stopped during the retry: the run ends here (handleTurnError books the retry), with no
|
|
1854
2147
|
// turn_completed or follow-up after it. The first provider turn was paid for: book it first.
|
|
1855
|
-
|
|
2148
|
+
// Suspended for a server restart during it: the turn stays in flight for the resume.
|
|
2149
|
+
if (liveState.abortReason || liveState.suspending) { accumulateCost(entry, raw); throw error; }
|
|
1856
2150
|
retryCost = accumulateCost(entry, error);
|
|
1857
2151
|
if (entry.provider === 'codex') settleCodexTurn(entry, liveState, error);
|
|
1858
2152
|
entry.notes.push(`result retry failed: ${error?.message || error}`);
|
|
@@ -1893,7 +2187,11 @@ export function createAssistantDispatcher({
|
|
|
1893
2187
|
// Rows that settle from here on (a late rollout record) go to the record itself.
|
|
1894
2188
|
liveState.turnTokens = null;
|
|
1895
2189
|
entry.turns = [...(entry.turns || []), turn].slice(-50);
|
|
2190
|
+
if (entry.resumeInfo) delete entry.resumeInfo.inFlight;
|
|
1896
2191
|
entry.lastResult = parsed;
|
|
2192
|
+
// The run's own descriptor (lib/native-loop-runtime.js) carries it too, for the surfaces that read that one
|
|
2193
|
+
// (the OpenCode panel's pill): a warm worker whose turn ended asking waits on the user, it does not rest.
|
|
2194
|
+
ctx.setOutcome?.(parsed.status || null);
|
|
1897
2195
|
entry.lastText = text.slice(0, 20_000);
|
|
1898
2196
|
// The turn has ended: its result is in. Idle time from here on is not work.
|
|
1899
2197
|
pauseWork(entry, liveState);
|
|
@@ -1901,6 +2199,8 @@ export function createAssistantDispatcher({
|
|
|
1901
2199
|
// itself stays warm for follow-ups; the brain or the user decides). A
|
|
1902
2200
|
// status Jev read from the message is a usable result.
|
|
1903
2201
|
entry.escalation = parsed.status === 'blocked' || (!parsed.found && parsed.source !== 'jev') ? computeEscalation(entry, 'idle') : null;
|
|
2202
|
+
// A provider's usage limit that stopped it: when it resets (the panel's waiting card counts to it).
|
|
2203
|
+
if (parsed.status === 'blocked' || !parsed.found) noteLimit(entry); else entry.limit = null;
|
|
1904
2204
|
if (judged?.logId) {
|
|
1905
2205
|
try {
|
|
1906
2206
|
judge.annotate?.(judged.logId, {
|
|
@@ -1924,13 +2224,24 @@ export function createAssistantDispatcher({
|
|
|
1924
2224
|
isAlive: () => !liveState.dead && (typeof inner.isAlive === 'function' ? inner.isAlive() : true),
|
|
1925
2225
|
describe: () => ({ ...(typeof inner.describe === 'function' ? inner.describe() : {}), runMode: 'task' }),
|
|
1926
2226
|
async runTurn(prompt, meta = {}) {
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2227
|
+
// A run resumed after a server restart (resumeRun): one that was in a turn sends the continuation prompt (the
|
|
2228
|
+
// runtime's buildPrompt for it) as a turn of its own; one that waited for a follow-up waits again, with a fresh
|
|
2229
|
+
// idle timer; one that never got its first turn going sends its task as a new run would.
|
|
2230
|
+
const resume = liveState.resume || null;
|
|
2231
|
+
liveState.resume = null;
|
|
2232
|
+
if (resume?.mode !== 'idle') {
|
|
2233
|
+
try {
|
|
2234
|
+
await runInnerTurn(prompt, meta, resume?.mode === 'mid_turn'
|
|
2235
|
+
? { origin: 'resume', label: 'resumed after a server restart', taskId: resume.taskId || entry.taskId || null }
|
|
2236
|
+
: { origin: 'dispatch' });
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
pauseWork(entry, liveState);
|
|
2239
|
+
return handleTurnError(error, liveState, entry, ctx);
|
|
2240
|
+
}
|
|
1932
2241
|
}
|
|
1933
2242
|
for (;;) {
|
|
2243
|
+
// Suspended for a server shutdown (suspendForShutdown): the next start resumes it; nothing ends here.
|
|
2244
|
+
if (liveState.suspending) return { providerSessionId: null };
|
|
1934
2245
|
// Stopped while the turn ran (a stop that did not fail it): never idle, never take a queued follow-up.
|
|
1935
2246
|
if (liveState.abortReason) { entry.completionReason = entry.completionReason || liveState.abortReason; return { providerSessionId: inner.identity?.()?.providerSessionId }; }
|
|
1936
2247
|
// Out of money (its own ceiling, the session's budget, unpriced usage): stop instead of idling
|
|
@@ -1943,6 +2254,7 @@ export function createAssistantDispatcher({
|
|
|
1943
2254
|
ctx.setTurnState('idle');
|
|
1944
2255
|
emit(entry, 'idle');
|
|
1945
2256
|
const next = await waitForFollowUp();
|
|
2257
|
+
if (liveState.suspending) return { providerSessionId: null };
|
|
1946
2258
|
if (next.kind === 'abort') { entry.completionReason = entry.completionReason || liveState.abortReason || 'aborted'; return { providerSessionId: inner.identity?.()?.providerSessionId }; }
|
|
1947
2259
|
if (next.kind === 'complete') { entry.completionReason = next.reason || 'complete'; liveState.completed = true; return { providerSessionId: inner.identity?.()?.providerSessionId }; }
|
|
1948
2260
|
if (next.kind === 'timeout') { entry.completionReason = 'idle_timeout'; return { providerSessionId: inner.identity?.()?.providerSessionId }; }
|
|
@@ -2054,6 +2366,14 @@ export function createAssistantDispatcher({
|
|
|
2054
2366
|
}
|
|
2055
2367
|
|
|
2056
2368
|
function handleTurnError(error, liveState, entry, ctx) {
|
|
2369
|
+
// Suspended for a server shutdown (suspendForShutdown): the turn the shutdown cut off is resumed on the next start
|
|
2370
|
+
// (its in-flight mark stays), and what it spent was settled when it was suspended. Nothing ends here.
|
|
2371
|
+
if (liveState.suspending) {
|
|
2372
|
+
liveState.turnOpen = false;
|
|
2373
|
+
stopUsagePolling(liveState);
|
|
2374
|
+
return { providerSessionId: null };
|
|
2375
|
+
}
|
|
2376
|
+
if (entry.resumeInfo) delete entry.resumeInfo.inFlight;
|
|
2057
2377
|
// A failed turn was still paid for (a budget-capped run used to read $0).
|
|
2058
2378
|
liveState.turnOpen = false;
|
|
2059
2379
|
stopUsagePolling(liveState);
|
|
@@ -2435,8 +2755,8 @@ export function createAssistantDispatcher({
|
|
|
2435
2755
|
// OpenCode: every session of the run's own serve (the root and the `task` tool's child sessions).
|
|
2436
2756
|
function openCodeMeterOf(entry) {
|
|
2437
2757
|
const held = metersOf(entry);
|
|
2438
|
-
//
|
|
2439
|
-
if (!held.opencode) held.opencode = createOpenCodeMeter();
|
|
2758
|
+
// Message and mark snapshots survive with the session; replayed history must not be charged again.
|
|
2759
|
+
if (!held.opencode) held.opencode = createOpenCodeMeter({ state: entry.usageState?.opencode });
|
|
2440
2760
|
return held.opencode;
|
|
2441
2761
|
}
|
|
2442
2762
|
/** The run's own session id, for a message that names none. */
|
|
@@ -2494,7 +2814,7 @@ export function createAssistantDispatcher({
|
|
|
2494
2814
|
function bookOpenCodeMessage(entry, liveState, info) {
|
|
2495
2815
|
// A finished run keeps no message book: what comes late is dropped, not booked from zero.
|
|
2496
2816
|
if (entry.usageState?.closed) return;
|
|
2497
|
-
const book = liveState.meter || (liveState.meter = new Map());
|
|
2817
|
+
const book = liveState.meter || (liveState.meter = new Map(Object.entries(entry.usageState?.opencodeMessages || {})));
|
|
2498
2818
|
const key = `${info.sessionID || ''}:${info.id}`;
|
|
2499
2819
|
const prev = book.get(key) || { cost: 0, usage: null, completed: false, estimate: 0 };
|
|
2500
2820
|
const reported = Number(info.cost);
|
|
@@ -2525,6 +2845,8 @@ export function createAssistantDispatcher({
|
|
|
2525
2845
|
} else markUnpriced(entry, `OpenCode reported no cost for ${model || 'its default model'} and it has no list price`);
|
|
2526
2846
|
}
|
|
2527
2847
|
book.set(key, { cost, usage: used, completed, estimate });
|
|
2848
|
+
usageStateOf(entry).opencodeMessages = Object.fromEntries(book);
|
|
2849
|
+
usageStateOf(entry).opencode = openCodeMeterOf(entry).state();
|
|
2528
2850
|
if (delta > 0 || completed) enforceMidTurn(entry, liveState);
|
|
2529
2851
|
}
|
|
2530
2852
|
/**
|
|
@@ -2536,6 +2858,7 @@ export function createAssistantDispatcher({
|
|
|
2536
2858
|
if (entry.usageState?.closed) return null;
|
|
2537
2859
|
const meter = openCodeMeterOf(entry);
|
|
2538
2860
|
const delta = meter.markTurn();
|
|
2861
|
+
usageStateOf(entry).opencode = meter.state();
|
|
2539
2862
|
const fidelity = reconciled ? 'exact' : 'partial';
|
|
2540
2863
|
const reason = reconciled ? null : 'opencode-not-reconciled';
|
|
2541
2864
|
const source = reconciled ? 'opencode-messages' : 'opencode-events';
|
|
@@ -2684,6 +3007,335 @@ export function createAssistantDispatcher({
|
|
|
2684
3007
|
return true;
|
|
2685
3008
|
}
|
|
2686
3009
|
|
|
3010
|
+
// ── server restart: suspend, then resume ───────────────────────────────────
|
|
3011
|
+
// Every restart ends the provider processes of its workers (they are its children). A native worker that may
|
|
3012
|
+
// continue (lib/worker-resume.js resumeRefusal) is suspended as the server goes down and resumed on the next start
|
|
3013
|
+
// as the same run: its runId, provider conversation and sidepanel tab. See docs/assistant.md.
|
|
3014
|
+
|
|
3015
|
+
/**
|
|
3016
|
+
* The server is going down (`reason`: the shutdown's): every eligible native worker is suspended
|
|
3017
|
+
* (state 'suspended', no end, no end card), and its runId returned for the runtime to keep (native-loop-runtime.js
|
|
3018
|
+
* shutdown's `keep`: its CLI is ended, its record kept). Called first by the graceful shutdown (server.js), and by
|
|
3019
|
+
* shutdown() for an exit that skipped it. Idempotent.
|
|
3020
|
+
*/
|
|
3021
|
+
function suspendForShutdown(reason = 'server_shutdown') {
|
|
3022
|
+
closing = true;
|
|
3023
|
+
const kept = [];
|
|
3024
|
+
for (const entry of entries.values()) {
|
|
3025
|
+
if (!SUSPENDABLE_STATES.has(entry.state)) continue;
|
|
3026
|
+
const liveState = live.get(entry.runId) || null;
|
|
3027
|
+
// Only a run whose worker is up, or whose resume is starting it (its session is known): one still starting for
|
|
3028
|
+
// the first time has no session to resume yet.
|
|
3029
|
+
if (!liveState || liveState.dead || (!liveState.inner && !liveState.resuming)) continue;
|
|
3030
|
+
// Skill evaluation and drafting runs end as before (the runtime stops them).
|
|
3031
|
+
if (entry.confinement) continue;
|
|
3032
|
+
const refused = resumeRefusal(entry, { atShutdown: true, workLeftMs: workLeftMs(entry) });
|
|
3033
|
+
if (refused) {
|
|
3034
|
+
if (entry.refusedAtShutdown) continue;
|
|
3035
|
+
// A Claude Code worker that may not continue (resumed too often, out of work time) ends interrupted with why
|
|
3036
|
+
// when the runtime stops it (finalize), and is said again once the server is back (loadRegistry): the brain's
|
|
3037
|
+
// mailbox goes down with this server.
|
|
3038
|
+
entry.resumeRefused = refused;
|
|
3039
|
+
entry.refusedAtShutdown = true;
|
|
3040
|
+
entry.completionReason = entry.completionReason || 'server_restart';
|
|
3041
|
+
entry.error = `Not resumed after the server restart: ${refused}`;
|
|
3042
|
+
entry.notes.push(`not resumed: ${refused}`);
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
// A resume still starting keeps what it was resuming: the turn the earlier restart cut off (resumeInfo.inFlight)
|
|
3046
|
+
// or the wait for a follow-up.
|
|
3047
|
+
const midTurn = entry.state !== 'idle' && liveState.resume?.mode !== 'idle';
|
|
3048
|
+
pauseWork(entry, liveState);
|
|
3049
|
+
liveState.suspending = true;
|
|
3050
|
+
if (liveState.idleTimer) { clearTimeout(liveState.idleTimer); liveState.idleTimer = null; }
|
|
3051
|
+
entry.state = 'suspended';
|
|
3052
|
+
entry.turnState = 'suspended';
|
|
3053
|
+
entry.suspendedAt = iso(now());
|
|
3054
|
+
entry.suspendReason = String(reason || 'server_shutdown');
|
|
3055
|
+
delete entry.crashed;
|
|
3056
|
+
// An open permission request goes with the process: the resumed worker asks again.
|
|
3057
|
+
entry.pending = [];
|
|
3058
|
+
// What the cut-off turn spent so far is booked now (an estimate per call the stream showed: no result will
|
|
3059
|
+
// count them); the meter takes the resumed CLI's counters as its new baseline (rebaseClaudeUsage).
|
|
3060
|
+
try { suspendUsage(entry, liveState); } catch (error) { log(entry.runId, 'assistant:meter-error', error?.message || String(error)); }
|
|
3061
|
+
entry.notes.push(`suspended: the server went down (${entry.suspendReason}) ${midTurn ? 'during a turn' : 'while it waited for a follow-up'}; it resumes when the server is back`);
|
|
3062
|
+
emit(entry, 'suspended', { text: 'suspended: SynaBun is restarting; this run resumes as soon as the server is back (no action needed)' });
|
|
3063
|
+
kept.push(entry.runId);
|
|
3064
|
+
}
|
|
3065
|
+
if (kept.length) persist({ force: true });
|
|
3066
|
+
return kept;
|
|
3067
|
+
}
|
|
3068
|
+
/** Book pending provider usage and save the meter baseline before the process ends. */
|
|
3069
|
+
function suspendUsage(entry, liveState) {
|
|
3070
|
+
if (entry.provider === 'codex') pollCodex(entry, liveState, { discover: true });
|
|
3071
|
+
if (entry.provider === 'opencode' && liveState) bookOpenCodeTurn(entry, liveState);
|
|
3072
|
+
stopUsagePolling(liveState);
|
|
3073
|
+
if (liveState) liveState.turnOpen = false;
|
|
3074
|
+
const claude = meters.get(entry.runId)?.claude;
|
|
3075
|
+
if (claude && typeof claude.settlePending === 'function') {
|
|
3076
|
+
const rows = claude.settlePending({ reason: 'no-result' });
|
|
3077
|
+
if (rows.length) settleClaudeRows(entry, rows);
|
|
3078
|
+
usageStateOf(entry).claude = claude.state();
|
|
3079
|
+
}
|
|
3080
|
+
if (ledger && entry.assistantSessionId) { try { ledger.clearPending(entry.assistantSessionId, entry.runId); } catch { /* nothing pending */ } }
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* The process of a suspended run's CLI, if it still runs: told to stop (an exit handler cannot wait for it). Not
|
|
3084
|
+
* when the runtime is ending it already (its record is suspended): a second signal could cut its clean exit short.
|
|
3085
|
+
* Windows has no SIGTERM (a signal ends the CLI alone and leaves its tool's processes): the tree is ended with
|
|
3086
|
+
* taskkill, which returns once it is done.
|
|
3087
|
+
*/
|
|
3088
|
+
function signalWorkerProcess(entry) {
|
|
3089
|
+
try { if (safeRuntime()?.get?.(entry.runId)?.status === 'suspended') return; } catch { /* no runtime: signal it */ }
|
|
3090
|
+
let pid = null;
|
|
3091
|
+
try { pid = live.get(entry.runId)?.inner?.processInfo?.()?.pid || null; } catch { pid = null; }
|
|
3092
|
+
if (!pid) return;
|
|
3093
|
+
if (process.platform === 'win32') { try { endProcessTree(pid); } catch { /* gone, or not allowed */ } return; }
|
|
3094
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* gone */ }
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
/**
|
|
3098
|
+
* The resumed CLI starts its counters where its transcript's last cost-state left them (lib/worker-resume.js
|
|
3099
|
+
* readCostState): the Claude meter takes that as its baseline and the adapter its cost counter, so the first turn
|
|
3100
|
+
* after the resume is charged for itself only. What the cut-off turn spent was booked when the run was suspended; a
|
|
3101
|
+
* crash booked nothing, so what the CLI counted after the run's last settled result is settled now, from its own
|
|
3102
|
+
* counts (one result made of the cost-state). Returns the cost base for the adapter (costBaseUsd).
|
|
3103
|
+
*/
|
|
3104
|
+
function rebaseClaudeUsage(entry) {
|
|
3105
|
+
let costState = null;
|
|
3106
|
+
try {
|
|
3107
|
+
const transcript = findClaudeTranscript({ sessionId: entry.providerSessionId, configDir: entry.claudeConfigDir || null });
|
|
3108
|
+
costState = transcript ? readCostState(transcript, entry.providerSessionId) : null;
|
|
3109
|
+
} catch { costState = null; }
|
|
3110
|
+
const state = usageStateOf(entry);
|
|
3111
|
+
if (entry.crashed && costState?.modelUsage) {
|
|
3112
|
+
try {
|
|
3113
|
+
const meter = claudeMeterOf(entry);
|
|
3114
|
+
const before = meter.state().snapshot;
|
|
3115
|
+
if (!before || countersCover(costState.modelUsage, before)) {
|
|
3116
|
+
const { settled } = meter.onEvent({ type: 'result', subtype: 'success', uuid: `resume:${entry.runId}:${Number(entry.resumes) || 0}`, session_id: entry.providerSessionId, modelUsage: costState.modelUsage, usage: {} });
|
|
3117
|
+
if (settled) { settleClaudeRows(entry, settled.rows, settled); bookCost(entry, Number(settled.costUsd) || 0, 'reported'); }
|
|
3118
|
+
}
|
|
3119
|
+
} catch (error) { log(entry.runId, 'assistant:meter-error', error?.message || String(error)); }
|
|
3120
|
+
}
|
|
3121
|
+
const held = metersOf(entry);
|
|
3122
|
+
const saved = held.claude ? held.claude.state() : (state.claude || {});
|
|
3123
|
+
state.claude = { ...saved, snapshot: costState?.modelUsage || null, unbaselined: null, history: false };
|
|
3124
|
+
held.claude = null;
|
|
3125
|
+
delete entry.crashed;
|
|
3126
|
+
persist({ force: true });
|
|
3127
|
+
return costState ? costState.totalCostUsd : 0;
|
|
3128
|
+
}
|
|
3129
|
+
/** Whether per-model counters `now` are at or above `before` for every model and counter (the same process went on). */
|
|
3130
|
+
function countersCover(nowUsage, before) {
|
|
3131
|
+
const keys = ['inputTokens', 'outputTokens', 'cacheReadInputTokens', 'cacheCreationInputTokens'];
|
|
3132
|
+
return Object.entries(before || {}).every(([model, counters]) => {
|
|
3133
|
+
const current = nowUsage?.[model];
|
|
3134
|
+
return !!current && keys.every((key) => (Number(current[key]) || 0) >= (Number(counters?.[key]) || 0));
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
/**
|
|
3139
|
+
* After a restart (loadRegistry filled bootResumes / bootInterrupts): the runs that will not resume are said
|
|
3140
|
+
* ('interrupted', with why) and their runtime records ended; the others are resumed one at a time, in the order they
|
|
3141
|
+
* were dispatched, ahead of every queued dispatch. Runs a lowered concurrency limit holds back wait at the head of
|
|
3142
|
+
* the queue (processQueue resumes them).
|
|
3143
|
+
*/
|
|
3144
|
+
async function resumeOnBoot() {
|
|
3145
|
+
// The limits as the user set them (assistant-config.json), not the defaults.
|
|
3146
|
+
refreshLimits();
|
|
3147
|
+
const runtime = safeRuntime();
|
|
3148
|
+
const ids = bootResumes.splice(0)
|
|
3149
|
+
.map((runId) => entries.get(runId)).filter((entry) => entry?.state === 'resuming')
|
|
3150
|
+
.sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')))
|
|
3151
|
+
.map((entry) => entry.runId);
|
|
3152
|
+
// Held for this dispatcher from here on: the runtime's own hold no longer ends them.
|
|
3153
|
+
for (const runId of ids) { try { runtime?.keepSuspended?.(runId); } catch { /* ended meanwhile */ } }
|
|
3154
|
+
for (const runId of bootInterrupts.splice(0)) {
|
|
3155
|
+
const entry = entries.get(runId);
|
|
3156
|
+
if (!entry) continue;
|
|
3157
|
+
// A CLI a crash left behind goes with the run.
|
|
3158
|
+
await endOldProcess(entry);
|
|
3159
|
+
try { await runtime?.endSuspended?.(runId, 'server_restart', { error: entry.resumeRefused ? `not resumed: ${entry.resumeRefused}` : null }); } catch { /* not held */ }
|
|
3160
|
+
emit(entry, 'interrupted', { text: `not resumed after the server restart: ${entry.resumeRefused || 'it could not continue'}` });
|
|
3161
|
+
}
|
|
3162
|
+
// A resumed CLI connects to SynaBun's MCP as it starts: not before this server answers.
|
|
3163
|
+
if (ids.length && typeof serverReady === 'function') { try { await serverReady(); } catch { /* resume anyway */ } }
|
|
3164
|
+
const held = [];
|
|
3165
|
+
for (const runId of ids) {
|
|
3166
|
+
const entry = entries.get(runId);
|
|
3167
|
+
if (!entry || entry.state !== 'resuming') continue;
|
|
3168
|
+
const spec = { provider: entry.provider, accountId: entry.accountId, workflowId: entry.workflowId, usesComputer: !!entry.usesComputer };
|
|
3169
|
+
const block = railBlock(spec, entry.assistantSessionId, { except: runId });
|
|
3170
|
+
if (block) { entry.notes.push(`resume queued: ${block}`); held.push(runId); persist(); continue; }
|
|
3171
|
+
// What it threw (a folder that cannot be made, a full disk) ends it, with why: nothing else would.
|
|
3172
|
+
try { await resumeRun(entry); } catch (error) { log(runId, 'assistant:resume-error', error?.message || String(error)); notResumed(entry, error?.message || String(error)); }
|
|
3173
|
+
}
|
|
3174
|
+
if (held.length) { queue.unshift(...held); processQueue().catch(() => {}); }
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
/**
|
|
3178
|
+
* Continue a run a server restart cut off (state 'resuming'): what start() does for a new run (the budget as it is
|
|
3179
|
+
* now, a browser tab, the desktop grant, the permission broker), then the runtime resumes the run's record with its
|
|
3180
|
+
* Claude session (resumeSessionId). A run that was in a turn gets the continuation prompt (buildResumePrompt) as its
|
|
3181
|
+
* next turn; one that waited for a follow-up waits again; one that never got its first turn going is sent its task.
|
|
3182
|
+
* A run that cannot be resumed ends as interrupted, with why. It holds its provider's slot from the call on (the
|
|
3183
|
+
* callers check the rails just before it, in the same tick).
|
|
3184
|
+
*/
|
|
3185
|
+
async function resumeRun(entry) {
|
|
3186
|
+
if (entry.state !== 'resuming') return null;
|
|
3187
|
+
resumeInProgress.add(entry.runId);
|
|
3188
|
+
try { return await continueRun(entry); } finally { resumeInProgress.delete(entry.runId); }
|
|
3189
|
+
}
|
|
3190
|
+
async function continueRun(entry) {
|
|
3191
|
+
if (closing) return null;
|
|
3192
|
+
const runtime = safeRuntime();
|
|
3193
|
+
if (!runtime?.resume) return notResumed(entry, 'the native loop runtime is not available');
|
|
3194
|
+
refreshLimits();
|
|
3195
|
+
const refusal = resumeRefusal(entry, { now: now(), workLeftMs: workLeftMs(entry), isDirectory, accountUnusable: providerAccountUnusable });
|
|
3196
|
+
if (refusal) return notResumed(entry, refusal);
|
|
3197
|
+
try { await fitBudget(entry, { routed: true }); } catch (error) { return notResumed(entry, `the budget does not allow it (${error?.message || error})`); }
|
|
3198
|
+
if (entry.state !== 'resuming' || closing) return null; // stopped meanwhile, or the server is going down again
|
|
3199
|
+
// Its own ceiling (budget_usd), with what its cut-off turn was booked at the suspension (fitBudget checks the
|
|
3200
|
+
// session and the share only): a run out of money is not resumed, as a turn that ends out of money does not idle.
|
|
3201
|
+
const spentOut = budgetBlock(entry);
|
|
3202
|
+
if (spentOut && spentOut.code !== 'BUDGET_SHARE_EXHAUSTED') {
|
|
3203
|
+
entry.completionReason = spentOut.reason;
|
|
3204
|
+
return notResumed(entry, `the budget does not allow it (${spentOut.message})`);
|
|
3205
|
+
}
|
|
3206
|
+
if (!runtime.isSuspended?.(entry.runId)) return notResumed(entry, 'its runtime record is gone');
|
|
3207
|
+
// A crash ended no process: the CLI the run had may still be there, finishing the cut-off tool call. It is ended
|
|
3208
|
+
// first; one that cannot be checked keeps the run from resuming (two processes on one session would both write its
|
|
3209
|
+
// transcript).
|
|
3210
|
+
const orphan = await endOldProcess(entry);
|
|
3211
|
+
if (orphan.state === 'unknown' || orphan.state === 'still_running') {
|
|
3212
|
+
return notResumed(entry, `its old ${providerName(entry.provider)} process (pid ${orphan.pid}) ${orphan.state === 'unknown' ? 'may still be running and could not be checked' : 'did not stop'}`);
|
|
3213
|
+
}
|
|
3214
|
+
if (entry.state !== 'resuming' || closing) return null;
|
|
3215
|
+
const costBaseUsd = entry.provider === 'claude-code' ? rebaseClaudeUsage(entry) : 0;
|
|
3216
|
+
if (loopDir && !existsSync(loopDir)) mkdirSync(loopDir, { recursive: true });
|
|
3217
|
+
// Its browser session went with the server: a new one (the resume prompt names it).
|
|
3218
|
+
let browserSessionId = null;
|
|
3219
|
+
let browserTabId = null;
|
|
3220
|
+
if (entry.usesBrowser && typeof acquireLoopBrowserAndTab === 'function') {
|
|
3221
|
+
const acq = await acquireLoopBrowserAndTab({ requestedBrowserSessionId: entry.requestedBrowserSessionId, terminalSessionId: entry.runId, logTag: 'assistant:browser' }).catch((error) => ({ error: error?.message || String(error) }));
|
|
3222
|
+
if (acq?.error) return notResumed(entry, `no browser tab: ${acq.error}`);
|
|
3223
|
+
browserSessionId = acq.browserSessionId || null;
|
|
3224
|
+
browserTabId = acq.browserTabId || null;
|
|
3225
|
+
}
|
|
3226
|
+
if (entry.state !== 'resuming' || closing) {
|
|
3227
|
+
if (browserSessionId) await releaseSharedBrowserTab(browserSessionId, entry.runId).catch(() => {});
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
entry.browserSessionId = browserSessionId;
|
|
3231
|
+
entry.browserTabId = browserTabId;
|
|
3232
|
+
if (entry.scratchDir) { try { mkdirSync(entry.scratchDir, { recursive: true }); } catch { /* the guard says so when it needs it */ } }
|
|
3233
|
+
const inFlight = entry.resumeInfo?.inFlight || null;
|
|
3234
|
+
const turned = lastTurnNumber(entry);
|
|
3235
|
+
const mode = inFlight ? 'mid_turn' : turned > 0 ? 'idle' : 'fresh';
|
|
3236
|
+
const liveState = newLiveState(entry);
|
|
3237
|
+
liveState.turnCounter = turned;
|
|
3238
|
+
liveState.resume = { mode, taskId: inFlight?.taskId || null };
|
|
3239
|
+
// Follow-ups sent while it was away wait for it like any queued follow-up.
|
|
3240
|
+
const waiting = awaitingResume.get(entry.runId) || [];
|
|
3241
|
+
awaitingResume.delete(entry.runId);
|
|
3242
|
+
liveState.followUps.push(...waiting);
|
|
3243
|
+
live.set(entry.runId, liveState);
|
|
3244
|
+
const desktopGrant = mintDesktopGrant(entry);
|
|
3245
|
+
const state = runState(entry, {
|
|
3246
|
+
browserSessionId, browserTabId, desktopGrant,
|
|
3247
|
+
startedAt: entry.startedAt || iso(now()),
|
|
3248
|
+
extra: { resumeSessionId: entry.providerSessionId, resumeThreadId: entry.providerThreadId, costBaseUsd, resumingWorker: true },
|
|
3249
|
+
});
|
|
3250
|
+
// The CLI's spend stop (--max-budget-usd) counts from the total the resumed CLI restores (costBaseUsd): what the run
|
|
3251
|
+
// may still spend (its ceiling less what it spent, never more than the session has left) goes on top of that. A
|
|
3252
|
+
// backstop only: the dispatcher holds the run to its ceiling and its share between calls.
|
|
3253
|
+
const allowanceUsd = claudeRunCap(entry, { spentUsd: entry.costUsd });
|
|
3254
|
+
if (Number(allowanceUsd) > 0) state.maxBudgetUsd = Number((Number(allowanceUsd) + Math.max(0, Number(costBaseUsd) || 0)).toFixed(2));
|
|
3255
|
+
if (entry.permissionPolicy === 'ask') state.permissionBroker = createBroker(entry.runId);
|
|
3256
|
+
const at = clockTime(entry.suspendedAt);
|
|
3257
|
+
const selfRestarts = isInside(entry.cwd, PACKAGE_ROOT);
|
|
3258
|
+
const playbook = mode === 'fresh' ? playbookFor(entry) : null;
|
|
3259
|
+
const styleGuide = mode === 'fresh' ? styleGuideFor(entry) : null;
|
|
3260
|
+
const prompt = () => (mode === 'mid_turn'
|
|
3261
|
+
? buildResumePrompt(state, { at, selfRestarts })
|
|
3262
|
+
: buildTaskPrompt({ ...state, ...(playbook ? { playbook } : {}), ...(styleGuide ? { styleGuide } : {}), testDensity: briefTestDensity(entry) }));
|
|
3263
|
+
entry.resumes = (Number(entry.resumes) || 0) + 1;
|
|
3264
|
+
entry.resumedAt = iso(now());
|
|
3265
|
+
entry.state = 'starting';
|
|
3266
|
+
entry.turnState = 'starting';
|
|
3267
|
+
entry.notes.push(`resumed after the server restart${at ? ` at ${at}` : ''} (${entry.resumes} of ${MAX_RESUMES}): ${mode === 'mid_turn' ? 'it continues the turn the restart cut off' : mode === 'idle' ? 'it waits for a follow-up again' : 'its first turn starts again'}`);
|
|
3268
|
+
// A shutdown while its CLI starts suspends it again (suspendForShutdown): the next start resumes it.
|
|
3269
|
+
liveState.resuming = true;
|
|
3270
|
+
try {
|
|
3271
|
+
const descriptor = await runtime.resume({
|
|
3272
|
+
runId: entry.runId,
|
|
3273
|
+
state,
|
|
3274
|
+
buildPrompt: prompt,
|
|
3275
|
+
wrapAdapter: (adapter, ctx) => createTaskAdapter(adapter, ctx, liveState, state),
|
|
3276
|
+
});
|
|
3277
|
+
entry.runtime = { status: descriptor.status };
|
|
3278
|
+
} catch (error) {
|
|
3279
|
+
// Suspended again by a shutdown while its CLI started: the start was cancelled, nothing ended.
|
|
3280
|
+
if (entry.state === 'suspended') return null;
|
|
3281
|
+
// The runtime ended its record (resume_failed): its run-stopped event ended the run here too.
|
|
3282
|
+
const why = error?.message || String(error);
|
|
3283
|
+
if (browserSessionId) await releaseSharedBrowserTab(browserSessionId, entry.runId).catch(() => {});
|
|
3284
|
+
if (!TERMINAL_STATES.has(entry.state)) return notResumed(entry, why);
|
|
3285
|
+
entry.notes.push(`not resumed: ${why}`);
|
|
3286
|
+
persist();
|
|
3287
|
+
return null;
|
|
3288
|
+
}
|
|
3289
|
+
if (entry.state === 'suspended') return null;
|
|
3290
|
+
delete entry.suspendedAt;
|
|
3291
|
+
delete entry.suspendReason;
|
|
3292
|
+
emit(entry, 'resumed', {
|
|
3293
|
+
resumes: entry.resumes,
|
|
3294
|
+
text: `resumed after the server restart${at ? ` at ${at}` : ''} (${entry.resumes} of ${MAX_RESUMES}): ${mode === 'mid_turn' ? 'it continues the turn the restart cut off; its result arrives as usual' : mode === 'idle' ? 'it waits for a follow-up again' : 'it starts its task again'}. Nothing to do.`,
|
|
3295
|
+
});
|
|
3296
|
+
reconcileKeepAwake();
|
|
3297
|
+
return view(entry);
|
|
3298
|
+
}
|
|
3299
|
+
/**
|
|
3300
|
+
* The CLI a run had before a crash may still be there, finishing its cut-off tool call (a crash ends no process): it
|
|
3301
|
+
* is ended once its command line shows it is this run's (lib/worker-resume.js endOrphanProcess), before a resume and
|
|
3302
|
+
* when a run a restart cut off ends without one. → { state, pid } ('gone' without a saved pid).
|
|
3303
|
+
*/
|
|
3304
|
+
async function endOldProcess(entry) {
|
|
3305
|
+
const pid = Number(entry?.resumeInfo?.pid) || null;
|
|
3306
|
+
if (!pid) return { state: 'gone', pid: null };
|
|
3307
|
+
let orphan = null;
|
|
3308
|
+
try { orphan = await endOrphanProcess({ pid, runId: entry.runId }); } catch { orphan = { state: 'unknown' }; }
|
|
3309
|
+
if (orphan.state === 'ended' || orphan.state === 'killed') entry.notes.push(`its old ${providerName(entry.provider)} process (pid ${pid}) still ran after the restart and was ${orphan.state}`);
|
|
3310
|
+
if (orphan.state !== 'unknown' && orphan.state !== 'still_running' && entry.resumeInfo) {
|
|
3311
|
+
delete entry.resumeInfo.pid;
|
|
3312
|
+
delete entry.resumeInfo.pidStartedAt;
|
|
3313
|
+
}
|
|
3314
|
+
return { ...orphan, pid };
|
|
3315
|
+
}
|
|
3316
|
+
/** A run that could not be resumed after a restart ends as interrupted, with why (its notes, its error, its event). */
|
|
3317
|
+
function notResumed(entry, why) {
|
|
3318
|
+
if (TERMINAL_STATES.has(entry.state)) return null;
|
|
3319
|
+
const index = queue.indexOf(entry.runId);
|
|
3320
|
+
if (index !== -1) queue.splice(index, 1);
|
|
3321
|
+
// Follow-ups accepted for it while it was away (agent_send) reach nobody: its end says so, with the first of them.
|
|
3322
|
+
const waiting = awaitingResume.get(entry.runId) || [];
|
|
3323
|
+
awaitingResume.delete(entry.runId);
|
|
3324
|
+
const undelivered = waiting.length ? `; ${waiting.length} follow-up${waiting.length === 1 ? '' : 's'} sent while it was away ${waiting.length === 1 ? 'was' : 'were'} not delivered (${waiting.length === 1 ? '' : 'the first: '}"${clip(waiting[0].text, 160)}")` : '';
|
|
3325
|
+
// Nothing of it may go on: a CLI a crash left behind is ended too.
|
|
3326
|
+
endOldProcess(entry).then(() => persist()).catch(() => {});
|
|
3327
|
+
entry.notes.push(`not resumed: ${why}${undelivered}`);
|
|
3328
|
+
entry.resumeRefused = why;
|
|
3329
|
+
entry.completionReason = entry.completionReason || 'server_restart';
|
|
3330
|
+
const error = `Not resumed after the server restart: ${why}${undelivered}`;
|
|
3331
|
+
let ended = false;
|
|
3332
|
+
try { ended = !!safeRuntime()?.isSuspended?.(entry.runId); } catch { ended = false; }
|
|
3333
|
+
// The runtime's run-stopped event finalizes the run here (onRuntimeMessage); without a record it is done here.
|
|
3334
|
+
if (ended) safeRuntime().endSuspended(entry.runId, 'server_restart', { error }).catch(() => {});
|
|
3335
|
+
if (!TERMINAL_STATES.has(entry.state)) finalize(entry, { status: 'interrupted', stoppedReason: 'server_restart', error });
|
|
3336
|
+
return null;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
2687
3339
|
// ── follow-ups / lifecycle controls ────────────────────────────────────────
|
|
2688
3340
|
function requireEntry(runId) {
|
|
2689
3341
|
const entry = entries.get(runId);
|
|
@@ -2697,9 +3349,12 @@ export function createAssistantDispatcher({
|
|
|
2697
3349
|
if (message.length > 20_000) throw new DispatchError('MESSAGE_TOO_LONG', 'text must be at most 20000 characters');
|
|
2698
3350
|
if (TERMINAL_STATES.has(entry.state)) throw new DispatchError('RUN_NOT_ACTIVE', `Run ${runId} is ${entry.state}; dispatch a new run with the previous result as context.`, { status: 409, resumable: false });
|
|
2699
3351
|
if (entry.state === 'queued') throw new DispatchError('RUN_NOT_STARTED', `Run ${runId} is still queued`, { status: 409 });
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3352
|
+
// Away for a server restart (suspended, or being resumed): the follow-up waits for the run and goes to it after
|
|
3353
|
+
// its resume, like a follow-up queued behind a turn. Should the run not resume, it ends and says why.
|
|
3354
|
+
const away = entry.state === 'suspended' || entry.state === 'resuming';
|
|
3355
|
+
const liveState = away ? null : live.get(runId);
|
|
3356
|
+
if (!away && !liveState) throw new DispatchError('RUN_NOT_ACTIVE', `Run ${runId} has no live worker`, { status: 409 });
|
|
3357
|
+
if ((away ? awayTurns(entry) : liveState.turnCounter) >= entry.maxTurns) throw new DispatchError('MAX_TURNS', `Run ${runId} reached its max turns (${entry.maxTurns})`, { status: 409 });
|
|
2703
3358
|
refreshLimits();
|
|
2704
3359
|
const blocked = budgetBlock(entry);
|
|
2705
3360
|
if (blocked) {
|
|
@@ -2717,6 +3372,14 @@ export function createAssistantDispatcher({
|
|
|
2717
3372
|
// A follow-up works for the task in progress when it is sent, which may be a later one than the run's.
|
|
2718
3373
|
const item = { kind: 'follow-up', text: message, origin, at: iso(now()), taskId: taskNow(entry.assistantSessionId) || entry.taskId || null };
|
|
2719
3374
|
if (origin === 'user') emit(entry, 'user_intervened', { text: clip(message, 200) });
|
|
3375
|
+
if (away) {
|
|
3376
|
+
if (!allowQueue) throw new DispatchError('TURN_ACTIVE', `Run ${runId} is ${entry.state === 'suspended' ? 'suspended while the server restarts' : 'being resumed after a server restart'}; retry when it is idle or pass queue:true`, { status: 409 });
|
|
3377
|
+
const waiting = awaitingResume.get(runId) || [];
|
|
3378
|
+
waiting.push(item);
|
|
3379
|
+
awaitingResume.set(runId, waiting);
|
|
3380
|
+
emit(entry, 'follow_up_queued', { queued: waiting.length });
|
|
3381
|
+
return { ok: true, queued: true, position: waiting.length, run: view(entry), note: 'The run resumes after the server restart; this follow-up goes to it then.' };
|
|
3382
|
+
}
|
|
2720
3383
|
if (liveState.waitResolve) { liveState.waitResolve(item); return { ok: true, queued: false, turn: liveState.turnCounter + 1, run: view(entry) }; }
|
|
2721
3384
|
if (!allowQueue) throw new DispatchError('TURN_ACTIVE', `Run ${runId} is busy; retry when idle or pass queue:true`, { status: 409 });
|
|
2722
3385
|
liveState.followUps.push(item);
|
|
@@ -2774,6 +3437,15 @@ export function createAssistantDispatcher({
|
|
|
2774
3437
|
}
|
|
2775
3438
|
if (TERMINAL_STATES.has(entry.state)) return { ok: true, run: view(entry), alreadyTerminal: true };
|
|
2776
3439
|
entry.completionReason = entry.completionReason || reason;
|
|
3440
|
+
// A run waiting for its resume after a restart leaves the queue and drops the follow-ups that waited for it; its
|
|
3441
|
+
// runtime record (suspended) ends as stopped.
|
|
3442
|
+
if (entry.state === 'resuming' || entry.state === 'suspended') {
|
|
3443
|
+
const index = queue.indexOf(runId);
|
|
3444
|
+
if (index !== -1) queue.splice(index, 1);
|
|
3445
|
+
awaitingResume.delete(runId);
|
|
3446
|
+
// A CLI a crash left behind (this start has not ended it yet) goes with the run.
|
|
3447
|
+
if (entry.state === 'resuming') await endOldProcess(entry);
|
|
3448
|
+
}
|
|
2777
3449
|
const runtime = safeRuntime();
|
|
2778
3450
|
const stopped = runtime ? await runtime.stop(runId, reason) : false;
|
|
2779
3451
|
if (!stopped) finalize(entry, { status: 'stopped', stoppedReason: reason });
|
|
@@ -2792,11 +3464,15 @@ export function createAssistantDispatcher({
|
|
|
2792
3464
|
function finalize(entry, runtimeRun) {
|
|
2793
3465
|
if (TERMINAL_STATES.has(entry.state)) return;
|
|
2794
3466
|
const liveState = live.get(entry.runId);
|
|
3467
|
+
// Follow-ups that waited for its resume after a restart go with it.
|
|
3468
|
+
awaitingResume.delete(entry.runId);
|
|
2795
3469
|
pauseWork(entry, liveState);
|
|
2796
3470
|
let status = runtimeRun?.status || 'completed';
|
|
2797
3471
|
// A completed worker whose loop file was swept meanwhile (a stale-file cleanup) reaches the
|
|
2798
3472
|
// runtime's next check as "stopped": its turn loop had already ended on the completion.
|
|
2799
3473
|
if (status === 'stopped' && liveState?.completed && /^state_(removed|inactive)$/.test(String(runtimeRun?.stoppedReason || ''))) status = 'completed';
|
|
3474
|
+
// A Claude Code worker the shutdown could not suspend (suspendForShutdown) ends interrupted, with why.
|
|
3475
|
+
if (status === 'stopped' && entry.refusedAtShutdown) status = 'interrupted';
|
|
2800
3476
|
entry.state = TERMINAL_STATES.has(status) ? status : 'completed';
|
|
2801
3477
|
entry.turnState = 'terminal';
|
|
2802
3478
|
entry.finishedAt = iso(now());
|
|
@@ -2811,18 +3487,21 @@ export function createAssistantDispatcher({
|
|
|
2811
3487
|
}
|
|
2812
3488
|
entry.pending = [];
|
|
2813
3489
|
releaseDesktop(entry);
|
|
3490
|
+
// The run's scratch folder goes when it left nothing there (what it wrote is the user's to look at).
|
|
3491
|
+
if (entry.scratchDir) { try { rmdirSync(entry.scratchDir); } catch { /* not empty, or already gone */ } }
|
|
2814
3492
|
// A run that ended gives back what it did not spend: the session's split is made again (debounced).
|
|
2815
3493
|
if (entry.assistantSessionId) { try { budget.onRunEnded(entry.assistantSessionId); } catch { /* the split stays as it is */ } }
|
|
2816
|
-
if (entry.state === 'failed' && !budgetStopReason(entry.completionReason) && judge?.workerOutcome) {
|
|
3494
|
+
if (entry.state === 'failed' && !budgetStopReason(entry.completionReason) && judge?.workerOutcome && !isEvalRun(entry)) {
|
|
2817
3495
|
// State, queue and desktop move on now; the 'failed' event waits for its cause.
|
|
2818
3496
|
emitFailed(entry, liveState?.evidence || null);
|
|
2819
3497
|
processQueue().catch(() => {});
|
|
2820
3498
|
return;
|
|
2821
3499
|
}
|
|
2822
|
-
if (entry.state === 'failed' || entry.lastResult?.status === 'blocked') entry.escalation = computeEscalation(entry);
|
|
3500
|
+
if (entry.state === 'failed' || entry.lastResult?.status === 'blocked') { noteLimit(entry); entry.escalation = computeEscalation(entry); }
|
|
2823
3501
|
emit(entry, entry.state);
|
|
2824
3502
|
// A memory already stored for the run (by the worker or the brain) is the run's memory.
|
|
2825
|
-
|
|
3503
|
+
// An evaluation run (lib/skills/evals.js) is a measurement, not work to remember: it stores no memory.
|
|
3504
|
+
if (memory?.rememberDispatch && !isEvalRun(entry) && !syncMemory(entry, { announce: true }) && (entry.state === 'completed' || (entry.state === 'stopped' && entry.lastResult))) {
|
|
2826
3505
|
Promise.resolve(memory.rememberDispatch(view(entry))).then((memoryId) => {
|
|
2827
3506
|
if (!memoryId) return;
|
|
2828
3507
|
entry.memoryId = memoryId;
|
|
@@ -2837,6 +3516,8 @@ export function createAssistantDispatcher({
|
|
|
2837
3516
|
if (!runId || !entries.has(runId)) return;
|
|
2838
3517
|
const entry = entries.get(runId);
|
|
2839
3518
|
if (message.type === 'sidepanel:provider-event') {
|
|
3519
|
+
if (!isEvalRun(entry)) { try { observeSkillEvent(entry, message.event || {}); } catch {} }
|
|
3520
|
+
if (entry.provider === 'codex' && Number(message.event?.providerTurn) > 0) resumeInfoOf(entry).providerTurn = Number(message.event.providerTurn);
|
|
2840
3521
|
const liveState = live.get(runId);
|
|
2841
3522
|
// Not tied to the live turn: a result that arrives after an abort is still settled.
|
|
2842
3523
|
if (entry.provider === 'claude-code') { try { meterClaude(entry, message.event?.event); } catch (error) { log(runId, 'assistant:meter-error', error?.message || String(error)); } }
|
|
@@ -2844,8 +3525,18 @@ export function createAssistantDispatcher({
|
|
|
2844
3525
|
liveState.lastEventAt = now();
|
|
2845
3526
|
liveState.stalledFlagged = false;
|
|
2846
3527
|
if (liveState.evidence) { try { recordProviderEvent(liveState.evidence, message.event); } catch {} }
|
|
3528
|
+
// A skill-evaluation run: its tool calls (one entry per call id) and the catalog it started with, for its
|
|
3529
|
+
// trigger reading (lib/skills/eval-trace.js; read with transcript format 'tools').
|
|
3530
|
+
if (isEvalRun(entry)) { try { recordEvalCalls(liveState, message.event || {}); } catch {} }
|
|
2847
3531
|
if (entry.provider === 'opencode') { try { meterOpenCode(entry, liveState, message.event || {}); } catch (error) { log(runId, 'assistant:meter-error', error?.message || String(error)); } }
|
|
2848
3532
|
const summary = summarizeProviderEvent(message.event || {});
|
|
3533
|
+
// A guard decision (lib/worker-confinement.js) is also a note of the run: what was refused or warned about,
|
|
3534
|
+
// with its reason, so the brain and the panel see it without the transcript. The first twenty.
|
|
3535
|
+
if ((message.event?.eventType || message.event?.event?.type) === 'synabun.confinement') {
|
|
3536
|
+
const count = (liveState.guardEvents = (liveState.guardEvents || 0) + 1);
|
|
3537
|
+
if (count <= 20) entry.notes.push(`guard ${message.event.event?.action || 'refused'}: ${clip(summary, 300)}`);
|
|
3538
|
+
else if (count === 21) entry.notes.push('guard: more decisions follow in the transcript');
|
|
3539
|
+
}
|
|
2849
3540
|
if (summary) {
|
|
2850
3541
|
const line = `${iso(now()).slice(11, 19)} ${summary}`;
|
|
2851
3542
|
liveState.ring.push(line);
|
|
@@ -2859,9 +3550,20 @@ export function createAssistantDispatcher({
|
|
|
2859
3550
|
return;
|
|
2860
3551
|
}
|
|
2861
3552
|
const run = message.run;
|
|
3553
|
+
if (message.reason === 'mcp-tools' && Array.isArray(run.tools)) {
|
|
3554
|
+
entry.tools = storedToolList({ tools: retainExternalRefs(run.tools, entry.tools) }, toolListOptions());
|
|
3555
|
+
// Keep the original request for escalation, and the current selection for restart.
|
|
3556
|
+
persist({ force: true });
|
|
3557
|
+
emit(entry, 'tools_changed');
|
|
3558
|
+
}
|
|
2862
3559
|
entry.runtime = { status: run.status, turnState: run.turnState, stoppedReason: run.stoppedReason, error: run.error, claimedBy: run.claimedBy, providerSessionId: run.providerSessionId, providerThreadId: run.providerThreadId };
|
|
3560
|
+
// A new provider identity is saved at once: a crash before the run's next write (its first alive mark comes up to
|
|
3561
|
+
// a minute later) would otherwise leave no session to resume.
|
|
3562
|
+
const newIdentity = (run.providerSessionId && run.providerSessionId !== entry.providerSessionId)
|
|
3563
|
+
|| (run.providerThreadId && run.providerThreadId !== entry.providerThreadId);
|
|
2863
3564
|
if (run.providerSessionId) entry.providerSessionId = run.providerSessionId;
|
|
2864
3565
|
if (run.providerThreadId) entry.providerThreadId = run.providerThreadId;
|
|
3566
|
+
if (newIdentity) persist({ force: true });
|
|
2865
3567
|
if (message.type === 'sidepanel:run-updated' && message.reason === 'started' && entry.state === 'starting') {
|
|
2866
3568
|
entry.state = 'running';
|
|
2867
3569
|
entry.turnState = 'running';
|
|
@@ -2883,10 +3585,13 @@ export function createAssistantDispatcher({
|
|
|
2883
3585
|
refreshLimits();
|
|
2884
3586
|
for (const runId of [...queue]) {
|
|
2885
3587
|
const entry = entries.get(runId);
|
|
2886
|
-
|
|
3588
|
+
// A run a restart cut off and a lowered limit held back (resumeOnBoot) waits at the head of the queue.
|
|
3589
|
+
const resuming = entry?.state === 'resuming';
|
|
3590
|
+
if (!entry || (entry.state !== 'queued' && !resuming)) { queue.splice(queue.indexOf(runId), 1); continue; }
|
|
2887
3591
|
const spec = { provider: entry.provider, accountId: entry.accountId, workflowId: entry.workflowId, usesComputer: !!entry.usesComputer };
|
|
2888
|
-
if (railBlock(spec, entry.assistantSessionId)) continue;
|
|
3592
|
+
if (railBlock(spec, entry.assistantSessionId, resuming ? { except: runId } : {})) continue;
|
|
2889
3593
|
queue.splice(queue.indexOf(runId), 1);
|
|
3594
|
+
if (resuming) { try { await resumeRun(entry); } catch (error) { notResumed(entry, error?.message || String(error)); } continue; }
|
|
2890
3595
|
try { await start(entry); emit(entry, 'queue_started'); } catch (error) { entry.notes.push(`queue start failed: ${error?.message || error}`); }
|
|
2891
3596
|
}
|
|
2892
3597
|
}
|
|
@@ -2959,6 +3664,8 @@ export function createAssistantDispatcher({
|
|
|
2959
3664
|
return { ...base, text: text.length > cap ? text.slice(-cap) : text, truncated: text.length > cap };
|
|
2960
3665
|
}
|
|
2961
3666
|
if (format === 'files') return { ...base, files: entry.lastResult?.files || [] };
|
|
3667
|
+
// A skill-evaluation run's tool calls and its start catalog (kept while the run lives).
|
|
3668
|
+
if (format === 'tools') return { ...base, calls: liveState?.evalCalls ? [...liveState.evalCalls] : [], init: liveState?.evalInit || null, unavailable: !liveState ? 'tool calls are only kept for live runs' : null };
|
|
2962
3669
|
if (format === 'events' || format === 'tail') {
|
|
2963
3670
|
const lines = liveState ? liveState.ring.slice(-Math.max(1, Math.min(500, Number(tail) || 40))) : [];
|
|
2964
3671
|
const joined = lines.join('\n');
|
|
@@ -3068,7 +3775,13 @@ export function createAssistantDispatcher({
|
|
|
3068
3775
|
async function shutdown(reason = 'server_shutdown') {
|
|
3069
3776
|
if (stalledTimer) clearInterval(stalledTimer);
|
|
3070
3777
|
if (unsubscribeRuntime) { try { unsubscribeRuntime(); } catch {} unsubscribeRuntime = null; }
|
|
3778
|
+
// An exit the graceful shutdown did not run before (a crash, an exit handler): the runs that may continue are
|
|
3779
|
+
// suspended here as well, and their CLIs told to stop (an exit handler cannot wait for them; the next start ends
|
|
3780
|
+
// what is left). A run waiting for its resume stays as it is, for the next start.
|
|
3781
|
+
suspendForShutdown(reason);
|
|
3071
3782
|
for (const entry of entries.values()) {
|
|
3783
|
+
if (entry.state === 'suspended') { signalWorkerProcess(entry); continue; }
|
|
3784
|
+
if (entry.state === 'resuming') continue;
|
|
3072
3785
|
if (ACTIVE_STATES.has(entry.state)) {
|
|
3073
3786
|
pauseWork(entry);
|
|
3074
3787
|
entry.state = 'interrupted'; entry.completionReason = reason; entry.finishedAt = iso(now()); entry.turnState = 'terminal'; entry.pending = [];
|
|
@@ -3083,12 +3796,17 @@ export function createAssistantDispatcher({
|
|
|
3083
3796
|
|
|
3084
3797
|
loadRegistry();
|
|
3085
3798
|
attachRuntime();
|
|
3086
|
-
stalledTimer = setInterval(() => { attachRuntime(); checkStalled(); }, 60_000);
|
|
3799
|
+
stalledTimer = setInterval(() => { attachRuntime(); checkStalled(); beat(); }, 60_000);
|
|
3087
3800
|
stalledTimer.unref?.();
|
|
3801
|
+
// After a restart: the cut-off runs are resumed, or said to have ended, once the Assistant listens (server.js makes
|
|
3802
|
+
// its runtime, which subscribes, right after this dispatcher, in the same tick).
|
|
3803
|
+
if (bootResumes.length || bootInterrupts.length) {
|
|
3804
|
+
setTimeout(() => { resumeOnBoot().catch((error) => log(null, 'assistant:resume-error', error?.message || String(error))); }, 0);
|
|
3805
|
+
}
|
|
3088
3806
|
|
|
3089
3807
|
return {
|
|
3090
3808
|
dispatch, sendTurn, complete, completeClosedTab, stop, killAll, get, peek, list, totals, wait, transcript, pendingPermissions,
|
|
3091
|
-
respondPermission, subscribe, shutdown, limits,
|
|
3809
|
+
respondPermission, subscribe, shutdown, limits, suspendForShutdown,
|
|
3092
3810
|
sessionBudget, checkSession, refreshLimits, providerSpend, coverFollowUp, budget,
|
|
3093
3811
|
resolveRoute, declineRoute, heldRouteRefusal, escalate,
|
|
3094
3812
|
markDelivered, deliveryState,
|