@cohortapp/agent-sdk 2.17.0 → 2.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/wire/stall.mjs — inter-event idle timeout and stall diagnostics
|
|
3
|
+
* for a gateway stream (CF-156, client half).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. The W13 live measurement (docs/engine/eval.md §3) found
|
|
6
|
+
* `cohort-reason` stalling twice in 25 requests: one engine call hung past a
|
|
7
|
+
* 120 s client timeout (`req_fd528e0c1ab3f8947a77bb66`) and one raw Anthropic
|
|
8
|
+
* call took 45.5 s (`req_449cafa064e187c22993991e`), against ~1.5 s normally.
|
|
9
|
+
* Root cause was NOT diagnosed, and a stall left almost nothing behind to
|
|
10
|
+
* diagnose it with. Two occurrences in 25 requests is not a reproduction
|
|
11
|
+
* budget, so this module's job is to make the THIRD occurrence self-evident,
|
|
12
|
+
* and to end it inside the task budget it runs under instead of eating all of
|
|
13
|
+
* it. (Which budget, and why 180 s is not the dispatcher's, is set out on
|
|
14
|
+
* DEFAULT_STREAM_IDLE_TIMEOUT_MS below.)
|
|
15
|
+
*
|
|
16
|
+
* Two pieces, both usable without a socket:
|
|
17
|
+
*
|
|
18
|
+
* · `withIdleTimeout` — wraps an async iterable of SSE frames and ends it
|
|
19
|
+
* when no frame arrives for `idleMs`. The clock is INTER-EVENT, not
|
|
20
|
+
* total: a long completion that keeps streaming is never cut off, and a
|
|
21
|
+
* stream that goes quiet is. It closes the underlying iterator on the way
|
|
22
|
+
* out, so the fetch body is released rather than left reading.
|
|
23
|
+
*
|
|
24
|
+
* · `createStallWatch` — counts what a diagnosis needs (time to first byte,
|
|
25
|
+
* time to first event, time since the last event, the last event type,
|
|
26
|
+
* whether the terminal `cohort` frame had arrived) and builds a record
|
|
27
|
+
* ONLY when asked. Per frame it touches three numbers and a string; the
|
|
28
|
+
* record is built on trouble, never on the happy path.
|
|
29
|
+
*
|
|
30
|
+
* On keep-alive pings. The gateway sends `:` comment lines every 15 s and
|
|
31
|
+
* `readSse` drops them, by design — they are the SSE grammar's liveness
|
|
32
|
+
* filler, not events. So this timer measures silence in EVENTS, which is the
|
|
33
|
+
* thing that actually stalled: a gateway that keeps pinging while no content
|
|
34
|
+
* ever arrives is precisely the failure W13 could not tell apart from a slow
|
|
35
|
+
* model. The cost of that choice is that a tier which legitimately thinks for
|
|
36
|
+
* longer than `idleMs` without emitting anything would be cut off, which is
|
|
37
|
+
* why the default sits far above every latency yet measured and why it is
|
|
38
|
+
* named configuration rather than a constant.
|
|
39
|
+
*
|
|
40
|
+
* @module lib/engine/wire/stall
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Longest silence before a stream is called stalled — and, spent across a whole
|
|
45
|
+
* call rather than re-armed per attempt, the client's bound on a gateway that
|
|
46
|
+
* never speaks.
|
|
47
|
+
*
|
|
48
|
+
* 60 s, derived rather than chosen — and derived from a number this repo owns,
|
|
49
|
+
* which the first cut of this module did not:
|
|
50
|
+
*
|
|
51
|
+
* · WHAT ACTUALLY BOUNDS A TURN. Earlier drafts of this module, and
|
|
52
|
+
* docs/engine/eval.md §3, called 180 s "the dispatcher's task timeout". It
|
|
53
|
+
* is not. The live dispatcher times a task out at 10 min to 12 h depending
|
|
54
|
+
* on model and source (`scripts/daemon/dispatcher.mjs`,
|
|
55
|
+
* `SONNET_INBOX_TIMEOUT` … `OPUS_BACKLOG_TIMEOUT`). The real 180 s in this
|
|
56
|
+
* repo is `DEFAULT_TASK_TIMEOUT_MS` (`scripts/eval/replay/lib/config.mjs`),
|
|
57
|
+
* the per-task budget of the design §8.4.2 replay eval — and the TIGHTEST
|
|
58
|
+
* budget the engine runs a task under here. Deriving against the tightest
|
|
59
|
+
* is the conservative choice, and unlike "the dispatcher's 180 s" it is a
|
|
60
|
+
* constant this repo owns, so the test pins it by import instead of
|
|
61
|
+
* restating a literal.
|
|
62
|
+
* · THE BOUND THIS BUYS. A stalled call ends within 2 × this value, never
|
|
63
|
+
* more: either the silence budget is spent before acceptance and the call
|
|
64
|
+
* returns without resending, or the gateway accepts inside it and then one
|
|
65
|
+
* silent inter-event gap ends the stream. 120 s at the default — inside the
|
|
66
|
+
* 180 s replay budget, and far inside the dispatcher's 10-minute floor.
|
|
67
|
+
* · ABOVE EVERY REAL LATENCY. The worst legitimate latency ever recorded
|
|
68
|
+
* against this gateway is W13's 45.5 s WHOLE call, and this budget is per
|
|
69
|
+
* inter-event gap, which is strictly smaller. It is also well under the
|
|
70
|
+
* 120 s client timeout that a real stall already ran past.
|
|
71
|
+
*
|
|
72
|
+
* It is NOT a bound on a healthy call's total duration, by design: a stream
|
|
73
|
+
* that keeps emitting events is never cut off, however long it runs.
|
|
74
|
+
*/
|
|
75
|
+
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The band a configured budget is held to.
|
|
79
|
+
*
|
|
80
|
+
* Below the floor the guard would cut off healthy streams; above the ceiling it
|
|
81
|
+
* would exceed the very task budget the default is derived from, while still
|
|
82
|
+
* looking armed. Both are silent failures, so a value outside the band is
|
|
83
|
+
* clamped and `idleTimeoutNote` says so out loud. `0` (explicitly off) is not
|
|
84
|
+
* clamped — disabling the guard is a deliberate, documented escape hatch.
|
|
85
|
+
*/
|
|
86
|
+
export const MIN_STREAM_IDLE_TIMEOUT_MS = 5_000;
|
|
87
|
+
/** @see MIN_STREAM_IDLE_TIMEOUT_MS */
|
|
88
|
+
export const MAX_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A stream that finished but took longer than this is recorded as `slow`.
|
|
92
|
+
*
|
|
93
|
+
* 20 s: above the ~1.5 s typical turn and above the 7.9 s raw
|
|
94
|
+
* `cohort-reason`/OpenAI call W13 saw, but below the 45.5 s outlier — so the
|
|
95
|
+
* outlier would have left a record, and ordinary traffic leaves none.
|
|
96
|
+
*/
|
|
97
|
+
export const DEFAULT_SLOW_STREAM_MS = 20_000;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Thrown by `withIdleTimeout` when the silence budget is spent. A marker type
|
|
101
|
+
* so the HTTP edge can tell a stall from a socket fault without matching on a
|
|
102
|
+
* message.
|
|
103
|
+
*/
|
|
104
|
+
export class StreamIdleTimeoutSignal extends Error {
|
|
105
|
+
/** @param {number} idleMs */
|
|
106
|
+
constructor(idleMs) {
|
|
107
|
+
super(`no SSE event for ${idleMs}ms`);
|
|
108
|
+
this.name = "StreamIdleTimeoutSignal";
|
|
109
|
+
/** @type {number} */
|
|
110
|
+
this.idleMs = idleMs;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse a configured idle timeout. Accepts milliseconds; `0`, `off`, `false`
|
|
116
|
+
* and `none` disable the timer (0). Anything unparseable keeps the default, so
|
|
117
|
+
* a typo in the environment loosens nothing silently — it just does not apply.
|
|
118
|
+
* A finite value outside {@link MIN_STREAM_IDLE_TIMEOUT_MS} …
|
|
119
|
+
* {@link MAX_STREAM_IDLE_TIMEOUT_MS} is CLAMPED into the band: a 1 ms budget
|
|
120
|
+
* that cuts off every healthy stream and a 10-minute one that outlives the task
|
|
121
|
+
* it is protecting both look armed and are both useless. Pair with
|
|
122
|
+
* {@link idleTimeoutNote} so the adjustment is visible rather than silent.
|
|
123
|
+
*
|
|
124
|
+
* @param {unknown} value
|
|
125
|
+
* @param {number} [fallback]
|
|
126
|
+
* @returns {number} milliseconds, or 0 for "no timeout"
|
|
127
|
+
*/
|
|
128
|
+
export function parseIdleTimeoutMs(value, fallback = DEFAULT_STREAM_IDLE_TIMEOUT_MS) {
|
|
129
|
+
const raw = String(value ?? "").trim().toLowerCase();
|
|
130
|
+
if (raw === "") return fallback;
|
|
131
|
+
if (["0", "off", "false", "no", "none"].includes(raw)) return 0;
|
|
132
|
+
const n = Number(raw);
|
|
133
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
134
|
+
return Math.min(MAX_STREAM_IDLE_TIMEOUT_MS, Math.max(MIN_STREAM_IDLE_TIMEOUT_MS, Math.round(n)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The one line to print when a configured budget was not taken at face value —
|
|
139
|
+
* either it did not parse, or it was clamped into the band. `null` when the
|
|
140
|
+
* value was empty, an explicit "off", or applied exactly as written.
|
|
141
|
+
*
|
|
142
|
+
* Separate from {@link parseIdleTimeoutMs} so that function stays pure and
|
|
143
|
+
* total: the parse has no seam to warn through, and the CLI does.
|
|
144
|
+
*
|
|
145
|
+
* @param {unknown} value the raw configured value
|
|
146
|
+
* @param {number} [applied] what {@link parseIdleTimeoutMs} returned for it
|
|
147
|
+
* @returns {string|null}
|
|
148
|
+
*/
|
|
149
|
+
export function idleTimeoutNote(value, applied = parseIdleTimeoutMs(value)) {
|
|
150
|
+
const raw = String(value ?? "").trim();
|
|
151
|
+
if (raw === "") return null;
|
|
152
|
+
const lower = raw.toLowerCase();
|
|
153
|
+
if (["0", "off", "false", "no", "none"].includes(lower)) return null;
|
|
154
|
+
const n = Number(lower);
|
|
155
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
156
|
+
return `COHORT_LLM_STREAM_IDLE_TIMEOUT_MS=${raw} is not a number of milliseconds; using ${applied}ms`;
|
|
157
|
+
}
|
|
158
|
+
if (Math.round(n) === applied) return null;
|
|
159
|
+
return (
|
|
160
|
+
`COHORT_LLM_STREAM_IDLE_TIMEOUT_MS=${raw} is outside the ` +
|
|
161
|
+
`${MIN_STREAM_IDLE_TIMEOUT_MS}–${MAX_STREAM_IDLE_TIMEOUT_MS}ms band; using ${applied}ms`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Yield an async iterable's values, failing with {@link StreamIdleTimeoutSignal}
|
|
167
|
+
* when more than `idleMs` passes between them.
|
|
168
|
+
*
|
|
169
|
+
* The underlying iterator is closed in a `finally`, so a stalled fetch body is
|
|
170
|
+
* cancelled rather than left pending. A non-positive or non-finite `idleMs`
|
|
171
|
+
* passes straight through with no timer at all.
|
|
172
|
+
*
|
|
173
|
+
* @template T
|
|
174
|
+
* @param {AsyncIterable<T>} source
|
|
175
|
+
* @param {number} idleMs
|
|
176
|
+
* @param {{ setTimer?: typeof setTimeout, clearTimer?: typeof clearTimeout }} [deps]
|
|
177
|
+
* @returns {AsyncGenerator<T>}
|
|
178
|
+
*/
|
|
179
|
+
export async function* withIdleTimeout(source, idleMs, deps = {}) {
|
|
180
|
+
if (!Number.isFinite(idleMs) || idleMs <= 0) {
|
|
181
|
+
yield* source;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const setTimer = deps.setTimer ?? setTimeout;
|
|
185
|
+
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
186
|
+
const it = source[Symbol.asyncIterator]();
|
|
187
|
+
try {
|
|
188
|
+
for (;;) {
|
|
189
|
+
const next = it.next();
|
|
190
|
+
// The loser of the race is orphaned: keep it from becoming an unhandled
|
|
191
|
+
// rejection when the timer wins and the socket errors afterwards.
|
|
192
|
+
next.catch(() => {});
|
|
193
|
+
/** @type {any} */
|
|
194
|
+
let timer;
|
|
195
|
+
const idle = new Promise((_resolve, reject) => {
|
|
196
|
+
timer = setTimer(() => reject(new StreamIdleTimeoutSignal(idleMs)), idleMs);
|
|
197
|
+
});
|
|
198
|
+
idle.catch(() => {});
|
|
199
|
+
let step;
|
|
200
|
+
try {
|
|
201
|
+
step = await Promise.race([next, idle]);
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimer(timer);
|
|
204
|
+
}
|
|
205
|
+
if (step.done) return;
|
|
206
|
+
yield step.value;
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
// NEVER await this. An async generator suspended at an un-resolving
|
|
210
|
+
// `await` — which is exactly what a stalled fetch body is — only processes
|
|
211
|
+
// `return()` when it next resumes, so awaiting here would hang on the very
|
|
212
|
+
// stall just detected. Ask it to close and move on; releasing the socket
|
|
213
|
+
// is the caller's job (wire/http.mjs cancels the response body).
|
|
214
|
+
try {
|
|
215
|
+
void Promise.resolve(it.return?.()).catch(() => {});
|
|
216
|
+
} catch {
|
|
217
|
+
/* a source with no return() needs no closing */
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @typedef {Object} StreamDiagnostic
|
|
224
|
+
* @property {'stalled'|'failed'|'slow'} outcome
|
|
225
|
+
* @property {string|null} requestId x-cohort-request-id (or the cohort frame's)
|
|
226
|
+
* @property {string|null} wire 'openai' | 'anthropic'
|
|
227
|
+
* @property {string|null} tier the model tier asked for
|
|
228
|
+
* @property {boolean} accepted the gateway had returned 2xx
|
|
229
|
+
* @property {number|null} ttfbMs request start → response headers
|
|
230
|
+
* @property {number|null} firstEventMs response headers → first SSE event
|
|
231
|
+
* @property {number|null} sinceLastEventMs last SSE event → now (the silence)
|
|
232
|
+
* @property {number} events SSE events seen (the cohort frame included)
|
|
233
|
+
* @property {string|null} lastEventType the last event name seen
|
|
234
|
+
* @property {boolean} cohortFrameSeen the terminal `event: cohort` frame had arrived
|
|
235
|
+
* @property {number} totalMs THIS ATTEMPT's start → now
|
|
236
|
+
* @property {number} callMs the whole call's start → now (every attempt and backoff sleep included)
|
|
237
|
+
* @property {number} attempts attempts made on this call (1 unless the gateway was retried)
|
|
238
|
+
* @property {string|null} code the wire error's code, when it failed
|
|
239
|
+
* @property {string|null} message the wire error's message, when it failed.
|
|
240
|
+
* MAY CONTAIN CONTENT: a protocol error quotes the offending stream chunk
|
|
241
|
+
* (see `openai-chat.mjs` / `anthropic-messages.mjs`), which is up to 120
|
|
242
|
+
* characters of a tenant's completion. `formatStreamDiagnostic` omits it; any
|
|
243
|
+
* other observer that forwards a record off this machine must redact it.
|
|
244
|
+
*/
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Count what diagnosing a stall needs. Cheap per frame; the record is built
|
|
248
|
+
* only when `report` is called, which the HTTP edge does on a stall, on a
|
|
249
|
+
* mid-stream fault, and on a stream slower than its `slow` threshold.
|
|
250
|
+
*
|
|
251
|
+
* `startedAt` is THIS ATTEMPT's start, so every timing in the record describes
|
|
252
|
+
* the attempt that actually ran; `callStartedAt` (defaulting to it) is the
|
|
253
|
+
* whole call's, so a record can never be read as describing more than it does.
|
|
254
|
+
* The two differ only after a retry, and `attempts` says when that happened —
|
|
255
|
+
* a record labelled `slow` that describes a 3 ms stream was exactly the
|
|
256
|
+
* confusion this separation removes.
|
|
257
|
+
*
|
|
258
|
+
* @param {{ now: () => number, startedAt: number, callStartedAt?: number, attempts?: number, wire?: string|null, tier?: string|null }} p
|
|
259
|
+
*/
|
|
260
|
+
export function createStallWatch({ now, startedAt, callStartedAt = startedAt, attempts: attemptCount = 1, wire = null, tier = null }) {
|
|
261
|
+
let acceptedAt = /** @type {number|null} */ (null);
|
|
262
|
+
let firstEventAt = /** @type {number|null} */ (null);
|
|
263
|
+
let lastEventAt = /** @type {number|null} */ (null);
|
|
264
|
+
let lastEventType = /** @type {string|null} */ (null);
|
|
265
|
+
let events = 0;
|
|
266
|
+
let cohortFrameSeen = false;
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
/** The gateway answered 2xx: headers are in, the body is open. */
|
|
270
|
+
accepted() {
|
|
271
|
+
acceptedAt = now();
|
|
272
|
+
},
|
|
273
|
+
/** One SSE frame arrived. @param {{event?:string}} frame */
|
|
274
|
+
event(frame) {
|
|
275
|
+
events++;
|
|
276
|
+
lastEventAt = now();
|
|
277
|
+
if (firstEventAt === null) firstEventAt = lastEventAt;
|
|
278
|
+
lastEventType = frame?.event || "message";
|
|
279
|
+
},
|
|
280
|
+
/** The terminal `event: cohort` frame arrived. */
|
|
281
|
+
cohortFrame() {
|
|
282
|
+
cohortFrameSeen = true;
|
|
283
|
+
},
|
|
284
|
+
get cohortFrameSeen() {
|
|
285
|
+
return cohortFrameSeen;
|
|
286
|
+
},
|
|
287
|
+
/**
|
|
288
|
+
* @param {{ outcome:'stalled'|'failed'|'slow', requestId?:string|null, accepted?:boolean, attempts?:number, error?:{code?:string, message?:string}|null }} p
|
|
289
|
+
* @returns {StreamDiagnostic}
|
|
290
|
+
*/
|
|
291
|
+
report({ outcome, requestId = null, accepted = acceptedAt !== null, attempts = attemptCount, error = null }) {
|
|
292
|
+
const at = now();
|
|
293
|
+
return {
|
|
294
|
+
outcome,
|
|
295
|
+
requestId,
|
|
296
|
+
wire,
|
|
297
|
+
tier,
|
|
298
|
+
accepted,
|
|
299
|
+
ttfbMs: acceptedAt === null ? null : acceptedAt - startedAt,
|
|
300
|
+
firstEventMs: firstEventAt === null || acceptedAt === null ? null : firstEventAt - acceptedAt,
|
|
301
|
+
sinceLastEventMs: lastEventAt === null ? (acceptedAt === null ? null : at - acceptedAt) : at - lastEventAt,
|
|
302
|
+
events,
|
|
303
|
+
lastEventType,
|
|
304
|
+
cohortFrameSeen,
|
|
305
|
+
totalMs: at - startedAt,
|
|
306
|
+
callMs: at - callStartedAt,
|
|
307
|
+
attempts,
|
|
308
|
+
code: error?.code ?? null,
|
|
309
|
+
message: error?.message ?? null,
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* One stderr line for a diagnostic.
|
|
317
|
+
*
|
|
318
|
+
* THIS LINE carries only ids, timings and event names — never a token, a prompt
|
|
319
|
+
* or a completion — because it goes to a seat's log. That promise is this
|
|
320
|
+
* function's, not the record's: `StreamDiagnostic.message` can quote a stream
|
|
321
|
+
* chunk, so `message` is deliberately omitted here and any other observer must
|
|
322
|
+
* redact it itself.
|
|
323
|
+
*
|
|
324
|
+
* `total` is this attempt; `call` and `attempts` appear only when the gateway
|
|
325
|
+
* was retried, so a record can never be read as describing more than it does.
|
|
326
|
+
*
|
|
327
|
+
* @param {StreamDiagnostic} d
|
|
328
|
+
* @returns {string}
|
|
329
|
+
*/
|
|
330
|
+
export function formatStreamDiagnostic(d) {
|
|
331
|
+
const retried = (d.attempts ?? 1) > 1;
|
|
332
|
+
const parts = [
|
|
333
|
+
`stream ${d.outcome}`,
|
|
334
|
+
d.wire ? `wire=${d.wire}` : null,
|
|
335
|
+
d.tier ? `tier=${d.tier}` : null,
|
|
336
|
+
`request=${d.requestId ?? "unknown"}`,
|
|
337
|
+
d.ttfbMs == null ? null : `ttfb=${d.ttfbMs}ms`,
|
|
338
|
+
d.firstEventMs == null ? null : `first-event=${d.firstEventMs}ms`,
|
|
339
|
+
d.sinceLastEventMs == null ? null : `silent=${d.sinceLastEventMs}ms`,
|
|
340
|
+
`events=${d.events}`,
|
|
341
|
+
`last-event=${d.lastEventType ?? "none"}`,
|
|
342
|
+
`cohort-frame=${d.cohortFrameSeen ? "yes" : "no"}`,
|
|
343
|
+
`total=${d.totalMs}ms`,
|
|
344
|
+
retried ? `call=${d.callMs}ms` : null,
|
|
345
|
+
retried ? `attempts=${d.attempts}` : null,
|
|
346
|
+
d.code ? `code=${d.code}` : null,
|
|
347
|
+
].filter(Boolean);
|
|
348
|
+
return parts.join(" ");
|
|
349
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/wire/token-provider.mjs — the gateway credential for a run that
|
|
3
|
+
* may outlive its seat token.
|
|
4
|
+
*
|
|
5
|
+
* A seat token (`cst_…`) is short-lived. A headless run finishes well inside
|
|
6
|
+
* its life, but a `cohort session` front door runs for days, so the engine
|
|
7
|
+
* must not read `COHORT_LLM_TOKEN` once and keep it. In order:
|
|
8
|
+
*
|
|
9
|
+
* COHORT_LLM_TOKEN_HELPER a shell command that prints a fresh token on its
|
|
10
|
+
* first stdout line (maestro sets it to
|
|
11
|
+
* scripts/cohort-llm/api-key-helper.mjs). Its token
|
|
12
|
+
* is cached for COHORT_LLM_TOKEN_TTL_MS (default 10
|
|
13
|
+
* minutes) and the command runs again when the
|
|
14
|
+
* cache has aged out or the gateway answered 401
|
|
15
|
+
* (`{refresh:true}` from wire/http.mjs).
|
|
16
|
+
* COHORT_LLM_TOKEN the token itself. With a helper it seeds the cache
|
|
17
|
+
* (the adapter minted it at spawn), so a short run
|
|
18
|
+
* never forks the helper; without one it is used as
|
|
19
|
+
* given, as before.
|
|
20
|
+
*
|
|
21
|
+
* The provider returns what the wire takes: a string (env only) or a token
|
|
22
|
+
* function (helper). Concurrent refreshes share one helper run. The helper's
|
|
23
|
+
* output never reaches an error message; only its exit and its stderr's first
|
|
24
|
+
* line do.
|
|
25
|
+
*
|
|
26
|
+
* Re-mints after a 401 are bounded (CF-136): a refresh naming a `rejected`
|
|
27
|
+
* token that the cache has already replaced returns the replacement with no
|
|
28
|
+
* helper run; the helper is told the rejected token's fingerprint
|
|
29
|
+
* (COHORT_LLM_TOKEN_REJECTED_FP) so the seat's manager mints only when its own
|
|
30
|
+
* token is the refused one; and at most MAX_REFRESHES_PER_WINDOW re-mints run
|
|
31
|
+
* per REFRESH_WINDOW_MS — past that the token function throws, which the wire
|
|
32
|
+
* returns as a non-retryable `token_unavailable`. Only a 401 asks for a
|
|
33
|
+
* refresh: a 429 or a 503 never reaches this provider's refresh path.
|
|
34
|
+
*
|
|
35
|
+
* @module lib/engine/wire/token-provider
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { execFile } from "node:child_process";
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_TOKEN_TTL_MS = 10 * 60 * 1000;
|
|
42
|
+
export const TOKEN_HELPER_TIMEOUT_MS = 20_000;
|
|
43
|
+
|
|
44
|
+
/** @param {Record<string,string|undefined>} env */
|
|
45
|
+
export function tokenTtlMs(env) {
|
|
46
|
+
const n = Number(env.COHORT_LLM_TOKEN_TTL_MS);
|
|
47
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_TOKEN_TTL_MS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run a helper command in /bin/sh and resolve its stdout.
|
|
52
|
+
* @param {string} command
|
|
53
|
+
* @param {{env:Record<string,string|undefined>, timeoutMs?:number}} o
|
|
54
|
+
* @returns {Promise<string>}
|
|
55
|
+
*/
|
|
56
|
+
export function runShellTokenHelper(command, { env, timeoutMs = TOKEN_HELPER_TIMEOUT_MS }) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
execFile("/bin/sh", ["-c", command], { env: /** @type any */ (env), timeout: timeoutMs, maxBuffer: 64 * 1024, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
59
|
+
if (err) {
|
|
60
|
+
const why = String(stderr || "").split(/\r?\n/).find((l) => l.trim() !== "") ?? (/** @type any */ (err).killed ? "timed out" : err.message);
|
|
61
|
+
reject(new Error(`COHORT_LLM_TOKEN_HELPER failed: ${why.slice(0, 300)}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve(String(stdout));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {{refresh?:boolean, rejected?:string}} TokenRequest
|
|
71
|
+
* @typedef {{ok:true, source:'env'|'helper', token:string|((o?:TokenRequest)=>Promise<string>), helperRuns:()=>number}
|
|
72
|
+
* | {ok:false, error:{code:'missing_token', message:string}}} TokenProvider
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {object} p
|
|
77
|
+
* @param {Record<string,string|undefined>} p.env
|
|
78
|
+
* @param {() => number} [p.now]
|
|
79
|
+
* @param {(command:string, o:{env:Record<string,string|undefined>}) => Promise<string>} [p.runHelper]
|
|
80
|
+
* @param {{max?:number, windowMs?:number}} [p.refreshCaps] overrides DEFAULT_REFRESH_CAPS
|
|
81
|
+
* @returns {TokenProvider}
|
|
82
|
+
*/
|
|
83
|
+
export function createTokenProvider({ env, now = Date.now, runHelper = runShellTokenHelper, refreshCaps = {} }) {
|
|
84
|
+
const caps = { ...DEFAULT_REFRESH_CAPS, ...refreshCaps };
|
|
85
|
+
const helper = String(env.COHORT_LLM_TOKEN_HELPER ?? "").trim();
|
|
86
|
+
const envToken = String(env.COHORT_LLM_TOKEN ?? "").trim();
|
|
87
|
+
if (!helper && !envToken) {
|
|
88
|
+
return { ok: false, error: { code: "missing_token", message: "no gateway credential: set COHORT_LLM_TOKEN (or COHORT_LLM_TOKEN_HELPER)" } };
|
|
89
|
+
}
|
|
90
|
+
if (!helper) return { ok: true, source: "env", token: envToken, helperRuns: () => 0 };
|
|
91
|
+
|
|
92
|
+
const ttl = tokenTtlMs(env);
|
|
93
|
+
/** @type {{token:string, at:number}|null} */
|
|
94
|
+
let cached = envToken ? { token: envToken, at: now() } : null;
|
|
95
|
+
/** @type {Promise<string>|null} */
|
|
96
|
+
let inflight = null;
|
|
97
|
+
let runs = 0;
|
|
98
|
+
/** @type {number[]} when each 401-driven re-mint started (the refresh window) */
|
|
99
|
+
let refreshes = [];
|
|
100
|
+
|
|
101
|
+
/** @param {string|null} rejectedToken */
|
|
102
|
+
const mint = async (rejectedToken) => {
|
|
103
|
+
runs++;
|
|
104
|
+
// The helper learns WHICH token was refused (a fingerprint, never the token),
|
|
105
|
+
// so its seat manager re-mints only when its cached token is that one.
|
|
106
|
+
const helperEnv = rejectedToken ? { ...env, [REJECTED_TOKEN_FP_ENV]: tokenFingerprint(rejectedToken) } : env;
|
|
107
|
+
const out = await runHelper(helper, { env: helperEnv });
|
|
108
|
+
const fresh = String(out).split(/\r?\n/)[0].trim();
|
|
109
|
+
if (!fresh) throw new Error("COHORT_LLM_TOKEN_HELPER printed no token");
|
|
110
|
+
cached = { token: fresh, at: now() };
|
|
111
|
+
return fresh;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
source: "helper",
|
|
117
|
+
helperRuns: () => runs,
|
|
118
|
+
/** @param {TokenRequest} [o] */
|
|
119
|
+
token: async ({ refresh = false, rejected } = {}) => {
|
|
120
|
+
if (!refresh) {
|
|
121
|
+
if (cached && now() - cached.at < ttl) return cached.token;
|
|
122
|
+
if (!inflight) inflight = mint(null).finally(() => (inflight = null));
|
|
123
|
+
return inflight;
|
|
124
|
+
}
|
|
125
|
+
// A 401. A concurrent request may already have replaced the rejected token.
|
|
126
|
+
if (rejected && cached && cached.token !== rejected && now() - cached.at < ttl) return cached.token;
|
|
127
|
+
if (inflight) return inflight;
|
|
128
|
+
const t = now();
|
|
129
|
+
const budget = refreshBudget(refreshes, t, caps);
|
|
130
|
+
refreshes = budget.recent;
|
|
131
|
+
if (!budget.allowed) {
|
|
132
|
+
throw new Error(`COHORT_LLM_TOKEN_HELPER re-mint capped: ${caps.max} refreshes after a 401 within ${Math.round(caps.windowMs / 1000)}s; the gateway keeps refusing fresh tokens`);
|
|
133
|
+
}
|
|
134
|
+
refreshes.push(t);
|
|
135
|
+
const rejectedToken = rejected || (cached ? cached.token : null);
|
|
136
|
+
cached = null; // a rejected token is never served again
|
|
137
|
+
inflight = mint(rejectedToken).finally(() => (inflight = null));
|
|
138
|
+
return inflight;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Env var naming the fingerprint of the token a 401 rejected (read by maestro's api-key-helper). */
|
|
144
|
+
export const REJECTED_TOKEN_FP_ENV = "COHORT_LLM_TOKEN_REJECTED_FP";
|
|
145
|
+
/** A 401-driven re-mint may run at most MAX_REFRESHES_PER_WINDOW times per REFRESH_WINDOW_MS. */
|
|
146
|
+
export const REFRESH_WINDOW_MS = 60_000;
|
|
147
|
+
export const MAX_REFRESHES_PER_WINDOW = 3;
|
|
148
|
+
/**
|
|
149
|
+
* The engine's re-mint cap as one named config (CF-143). The values are the W7
|
|
150
|
+
* defaults, pending owner confirmation; `createTokenProvider({ refreshCaps })`
|
|
151
|
+
* overrides them. docs/engine/README.md "Retry caps" lists every cap.
|
|
152
|
+
*/
|
|
153
|
+
export const DEFAULT_REFRESH_CAPS = Object.freeze({ max: MAX_REFRESHES_PER_WINDOW, windowMs: REFRESH_WINDOW_MS });
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* sha256 of a token, first 16 hex chars — the same shape as lib/org/llm-token.mjs
|
|
157
|
+
* `keyFingerprint`, so the helper compares like with like.
|
|
158
|
+
* @param {string} token
|
|
159
|
+
*/
|
|
160
|
+
export function tokenFingerprint(token) {
|
|
161
|
+
return createHash("sha256").update(String(token || "")).digest("hex").slice(0, 16);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The sliding-window re-mint budget. PURE.
|
|
166
|
+
* @param {number[]} history start times of earlier re-mints
|
|
167
|
+
* @param {number} nowMs
|
|
168
|
+
* @param {{max?:number, windowMs?:number}} [o]
|
|
169
|
+
* @returns {{allowed:boolean, recent:number[], retryAt:number|null}}
|
|
170
|
+
*/
|
|
171
|
+
export function refreshBudget(history, nowMs, { max = MAX_REFRESHES_PER_WINDOW, windowMs = REFRESH_WINDOW_MS } = {}) {
|
|
172
|
+
const recent = (Array.isArray(history) ? history : []).filter((at) => Number.isFinite(at) && at > nowMs - windowMs && at <= nowMs).sort((a, b) => a - b);
|
|
173
|
+
if (recent.length < max) return { allowed: true, recent, retryAt: null };
|
|
174
|
+
return { allowed: false, recent, retryAt: recent[recent.length - max] + windowMs };
|
|
175
|
+
}
|