@cohortapp/agent-sdk 2.17.0 → 2.18.4
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/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -30,17 +30,37 @@ let governor = resourceGovernor;
|
|
|
30
30
|
let rateGuard = rateGuardModule;
|
|
31
31
|
let budgetGuard = budgetGuardModule;
|
|
32
32
|
const RATE_PROVIDER = process.env.MAESTRO_RATE_PROVIDER || "anthropic";
|
|
33
|
+
// cohort-llm clients (seat token + quota). `undefined` = resolve lazily from the
|
|
34
|
+
// seat's config; `null` = none. Injectable for tests.
|
|
35
|
+
let cohortClients = undefined;
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Test seam: swap the governance modules for fakes. Each arg is optional;
|
|
36
39
|
* omitted modules keep the real implementation. Returns a restore fn.
|
|
40
|
+
* `cohort` is `{quota, token}` (lib/org/llm-token.mjs seatCohortClients shape).
|
|
37
41
|
*/
|
|
38
|
-
export function setGovernanceForTests({ governor: g, rateGuard: r, budgetGuard: b } = {}) {
|
|
39
|
-
const prev = { governor, rateGuard, budgetGuard };
|
|
42
|
+
export function setGovernanceForTests({ governor: g, rateGuard: r, budgetGuard: b, cohort: c } = {}) {
|
|
43
|
+
const prev = { governor, rateGuard, budgetGuard, cohortClients };
|
|
40
44
|
if (g) governor = g;
|
|
41
45
|
if (r) rateGuard = r;
|
|
42
46
|
if (b) budgetGuard = b;
|
|
43
|
-
|
|
47
|
+
if (c !== undefined) cohortClients = c;
|
|
48
|
+
return () => { governor = prev.governor; rateGuard = prev.rateGuard; budgetGuard = prev.budgetGuard; cohortClients = prev.cohortClients; };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The seat's cohort-llm clients, or null when it has none. Never throws. */
|
|
52
|
+
function getCohortClients() {
|
|
53
|
+
if (cohortClients === undefined) {
|
|
54
|
+
try {
|
|
55
|
+
cohortClients = seatCohortClients({
|
|
56
|
+
agentRoot: AGENT_REPO_DIR,
|
|
57
|
+
log: (level, msg) => { try { console[level === "error" ? "error" : "warn"](`[dispatcher] cohort-llm: ${msg}`); } catch { /* */ } },
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
cohortClients = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return cohortClients;
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
// Indirection so a test can capture the EXACT argv handed to `claude` by the
|
|
@@ -99,6 +119,10 @@ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.ur
|
|
|
99
119
|
// Resolve the claude binary against the agent's PATH (not launchd's bare
|
|
100
120
|
// env). Without this, every daemon-spawned `claude --print` exits ENOENT.
|
|
101
121
|
import { resolveClaudeBin, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
|
|
122
|
+
import { buildSpawn, admitSpawnCached, isCohortRouted, readSeatEngine } from "../../lib/runtime/adapter.mjs";
|
|
123
|
+
import { seatEngineFields, engineRunShape, sessionBudgetUsd, seatTokenSync } from "../../lib/runtime/seat-engine.mjs";
|
|
124
|
+
import { seatCohortClients } from "../../lib/org/llm-token.mjs";
|
|
125
|
+
import { COHORT_LLM_BILLED_BY } from "../../lib/org/cost-sync.mjs";
|
|
102
126
|
const CLAUDE_BIN = resolveClaudeBin();
|
|
103
127
|
|
|
104
128
|
// Model router — opt-in. When config/model-routing.yaml is present in the
|
|
@@ -112,10 +136,11 @@ const CLAUDE_BIN = resolveClaudeBin();
|
|
|
112
136
|
// - v2 (schema_version: 2): resolveChain → RouteDecision. The decision carries
|
|
113
137
|
// the chosen backend, the session-retarget env, the spawn knobs, the
|
|
114
138
|
// ordered failover chain, an estimated cost, and a decision_id that joins
|
|
115
|
-
// the ledger row ↔ resume marker ↔ routing audit.
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
139
|
+
// the ledger row ↔ resume marker ↔ routing audit. The argv + child env are
|
|
140
|
+
// built from the decision by the runtime adapter (lib/runtime/adapter.mjs),
|
|
141
|
+
// whose retarget env goes through the same runtime env builder
|
|
142
|
+
// (lib/runtime/child-env.mjs) that spawnRouted uses, so the credential scrub
|
|
143
|
+
// matches spawnRouted exactly.
|
|
119
144
|
//
|
|
120
145
|
// NOTE on spawnRouted: the daemon's spawnSession() must register the child
|
|
121
146
|
// process SYNCHRONOUSLY (tests + getStatus() observe active_sessions right after
|
|
@@ -135,7 +160,6 @@ import {
|
|
|
135
160
|
modelFlagFor,
|
|
136
161
|
resolveChain,
|
|
137
162
|
} from "../../lib/model-router.mjs";
|
|
138
|
-
import { buildSpawnArgs, buildChildEnv } from "../../lib/model-router/spawn.mjs";
|
|
139
163
|
import { budgetLadder, spawnKnobsFor } from "../../lib/model-router/economics.mjs";
|
|
140
164
|
// Observability spine (WS — diagnostics). The dispatcher emits `dispatched` at
|
|
141
165
|
// spawn and `session_opened`/`session_closed` from the existing close hook,
|
|
@@ -151,6 +175,7 @@ import {
|
|
|
151
175
|
releaseSession,
|
|
152
176
|
} from "./lib/session-router.mjs";
|
|
153
177
|
import { replyTier } from "../../lib/assurance/tier.mjs";
|
|
178
|
+
import { activeEntryFor, linkBoardTask } from "../../lib/session/current-work.mjs";
|
|
154
179
|
|
|
155
180
|
/**
|
|
156
181
|
* The reply tier comes from `lib/assurance/tier.mjs` (design §5.1) — the ONE
|
|
@@ -364,14 +389,15 @@ export function parseUsageFromText(text) {
|
|
|
364
389
|
}
|
|
365
390
|
}
|
|
366
391
|
if (!obj || typeof obj !== "object") return { ok: false, reason: "no-json-object" };
|
|
392
|
+
const cohortMeta = cohortResultMeta(obj);
|
|
367
393
|
const usage = obj.usage && typeof obj.usage === "object" ? obj.usage : null;
|
|
368
|
-
if (!usage) return { ok: false, reason: "no-usage-field" };
|
|
394
|
+
if (!usage) return { ok: false, reason: "no-usage-field", ...cohortMeta };
|
|
369
395
|
const inputTokens = Number(usage.input_tokens);
|
|
370
396
|
const outputTokens = Number(usage.output_tokens);
|
|
371
397
|
if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens)) {
|
|
372
|
-
return { ok: false, reason: "non-numeric-tokens" };
|
|
398
|
+
return { ok: false, reason: "non-numeric-tokens", ...cohortMeta };
|
|
373
399
|
}
|
|
374
|
-
const out = { ok: true, inputTokens, outputTokens };
|
|
400
|
+
const out = { ok: true, inputTokens, outputTokens, ...cohortMeta };
|
|
375
401
|
const cacheRead = Number(usage.cache_read_input_tokens);
|
|
376
402
|
if (Number.isFinite(cacheRead)) out.cacheReadTokens = cacheRead;
|
|
377
403
|
// Cache CREATION tokens bill at 1.25x input and were previously dropped on
|
|
@@ -380,14 +406,35 @@ export function parseUsageFromText(text) {
|
|
|
380
406
|
// the CLI's total_cost_usd.
|
|
381
407
|
const cacheWrite = Number(usage.cache_creation_input_tokens);
|
|
382
408
|
if (Number.isFinite(cacheWrite)) out.cacheWriteTokens = cacheWrite;
|
|
383
|
-
|
|
384
|
-
|
|
409
|
+
// A NUMBER, not anything Number() will coerce. The Cohort Engine writes
|
|
410
|
+
// `total_cost_usd: null` when not every request reported gateway cost
|
|
411
|
+
// (lib/engine/output/json.mjs), and `Number(null)` is 0 — finite, >= 0 —
|
|
412
|
+
// which recorded such a session as $0.00 authoritative: the free-looking
|
|
413
|
+
// row this whole parser exists to never write. The stock CLI always sends a
|
|
414
|
+
// number, so the coercion was harmless until the engine port made null a
|
|
415
|
+
// value this field can carry. (engine-envelope-parity.test.mjs)
|
|
416
|
+
const totalCost = obj.total_cost_usd;
|
|
417
|
+
if (typeof totalCost === "number" && Number.isFinite(totalCost) && totalCost >= 0) out.totalCostUsd = totalCost;
|
|
385
418
|
if (typeof obj.model === "string" && obj.model) {
|
|
386
419
|
out.model = /opus/i.test(obj.model) ? "opus" : /haiku/i.test(obj.model) ? "haiku" : "sonnet";
|
|
387
420
|
}
|
|
388
421
|
return out;
|
|
389
422
|
}
|
|
390
423
|
|
|
424
|
+
/**
|
|
425
|
+
* What a Cohort Engine result says about the gateway that served it: the
|
|
426
|
+
* `cohort` block (lib/engine/output/json.mjs) carries the gateway request ids.
|
|
427
|
+
* PURE. Empty for a claude result.
|
|
428
|
+
* @param {object} obj the parsed run result
|
|
429
|
+
* @returns {{servedByCohort?:true, requestIds?:string[]}}
|
|
430
|
+
*/
|
|
431
|
+
export function cohortResultMeta(obj) {
|
|
432
|
+
const c = obj && typeof obj === "object" && obj.cohort && typeof obj.cohort === "object" ? obj.cohort : null;
|
|
433
|
+
if (!c) return {};
|
|
434
|
+
const ids = Array.isArray(c.requestIds) ? c.requestIds.filter((x) => typeof x === "string" && x) : [];
|
|
435
|
+
return { servedByCohort: true, requestIds: ids };
|
|
436
|
+
}
|
|
437
|
+
|
|
391
438
|
/**
|
|
392
439
|
* Append a TRUTHFUL cost-ledger row for a finished dispatcher session via
|
|
393
440
|
* scripts/cost/track-claude-usage.mjs (source "dispatcher"). Real token counts
|
|
@@ -403,7 +450,7 @@ export function parseUsageFromText(text) {
|
|
|
403
450
|
*
|
|
404
451
|
* Best-effort + detached; never blocks the close path.
|
|
405
452
|
*/
|
|
406
|
-
function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId }) {
|
|
453
|
+
function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId, billedBy = null }) {
|
|
407
454
|
const usage = parseUsageFromText(stdout);
|
|
408
455
|
try {
|
|
409
456
|
const trackerPath = join(AGENT_REPO_DIR, "scripts/cost/track-claude-usage.mjs");
|
|
@@ -420,6 +467,10 @@ function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId
|
|
|
420
467
|
// routing decision + resume marker (SPEC §4.7). The tracker captures it as a
|
|
421
468
|
// generic flag; absent on the v1 / no-config paths.
|
|
422
469
|
if (decisionId) trackerArgs.push("--decision-id", String(decisionId));
|
|
470
|
+
// A session cohort-llm served was metered by the gateway: mark the row so
|
|
471
|
+
// org cost-sync never reports it twice, and keep the gateway request ids.
|
|
472
|
+
if (billedBy || usage.servedByCohort) trackerArgs.push("--billed-by", String(billedBy || COHORT_LLM_BILLED_BY));
|
|
473
|
+
if (Array.isArray(usage.requestIds) && usage.requestIds.length) trackerArgs.push("--request-ids", usage.requestIds.join(","));
|
|
423
474
|
if (usage.ok) {
|
|
424
475
|
trackerArgs.push("--input-tokens", String(usage.inputTokens));
|
|
425
476
|
trackerArgs.push("--output-tokens", String(usage.outputTokens));
|
|
@@ -705,6 +756,21 @@ export function reconcileResumePending(opts = {}) {
|
|
|
705
756
|
continue;
|
|
706
757
|
}
|
|
707
758
|
|
|
759
|
+
// cohort-llm admission: a marker whose work now routes to cohort-llm is not
|
|
760
|
+
// resumed into a refused quota. A deferral is not a try (no strike).
|
|
761
|
+
// W4-E1: the admission (with the engine it resolved and its token) rides to the spawn.
|
|
762
|
+
let admission = { ok: true };
|
|
763
|
+
{
|
|
764
|
+
const admitResume = typeof opts.admitResume === "function" ? opts.admitResume : defaultAdmitResume;
|
|
765
|
+
try { admission = admitResume({ marker }) || { ok: true }; } catch { admission = { ok: true }; }
|
|
766
|
+
if (!admission.ok) {
|
|
767
|
+
const d = admission.deferral || {};
|
|
768
|
+
logSession({ event: "resume_cohort_deferred", sessionId: marker.sessionId, item_id: marker.itemId, code: d.code || null, kind: d.kind || null, until: Number.isFinite(d.until) ? new Date(d.until).toISOString() : null, cold: admission.cold === true });
|
|
769
|
+
stats.deferred++;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
708
774
|
// Bump the attempt count on the marker BEFORE re-dispatch so a crash mid-
|
|
709
775
|
// resume still advances toward the 3-strike cap (no infinite loop).
|
|
710
776
|
marker.recoveryAttempts = attempts + 1;
|
|
@@ -716,7 +782,7 @@ export function reconcileResumePending(opts = {}) {
|
|
|
716
782
|
|
|
717
783
|
const spawnResume = typeof opts.spawnResume === "function" ? opts.spawnResume : defaultSpawnResume;
|
|
718
784
|
try {
|
|
719
|
-
spawnResume({ marker });
|
|
785
|
+
spawnResume({ marker, admission });
|
|
720
786
|
logSession({ event: "resume_dispatched", sessionId: marker.sessionId, claudeSessionId: marker.claudeSessionId, item_id: marker.itemId, attempt: marker.recoveryAttempts });
|
|
721
787
|
stats.resumed++;
|
|
722
788
|
} catch (err) {
|
|
@@ -771,9 +837,12 @@ export function resumeModelFlag(marker, target) {
|
|
|
771
837
|
* [--print, --output-format json, <permission args>, <daemon args>,
|
|
772
838
|
* <router knobs>, --session-id <id>, --model <flag>, <prompt>]
|
|
773
839
|
*
|
|
774
|
-
* and the same env rule
|
|
775
|
-
*
|
|
776
|
-
*
|
|
840
|
+
* and the same env rule (both built by the runtime adapter): a retarget
|
|
841
|
+
* (envForSpawn carries ANTHROPIC_BASE_URL) goes through the runtime env
|
|
842
|
+
* builder's credential scrub — foreign keys AND the seat's OAuth token — and a
|
|
843
|
+
* retarget without a third-party token is refused and resumed on the stock
|
|
844
|
+
* path instead (`refusedRetarget`); the Anthropic path keeps the explicit
|
|
845
|
+
* empty-key posture (keychain / OAuth-token fallthrough).
|
|
777
846
|
* MAESTRO_SESSION_ID rides the env so delivery receipts attribute correctly.
|
|
778
847
|
*
|
|
779
848
|
* @param {object} o
|
|
@@ -789,31 +858,67 @@ export function buildResumeSpawn(o = {}) {
|
|
|
789
858
|
const marker = o.marker || {};
|
|
790
859
|
const target = o.target && typeof o.target === "object" ? o.target : null;
|
|
791
860
|
const pathFn = typeof o.augmentedPath === "function" ? o.augmentedPath : augmentedPath;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (target) {
|
|
795
|
-
if (target.maxTurns != null && Number.isFinite(Number(target.maxTurns))) knobArgs.push("--max-turns", String(target.maxTurns));
|
|
796
|
-
if (target.effort) knobArgs.push("--effort", String(target.effort));
|
|
797
|
-
if (target.agentsJson) knobArgs.push("--agents", typeof target.agentsJson === "string" ? target.agentsJson : JSON.stringify(target.agentsJson));
|
|
798
|
-
}
|
|
799
|
-
const args = [
|
|
800
|
-
"--print",
|
|
801
|
-
"--output-format", "json",
|
|
802
|
-
...(Array.isArray(o.permissionArgs) ? o.permissionArgs : []),
|
|
803
|
-
...(Array.isArray(o.daemonArgs) ? o.daemonArgs : []),
|
|
804
|
-
...knobArgs,
|
|
805
|
-
"--session-id", marker.claudeSessionId,
|
|
806
|
-
"--model", modelFlag,
|
|
807
|
-
marker.prompt,
|
|
808
|
-
];
|
|
861
|
+
// argv + env come from the runtime adapter (lib/runtime/adapter.mjs, lane
|
|
862
|
+
// "resume"): the same contract as a fresh dispatcher spawn, built in one place.
|
|
809
863
|
const base = o.baseEnv && typeof o.baseEnv === "object" ? o.baseEnv : {};
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
:
|
|
815
|
-
|
|
816
|
-
|
|
864
|
+
const daemonArgs = Array.isArray(o.daemonArgs) ? o.daemonArgs : [];
|
|
865
|
+
const deps = {
|
|
866
|
+
augmentedPath: pathFn,
|
|
867
|
+
daemonArgs: () => daemonArgs,
|
|
868
|
+
resolveBin: typeof o.resolveBin === "function" ? o.resolveBin : () => CLAUDE_BIN,
|
|
869
|
+
};
|
|
870
|
+
// W4-E1: engine cohort continues the same --session-id through `cli.mjs run`
|
|
871
|
+
// (never --resume), on the marker's Anthropic slot mapped to a tier, with no retarget.
|
|
872
|
+
const engine = o.engine === "cohort" ? "cohort" : "claude";
|
|
873
|
+
if (engine === "cohort") {
|
|
874
|
+
const shape = engineRunShape({ target, laneModel: marker.model || "sonnet" });
|
|
875
|
+
const spec = buildSpawn({
|
|
876
|
+
lane: "resume",
|
|
877
|
+
model: shape.model,
|
|
878
|
+
prompt: marker.prompt,
|
|
879
|
+
sessionId: marker.claudeSessionId,
|
|
880
|
+
permissions: Array.isArray(o.permissionArgs) ? o.permissionArgs : [],
|
|
881
|
+
mcp: {},
|
|
882
|
+
knobs: shape.knobs,
|
|
883
|
+
env: base,
|
|
884
|
+
retarget: shape.retarget,
|
|
885
|
+
extraEnv: { MAESTRO_SESSION_ID: marker.sessionId || undefined },
|
|
886
|
+
...seatEngineFields({ engine, agentRoot: o.agentRoot || null, token: o.cohortToken || null, baseUrl: o.cohortBaseUrl || null }),
|
|
887
|
+
...(o.maxBudgetUsd != null ? { maxBudgetUsd: o.maxBudgetUsd } : {}),
|
|
888
|
+
}, deps);
|
|
889
|
+
if (!spec.ok) throw new Error(`buildResumeSpawn: ${spec.error.message}`);
|
|
890
|
+
return { args: spec.argv, env: spec.env, bin: spec.bin, modelFlag: shape.model, retargeting: false, refusedRetarget: null, engine };
|
|
891
|
+
}
|
|
892
|
+
const spawnInput = (t, flag) => ({
|
|
893
|
+
lane: "resume",
|
|
894
|
+
model: flag,
|
|
895
|
+
prompt: marker.prompt,
|
|
896
|
+
sessionId: marker.claudeSessionId,
|
|
897
|
+
permissions: Array.isArray(o.permissionArgs) ? o.permissionArgs : [],
|
|
898
|
+
mcp: {},
|
|
899
|
+
knobs: t,
|
|
900
|
+
env: base,
|
|
901
|
+
retarget: (t && t.envForSpawn) || {},
|
|
902
|
+
extraEnv: { MAESTRO_SESSION_ID: marker.sessionId || undefined },
|
|
903
|
+
});
|
|
904
|
+
let modelFlag = resumeModelFlag(marker, target);
|
|
905
|
+
let spec = buildSpawn(spawnInput(target, modelFlag), deps);
|
|
906
|
+
let refusedRetarget = null;
|
|
907
|
+
if (!spec.ok && /^retarget_/.test(spec.error.code)) {
|
|
908
|
+
// The retarget was refused (no third-party token): resume on the stock
|
|
909
|
+
// Anthropic path instead of handing that backend the seat's own
|
|
910
|
+
// credential. resumeModelFlag with a non-retarget target keeps F11 — a
|
|
911
|
+
// foreign pinned id is never passed to Anthropic.
|
|
912
|
+
refusedRetarget = spec.error;
|
|
913
|
+
// CF-11: the Anthropic rerun must not inherit the third-party decision's
|
|
914
|
+
// knobs (--max-turns/--effort/--agents were sized for that backend).
|
|
915
|
+
const stock = { ...target, envForSpawn: {}, modelFlag: null, maxTurns: null, effort: null, agentsJson: null };
|
|
916
|
+
modelFlag = resumeModelFlag(marker, stock);
|
|
917
|
+
spec = buildSpawn(spawnInput(stock, modelFlag), deps);
|
|
918
|
+
}
|
|
919
|
+
// A claude resume past the retarget fallback cannot fail: a failure is a defect.
|
|
920
|
+
if (!spec.ok) throw new Error(`buildResumeSpawn: ${spec.error.message}`);
|
|
921
|
+
return { args: spec.argv, env: spec.env, bin: spec.bin, modelFlag, retargeting: spec.retargeting, refusedRetarget, engine };
|
|
817
922
|
}
|
|
818
923
|
|
|
819
924
|
/**
|
|
@@ -832,7 +937,7 @@ export function buildResumeSpawn(o = {}) {
|
|
|
832
937
|
* and the fresh prompt continues the work.
|
|
833
938
|
*
|
|
834
939
|
* Audit F11: the argv and env are built through the SAME resolveSpawnTarget /
|
|
835
|
-
*
|
|
940
|
+
* runtime-adapter path as spawnSession (buildResumeSpawn), so a retargeted
|
|
836
941
|
* session resumes on its backend, the router's knobs ride along, and
|
|
837
942
|
* `--output-format json` makes the run's usage measurable for the ledger.
|
|
838
943
|
*
|
|
@@ -840,41 +945,85 @@ export function buildResumeSpawn(o = {}) {
|
|
|
840
945
|
* Best-effort; failures are logged, the marker stays (next reconcile retries
|
|
841
946
|
* up to the strike cap). On a clean close the marker is cleared here.
|
|
842
947
|
*/
|
|
843
|
-
|
|
948
|
+
/**
|
|
949
|
+
* The router's CURRENT decision for a resume marker. resolveSpawnTarget never
|
|
950
|
+
* throws (it degrades to the v1 path / stock CLI internally); the request is
|
|
951
|
+
* rebuilt from what the marker kept of the original classification. null when
|
|
952
|
+
* there is no router or it faulted.
|
|
953
|
+
*/
|
|
954
|
+
function resumeTargetFor(marker) {
|
|
844
955
|
const source = marker.source || "backlog";
|
|
845
|
-
// The router's CURRENT decision for this work. resolveSpawnTarget never
|
|
846
|
-
// throws (it degrades to the v1 path / stock CLI internally); the request is
|
|
847
|
-
// rebuilt from what the marker kept of the original classification.
|
|
848
|
-
let target = null;
|
|
849
956
|
try {
|
|
850
957
|
const routingConfig = getRoutingConfig();
|
|
851
|
-
if (routingConfig)
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
958
|
+
if (!routingConfig) return null;
|
|
959
|
+
const classResult = { model: marker.model || "sonnet", priority: marker.priority || "normal", summary: marker.summary || null };
|
|
960
|
+
const routingRequest = requestFromClassifierResult(classResult, { source, role: "responder" });
|
|
961
|
+
return resolveSpawnTarget(routingConfig, routingRequest, classResult, source, marker.modelFlag || marker.model || "sonnet");
|
|
856
962
|
} catch (err) {
|
|
857
963
|
// A router fault must not block a resume: fall back to the marker's flag.
|
|
858
964
|
console.warn(`[dispatcher] resume routing failed for ${marker.sessionId}, using marker flag: ${err.message}`);
|
|
859
|
-
|
|
965
|
+
return null;
|
|
860
966
|
}
|
|
861
|
-
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** The cohort-llm gate for a resume; a cold cache primes itself for the next reconcile. */
|
|
970
|
+
function defaultAdmitResume({ marker }) {
|
|
971
|
+
const target = resumeTargetFor(marker);
|
|
972
|
+
// W4-E1: the engine is resolved once here and rides the admission to the spawn.
|
|
973
|
+
const engine = readSeatEngine(AGENT_REPO_DIR);
|
|
974
|
+
const gate = cohortGateFor(target, engine);
|
|
975
|
+
if (!gate.ok && gate.cold) {
|
|
976
|
+
const c = getCohortClients();
|
|
977
|
+
if (c && c.quota) Promise.resolve().then(() => c.quota.canAdmit()).catch(() => null);
|
|
978
|
+
}
|
|
979
|
+
if (gate.ok && engine === "cohort") {
|
|
980
|
+
// An engine-cohort resume starts with a seat token. None cached (a cleared
|
|
981
|
+
// cache) is a short retry, exactly as a fresh dispatch parks it: a deferral,
|
|
982
|
+
// not a try, so the marker takes no strike. Mint one for the next reconcile.
|
|
983
|
+
const token = gate.token || seatTokenSync(getCohortClients()?.token);
|
|
984
|
+
if (!token) {
|
|
985
|
+
const tm = getCohortClients()?.token;
|
|
986
|
+
if (tm && typeof tm.getToken === "function") Promise.resolve().then(() => tm.getToken()).catch(() => null);
|
|
987
|
+
return { ok: false, cohort: true, engine, deferral: { kind: "retry", until: Date.now() + 30_000, code: "cohort_token_unavailable", message: "no cohort-llm seat token for the engine", refusal: null } };
|
|
988
|
+
}
|
|
989
|
+
return { ...gate, engine, token };
|
|
990
|
+
}
|
|
991
|
+
return { ...gate, engine };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function defaultSpawnResume({ marker, admission = null }) {
|
|
995
|
+
const target = resumeTargetFor(marker);
|
|
996
|
+
// W4-E1: the engine the admission resolved (else read now), its token, and the envelope's cap.
|
|
997
|
+
const engine = admission && (admission.engine === "cohort" || admission.engine === "claude") ? admission.engine : readSeatEngine(AGENT_REPO_DIR);
|
|
998
|
+
const engineFields = engine === "cohort"
|
|
999
|
+
? { engine, agentRoot: AGENT_REPO_DIR, cohortToken: (admission && admission.token) || seatTokenSync(getCohortClients()?.token), cohortBaseUrl: getCohortClients()?.token?.baseUrl || null, ...(() => { const b = engineSessionBudget(); return b.maxBudgetUsd != null ? { maxBudgetUsd: b.maxBudgetUsd } : {}; })() }
|
|
1000
|
+
: {};
|
|
1001
|
+
const { args, env: spawnEnv, bin, modelFlag, refusedRetarget } = buildResumeSpawn({
|
|
862
1002
|
marker,
|
|
863
1003
|
target,
|
|
864
1004
|
baseEnv: process.env,
|
|
865
1005
|
permissionArgs: sessionPermissionArgs({ source: "dispatcher-resume", priority: marker.priority }),
|
|
866
1006
|
daemonArgs: daemonClaudeArgs(),
|
|
1007
|
+
...engineFields,
|
|
867
1008
|
});
|
|
1009
|
+
if (refusedRetarget) {
|
|
1010
|
+
logSession({ event: "resume_retarget_refused", sessionId: marker.sessionId, code: refusedRetarget.code, error: refusedRetarget.message });
|
|
1011
|
+
}
|
|
1012
|
+
// An engine-cohort seat is cohort-served whatever routing picked (the same test
|
|
1013
|
+
// cohortGateFor applies), so its refusals key the "cohort" backend.
|
|
1014
|
+
const resumeEngineCohort = engine === "cohort";
|
|
1015
|
+
const resumeServedByCohort = resumeEngineCohort || (!refusedRetarget && isCohortRouted({ target }));
|
|
1016
|
+
const resumeBreaker = resumeServedByCohort ? rateGuardModule.COHORT_BACKEND : RATE_PROVIDER;
|
|
868
1017
|
const decisionId = (target && target.decisionId) || marker.decision_id || null;
|
|
869
1018
|
const startedAt = Date.now();
|
|
870
|
-
const proc = _spawn(
|
|
1019
|
+
const proc = _spawn(bin, args, { cwd: AGENT_REPO_DIR, env: spawnEnv, stdio: ["ignore", "pipe", "pipe"] });
|
|
871
1020
|
let stdout = "";
|
|
872
1021
|
let stderr = "";
|
|
873
1022
|
proc.stdout.on("data", (c) => { stdout += c.toString(); });
|
|
874
1023
|
proc.stderr.on("data", (c) => { stderr += c.toString(); });
|
|
875
1024
|
proc.on("close", (code) => {
|
|
876
1025
|
// A resumed run costs money too: parse its --output-format json usage.
|
|
877
|
-
try { recordDispatcherCost({ stdout, model: marker.model, durationMs: Date.now() - startedAt, exitCode: code, decisionId }); }
|
|
1026
|
+
try { recordDispatcherCost({ stdout, model: marker.model, durationMs: Date.now() - startedAt, exitCode: code, decisionId, billedBy: resumeServedByCohort ? COHORT_LLM_BILLED_BY : null }); }
|
|
878
1027
|
catch { /* cost tracking is best-effort */ }
|
|
879
1028
|
if (code === 0) {
|
|
880
1029
|
// Resume succeeded — the underlying work is done; clear the marker.
|
|
@@ -883,11 +1032,18 @@ function defaultSpawnResume({ marker }) {
|
|
|
883
1032
|
} else {
|
|
884
1033
|
// A usage limit or 429 during resume must open the breaker so subsequent
|
|
885
1034
|
// spawns gate. Usage-limit → hold until reset; 429 → transient backoff.
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1035
|
+
// Structured cohort refusal FIRST; an engine-cohort lane never reads the
|
|
1036
|
+
// English subscription-limit text (see the spawn path below).
|
|
1037
|
+
const resumeRefusal = resumeServedByCohort ? (rateGuard.classifyCohortRefusal?.(`${stdout}\n${stderr}`) ?? null) : null;
|
|
1038
|
+
const resumeUlText = resumeRefusal ? { isLimit: false, resetAt: null } : (rateGuard.classifyUsageLimit?.(stderr, { agentRoot: AGENT_REPO_DIR }) ?? { isLimit: false, resetAt: null });
|
|
1039
|
+
const ul = resumeEngineCohort ? { isLimit: false, resetAt: null } : resumeUlText;
|
|
1040
|
+
if (resumeEngineCohort && resumeUlText.isLimit) logSession({ event: "usage_limit_text_ignored", sessionId: marker.sessionId, engine: "cohort" });
|
|
1041
|
+
if (resumeRefusal) {
|
|
1042
|
+
try { rateGuard.recordCohortRefusal?.(resumeRefusal, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
|
|
1043
|
+
} else if (ul.isLimit) {
|
|
1044
|
+
try { rateGuard.recordUsageLimit(resumeBreaker, ul.resetAt, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
|
|
889
1045
|
} else if (rateGuard.classifyStderr(stderr)) {
|
|
890
|
-
try { rateGuard.recordRateLimit(
|
|
1046
|
+
try { rateGuard.recordRateLimit(resumeBreaker, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
|
|
891
1047
|
}
|
|
892
1048
|
logSession({ event: "resume_exit_nonzero", sessionId: marker.sessionId, item_id: marker.itemId, exit_code: code });
|
|
893
1049
|
}
|
|
@@ -897,18 +1053,29 @@ function defaultSpawnResume({ marker }) {
|
|
|
897
1053
|
});
|
|
898
1054
|
}
|
|
899
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* The entry shape is `lib/session/current-work.activeEntryFor` — the SAME
|
|
1058
|
+
* module that reads active.json back into the "what am I doing right now"
|
|
1059
|
+
* block, so the writer and the reader cannot drift. It carries the Cohort ask
|
|
1060
|
+
* identity (channelId + messageId) so the board row hq opens for an inbox ask
|
|
1061
|
+
* can be linked to this session ({@link linkActiveSessionBoard}), and the
|
|
1062
|
+
* board item id when a backlog row is for one.
|
|
1063
|
+
*/
|
|
900
1064
|
function writeActiveSession(sessionId, entry) {
|
|
901
1065
|
try {
|
|
902
1066
|
let active = {};
|
|
903
1067
|
try { active = JSON.parse(readFileSync(ACTIVE_PATH, "utf-8")); } catch {}
|
|
904
|
-
active[sessionId] = {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1068
|
+
active[sessionId] = activeEntryFor({ item: entry.item, classResult: entry.classResult, source: entry.source, now: Date.now() });
|
|
1069
|
+
// A board row that resolved BEFORE the spawn (the daemon's `working` track
|
|
1070
|
+
// is fire-and-forget and can win the race) is applied now, not lost.
|
|
1071
|
+
const ask = active[sessionId].ask;
|
|
1072
|
+
if (ask) {
|
|
1073
|
+
const pending = pendingBoardLinks.get(`${ask.channelId}:${ask.messageId}`);
|
|
1074
|
+
if (pending) {
|
|
1075
|
+
active[sessionId] = { ...active[sessionId], boardTaskId: pending.taskId, boardCol: pending.col || null };
|
|
1076
|
+
pendingBoardLinks.delete(`${ask.channelId}:${ask.messageId}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
912
1079
|
const tmpPath = ACTIVE_PATH + ".tmp";
|
|
913
1080
|
writeFileSync(tmpPath, JSON.stringify(active, null, 2));
|
|
914
1081
|
renameSync(tmpPath, ACTIVE_PATH);
|
|
@@ -917,6 +1084,45 @@ function writeActiveSession(sessionId, entry) {
|
|
|
917
1084
|
}
|
|
918
1085
|
}
|
|
919
1086
|
|
|
1087
|
+
/** Board rows that resolved for an ask whose session is not in active.json yet. Bounded. */
|
|
1088
|
+
const pendingBoardLinks = new Map();
|
|
1089
|
+
const PENDING_BOARD_LINKS_MAX = 64;
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Attach the board row hq opened for an ask to the live session working it,
|
|
1093
|
+
* so the current-work block can cite the item id. Called by the daemon when
|
|
1094
|
+
* its `working` board.track resolves with a taskId (agent-daemon.mjs).
|
|
1095
|
+
*
|
|
1096
|
+
* PURE core (`linkBoardTask`) + one atomic rewrite of active.json. Fail-open:
|
|
1097
|
+
* a board link must never break a dispatch. When no live session carries the
|
|
1098
|
+
* ask yet (the track won the race against the spawn), the link is parked and
|
|
1099
|
+
* applied by the next writeActiveSession for that ask.
|
|
1100
|
+
*
|
|
1101
|
+
* @param {{channelId:string, messageId:string}} ask
|
|
1102
|
+
* @param {{taskId:string, col?:string|null}} row
|
|
1103
|
+
* @returns {{linked:boolean, sessionId:string|null, parked:boolean}}
|
|
1104
|
+
*/
|
|
1105
|
+
export function linkActiveSessionBoard(ask, row) {
|
|
1106
|
+
try {
|
|
1107
|
+
if (!ask || !ask.channelId || !ask.messageId || !row || !row.taskId) return { linked: false, sessionId: null, parked: false };
|
|
1108
|
+
let active = {};
|
|
1109
|
+
try { active = JSON.parse(readFileSync(ACTIVE_PATH, "utf-8")); } catch {}
|
|
1110
|
+
const r = linkBoardTask(active, ask, row);
|
|
1111
|
+
if (!r.sessionId) {
|
|
1112
|
+
if (pendingBoardLinks.size >= PENDING_BOARD_LINKS_MAX) pendingBoardLinks.delete(pendingBoardLinks.keys().next().value);
|
|
1113
|
+
pendingBoardLinks.set(`${ask.channelId}:${ask.messageId}`, { taskId: String(row.taskId), col: row.col || null });
|
|
1114
|
+
return { linked: false, sessionId: null, parked: true };
|
|
1115
|
+
}
|
|
1116
|
+
const tmpPath = ACTIVE_PATH + ".tmp";
|
|
1117
|
+
writeFileSync(tmpPath, JSON.stringify(r.active, null, 2));
|
|
1118
|
+
renameSync(tmpPath, ACTIVE_PATH);
|
|
1119
|
+
return { linked: true, sessionId: r.sessionId, parked: false };
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
console.warn(`[dispatcher] Failed to link board row to session: ${err.message}`);
|
|
1122
|
+
return { linked: false, sessionId: null, parked: false };
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
920
1126
|
function removeActiveSession(sessionId) {
|
|
921
1127
|
try {
|
|
922
1128
|
let active = {};
|
|
@@ -1084,6 +1290,13 @@ export function canDispatchBacklog(item) {
|
|
|
1084
1290
|
* fires onClose once it is later drained and spawned). It does NOT fire on the
|
|
1085
1291
|
* pre-spawn drop paths (backlog dedup / claim-denied / governor-DEFER backlog),
|
|
1086
1292
|
* which leave the durable on-disk item untouched for the next sweep anyway.
|
|
1293
|
+
* @param {(o:{sessionId:string, item:object})=>void} [opts.onSpawn]
|
|
1294
|
+
* Invoked EXACTLY ONCE, when a child process has actually been started for
|
|
1295
|
+
* this entry, with the session id the dispatcher minted. This is the ONLY
|
|
1296
|
+
* truthful "started" signal: `dispatch()` returns silently on every not-now
|
|
1297
|
+
* path (rate-limited, governor QUEUE/DEFER, backlog cap, claim denied, the
|
|
1298
|
+
* cohort pre-spawn gate), and a caller that announced "started" at the call
|
|
1299
|
+
* site announced it for those too. Survives queueing like onClose.
|
|
1087
1300
|
*/
|
|
1088
1301
|
export function dispatch(prompt, item, classResult, source = "inbox", opts = {}) {
|
|
1089
1302
|
// `obligationKey` names the debt this session was spawned to discharge. It is
|
|
@@ -1096,6 +1309,8 @@ export function dispatch(prompt, item, classResult, source = "inbox", opts = {})
|
|
|
1096
1309
|
prompt, item, classResult, source,
|
|
1097
1310
|
obligationKey: opts.obligationKey || null,
|
|
1098
1311
|
onClose: typeof opts.onClose === "function" ? opts.onClose : null,
|
|
1312
|
+
onSpawn: typeof opts.onSpawn === "function" ? opts.onSpawn : null,
|
|
1313
|
+
cohortPrimed: opts.cohortPrimed === true,
|
|
1099
1314
|
};
|
|
1100
1315
|
const priority = classResult.priority;
|
|
1101
1316
|
const isPriorityInbox = source === "inbox" && (priority === "critical" || priority === "high");
|
|
@@ -1386,13 +1601,6 @@ function spawnSession(entry) {
|
|
|
1386
1601
|
? (source === "backlog" ? OPUS_BACKLOG_TIMEOUT : OPUS_INBOX_TIMEOUT)
|
|
1387
1602
|
: (source === "backlog" ? SONNET_BACKLOG_TIMEOUT : SONNET_INBOX_TIMEOUT);
|
|
1388
1603
|
|
|
1389
|
-
// Track backlog items to prevent retry storms
|
|
1390
|
-
if (source === "backlog") {
|
|
1391
|
-
const key = backlogKey(item);
|
|
1392
|
-
activeBacklogKeys.add(key);
|
|
1393
|
-
backlogRetryCount.set(key, (backlogRetryCount.get(key) || 0) + 1);
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
1604
|
// Resolve the spawn target via the model router. v2 configs go through
|
|
1397
1605
|
// resolveChain (full RouteDecision: model + retarget env + spawn knobs +
|
|
1398
1606
|
// decision_id + estimated cost + failover chain); v1 configs keep the legacy
|
|
@@ -1401,7 +1609,44 @@ function spawnSession(entry) {
|
|
|
1401
1609
|
const routingConfig = getRoutingConfig();
|
|
1402
1610
|
const routingRequest = requestFromClassifierResult(classResult, { source, role: "responder" });
|
|
1403
1611
|
const target = resolveSpawnTarget(routingConfig, routingRequest, classResult, source, model, item);
|
|
1404
|
-
|
|
1612
|
+
let effectiveModelFlag = target.modelFlag || model;
|
|
1613
|
+
|
|
1614
|
+
// cohort-llm pre-spawn gate (design C6 / §8.3). Stock Claude Code retries a
|
|
1615
|
+
// 429 ten times and ignores the gateway's quota headers, so a session that
|
|
1616
|
+
// cohort-llm would refuse must not start. Decided BEFORE any counter, claim
|
|
1617
|
+
// or lock below moves, so a deferral leaves no trace but the requeue. Human
|
|
1618
|
+
// replies are not exempt: a refused seat would 429 them just the same.
|
|
1619
|
+
//
|
|
1620
|
+
// W4-E1 (CF-44): the seat's engine is resolved ONCE for this spawn and used by
|
|
1621
|
+
// the gate, the spawn and the breaker alike. Read per spawn, so flipping
|
|
1622
|
+
// runtime.engine back to "claude" restores claude on the next spawn.
|
|
1623
|
+
const seatEngine = readSeatEngine(AGENT_REPO_DIR);
|
|
1624
|
+
const engineCohortSpawn = seatEngine === "cohort";
|
|
1625
|
+
let engineToken = null;
|
|
1626
|
+
{
|
|
1627
|
+
const gate = cohortGateFor(target, seatEngine);
|
|
1628
|
+
if (!gate.ok) {
|
|
1629
|
+
deferSpawnForCohort(entry, gate, target);
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
if (engineCohortSpawn) {
|
|
1633
|
+
// The engine starts with the seat token the admission read. None (a
|
|
1634
|
+
// cleared cache) is a short retry, parked like any cohort deferral —
|
|
1635
|
+
// never a claude spawn on the seat's Anthropic credential.
|
|
1636
|
+
engineToken = gate.token || seatTokenSync(getCohortClients()?.token);
|
|
1637
|
+
if (!engineToken) {
|
|
1638
|
+
deferSpawnForCohort(entry, { ok: false, cohort: true, deferral: { kind: "retry", until: Date.now() + 30_000, code: "cohort_token_unavailable", message: "no cohort-llm seat token for the engine", refusal: null } }, target);
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Track backlog items to prevent retry storms
|
|
1645
|
+
if (source === "backlog") {
|
|
1646
|
+
const key = backlogKey(item);
|
|
1647
|
+
activeBacklogKeys.add(key);
|
|
1648
|
+
backlogRetryCount.set(key, (backlogRetryCount.get(key) || 0) + 1);
|
|
1649
|
+
}
|
|
1405
1650
|
|
|
1406
1651
|
// Session continuity. A fresh `randomUUID()` per dispatch meant every full
|
|
1407
1652
|
// session in a live thread started COLD — the agent re-read the room, re-did
|
|
@@ -1452,72 +1697,71 @@ function spawnSession(entry) {
|
|
|
1452
1697
|
}
|
|
1453
1698
|
const claudeSessionId = resumedSessionId || randomUUID();
|
|
1454
1699
|
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1457
|
-
//
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
"--model", effectiveModelFlag,
|
|
1700
|
+
// argv + env come from the runtime adapter (lib/runtime/adapter.mjs, lane
|
|
1701
|
+
// "dispatcher"). What it builds, unchanged from the inline version:
|
|
1702
|
+
// argv --print --output-format json <permissions> <daemon MCP args>
|
|
1703
|
+
// <router knobs> --session-id <id> --model <flag> <prompt>
|
|
1704
|
+
// --output-format json so stdout carries the REAL token usage for the
|
|
1705
|
+
// cost ledger (recovery C1); the dispatcher's stdout only goes to a
|
|
1706
|
+
// per-session log, never to a user. The v2 knobs (--max-turns,
|
|
1707
|
+
// --effort, --agents Haiku-Explore) are null on v1 / no-config spawns.
|
|
1708
|
+
// env no router / an Anthropic decision: stock posture — strip
|
|
1709
|
+
// ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN so `claude` rides the seat's
|
|
1710
|
+
// subscription credential (CEO directive 2026-04-27).
|
|
1711
|
+
// A v2 retarget (envForSpawn carries ANTHROPIC_BASE_URL): the one
|
|
1712
|
+
// runtime env builder scrubs every foreign credential AND the seat's
|
|
1713
|
+
// OAuth token, and forces ANTHROPIC_API_KEY="" and
|
|
1714
|
+
// CLAUDE_CODE_ATTRIBUTION_HEADER=0.
|
|
1715
|
+
// MAESTRO_SESSION_ID / MAESTRO_OBLIGATION_KEY are set after every merge so a
|
|
1716
|
+
// router target can never drop the delivery-receipt attribution; they are
|
|
1717
|
+
// plain identifiers, never credentials.
|
|
1718
|
+
// W4-E1: an engine-cohort seat runs `cli.mjs run` — its tier from the router
|
|
1719
|
+
// only when the router chose cohort, no retarget, and a per-session spend cap
|
|
1720
|
+
// from the seat's funded envelope. A claude seat's input is exactly as before.
|
|
1721
|
+
const engineShape = engineCohortSpawn ? engineRunShape({ target, laneModel: model }) : null;
|
|
1722
|
+
const spawnInput = {
|
|
1723
|
+
lane: "dispatcher",
|
|
1724
|
+
bin: CLAUDE_BIN,
|
|
1725
|
+
model: engineShape ? engineShape.model : effectiveModelFlag,
|
|
1482
1726
|
prompt,
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
:
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
const proc = _spawn(
|
|
1727
|
+
sessionId: claudeSessionId,
|
|
1728
|
+
permissions: sessionPermissionArgs({ source: "dispatcher", priority: classResult?.priority }),
|
|
1729
|
+
mcp: {},
|
|
1730
|
+
knobs: engineShape ? engineShape.knobs : target,
|
|
1731
|
+
env: process.env,
|
|
1732
|
+
retarget: engineShape ? engineShape.retarget : (target.envForSpawn || {}),
|
|
1733
|
+
extraEnv: { MAESTRO_SESSION_ID: sessionId, MAESTRO_OBLIGATION_KEY: entry.obligationKey || undefined },
|
|
1734
|
+
...seatEngineFields({ engine: seatEngine, agentRoot: AGENT_REPO_DIR, token: engineToken, baseUrl: engineCohortSpawn ? getCohortClients()?.token?.baseUrl : null }),
|
|
1735
|
+
...(engineCohortSpawn ? engineSessionBudget() : {}),
|
|
1736
|
+
};
|
|
1737
|
+
let spawnSpec = buildSpawn(spawnInput);
|
|
1738
|
+
let refusedRetarget = null;
|
|
1739
|
+
if (!spawnSpec.ok && /^retarget_/.test(spawnSpec.error.code)) {
|
|
1740
|
+
// Refused retarget (no third-party token). Answer on the stock Anthropic
|
|
1741
|
+
// path with the classifier's Anthropic model rather than send that backend
|
|
1742
|
+
// the seat's own credential — or drop the item.
|
|
1743
|
+
console.warn(`[dispatcher] ${spawnSpec.error.message} — spawning ${sessionId} on the stock path`);
|
|
1744
|
+
logSession({ event: "retarget_refused", sessionId, code: spawnSpec.error.code, decision_id: target.decisionId || null });
|
|
1745
|
+
refusedRetarget = spawnSpec.error;
|
|
1746
|
+
effectiveModelFlag = model;
|
|
1747
|
+
// CF-11: the rerun is an ANTHROPIC session. The third-party decision's
|
|
1748
|
+
// --max-turns/--effort/--agents were sized for that backend and must not
|
|
1749
|
+
// ride along.
|
|
1750
|
+
spawnSpec = buildSpawn({ ...spawnInput, model, knobs: null, retarget: {} });
|
|
1751
|
+
}
|
|
1752
|
+
// A seat on engine cohort is cohort-served whatever routing picked — the same
|
|
1753
|
+
// test cohortGateFor admits it by — so its breaker is the "cohort" backend.
|
|
1754
|
+
const engineCohort = engineCohortSpawn;
|
|
1755
|
+
const servedByCohort = engineCohort || (!refusedRetarget && isCohortRouted({ target }));
|
|
1756
|
+
const breakerProvider = servedByCohort ? rateGuardModule.COHORT_BACKEND : RATE_PROVIDER;
|
|
1757
|
+
// Both engines are wired and the engine's token was checked before any claim,
|
|
1758
|
+
// so a failure past the retarget fallback is a defect.
|
|
1759
|
+
if (!spawnSpec.ok) throw new Error(`[dispatcher] runtime adapter: ${spawnSpec.error.message}`);
|
|
1760
|
+
const args = spawnSpec.argv;
|
|
1761
|
+
|
|
1762
|
+
const proc = _spawn(spawnSpec.bin, args, {
|
|
1519
1763
|
cwd: AGENT_REPO_DIR,
|
|
1520
|
-
env:
|
|
1764
|
+
env: spawnSpec.env,
|
|
1521
1765
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1522
1766
|
});
|
|
1523
1767
|
|
|
@@ -1525,14 +1769,20 @@ function spawnSession(entry) {
|
|
|
1525
1769
|
// and it isn't the stock Anthropic CLI no-op. The v2 path also carries the
|
|
1526
1770
|
// decision_id + the one-line explain so `maestro router why` can join this
|
|
1527
1771
|
// session to its routing-audit + ledger rows.
|
|
1772
|
+
//
|
|
1773
|
+
// CF-11: the event describes the run that ACTUALLY happened. After a refused
|
|
1774
|
+
// retarget that is the stock Anthropic session with the classifier's model
|
|
1775
|
+
// and no router knobs; the intended backend rides along as `refused`.
|
|
1528
1776
|
if (target.backend && target.transport !== "anthropic-cli") {
|
|
1529
1777
|
logSession({
|
|
1530
1778
|
event: "routed",
|
|
1531
1779
|
sessionId,
|
|
1532
1780
|
decision_id: target.decisionId,
|
|
1533
|
-
backend: target.backend,
|
|
1534
|
-
transport: target.transport,
|
|
1535
|
-
model: target.model,
|
|
1781
|
+
backend: refusedRetarget ? "anthropic" : target.backend,
|
|
1782
|
+
transport: refusedRetarget ? "anthropic-cli" : target.transport,
|
|
1783
|
+
model: refusedRetarget ? model : target.model,
|
|
1784
|
+
knobs: refusedRetarget ? null : { maxTurns: target.maxTurns ?? null, effort: target.effort ?? null, agents: target.agentsJson ? true : false },
|
|
1785
|
+
...(refusedRetarget ? { refused: { backend: target.backend, model: target.model, code: refusedRetarget.code } } : {}),
|
|
1536
1786
|
tried: target.decision ? target.decision.tried : target._v1Resolved?.tried,
|
|
1537
1787
|
fallback_reason: target.decision ? target.decision.audit?.fallback_reason : target._v1Resolved?.fallback_reason,
|
|
1538
1788
|
explain: target.explain,
|
|
@@ -1562,6 +1812,11 @@ function spawnSession(entry) {
|
|
|
1562
1812
|
const startTime = Date.now();
|
|
1563
1813
|
activeSessions.set(sessionId, { process: proc, item, classResult, startTime, model, source, claudeSessionId });
|
|
1564
1814
|
writeActiveSession(sessionId, entry);
|
|
1815
|
+
// A child exists from this line on — the one moment "started" is true.
|
|
1816
|
+
if (entry.onSpawn) {
|
|
1817
|
+
try { entry.onSpawn({ sessionId, item }); }
|
|
1818
|
+
catch (err) { console.warn(`[dispatcher] onSpawn callback threw for ${sessionId}: ${err.message}`); }
|
|
1819
|
+
}
|
|
1565
1820
|
|
|
1566
1821
|
// WS4: drop the in-flight resume marker. Present-after-crash = mid-flight, so
|
|
1567
1822
|
// a startup reconcile can re-dispatch `claude --print --resume`. Cleared on a
|
|
@@ -1667,7 +1922,7 @@ function spawnSession(entry) {
|
|
|
1667
1922
|
// bound re-dispatch.
|
|
1668
1923
|
if (code === 0) {
|
|
1669
1924
|
clearResumePending(sessionId);
|
|
1670
|
-
try { rateGuard.recordSuccess(
|
|
1925
|
+
try { rateGuard.recordSuccess(breakerProvider, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
|
|
1671
1926
|
} else {
|
|
1672
1927
|
// A Max/subscription USAGE limit ("you've hit your session limit · resets
|
|
1673
1928
|
// 9:40pm") is checked FIRST and on BOTH streams — for `claude --print` it
|
|
@@ -1675,15 +1930,32 @@ function spawnSession(entry) {
|
|
|
1675
1930
|
// RESETS (not a decorrelated 429 backoff), so the seat stops storming a
|
|
1676
1931
|
// drained pool. A plain 429/overload falls through to the transient path.
|
|
1677
1932
|
const combined = `${stdout || ""}\n${stderr || ""}`;
|
|
1678
|
-
|
|
1679
|
-
|
|
1933
|
+
// A cohort-llm refusal is checked FIRST on a cohort-served run: its body
|
|
1934
|
+
// says "rate_limit_error", and the English path would otherwise read a
|
|
1935
|
+
// spent seat window as an Anthropic incident. It holds the "cohort"
|
|
1936
|
+
// backend until the refusal's resetsAt and never opens a provider breaker.
|
|
1937
|
+
const cohortRefusal = servedByCohort ? (rateGuard.classifyCohortRefusal?.(combined) ?? null) : null;
|
|
1938
|
+
// An engine-cohort lane has no Claude subscription: its limits arrive as the
|
|
1939
|
+
// structured refusal above. Its English text ("Session limit reached") is
|
|
1940
|
+
// never read as a subscription usage limit — without a structured refusal
|
|
1941
|
+
// it is logged and left to the transient-429 check (cohort backend).
|
|
1942
|
+
const ulText = cohortRefusal ? { isLimit: false, resetAt: null } : (rateGuard.classifyUsageLimit?.(combined, { agentRoot: AGENT_REPO_DIR }) ?? { isLimit: false, resetAt: null });
|
|
1943
|
+
const ul = engineCohort ? { isLimit: false, resetAt: null } : ulText;
|
|
1944
|
+
if (engineCohort && ulText.isLimit) logSession({ event: "usage_limit_text_ignored", sessionId, engine: "cohort" });
|
|
1945
|
+
if (cohortRefusal) {
|
|
1680
1946
|
try {
|
|
1681
|
-
const rec = rateGuard.
|
|
1947
|
+
const rec = rateGuard.recordCohortRefusal(cohortRefusal, { agentRoot: AGENT_REPO_DIR });
|
|
1948
|
+
logSession({ event: "cohort_refusal_recorded", sessionId, code: cohortRefusal.code, disposition: rec.disposition && rec.disposition.kind, open_until: rec.openUntil });
|
|
1949
|
+
} catch { /* */ }
|
|
1950
|
+
try { const c = getCohortClients(); if (c && c.quota && typeof c.quota.invalidate === "function") c.quota.invalidate(); } catch { /* */ }
|
|
1951
|
+
} else if (ul.isLimit) {
|
|
1952
|
+
try {
|
|
1953
|
+
const rec = rateGuard.recordUsageLimit(breakerProvider, ul.resetAt, { agentRoot: AGENT_REPO_DIR });
|
|
1682
1954
|
logSession({ event: "usage_limit_recorded", sessionId, open_until: rec.openUntil, reset_at: rec.resetAt });
|
|
1683
1955
|
} catch { /* */ }
|
|
1684
1956
|
} else if (rateGuard.classifyStderr(stderr)) {
|
|
1685
1957
|
try {
|
|
1686
|
-
const rec = rateGuard.recordRateLimit(
|
|
1958
|
+
const rec = rateGuard.recordRateLimit(breakerProvider, { agentRoot: AGENT_REPO_DIR });
|
|
1687
1959
|
logSession({ event: "rate_limit_recorded", sessionId, open_until: rec.openUntil, consecutive: rec.consecutive429 });
|
|
1688
1960
|
} catch { /* */ }
|
|
1689
1961
|
}
|
|
@@ -1777,6 +2049,7 @@ function spawnSession(entry) {
|
|
|
1777
2049
|
durationMs: Date.now() - startTime,
|
|
1778
2050
|
exitCode: code,
|
|
1779
2051
|
decisionId: target.decisionId,
|
|
2052
|
+
billedBy: servedByCohort ? COHORT_LLM_BILLED_BY : null,
|
|
1780
2053
|
});
|
|
1781
2054
|
if (!costUsage.ok) {
|
|
1782
2055
|
logSession({ event: "cost_usage_parse_failed", sessionId, reason: costUsage.reason });
|
|
@@ -1961,10 +2234,181 @@ function drainQueue() {
|
|
|
1961
2234
|
// Commit: remove from whichever queue it sat at the head of, then spawn.
|
|
1962
2235
|
if (priorityQueue[0] === next) priorityQueue.shift();
|
|
1963
2236
|
else normalQueue.shift();
|
|
2237
|
+
// A cohort-llm deferral parks the entry OFF the queue (cohortParked) and
|
|
2238
|
+
// returns false; keep draining — the entries behind it may route elsewhere.
|
|
1964
2239
|
spawnSession(next);
|
|
1965
2240
|
}
|
|
1966
2241
|
}
|
|
1967
2242
|
|
|
2243
|
+
// ── cohort-llm pre-spawn gate ────────────────────────────────────────────────
|
|
2244
|
+
|
|
2245
|
+
/**
|
|
2246
|
+
* The gate for one spawn target. Synchronous (spawnSession must register its
|
|
2247
|
+
* child synchronously), so it reads the quota CACHE; a cold cache answers
|
|
2248
|
+
* `{cold:true}` and the caller primes it.
|
|
2249
|
+
*/
|
|
2250
|
+
function cohortGateFor(target, engine = readSeatEngine(AGENT_REPO_DIR)) {
|
|
2251
|
+
// A seat on engine cohort is cohort-metered whatever the router picked: the
|
|
2252
|
+
// engine talks to cohort-llm directly, so its spawns are gated even when no
|
|
2253
|
+
// model routing targets the cohort provider. The caller passes the engine it
|
|
2254
|
+
// resolved for this spawn (W4-E1), so the gate and the spawn cannot disagree.
|
|
2255
|
+
if (!isCohortRouted({ engine, target })) return { ok: true, cohort: false };
|
|
2256
|
+
const clients = getCohortClients();
|
|
2257
|
+
if (!clients || !clients.quota) {
|
|
2258
|
+
return {
|
|
2259
|
+
ok: false,
|
|
2260
|
+
cohort: true,
|
|
2261
|
+
deferral: { kind: "retry", until: Date.now() + 30_000, code: "cohort_quota_unconfigured", message: "this seat has no cohort-llm client (no OrgApiKey?)", refusal: null },
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
return admitSpawnCached({ engine, target }, { quota: clients.quota, token: clients.token, rateGuard, rateGuardDeps: { agentRoot: AGENT_REPO_DIR } });
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/**
|
|
2268
|
+
* W4-E1 (design §8.3, A2 "no per-session dollar cap"): an engine-cohort
|
|
2269
|
+
* session's `--max-budget-usd`, from the seat's funded envelope — one day's
|
|
2270
|
+
* binding allowance (lib/runtime/seat-engine.mjs sessionBudgetUsd). No funded
|
|
2271
|
+
* envelope, or a budget guard that cannot say: no cap is invented.
|
|
2272
|
+
* @returns {{maxBudgetUsd:number}|{}}
|
|
2273
|
+
*/
|
|
2274
|
+
function engineSessionBudget() {
|
|
2275
|
+
try {
|
|
2276
|
+
const allowance = typeof budgetGuard.dailyAllowance === "function" ? budgetGuard.dailyAllowance({ agentRoot: AGENT_REPO_DIR }) : null;
|
|
2277
|
+
const usd = sessionBudgetUsd(allowance);
|
|
2278
|
+
return usd == null ? {} : { maxBudgetUsd: usd };
|
|
2279
|
+
} catch {
|
|
2280
|
+
return {}; // the guard's own bands still govern the seat
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* Hand a deferred spawn back without losing it:
|
|
2286
|
+
* inbox PARKED off the queue (cohortParked — never dropped) until the
|
|
2287
|
+
* deferral lifts or the cold cache is primed, then put back at the
|
|
2288
|
+
* head of its queue. Parking, not requeueing at the head: a seat
|
|
2289
|
+
* that routes only SOME work to cohort must keep draining the
|
|
2290
|
+
* entries behind a refused one (they may route to Anthropic), and a
|
|
2291
|
+
* 7d/30d window or a credits hold must not stall the whole inbox.
|
|
2292
|
+
* backlog the item claim is released and the item cools until the deferral
|
|
2293
|
+
* lifts (the on-disk item is retried by the sweep).
|
|
2294
|
+
* A cold cache is primed and the work retried once the reading lands. The
|
|
2295
|
+
* deferral is logged once per hold, not on every re-gate.
|
|
2296
|
+
*/
|
|
2297
|
+
function deferSpawnForCohort(entry, gate, target) {
|
|
2298
|
+
const { item, classResult, source } = entry;
|
|
2299
|
+
const d = gate.deferral || {};
|
|
2300
|
+
const until = Number.isFinite(d.until) ? d.until : Date.now() + 30_000;
|
|
2301
|
+
const holdKey = `${gate.cold === true ? "cold" : `${d.code || ""}|${d.kind || ""}|${until}`}`;
|
|
2302
|
+
if (entry.cohortHoldKey !== holdKey) {
|
|
2303
|
+
entry.cohortHoldKey = holdKey;
|
|
2304
|
+
logSession({
|
|
2305
|
+
event: "cohort_deferred",
|
|
2306
|
+
source,
|
|
2307
|
+
code: d.code || null,
|
|
2308
|
+
kind: d.kind || null,
|
|
2309
|
+
until: new Date(until).toISOString(),
|
|
2310
|
+
cold: gate.cold === true,
|
|
2311
|
+
decision_id: (target && target.decisionId) || null,
|
|
2312
|
+
summary: classResult && classResult.summary,
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
if (source === "backlog") {
|
|
2316
|
+
if (item && item.id) { try { releaseItemClaim(item.id); } catch { /* */ } }
|
|
2317
|
+
if (!gate.cold) {
|
|
2318
|
+
backlogCooldownUntil.set(backlogKey(item), until);
|
|
2319
|
+
try { saveCooldowns(); } catch { /* */ }
|
|
2320
|
+
}
|
|
2321
|
+
} else {
|
|
2322
|
+
parkForCohort(entry, gate.cold ? null : until);
|
|
2323
|
+
}
|
|
2324
|
+
if (gate.cold) primeCohortQuota(entry);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// ── cohort-parked inbox entries ──────────────────────────────────────────────
|
|
2328
|
+
// {entry, until} — `until` null means "waiting on a cold-cache prime".
|
|
2329
|
+
const cohortParked = [];
|
|
2330
|
+
let cohortParkTimer = null;
|
|
2331
|
+
/** Parked entries re-check at least this often (and setTimeout cannot hold a 30d delay). */
|
|
2332
|
+
const COHORT_PARK_RECHECK_MAX_MS = 10 * 60_000;
|
|
2333
|
+
const COHORT_PARK_RECHECK_MIN_MS = 1_000;
|
|
2334
|
+
let cohortInboxPrimeInFlight = false;
|
|
2335
|
+
|
|
2336
|
+
function parkForCohort(entry, until) {
|
|
2337
|
+
cohortParked.push({ entry, until });
|
|
2338
|
+
armCohortRelease();
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function armCohortRelease() {
|
|
2342
|
+
if (cohortParkTimer) { clearTimeout(cohortParkTimer); cohortParkTimer = null; }
|
|
2343
|
+
const timed = cohortParked.filter((p) => p.until !== null);
|
|
2344
|
+
if (timed.length === 0) return;
|
|
2345
|
+
const earliest = Math.min(...timed.map((p) => p.until));
|
|
2346
|
+
const delay = Math.max(COHORT_PARK_RECHECK_MIN_MS, Math.min(COHORT_PARK_RECHECK_MAX_MS, earliest - Date.now()));
|
|
2347
|
+
cohortParkTimer = setTimeout(() => {
|
|
2348
|
+
cohortParkTimer = null;
|
|
2349
|
+
try { releaseCohortParked(); } catch (err) { console.warn(`[dispatcher] cohort-llm release failed: ${err.message}`); }
|
|
2350
|
+
}, delay);
|
|
2351
|
+
if (cohortParkTimer && typeof cohortParkTimer.unref === "function") cohortParkTimer.unref();
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* Put parked entries back at the head of their queues (original order) and
|
|
2356
|
+
* drain: `cold:true` releases the entries waiting on a prime; otherwise the
|
|
2357
|
+
* ones whose deferral has lifted. They are gated again on the way out.
|
|
2358
|
+
*/
|
|
2359
|
+
function releaseCohortParked({ cold = false } = {}) {
|
|
2360
|
+
const now = Date.now();
|
|
2361
|
+
const back = [];
|
|
2362
|
+
const keep = [];
|
|
2363
|
+
for (const p of cohortParked) {
|
|
2364
|
+
const due = cold ? p.until === null : p.until !== null && p.until <= now;
|
|
2365
|
+
(due ? back : keep).push(p);
|
|
2366
|
+
}
|
|
2367
|
+
cohortParked.length = 0;
|
|
2368
|
+
cohortParked.push(...keep);
|
|
2369
|
+
for (let i = back.length - 1; i >= 0; i--) {
|
|
2370
|
+
const e = back[i].entry;
|
|
2371
|
+
const high = e.classResult && (e.classResult.priority === "critical" || e.classResult.priority === "high");
|
|
2372
|
+
(high ? priorityQueue : normalQueue).unshift(e);
|
|
2373
|
+
}
|
|
2374
|
+
armCohortRelease();
|
|
2375
|
+
if (back.length > 0) drainQueue();
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
/** Fetch a quota reading, then retry the deferred work once. */
|
|
2379
|
+
function primeCohortQuota(entry) {
|
|
2380
|
+
const clients = getCohortClients();
|
|
2381
|
+
if (!clients || !clients.quota) return;
|
|
2382
|
+
if (entry.source !== "backlog") {
|
|
2383
|
+
if (cohortInboxPrimeInFlight) return; // one read releases every cold-parked entry
|
|
2384
|
+
cohortInboxPrimeInFlight = true;
|
|
2385
|
+
}
|
|
2386
|
+
Promise.resolve()
|
|
2387
|
+
.then(() => clients.quota.canAdmit())
|
|
2388
|
+
.catch(() => null)
|
|
2389
|
+
.then(() => {
|
|
2390
|
+
try {
|
|
2391
|
+
if (entry.source === "backlog") {
|
|
2392
|
+
// One re-dispatch per prime. A failed read is cached too, so the
|
|
2393
|
+
// second gate answers with a real deferral instead of cold again.
|
|
2394
|
+
if (entry.cohortPrimed) return;
|
|
2395
|
+
dispatch(entry.prompt, entry.item, entry.classResult, "backlog", { obligationKey: entry.obligationKey, onClose: entry.onClose, onSpawn: entry.onSpawn, cohortPrimed: true });
|
|
2396
|
+
} else {
|
|
2397
|
+
cohortInboxPrimeInFlight = false;
|
|
2398
|
+
releaseCohortParked({ cold: true });
|
|
2399
|
+
}
|
|
2400
|
+
} catch (err) {
|
|
2401
|
+
cohortInboxPrimeInFlight = false;
|
|
2402
|
+
console.warn(`[dispatcher] cohort-llm retry after quota prime failed: ${err.message}`);
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/** Test seam: run one drain pass now (the queue is otherwise drained by closes and timers). */
|
|
2408
|
+
export function _drainQueueForTests() {
|
|
2409
|
+
drainQueue();
|
|
2410
|
+
}
|
|
2411
|
+
|
|
1968
2412
|
/** Get current status for health checks */
|
|
1969
2413
|
export function getStatus() {
|
|
1970
2414
|
return {
|
|
@@ -1972,6 +2416,8 @@ export function getStatus() {
|
|
|
1972
2416
|
max_concurrent: MAX_CONCURRENT,
|
|
1973
2417
|
priority_queue_length: priorityQueue.length,
|
|
1974
2418
|
normal_queue_length: normalQueue.length,
|
|
2419
|
+
// Inbox entries held off the queues by a cohort-llm deferral (never dropped).
|
|
2420
|
+
cohort_parked_length: cohortParked.length,
|
|
1975
2421
|
sessions: Array.from(activeSessions.entries()).map(([id, s]) => ({
|
|
1976
2422
|
id,
|
|
1977
2423
|
model: s.model,
|