@cohortapp/agent-sdk 2.16.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 +23 -7
- 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/poller-daemon-setup.md +4 -1
- 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/context/budget.mjs +327 -0
- package/lib/context/history-scope.mjs +138 -0
- 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/economics.mjs +9 -0
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +95 -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 +82 -4
- package/lib/org/inbound/hydrate.mjs +555 -51
- 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 +15 -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 +104 -59
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +804 -157
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/lib/session-router.mjs +310 -42
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +121 -12
- package/scripts/daemon/responder.mjs +315 -146
- 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/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 -453
- 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 -300
- 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/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 -295
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -344
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -185
- 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,
|
|
@@ -143,6 +167,126 @@ import { budgetLadder, spawnKnobsFor } from "../../lib/model-router/economics.mj
|
|
|
143
167
|
// AsyncLocalStorage cannot cross the proc-event boundary) + the v2 decision_id.
|
|
144
168
|
// emitEvent is fail-open (never throws), so this is purely additive telemetry.
|
|
145
169
|
import { emitEvent, EVENT_TYPES } from "../../lib/diagnostics/events.mjs";
|
|
170
|
+
import {
|
|
171
|
+
createRouterSync,
|
|
172
|
+
routingKey as deriveRoutingKey,
|
|
173
|
+
routerItemFromDaemonItem,
|
|
174
|
+
claimSession,
|
|
175
|
+
releaseSession,
|
|
176
|
+
} from "./lib/session-router.mjs";
|
|
177
|
+
import { replyTier } from "../../lib/assurance/tier.mjs";
|
|
178
|
+
import { activeEntryFor, linkBoardTask } from "../../lib/session/current-work.mjs";
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The reply tier comes from `lib/assurance/tier.mjs` (design §5.1) — the ONE
|
|
182
|
+
* definition, imported statically now that both halves of the design have
|
|
183
|
+
* merged.
|
|
184
|
+
*
|
|
185
|
+
* There used to be a `localReplyTier` here carrying "the same table" for the
|
|
186
|
+
* window in which this package shipped before the acknowledgement package. It
|
|
187
|
+
* was not the same table: it predated the review fix that added
|
|
188
|
+
* `willSpawnSession !== true` to rule (2), so an item the quick path fell
|
|
189
|
+
* through on would have been tiered `answer` — total silence — while a long
|
|
190
|
+
* session ran. The duplicate's own test ("localReplyTier agrees with
|
|
191
|
+
* lib/assurance/tier.mjs wherever that module exists") is what caught it at
|
|
192
|
+
* the merge, which is the only reason the divergence is a footnote and not an
|
|
193
|
+
* outage. Two copies of one decision is the bug; there is now one.
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/** The reply tier → the router task class that carries its effort knobs. */
|
|
197
|
+
const TASK_CLASS_BY_TIER = Object.freeze({
|
|
198
|
+
answer: "session.answer",
|
|
199
|
+
work: "session.responder",
|
|
200
|
+
plan: "session.plan",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/** Rungs, as `lib/execution/route.RUNGS` numbers them. */
|
|
204
|
+
const ANSWER_MAX_RUNG = 1;
|
|
205
|
+
const PLAN_MIN_RUNG = 3;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A rung worth reasoning from, or null. An out-of-range value, a string "3" or
|
|
209
|
+
* a NaN is a stale field, and reading one as a rung would let a typo pick the
|
|
210
|
+
* turn's effort. Same coercion as `lib/assurance/tier.mjs#routedRung`.
|
|
211
|
+
*/
|
|
212
|
+
function routedRung(rung) {
|
|
213
|
+
if (typeof rung !== "number" || !Number.isInteger(rung)) return null;
|
|
214
|
+
return rung < 0 || rung > 5 ? null : rung;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The routed execution rung for an item, or null.
|
|
220
|
+
*
|
|
221
|
+
* `agent-daemon.mjs:617` stamps the ladder's decision onto `item.execution`
|
|
222
|
+
* precisely so everything downstream can see which rung the turn is serving.
|
|
223
|
+
* The classifier emits no `rung` field of its own, so reading `classResult.rung`
|
|
224
|
+
* finds `undefined` forever — which would make `session.answer`'s knobs dead
|
|
225
|
+
* code and the tier table a decoration.
|
|
226
|
+
*/
|
|
227
|
+
export function rungForItem(item) {
|
|
228
|
+
const ex = item && typeof item === "object" ? item.execution : null;
|
|
229
|
+
if (ex && typeof ex === "object") {
|
|
230
|
+
const r = routedRung(ex.rung);
|
|
231
|
+
if (r != null) return r;
|
|
232
|
+
}
|
|
233
|
+
return routedRung(item && item.rung);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The task class for this spawn. Was hardcoded `"session.responder"`, so every
|
|
238
|
+
* session took the same 40-turn, null-effort knobs no matter what was asked
|
|
239
|
+
* (design §3 R13).
|
|
240
|
+
*
|
|
241
|
+
* @param {object} classResult
|
|
242
|
+
* @param {object} [item] the inbox item, which carries the ladder's decision
|
|
243
|
+
*/
|
|
244
|
+
export function taskClassFor(classResult, item = null) {
|
|
245
|
+
const c = classResult || {};
|
|
246
|
+
const args = {
|
|
247
|
+
answerable: c.answerable,
|
|
248
|
+
action: c.action,
|
|
249
|
+
priority: c.priority,
|
|
250
|
+
rung: rungForItem(item) ?? routedRung(c.rung),
|
|
251
|
+
// DELIBERATELY LEFT UNKNOWN, even though this function is only ever called
|
|
252
|
+
// on the spawn path and `true` is the literally accurate value.
|
|
253
|
+
//
|
|
254
|
+
// `replyTier` answers two different questions and the spawn flag matters to
|
|
255
|
+
// only one of them:
|
|
256
|
+
//
|
|
257
|
+
// "will the reply arrive in this turn?" — the ACKNOWLEDGEMENT question.
|
|
258
|
+
// A spawning item must never be tiered `answer`, because `answer`
|
|
259
|
+
// means total silence and the premise (an in-turn reply) is false.
|
|
260
|
+
// `assurance.mjs` asserts `willSpawnSession: true` for exactly that.
|
|
261
|
+
//
|
|
262
|
+
// "how hard is this ask?" — the EFFORT question, ours.
|
|
263
|
+
// Whether a session is spawning says nothing about it. A cheap,
|
|
264
|
+
// answerable, rung-0 ask deserves `session.answer`'s knobs whether it
|
|
265
|
+
// is answered in-turn or in a session.
|
|
266
|
+
//
|
|
267
|
+
// Asserting `true` here collapsed the two: rule (2) requires
|
|
268
|
+
// `willSpawnSession !== true`, so on the spawn path — the only path this
|
|
269
|
+
// function has — NOTHING could ever reach the `answer` tier and the
|
|
270
|
+
// `session.answer` effort class became unreachable. Caught at the merge by
|
|
271
|
+
// "taskClassFor maps each tier onto the class that carries its knobs".
|
|
272
|
+
// Leaving it unknown asks the tier the question this caller actually has.
|
|
273
|
+
willSpawnSession: undefined,
|
|
274
|
+
};
|
|
275
|
+
// A tier module that throws must not stop a dispatch: `work` is the tier the
|
|
276
|
+
// flood was made of and the one that speaks at most once, so it is the right
|
|
277
|
+
// thing to fall back to.
|
|
278
|
+
let tier = "work";
|
|
279
|
+
try { tier = replyTier(args) || "work"; } catch { tier = "work"; }
|
|
280
|
+
return TASK_CLASS_BY_TIER[tier] || "session.responder";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The session-continuity registry, shared with `responder.mjs` — same file,
|
|
285
|
+
* same key function, same decision. Synchronous because `spawnSession` builds
|
|
286
|
+
* its argv and spawns in one pass; see `createRouterSync`'s note.
|
|
287
|
+
*/
|
|
288
|
+
const SESSION_REGISTRY_PATH = join(AGENT_REPO_DIR, "state", "daemon", "session-router-registry.json");
|
|
289
|
+
const sessionRouter = createRouterSync({ registryPath: SESSION_REGISTRY_PATH });
|
|
146
290
|
// Lazy + cached so a misconfigured YAML doesn't break agents that didn't
|
|
147
291
|
// opt in. The cache is invalidated only on daemon restart.
|
|
148
292
|
let _routingConfigCache;
|
|
@@ -245,14 +389,15 @@ export function parseUsageFromText(text) {
|
|
|
245
389
|
}
|
|
246
390
|
}
|
|
247
391
|
if (!obj || typeof obj !== "object") return { ok: false, reason: "no-json-object" };
|
|
392
|
+
const cohortMeta = cohortResultMeta(obj);
|
|
248
393
|
const usage = obj.usage && typeof obj.usage === "object" ? obj.usage : null;
|
|
249
|
-
if (!usage) return { ok: false, reason: "no-usage-field" };
|
|
394
|
+
if (!usage) return { ok: false, reason: "no-usage-field", ...cohortMeta };
|
|
250
395
|
const inputTokens = Number(usage.input_tokens);
|
|
251
396
|
const outputTokens = Number(usage.output_tokens);
|
|
252
397
|
if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens)) {
|
|
253
|
-
return { ok: false, reason: "non-numeric-tokens" };
|
|
398
|
+
return { ok: false, reason: "non-numeric-tokens", ...cohortMeta };
|
|
254
399
|
}
|
|
255
|
-
const out = { ok: true, inputTokens, outputTokens };
|
|
400
|
+
const out = { ok: true, inputTokens, outputTokens, ...cohortMeta };
|
|
256
401
|
const cacheRead = Number(usage.cache_read_input_tokens);
|
|
257
402
|
if (Number.isFinite(cacheRead)) out.cacheReadTokens = cacheRead;
|
|
258
403
|
// Cache CREATION tokens bill at 1.25x input and were previously dropped on
|
|
@@ -261,14 +406,35 @@ export function parseUsageFromText(text) {
|
|
|
261
406
|
// the CLI's total_cost_usd.
|
|
262
407
|
const cacheWrite = Number(usage.cache_creation_input_tokens);
|
|
263
408
|
if (Number.isFinite(cacheWrite)) out.cacheWriteTokens = cacheWrite;
|
|
264
|
-
|
|
265
|
-
|
|
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;
|
|
266
418
|
if (typeof obj.model === "string" && obj.model) {
|
|
267
419
|
out.model = /opus/i.test(obj.model) ? "opus" : /haiku/i.test(obj.model) ? "haiku" : "sonnet";
|
|
268
420
|
}
|
|
269
421
|
return out;
|
|
270
422
|
}
|
|
271
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
|
+
|
|
272
438
|
/**
|
|
273
439
|
* Append a TRUTHFUL cost-ledger row for a finished dispatcher session via
|
|
274
440
|
* scripts/cost/track-claude-usage.mjs (source "dispatcher"). Real token counts
|
|
@@ -284,7 +450,7 @@ export function parseUsageFromText(text) {
|
|
|
284
450
|
*
|
|
285
451
|
* Best-effort + detached; never blocks the close path.
|
|
286
452
|
*/
|
|
287
|
-
function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId }) {
|
|
453
|
+
function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId, billedBy = null }) {
|
|
288
454
|
const usage = parseUsageFromText(stdout);
|
|
289
455
|
try {
|
|
290
456
|
const trackerPath = join(AGENT_REPO_DIR, "scripts/cost/track-claude-usage.mjs");
|
|
@@ -301,6 +467,10 @@ function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId
|
|
|
301
467
|
// routing decision + resume marker (SPEC §4.7). The tracker captures it as a
|
|
302
468
|
// generic flag; absent on the v1 / no-config paths.
|
|
303
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(","));
|
|
304
474
|
if (usage.ok) {
|
|
305
475
|
trackerArgs.push("--input-tokens", String(usage.inputTokens));
|
|
306
476
|
trackerArgs.push("--output-tokens", String(usage.outputTokens));
|
|
@@ -586,6 +756,21 @@ export function reconcileResumePending(opts = {}) {
|
|
|
586
756
|
continue;
|
|
587
757
|
}
|
|
588
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
|
+
|
|
589
774
|
// Bump the attempt count on the marker BEFORE re-dispatch so a crash mid-
|
|
590
775
|
// resume still advances toward the 3-strike cap (no infinite loop).
|
|
591
776
|
marker.recoveryAttempts = attempts + 1;
|
|
@@ -597,7 +782,7 @@ export function reconcileResumePending(opts = {}) {
|
|
|
597
782
|
|
|
598
783
|
const spawnResume = typeof opts.spawnResume === "function" ? opts.spawnResume : defaultSpawnResume;
|
|
599
784
|
try {
|
|
600
|
-
spawnResume({ marker });
|
|
785
|
+
spawnResume({ marker, admission });
|
|
601
786
|
logSession({ event: "resume_dispatched", sessionId: marker.sessionId, claudeSessionId: marker.claudeSessionId, item_id: marker.itemId, attempt: marker.recoveryAttempts });
|
|
602
787
|
stats.resumed++;
|
|
603
788
|
} catch (err) {
|
|
@@ -652,9 +837,12 @@ export function resumeModelFlag(marker, target) {
|
|
|
652
837
|
* [--print, --output-format json, <permission args>, <daemon args>,
|
|
653
838
|
* <router knobs>, --session-id <id>, --model <flag>, <prompt>]
|
|
654
839
|
*
|
|
655
|
-
* and the same env rule
|
|
656
|
-
*
|
|
657
|
-
*
|
|
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).
|
|
658
846
|
* MAESTRO_SESSION_ID rides the env so delivery receipts attribute correctly.
|
|
659
847
|
*
|
|
660
848
|
* @param {object} o
|
|
@@ -670,31 +858,67 @@ export function buildResumeSpawn(o = {}) {
|
|
|
670
858
|
const marker = o.marker || {};
|
|
671
859
|
const target = o.target && typeof o.target === "object" ? o.target : null;
|
|
672
860
|
const pathFn = typeof o.augmentedPath === "function" ? o.augmentedPath : augmentedPath;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (target) {
|
|
676
|
-
if (target.maxTurns != null && Number.isFinite(Number(target.maxTurns))) knobArgs.push("--max-turns", String(target.maxTurns));
|
|
677
|
-
if (target.effort) knobArgs.push("--effort", String(target.effort));
|
|
678
|
-
if (target.agentsJson) knobArgs.push("--agents", typeof target.agentsJson === "string" ? target.agentsJson : JSON.stringify(target.agentsJson));
|
|
679
|
-
}
|
|
680
|
-
const args = [
|
|
681
|
-
"--print",
|
|
682
|
-
"--output-format", "json",
|
|
683
|
-
...(Array.isArray(o.permissionArgs) ? o.permissionArgs : []),
|
|
684
|
-
...(Array.isArray(o.daemonArgs) ? o.daemonArgs : []),
|
|
685
|
-
...knobArgs,
|
|
686
|
-
"--session-id", marker.claudeSessionId,
|
|
687
|
-
"--model", modelFlag,
|
|
688
|
-
marker.prompt,
|
|
689
|
-
];
|
|
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.
|
|
690
863
|
const base = o.baseEnv && typeof o.baseEnv === "object" ? o.baseEnv : {};
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
:
|
|
696
|
-
|
|
697
|
-
|
|
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 };
|
|
698
922
|
}
|
|
699
923
|
|
|
700
924
|
/**
|
|
@@ -713,7 +937,7 @@ export function buildResumeSpawn(o = {}) {
|
|
|
713
937
|
* and the fresh prompt continues the work.
|
|
714
938
|
*
|
|
715
939
|
* Audit F11: the argv and env are built through the SAME resolveSpawnTarget /
|
|
716
|
-
*
|
|
940
|
+
* runtime-adapter path as spawnSession (buildResumeSpawn), so a retargeted
|
|
717
941
|
* session resumes on its backend, the router's knobs ride along, and
|
|
718
942
|
* `--output-format json` makes the run's usage measurable for the ledger.
|
|
719
943
|
*
|
|
@@ -721,41 +945,85 @@ export function buildResumeSpawn(o = {}) {
|
|
|
721
945
|
* Best-effort; failures are logged, the marker stays (next reconcile retries
|
|
722
946
|
* up to the strike cap). On a clean close the marker is cleared here.
|
|
723
947
|
*/
|
|
724
|
-
|
|
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) {
|
|
725
955
|
const source = marker.source || "backlog";
|
|
726
|
-
// The router's CURRENT decision for this work. resolveSpawnTarget never
|
|
727
|
-
// throws (it degrades to the v1 path / stock CLI internally); the request is
|
|
728
|
-
// rebuilt from what the marker kept of the original classification.
|
|
729
|
-
let target = null;
|
|
730
956
|
try {
|
|
731
957
|
const routingConfig = getRoutingConfig();
|
|
732
|
-
if (routingConfig)
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
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");
|
|
737
962
|
} catch (err) {
|
|
738
963
|
// A router fault must not block a resume: fall back to the marker's flag.
|
|
739
964
|
console.warn(`[dispatcher] resume routing failed for ${marker.sessionId}, using marker flag: ${err.message}`);
|
|
740
|
-
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
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);
|
|
741
978
|
}
|
|
742
|
-
|
|
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({
|
|
743
1002
|
marker,
|
|
744
1003
|
target,
|
|
745
1004
|
baseEnv: process.env,
|
|
746
1005
|
permissionArgs: sessionPermissionArgs({ source: "dispatcher-resume", priority: marker.priority }),
|
|
747
1006
|
daemonArgs: daemonClaudeArgs(),
|
|
1007
|
+
...engineFields,
|
|
748
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;
|
|
749
1017
|
const decisionId = (target && target.decisionId) || marker.decision_id || null;
|
|
750
1018
|
const startedAt = Date.now();
|
|
751
|
-
const proc = _spawn(
|
|
1019
|
+
const proc = _spawn(bin, args, { cwd: AGENT_REPO_DIR, env: spawnEnv, stdio: ["ignore", "pipe", "pipe"] });
|
|
752
1020
|
let stdout = "";
|
|
753
1021
|
let stderr = "";
|
|
754
1022
|
proc.stdout.on("data", (c) => { stdout += c.toString(); });
|
|
755
1023
|
proc.stderr.on("data", (c) => { stderr += c.toString(); });
|
|
756
1024
|
proc.on("close", (code) => {
|
|
757
1025
|
// A resumed run costs money too: parse its --output-format json usage.
|
|
758
|
-
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 }); }
|
|
759
1027
|
catch { /* cost tracking is best-effort */ }
|
|
760
1028
|
if (code === 0) {
|
|
761
1029
|
// Resume succeeded — the underlying work is done; clear the marker.
|
|
@@ -764,11 +1032,18 @@ function defaultSpawnResume({ marker }) {
|
|
|
764
1032
|
} else {
|
|
765
1033
|
// A usage limit or 429 during resume must open the breaker so subsequent
|
|
766
1034
|
// spawns gate. Usage-limit → hold until reset; 429 → transient backoff.
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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 { /* */ }
|
|
770
1045
|
} else if (rateGuard.classifyStderr(stderr)) {
|
|
771
|
-
try { rateGuard.recordRateLimit(
|
|
1046
|
+
try { rateGuard.recordRateLimit(resumeBreaker, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
|
|
772
1047
|
}
|
|
773
1048
|
logSession({ event: "resume_exit_nonzero", sessionId: marker.sessionId, item_id: marker.itemId, exit_code: code });
|
|
774
1049
|
}
|
|
@@ -778,18 +1053,29 @@ function defaultSpawnResume({ marker }) {
|
|
|
778
1053
|
});
|
|
779
1054
|
}
|
|
780
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
|
+
*/
|
|
781
1064
|
function writeActiveSession(sessionId, entry) {
|
|
782
1065
|
try {
|
|
783
1066
|
let active = {};
|
|
784
1067
|
try { active = JSON.parse(readFileSync(ACTIVE_PATH, "utf-8")); } catch {}
|
|
785
|
-
active[sessionId] = {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
+
}
|
|
793
1079
|
const tmpPath = ACTIVE_PATH + ".tmp";
|
|
794
1080
|
writeFileSync(tmpPath, JSON.stringify(active, null, 2));
|
|
795
1081
|
renameSync(tmpPath, ACTIVE_PATH);
|
|
@@ -798,6 +1084,45 @@ function writeActiveSession(sessionId, entry) {
|
|
|
798
1084
|
}
|
|
799
1085
|
}
|
|
800
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
|
+
|
|
801
1126
|
function removeActiveSession(sessionId) {
|
|
802
1127
|
try {
|
|
803
1128
|
let active = {};
|
|
@@ -965,6 +1290,13 @@ export function canDispatchBacklog(item) {
|
|
|
965
1290
|
* fires onClose once it is later drained and spawned). It does NOT fire on the
|
|
966
1291
|
* pre-spawn drop paths (backlog dedup / claim-denied / governor-DEFER backlog),
|
|
967
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.
|
|
968
1300
|
*/
|
|
969
1301
|
export function dispatch(prompt, item, classResult, source = "inbox", opts = {}) {
|
|
970
1302
|
// `obligationKey` names the debt this session was spawned to discharge. It is
|
|
@@ -977,6 +1309,8 @@ export function dispatch(prompt, item, classResult, source = "inbox", opts = {})
|
|
|
977
1309
|
prompt, item, classResult, source,
|
|
978
1310
|
obligationKey: opts.obligationKey || null,
|
|
979
1311
|
onClose: typeof opts.onClose === "function" ? opts.onClose : null,
|
|
1312
|
+
onSpawn: typeof opts.onSpawn === "function" ? opts.onSpawn : null,
|
|
1313
|
+
cohortPrimed: opts.cohortPrimed === true,
|
|
980
1314
|
};
|
|
981
1315
|
const priority = classResult.priority;
|
|
982
1316
|
const isPriorityInbox = source === "inbox" && (priority === "critical" || priority === "high");
|
|
@@ -1167,17 +1501,21 @@ function currentBudgetBand() {
|
|
|
1167
1501
|
* @param {object} classResult
|
|
1168
1502
|
* @param {string} source
|
|
1169
1503
|
* @param {string} fallbackModel the coarse sonnet/opus class for the no-config path
|
|
1504
|
+
* @param {object} [item] the inbox item, which carries the routed rung
|
|
1170
1505
|
*/
|
|
1171
|
-
function resolveSpawnTarget(routingConfig, routingRequest, classResult, source, fallbackModel) {
|
|
1506
|
+
function resolveSpawnTarget(routingConfig, routingRequest, classResult, source, fallbackModel, item = null) {
|
|
1172
1507
|
// No config at all → stock Claude-CLI-on-Max behaviour (unchanged).
|
|
1173
1508
|
if (!routingConfig) {
|
|
1174
|
-
return { modelFlag: fallbackModel, envForSpawn: {}, decisionId: null, backend: null, model: null, transport: "anthropic-cli", maxTurns: null, effort: null, agentsJson: null, explain: null, decision: null };
|
|
1509
|
+
return { taskClass: taskClassFor(classResult, item), modelFlag: fallbackModel, envForSpawn: {}, decisionId: null, backend: null, model: null, transport: "anthropic-cli", maxTurns: null, effort: null, agentsJson: null, explain: null, decision: null };
|
|
1175
1510
|
}
|
|
1176
1511
|
|
|
1177
1512
|
// v2 path — resolveChain produces a full RouteDecision.
|
|
1178
1513
|
if (routingConfig.schema_version === 2) {
|
|
1179
1514
|
try {
|
|
1180
|
-
|
|
1515
|
+
// The task class carries the spawn knobs (`spawnKnobsFor`), so hardcoding
|
|
1516
|
+
// one made `effort` always null and `maxTurns` always 40. It is now the
|
|
1517
|
+
// reply tier — see `taskClassFor`.
|
|
1518
|
+
const taskClass = taskClassFor(classResult, item);
|
|
1181
1519
|
const req = {
|
|
1182
1520
|
...routingRequest,
|
|
1183
1521
|
task_class: taskClass,
|
|
@@ -1194,6 +1532,7 @@ function resolveSpawnTarget(routingConfig, routingRequest, classResult, source,
|
|
|
1194
1532
|
const knobs = spawnKnobsFor(taskClass);
|
|
1195
1533
|
const sa = decision.spawnArgs || {};
|
|
1196
1534
|
return {
|
|
1535
|
+
taskClass,
|
|
1197
1536
|
modelFlag: sa.modelFlag || decision.chosen.model || fallbackModel,
|
|
1198
1537
|
envForSpawn: decision.envForSpawn || {},
|
|
1199
1538
|
decisionId: decision.decision_id || null,
|
|
@@ -1216,9 +1555,10 @@ function resolveSpawnTarget(routingConfig, routingRequest, classResult, source,
|
|
|
1216
1555
|
// v1 path — resolveBackend → modelFlagFor (preserved verbatim).
|
|
1217
1556
|
const resolved = resolveBackend(routingRequest, { config: routingConfig });
|
|
1218
1557
|
if (!resolved) {
|
|
1219
|
-
return { modelFlag: fallbackModel, envForSpawn: {}, decisionId: null, backend: null, model: null, transport: "anthropic-cli", maxTurns: null, effort: null, agentsJson: null, explain: null, decision: null };
|
|
1558
|
+
return { taskClass: taskClassFor(classResult, item), modelFlag: fallbackModel, envForSpawn: {}, decisionId: null, backend: null, model: null, transport: "anthropic-cli", maxTurns: null, effort: null, agentsJson: null, explain: null, decision: null };
|
|
1220
1559
|
}
|
|
1221
1560
|
return {
|
|
1561
|
+
taskClass: taskClassFor(classResult, item),
|
|
1222
1562
|
modelFlag: modelFlagFor(resolved, routingRequest),
|
|
1223
1563
|
envForSpawn: resolved.envForSpawn || {},
|
|
1224
1564
|
decisionId: null,
|
|
@@ -1261,13 +1601,6 @@ function spawnSession(entry) {
|
|
|
1261
1601
|
? (source === "backlog" ? OPUS_BACKLOG_TIMEOUT : OPUS_INBOX_TIMEOUT)
|
|
1262
1602
|
: (source === "backlog" ? SONNET_BACKLOG_TIMEOUT : SONNET_INBOX_TIMEOUT);
|
|
1263
1603
|
|
|
1264
|
-
// Track backlog items to prevent retry storms
|
|
1265
|
-
if (source === "backlog") {
|
|
1266
|
-
const key = backlogKey(item);
|
|
1267
|
-
activeBacklogKeys.add(key);
|
|
1268
|
-
backlogRetryCount.set(key, (backlogRetryCount.get(key) || 0) + 1);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
1604
|
// Resolve the spawn target via the model router. v2 configs go through
|
|
1272
1605
|
// resolveChain (full RouteDecision: model + retarget env + spawn knobs +
|
|
1273
1606
|
// decision_id + estimated cost + failover chain); v1 configs keep the legacy
|
|
@@ -1275,80 +1608,160 @@ function spawnSession(entry) {
|
|
|
1275
1608
|
// default exactly. NEVER throws (resolveSpawnTarget degrades internally).
|
|
1276
1609
|
const routingConfig = getRoutingConfig();
|
|
1277
1610
|
const routingRequest = requestFromClassifierResult(classResult, { source, role: "responder" });
|
|
1278
|
-
const target = resolveSpawnTarget(routingConfig, routingRequest, classResult, source, model);
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
//
|
|
1287
|
-
// the
|
|
1288
|
-
//
|
|
1289
|
-
//
|
|
1290
|
-
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1611
|
+
const target = resolveSpawnTarget(routingConfig, routingRequest, classResult, source, model, item);
|
|
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
|
+
}
|
|
1294
1642
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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);
|
|
1298
1649
|
}
|
|
1299
1650
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
"--output-format", "json",
|
|
1308
|
-
...sessionPermissionArgs({ source: "dispatcher", priority: classResult?.priority }),
|
|
1309
|
-
...daemonClaudeArgs(),
|
|
1310
|
-
...knobArgs,
|
|
1311
|
-
"--session-id", claudeSessionId,
|
|
1312
|
-
"--model", effectiveModelFlag,
|
|
1313
|
-
prompt,
|
|
1314
|
-
];
|
|
1315
|
-
|
|
1316
|
-
// Build the spawn env.
|
|
1317
|
-
// Default (no router): strip ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN so
|
|
1318
|
-
// `claude` falls through to the keychain OAuth (Max subscription) per
|
|
1319
|
-
// CEO directive 2026-04-27.
|
|
1320
|
-
// Router active: merge the resolved target's envForSpawn — this points the
|
|
1321
|
-
// CLI at the chosen backend (Moonshot, OpenRouter, NIM, etc.) and sets
|
|
1322
|
-
// ANTHROPIC_API_KEY="" explicitly so Claude Code doesn't fall back to
|
|
1323
|
-
// OAuth against api.anthropic.com.
|
|
1651
|
+
// Session continuity. A fresh `randomUUID()` per dispatch meant every full
|
|
1652
|
+
// session in a live thread started COLD — the agent re-read the room, re-did
|
|
1653
|
+
// the orientation work, and answered a follow-up as if it were an opening
|
|
1654
|
+
// (design §3 R10). The router the responder already consults keys the
|
|
1655
|
+
// conversation; a follow-up inside the TTL resumes the same session id, which
|
|
1656
|
+
// in this daemon IS continuation (`--session-id <id> <prompt>`, never
|
|
1657
|
+
// `--resume` — see the resume-pending notes above).
|
|
1324
1658
|
//
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1659
|
+
// Inbox only. Backlog work is not a conversation, and keying it would put
|
|
1660
|
+
// unrelated items in one session.
|
|
1661
|
+
//
|
|
1662
|
+
// Everything here is fail-open: no key, an unreadable registry, or a throw
|
|
1663
|
+
// from `routingKey` all fall back to the fresh UUID, which is exactly the
|
|
1664
|
+
// behaviour being replaced. Crash-recovery resume is UNCHANGED — it still
|
|
1665
|
+
// rides `writeResumePending(..., claudeSessionId, ...)` below, with whichever
|
|
1666
|
+
// id this resolved to.
|
|
1667
|
+
//
|
|
1668
|
+
// AND ONE PROCESS PER KEY. `route()` refuses to resume a key this daemon
|
|
1669
|
+
// already has a child on, and the claim below is what tells it so. A burst in
|
|
1670
|
+
// a busy top-level channel — the measured shape: ~38 messages an hour in one
|
|
1671
|
+
// room, where the thread lock does not apply because there is no thread id —
|
|
1672
|
+
// would otherwise put three or four `claude --print --session-id <same id>`
|
|
1673
|
+
// processes on ONE transcript. The later turns spawn cold instead, which is
|
|
1674
|
+
// exactly the pre-router behaviour, and continuity returns on the next turn.
|
|
1675
|
+
let sessionRoutingKey = null;
|
|
1676
|
+
let resumedSessionId = null;
|
|
1677
|
+
if (source === "inbox") {
|
|
1678
|
+
const routerItem = routerItemFromDaemonItem(item);
|
|
1679
|
+
if (routerItem) {
|
|
1680
|
+
try {
|
|
1681
|
+
const candidateKey = deriveRoutingKey(routerItem);
|
|
1682
|
+
const decision = sessionRouter.route(candidateKey);
|
|
1683
|
+
if (claimSession(candidateKey)) {
|
|
1684
|
+
sessionRoutingKey = candidateKey;
|
|
1685
|
+
if (decision.decision === "RESUME" && decision.resumeId) resumedSessionId = decision.resumeId;
|
|
1686
|
+
} else {
|
|
1687
|
+
// Something is already running on this room. Spawn cold, and do NOT
|
|
1688
|
+
// hold the key — the running session owns the registry row, and the
|
|
1689
|
+
// second turn must not overwrite it on close.
|
|
1690
|
+
console.log(`[dispatcher] ${candidateKey} already in flight — spawning cold (no session reuse)`);
|
|
1691
|
+
}
|
|
1692
|
+
} catch (err) {
|
|
1693
|
+
console.warn(`[dispatcher] session routing key failed: ${err.message} — spawning cold`);
|
|
1694
|
+
sessionRoutingKey = null;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const claudeSessionId = resumedSessionId || randomUUID();
|
|
1699
|
+
|
|
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,
|
|
1726
|
+
prompt,
|
|
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, {
|
|
1350
1763
|
cwd: AGENT_REPO_DIR,
|
|
1351
|
-
env:
|
|
1764
|
+
env: spawnSpec.env,
|
|
1352
1765
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1353
1766
|
});
|
|
1354
1767
|
|
|
@@ -1356,14 +1769,20 @@ function spawnSession(entry) {
|
|
|
1356
1769
|
// and it isn't the stock Anthropic CLI no-op. The v2 path also carries the
|
|
1357
1770
|
// decision_id + the one-line explain so `maestro router why` can join this
|
|
1358
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`.
|
|
1359
1776
|
if (target.backend && target.transport !== "anthropic-cli") {
|
|
1360
1777
|
logSession({
|
|
1361
1778
|
event: "routed",
|
|
1362
1779
|
sessionId,
|
|
1363
1780
|
decision_id: target.decisionId,
|
|
1364
|
-
backend: target.backend,
|
|
1365
|
-
transport: target.transport,
|
|
1366
|
-
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 } } : {}),
|
|
1367
1786
|
tried: target.decision ? target.decision.tried : target._v1Resolved?.tried,
|
|
1368
1787
|
fallback_reason: target.decision ? target.decision.audit?.fallback_reason : target._v1Resolved?.fallback_reason,
|
|
1369
1788
|
explain: target.explain,
|
|
@@ -1393,6 +1812,11 @@ function spawnSession(entry) {
|
|
|
1393
1812
|
const startTime = Date.now();
|
|
1394
1813
|
activeSessions.set(sessionId, { process: proc, item, classResult, startTime, model, source, claudeSessionId });
|
|
1395
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
|
+
}
|
|
1396
1820
|
|
|
1397
1821
|
// WS4: drop the in-flight resume marker. Present-after-crash = mid-flight, so
|
|
1398
1822
|
// a startup reconcile can re-dispatch `claude --print --resume`. Cleared on a
|
|
@@ -1416,6 +1840,9 @@ function spawnSession(entry) {
|
|
|
1416
1840
|
priority: classResult.priority,
|
|
1417
1841
|
summary: classResult.summary,
|
|
1418
1842
|
active_count: activeSessions.size,
|
|
1843
|
+
task_class: target.taskClass || null,
|
|
1844
|
+
session_key: sessionRoutingKey,
|
|
1845
|
+
continued: Boolean(resumedSessionId),
|
|
1419
1846
|
});
|
|
1420
1847
|
|
|
1421
1848
|
// Observability: the interaction's trace_id rode in on item.trace_id (set by
|
|
@@ -1448,6 +1875,7 @@ function spawnSession(entry) {
|
|
|
1448
1875
|
// happened. Skip to avoid double-count and double-release.
|
|
1449
1876
|
if (spawnErrorHandled.has(sessionId)) {
|
|
1450
1877
|
spawnErrorHandled.delete(sessionId);
|
|
1878
|
+
if (sessionRoutingKey) releaseSession(sessionRoutingKey);
|
|
1451
1879
|
clearResumePending(sessionId); // error path already terminal — no resume
|
|
1452
1880
|
// The error handler already fired onClose(ok:false) AND emitted
|
|
1453
1881
|
// session_closed; the single-fire guard makes the onClose a no-op, and we
|
|
@@ -1461,6 +1889,31 @@ function spawnSession(entry) {
|
|
|
1461
1889
|
recordSession(true, code === 0);
|
|
1462
1890
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1463
1891
|
|
|
1892
|
+
// Session continuity: a clean exit keeps the key live for the next turn in
|
|
1893
|
+
// this conversation; a non-zero one marks it killed so the next route
|
|
1894
|
+
// returns EPHEMERAL_REPLACE rather than resuming into a broken session.
|
|
1895
|
+
// Best-effort — a registry we cannot write costs continuity, never work.
|
|
1896
|
+
if (sessionRoutingKey) {
|
|
1897
|
+
try {
|
|
1898
|
+
if (code === 0) {
|
|
1899
|
+
sessionRouter.touch(sessionRoutingKey, {
|
|
1900
|
+
claudeSessionId,
|
|
1901
|
+
daemonSessionId: sessionId,
|
|
1902
|
+
model: effectiveModelFlag,
|
|
1903
|
+
});
|
|
1904
|
+
} else {
|
|
1905
|
+
sessionRouter.recordExit(sessionRoutingKey, code);
|
|
1906
|
+
}
|
|
1907
|
+
} catch (err) {
|
|
1908
|
+
console.warn(`[dispatcher] session router update failed for ${sessionRoutingKey}: ${err.message}`);
|
|
1909
|
+
} finally {
|
|
1910
|
+
// The key is free the moment the child is gone — released in a `finally`
|
|
1911
|
+
// so a registry write that throws cannot leave the room permanently
|
|
1912
|
+
// unable to continue a session.
|
|
1913
|
+
releaseSession(sessionRoutingKey);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1464
1917
|
// WS4: a clean exit retires the resume marker (work finished — nothing to
|
|
1465
1918
|
// resume) and closes the shared 429 breaker. A non-zero exit whose stderr
|
|
1466
1919
|
// looks like a rate limit opens the breaker so ALL spawn sources back off
|
|
@@ -1469,7 +1922,7 @@ function spawnSession(entry) {
|
|
|
1469
1922
|
// bound re-dispatch.
|
|
1470
1923
|
if (code === 0) {
|
|
1471
1924
|
clearResumePending(sessionId);
|
|
1472
|
-
try { rateGuard.recordSuccess(
|
|
1925
|
+
try { rateGuard.recordSuccess(breakerProvider, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
|
|
1473
1926
|
} else {
|
|
1474
1927
|
// A Max/subscription USAGE limit ("you've hit your session limit · resets
|
|
1475
1928
|
// 9:40pm") is checked FIRST and on BOTH streams — for `claude --print` it
|
|
@@ -1477,15 +1930,32 @@ function spawnSession(entry) {
|
|
|
1477
1930
|
// RESETS (not a decorrelated 429 backoff), so the seat stops storming a
|
|
1478
1931
|
// drained pool. A plain 429/overload falls through to the transient path.
|
|
1479
1932
|
const combined = `${stdout || ""}\n${stderr || ""}`;
|
|
1480
|
-
|
|
1481
|
-
|
|
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) {
|
|
1946
|
+
try {
|
|
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) {
|
|
1482
1952
|
try {
|
|
1483
|
-
const rec = rateGuard.recordUsageLimit(
|
|
1953
|
+
const rec = rateGuard.recordUsageLimit(breakerProvider, ul.resetAt, { agentRoot: AGENT_REPO_DIR });
|
|
1484
1954
|
logSession({ event: "usage_limit_recorded", sessionId, open_until: rec.openUntil, reset_at: rec.resetAt });
|
|
1485
1955
|
} catch { /* */ }
|
|
1486
1956
|
} else if (rateGuard.classifyStderr(stderr)) {
|
|
1487
1957
|
try {
|
|
1488
|
-
const rec = rateGuard.recordRateLimit(
|
|
1958
|
+
const rec = rateGuard.recordRateLimit(breakerProvider, { agentRoot: AGENT_REPO_DIR });
|
|
1489
1959
|
logSession({ event: "rate_limit_recorded", sessionId, open_until: rec.openUntil, consecutive: rec.consecutive429 });
|
|
1490
1960
|
} catch { /* */ }
|
|
1491
1961
|
}
|
|
@@ -1579,6 +2049,7 @@ function spawnSession(entry) {
|
|
|
1579
2049
|
durationMs: Date.now() - startTime,
|
|
1580
2050
|
exitCode: code,
|
|
1581
2051
|
decisionId: target.decisionId,
|
|
2052
|
+
billedBy: servedByCohort ? COHORT_LLM_BILLED_BY : null,
|
|
1582
2053
|
});
|
|
1583
2054
|
if (!costUsage.ok) {
|
|
1584
2055
|
logSession({ event: "cost_usage_parse_failed", sessionId, reason: costUsage.reason });
|
|
@@ -1622,6 +2093,9 @@ function spawnSession(entry) {
|
|
|
1622
2093
|
if (source === "inbox") { try { stopTyping(item); } catch { /* */ } }
|
|
1623
2094
|
// Mark so the trailing proc.on("close") doesn't double-process.
|
|
1624
2095
|
spawnErrorHandled.add(sessionId);
|
|
2096
|
+
// A spawn that never ran still holds the room's key. Release it here — the
|
|
2097
|
+
// close handler's release is behind an early return on this path.
|
|
2098
|
+
if (sessionRoutingKey) releaseSession(sessionRoutingKey);
|
|
1625
2099
|
activeSessions.delete(sessionId);
|
|
1626
2100
|
removeActiveSession(sessionId);
|
|
1627
2101
|
// Spawn never ran — there is nothing to resume; retire the marker so the
|
|
@@ -1760,10 +2234,181 @@ function drainQueue() {
|
|
|
1760
2234
|
// Commit: remove from whichever queue it sat at the head of, then spawn.
|
|
1761
2235
|
if (priorityQueue[0] === next) priorityQueue.shift();
|
|
1762
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.
|
|
1763
2239
|
spawnSession(next);
|
|
1764
2240
|
}
|
|
1765
2241
|
}
|
|
1766
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
|
+
|
|
1767
2412
|
/** Get current status for health checks */
|
|
1768
2413
|
export function getStatus() {
|
|
1769
2414
|
return {
|
|
@@ -1771,6 +2416,8 @@ export function getStatus() {
|
|
|
1771
2416
|
max_concurrent: MAX_CONCURRENT,
|
|
1772
2417
|
priority_queue_length: priorityQueue.length,
|
|
1773
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,
|
|
1774
2421
|
sessions: Array.from(activeSessions.entries()).map(([id, s]) => ({
|
|
1775
2422
|
id,
|
|
1776
2423
|
model: s.model,
|