@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/session-runtime/notifications.mjs — events that reach the model
|
|
3
|
+
* between turns: background shells that ended (CF-50), Monitor output lines,
|
|
4
|
+
* monitors that ended, and background subagents' reports (a drain source).
|
|
5
|
+
*
|
|
6
|
+
* The loop drains the queue before each model request (`drainNotifications`),
|
|
7
|
+
* so an event that arrives while a turn runs is seen at that turn's next
|
|
8
|
+
* request. In `cohort session`, an event that arrives while the session is
|
|
9
|
+
* idle wakes it: the inbox turns a batch into a turn of its own.
|
|
10
|
+
*
|
|
11
|
+
* Caps keep a chatty monitor from flooding the context:
|
|
12
|
+
* · one drain takes at most `maxBatch` events and `maxChars` characters; the
|
|
13
|
+
* rest stay queued for the next drain (nothing is lost below the queue cap);
|
|
14
|
+
* · a single line is clipped at `maxLineChars`;
|
|
15
|
+
* · at most `maxQueued` events wait; beyond that new events are counted and
|
|
16
|
+
* dropped, and the next batch says how many.
|
|
17
|
+
*
|
|
18
|
+
* The formatters are pure; `createNotificationQueue` holds the state.
|
|
19
|
+
*
|
|
20
|
+
* @module lib/engine/session-runtime/notifications
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const NOTICE_CAPS = Object.freeze({ maxQueued: 500, maxBatch: 40, maxChars: 16_000, maxLineChars: 2_000 });
|
|
24
|
+
|
|
25
|
+
/** @param {string} s @param {number} max */
|
|
26
|
+
function clip(s, max) {
|
|
27
|
+
return s.length <= max ? s : `${s.slice(0, max)}… [${s.length - max} more characters]`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One batch of queued events as a single user message. Pure.
|
|
32
|
+
* @param {string[]} texts @param {{dropped?:number}} [o]
|
|
33
|
+
*/
|
|
34
|
+
export function formatNoticeBatch(texts, { dropped = 0 } = {}) {
|
|
35
|
+
const lines = [texts.length === 1 && dropped === 0 ? "[Event]" : `[Events: ${texts.length}]`, ...texts];
|
|
36
|
+
if (dropped > 0) lines.push(`[${dropped} further event(s) were dropped because too many were waiting]`);
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The notice for a background shell that ended on its own. Pure.
|
|
42
|
+
* @param {{id:string, command:string, description?:string|null, kind?:string, status:string, exitCode:number|null, spillPath?:string|null}} s
|
|
43
|
+
*/
|
|
44
|
+
export function shellExitNotice(s) {
|
|
45
|
+
const what = s.kind === "monitor" ? "Monitor" : "Background shell";
|
|
46
|
+
const label = s.description ? `${s.description} (${clip(s.command, 120)})` : clip(s.command, 120);
|
|
47
|
+
const how = s.status === "completed" ? "exited with code 0" : `ended: ${s.exitCode == null ? "it could not run" : `exit code ${s.exitCode}`}`;
|
|
48
|
+
return `${what} ${s.id} ${how}: ${label}. Read what it printed with BashOutput {"bash_id":"${s.id}"}.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One Monitor output line as an event. Pure.
|
|
53
|
+
* @param {{id:string, description:string}} m @param {string} line
|
|
54
|
+
*/
|
|
55
|
+
export function monitorLineNotice(m, line) {
|
|
56
|
+
return `${m.id} (${m.description}): ${line}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{caps?:Partial<typeof NOTICE_CAPS>}} [o]
|
|
61
|
+
*/
|
|
62
|
+
export function createNotificationQueue({ caps = {} } = {}) {
|
|
63
|
+
const c = { ...NOTICE_CAPS, ...caps };
|
|
64
|
+
/** @type {string[]} */
|
|
65
|
+
const queue = [];
|
|
66
|
+
let dropped = 0;
|
|
67
|
+
let poked = false;
|
|
68
|
+
/** @type {Array<() => string[]>} */
|
|
69
|
+
const sources = [];
|
|
70
|
+
/** @type {Set<() => void>} */
|
|
71
|
+
const listeners = new Set();
|
|
72
|
+
const announce = () => {
|
|
73
|
+
for (const l of [...listeners]) {
|
|
74
|
+
try {
|
|
75
|
+
l();
|
|
76
|
+
} catch {
|
|
77
|
+
/* a listener defect must not lose the event */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
/** Queue one event. @param {string} text */
|
|
84
|
+
push(text) {
|
|
85
|
+
if (queue.length >= c.maxQueued) dropped++;
|
|
86
|
+
else queue.push(clip(String(text), c.maxLineChars));
|
|
87
|
+
announce();
|
|
88
|
+
},
|
|
89
|
+
/** Another producer's drain (background subagents keep their own queue). @param {() => string[]} drain */
|
|
90
|
+
addSource(drain) {
|
|
91
|
+
sources.push(drain);
|
|
92
|
+
},
|
|
93
|
+
/** A source has something ready. */
|
|
94
|
+
poke() {
|
|
95
|
+
poked = true;
|
|
96
|
+
announce();
|
|
97
|
+
},
|
|
98
|
+
/** @param {() => void} fn @returns {() => void} unsubscribe */
|
|
99
|
+
onAvailable(fn) {
|
|
100
|
+
listeners.add(fn);
|
|
101
|
+
return () => listeners.delete(fn);
|
|
102
|
+
},
|
|
103
|
+
hasPending: () => queue.length > 0 || dropped > 0 || poked,
|
|
104
|
+
get size() {
|
|
105
|
+
return queue.length;
|
|
106
|
+
},
|
|
107
|
+
/**
|
|
108
|
+
* Everything ready now, as user-message texts: the sources' own messages,
|
|
109
|
+
* then at most one capped batch of queued events.
|
|
110
|
+
* @returns {string[]}
|
|
111
|
+
*/
|
|
112
|
+
drain() {
|
|
113
|
+
poked = false;
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const s of sources) out.push(...s());
|
|
116
|
+
if (queue.length === 0 && dropped === 0) return out;
|
|
117
|
+
const take = [];
|
|
118
|
+
let chars = 0;
|
|
119
|
+
while (queue.length > 0 && take.length < c.maxBatch && (take.length === 0 || chars + queue[0].length <= c.maxChars)) {
|
|
120
|
+
const t = /** @type string */ (queue.shift());
|
|
121
|
+
chars += t.length;
|
|
122
|
+
take.push(t);
|
|
123
|
+
}
|
|
124
|
+
out.push(formatNoticeBatch(take, { dropped }));
|
|
125
|
+
dropped = 0;
|
|
126
|
+
return out;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/session-runtime/registry.mjs — the local session registry.
|
|
3
|
+
*
|
|
4
|
+
* One file per live `cohort session`: `<dir>/<name>.json`, where `<dir>` is
|
|
5
|
+
* `$COHORT_SESSION_DIR` or `~/.cohort/sessions` (mode 0700):
|
|
6
|
+
*
|
|
7
|
+
* { name, sessionId, pid, cwd, socketPath, startedAt, heartbeatAt, heartbeatMs,
|
|
8
|
+
* engine:"cohort", state:"starting"|"idle"|"busy", agentRoot? }
|
|
9
|
+
*
|
|
10
|
+
* · Names are unique among LIVE sessions. Every change to `<name>.json` is made
|
|
11
|
+
* under a short `<name>.lock` (created with O_EXCL, broken after 10 s), so two
|
|
12
|
+
* sessions started together cannot both take a name.
|
|
13
|
+
* · An entry is live only while its pid is alive AND its heartbeat is recent
|
|
14
|
+
* (within 3 × its `heartbeatMs`). A pid alone proves nothing after a crash:
|
|
15
|
+
* the number may belong to another process by now.
|
|
16
|
+
* dead (pid gone) listing prunes it — under the lock, after
|
|
17
|
+
* re-reading, and only if the file still names
|
|
18
|
+
* that same instance (pid, sessionId, startedAt)
|
|
19
|
+
* silent (pid there, no beat) hidden from listing and free to claim; its
|
|
20
|
+
* files are left alone until a claim takes it
|
|
21
|
+
* · Each instance binds its own socket file (`<name>.<nonce>.sock`), so removing
|
|
22
|
+
* an old instance's socket can never unlink a successor's.
|
|
23
|
+
* · The owner rewrites its entry on every idle/busy change and every heartbeat,
|
|
24
|
+
* restores it if it has vanished and the name is free, and removes it on exit.
|
|
25
|
+
* Refresh and release act only on the caller's own instance, so a replaced
|
|
26
|
+
* session cannot erase its successor.
|
|
27
|
+
*
|
|
28
|
+
* `planClaim`, `entryLiveness`, `describeSession`, `socketPathFor` and
|
|
29
|
+
* `formatAge` are pure; `createRegistry` is the edge (fs, clock and the pid
|
|
30
|
+
* probe are injectable).
|
|
31
|
+
*
|
|
32
|
+
* @module lib/engine/session-runtime/registry
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
37
|
+
import os from "node:os";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
import { writeJsonAtomic } from "../../fs-atomic.mjs";
|
|
40
|
+
|
|
41
|
+
export const HEARTBEAT_MS = 15_000;
|
|
42
|
+
export const LOCK_STALE_MS = 10_000;
|
|
43
|
+
/** An entry whose heartbeat is older than this many intervals is not live. */
|
|
44
|
+
export const STALE_HEARTBEATS = 3;
|
|
45
|
+
/** macOS allows 104 bytes for a Unix socket path (including the NUL); Linux 108. */
|
|
46
|
+
export const SOCKET_PATH_MAX = 100;
|
|
47
|
+
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
48
|
+
|
|
49
|
+
/** @param {unknown} name @returns {name is string} */
|
|
50
|
+
export function isValidSessionName(name) {
|
|
51
|
+
return typeof name === "string" && NAME_RE.test(name) && !name.includes("..");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {{env:Record<string,string|undefined>, homedir:string}} o */
|
|
55
|
+
export function defaultRegistryDir({ env, homedir }) {
|
|
56
|
+
return env.COHORT_SESSION_DIR || path.join(homedir, ".cohort", "sessions");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The control socket's path: `<dir>/<name>.<nonce>.sock`, or — when that is too
|
|
61
|
+
* long for a Unix socket — a hashed name in a per-user directory under the
|
|
62
|
+
* temp base (`os.tmpdir()`, per user on macOS), else under /tmp. The socket
|
|
63
|
+
* server refuses a directory that is not a real 0700 directory of this user
|
|
64
|
+
* (socket.mjs `ensurePrivateDir`). Pure given its inputs.
|
|
65
|
+
* @param {string} dir @param {string} name @param {{uid?:number, nonce?:string, tmpBase?:string}} [o]
|
|
66
|
+
*/
|
|
67
|
+
export function socketPathFor(dir, name, { uid = typeof process.getuid === "function" ? process.getuid() : 0, nonce = "", tmpBase = os.tmpdir() } = {}) {
|
|
68
|
+
const p = path.join(dir, nonce ? `${name}.${nonce}.sock` : `${name}.sock`);
|
|
69
|
+
if (Buffer.byteLength(p) <= SOCKET_PATH_MAX) return p;
|
|
70
|
+
const file = `${createHash("sha256").update(p).digest("hex").slice(0, 24)}.sock`;
|
|
71
|
+
const inTmp = path.join(tmpBase, `cohort-sessions-${uid}`, file);
|
|
72
|
+
return Buffer.byteLength(inTmp) <= SOCKET_PATH_MAX ? inTmp : path.join("/tmp", `cohort-sessions-${uid}`, file);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Two entries describe the same session instance. @param {any} a @param {any} b */
|
|
76
|
+
export function sameInstance(a, b) {
|
|
77
|
+
return Boolean(a && b && a.pid === b.pid && a.sessionId === b.sessionId && a.startedAt === b.startedAt);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Is an entry's session still there? Pure.
|
|
82
|
+
* @param {any} e @param {{isAlive:(pid:number)=>boolean, now:number}} o
|
|
83
|
+
* @returns {'live'|'silent'|'dead'}
|
|
84
|
+
*/
|
|
85
|
+
export function entryLiveness(e, { isAlive, now }) {
|
|
86
|
+
if (!isAlive(e.pid)) return "dead";
|
|
87
|
+
const beat = Date.parse(e.heartbeatAt);
|
|
88
|
+
const every = Number.isFinite(e.heartbeatMs) && e.heartbeatMs > 0 ? e.heartbeatMs : HEARTBEAT_MS;
|
|
89
|
+
if (!Number.isFinite(beat) || now - beat > STALE_HEARTBEATS * every) return "silent";
|
|
90
|
+
return "live";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @param {any} e */
|
|
94
|
+
export function isEntry(e) {
|
|
95
|
+
return Boolean(e && typeof e === "object" && isValidSessionName(e.name) && Number.isInteger(e.pid) && e.pid > 0 && typeof e.socketPath === "string");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* What a claim of `name` should do, given the entry already there. Pure.
|
|
100
|
+
* @param {{existing:any, isAlive:(pid:number)=>boolean, pid:number, now:number, force?:boolean}} p
|
|
101
|
+
* @returns {{kind:'free'}|{kind:'stale', entry:any}|{kind:'taken', entry:any}|{kind:'force', entry:any}}
|
|
102
|
+
*/
|
|
103
|
+
export function planClaim({ existing, isAlive, pid, now, force = false }) {
|
|
104
|
+
if (!isEntry(existing)) return { kind: "free" };
|
|
105
|
+
if (existing.pid === pid || entryLiveness(existing, { isAlive, now }) !== "live") return { kind: "stale", entry: existing };
|
|
106
|
+
return force ? { kind: "force", entry: existing } : { kind: "taken", entry: existing };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @param {number} ms */
|
|
110
|
+
export function formatAge(ms) {
|
|
111
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
112
|
+
if (s < 60) return `${s}s`;
|
|
113
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
|
114
|
+
if (s < 86_400) return `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, "0")}m`;
|
|
115
|
+
return `${Math.floor(s / 86_400)}d${Math.floor((s % 86_400) / 3600)}h`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A registry entry as ListAgents reports it. Pure.
|
|
120
|
+
* @param {any} e @param {{now:number, self?:string|null}} o
|
|
121
|
+
*/
|
|
122
|
+
export function describeSession(e, { now, self = null }) {
|
|
123
|
+
const started = Date.parse(e.startedAt);
|
|
124
|
+
const beat = Date.parse(e.heartbeatAt);
|
|
125
|
+
return {
|
|
126
|
+
name: e.name,
|
|
127
|
+
cwd: e.cwd,
|
|
128
|
+
status: e.state === "busy" ? "busy" : e.state === "starting" ? "starting" : "idle",
|
|
129
|
+
age: Number.isFinite(started) ? formatAge(now - started) : null,
|
|
130
|
+
lastHeartbeat: Number.isFinite(beat) ? formatAge(now - beat) : null,
|
|
131
|
+
...(e.name === self ? { self: true } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Is a process alive? EPERM means it exists under another user. A live pid is
|
|
137
|
+
* necessary, not sufficient: see `entryLiveness`. @param {number} pid
|
|
138
|
+
*/
|
|
139
|
+
export function isPidAlive(pid) {
|
|
140
|
+
try {
|
|
141
|
+
process.kill(pid, 0);
|
|
142
|
+
return true;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return /** @type {NodeJS.ErrnoException} */ (e).code === "EPERM";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {{dir:string, isAlive?:(pid:number)=>boolean, now?:()=>number}} o
|
|
150
|
+
*/
|
|
151
|
+
export function createRegistry({ dir, isAlive = isPidAlive, now = Date.now }) {
|
|
152
|
+
const entryPath = (/** @type string */ name) => path.join(dir, `${name}.json`);
|
|
153
|
+
const ensureDir = () => {
|
|
154
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
155
|
+
try {
|
|
156
|
+
chmodSync(dir, 0o700);
|
|
157
|
+
} catch {
|
|
158
|
+
/* not ours to change; the socket itself is still 0600 */
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const read = (/** @type string */ name) => {
|
|
162
|
+
try {
|
|
163
|
+
const e = JSON.parse(readFileSync(entryPath(name), "utf8"));
|
|
164
|
+
return isEntry(e) ? e : null;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const removeFiles = (/** @type any */ e) => {
|
|
170
|
+
for (const p of [entryPath(e.name), e.socketPath].filter(Boolean)) {
|
|
171
|
+
try {
|
|
172
|
+
unlinkSync(p);
|
|
173
|
+
} catch {
|
|
174
|
+
/* already gone */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Remove a dead instance's files — under the lock, and only if the entry on
|
|
181
|
+
* disk is still that instance and still dead (a successor may have claimed
|
|
182
|
+
* the name since it was read). @param {any} seen
|
|
183
|
+
*/
|
|
184
|
+
const prune = (seen) =>
|
|
185
|
+
withLock(seen.name, () => {
|
|
186
|
+
const cur = read(seen.name);
|
|
187
|
+
if (cur && sameInstance(cur, seen) && entryLiveness(cur, { isAlive, now: now() }) === "dead") removeFiles(cur);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/** @template T @param {string} name @param {() => T} fn @returns {T|null} null = could not take the lock */
|
|
191
|
+
const withLock = (name, fn) => {
|
|
192
|
+
ensureDir();
|
|
193
|
+
const lock = path.join(dir, `${name}.lock`);
|
|
194
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
195
|
+
try {
|
|
196
|
+
closeSync(openSync(lock, "wx", 0o600));
|
|
197
|
+
try {
|
|
198
|
+
return fn();
|
|
199
|
+
} finally {
|
|
200
|
+
try {
|
|
201
|
+
unlinkSync(lock);
|
|
202
|
+
} catch {
|
|
203
|
+
/* removed by a stale-lock breaker */
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
if (/** @type any */ (e)?.code !== "EEXIST") throw e;
|
|
208
|
+
try {
|
|
209
|
+
if (now() - statSync(lock).mtimeMs > LOCK_STALE_MS) unlinkSync(lock);
|
|
210
|
+
} catch {
|
|
211
|
+
/* raced with its owner */
|
|
212
|
+
}
|
|
213
|
+
const until = Date.now() + 20;
|
|
214
|
+
while (Date.now() < until) {
|
|
215
|
+
/* a claim holds the lock for one small write */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
dir,
|
|
224
|
+
entryPath,
|
|
225
|
+
/** Live entries; dead ones are pruned, silent ones hidden. */
|
|
226
|
+
list() {
|
|
227
|
+
let files = [];
|
|
228
|
+
try {
|
|
229
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
230
|
+
} catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
const live = [];
|
|
234
|
+
for (const f of files.sort()) {
|
|
235
|
+
const e = read(f.slice(0, -".json".length));
|
|
236
|
+
if (!e) continue;
|
|
237
|
+
const l = entryLiveness(e, { isAlive, now: now() });
|
|
238
|
+
if (l === "live") live.push(e);
|
|
239
|
+
else if (l === "dead") prune(e);
|
|
240
|
+
}
|
|
241
|
+
return live;
|
|
242
|
+
},
|
|
243
|
+
/** @param {string} name */
|
|
244
|
+
get(name) {
|
|
245
|
+
if (!isValidSessionName(name)) return null;
|
|
246
|
+
const e = read(name);
|
|
247
|
+
if (!e) return null;
|
|
248
|
+
const l = entryLiveness(e, { isAlive, now: now() });
|
|
249
|
+
if (l === "live") return e;
|
|
250
|
+
if (l === "dead") prune(e);
|
|
251
|
+
return null;
|
|
252
|
+
},
|
|
253
|
+
/**
|
|
254
|
+
* @param {any} entry @param {{force?:boolean}} [o]
|
|
255
|
+
* @returns {{ok:true}|{ok:false, code:'session_name_taken'|'registry_locked'|'invalid_session_name', message:string, entry?:any}}
|
|
256
|
+
*/
|
|
257
|
+
claim(entry, { force = false } = {}) {
|
|
258
|
+
if (!isValidSessionName(entry.name)) return { ok: false, code: "invalid_session_name", message: `invalid session name "${entry.name}"` };
|
|
259
|
+
const r = withLock(entry.name, () => {
|
|
260
|
+
const plan = planClaim({ existing: read(entry.name), isAlive, pid: entry.pid, now: now(), force });
|
|
261
|
+
if (plan.kind === "taken") return { ok: /** @type {false} */ (false), code: /** @type {'session_name_taken'} */ ("session_name_taken"), message: `a session named "${entry.name}" is already running (pid ${plan.entry.pid}, ${plan.entry.cwd})`, entry: plan.entry };
|
|
262
|
+
if (plan.kind === "stale") removeFiles(plan.entry);
|
|
263
|
+
writeJsonAtomic(entryPath(entry.name), entry, { mode: 0o600 });
|
|
264
|
+
return { ok: /** @type {true} */ (true) };
|
|
265
|
+
});
|
|
266
|
+
return r ?? { ok: false, code: "registry_locked", message: `could not lock the registry entry for "${entry.name}"` };
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* The owner's heartbeat and state writes: rewrite its own entry — or put it
|
|
270
|
+
* back if it has vanished (or only a dead or silent instance holds the name).
|
|
271
|
+
* "replaced" = another live session holds the name now; nothing is written.
|
|
272
|
+
* @param {any} entry the owner's full entry
|
|
273
|
+
* @returns {'ok'|'restored'|'replaced'|'locked'|'failed'}
|
|
274
|
+
*/
|
|
275
|
+
refresh(entry) {
|
|
276
|
+
const r = withLock(entry.name, () => {
|
|
277
|
+
const cur = read(entry.name);
|
|
278
|
+
const own = sameInstance(cur, entry);
|
|
279
|
+
if (cur && !own && planClaim({ existing: cur, isAlive, pid: entry.pid, now: now() }).kind === "taken") return "replaced";
|
|
280
|
+
try {
|
|
281
|
+
if (cur && !own) removeFiles({ ...cur, socketPath: cur.socketPath === entry.socketPath ? "" : cur.socketPath });
|
|
282
|
+
writeJsonAtomic(entryPath(entry.name), entry, { mode: 0o600 });
|
|
283
|
+
return own ? "ok" : "restored";
|
|
284
|
+
} catch {
|
|
285
|
+
return "failed";
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
return r ?? "locked";
|
|
289
|
+
},
|
|
290
|
+
/** Rewrite the caller's own entry (by pid). @param {string} name @param {number} pid @param {Record<string,unknown>} patch */
|
|
291
|
+
update(name, pid, patch) {
|
|
292
|
+
const r = withLock(name, () => {
|
|
293
|
+
const e = read(name);
|
|
294
|
+
if (!e || e.pid !== pid) return false;
|
|
295
|
+
try {
|
|
296
|
+
writeJsonAtomic(entryPath(name), { ...e, ...patch }, { mode: 0o600 });
|
|
297
|
+
return true;
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
return r ?? false;
|
|
303
|
+
},
|
|
304
|
+
/**
|
|
305
|
+
* Remove the caller's own entry (not a successor's).
|
|
306
|
+
* @param {string} name @param {number} pid @param {{sessionId:string, startedAt:string}} [instance]
|
|
307
|
+
*/
|
|
308
|
+
release(name, pid, instance) {
|
|
309
|
+
const r = withLock(name, () => {
|
|
310
|
+
const e = read(name);
|
|
311
|
+
if (!e || e.pid !== pid) return false;
|
|
312
|
+
if (instance && !sameInstance(e, { pid, ...instance })) return false;
|
|
313
|
+
try {
|
|
314
|
+
unlinkSync(entryPath(name));
|
|
315
|
+
} catch {
|
|
316
|
+
/* already gone */
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
});
|
|
320
|
+
return r ?? false;
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Where the registry lives for this process. @param {Record<string,string|undefined>} env */
|
|
326
|
+
export function registryDirFromEnv(env) {
|
|
327
|
+
return defaultRegistryDir({ env, homedir: env.HOME || os.homedir() });
|
|
328
|
+
}
|