@cohortapp/agent-sdk 2.11.15 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +37 -22
- package/README.md +2 -0
- package/bin/maestro.mjs +117 -39
- package/bin/maestro.test.mjs +175 -5
- package/docs/guides/front-door-session.md +313 -0
- package/docs/guides/mac-mini.md +100 -28
- package/docs/guides/org-onboarding.md +1 -1
- package/docs/guides/setup-wizard.md +9 -5
- package/docs/runbooks/cohort-cutover.md +11 -1
- package/docs/runbooks/mac-mini-bootstrap.md +38 -63
- package/lib/cadence-bus-requeue.test.mjs +83 -0
- package/lib/cadence-bus.mjs +43 -7
- package/lib/channels/inbox-item.mjs +59 -2
- package/lib/cli/board.mjs +285 -0
- package/lib/cli/board.test.mjs +227 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/doctor-checks.mjs +441 -0
- package/lib/cli/doctor-checks.test.mjs +336 -0
- package/lib/cli/global-setup-extras.mjs +454 -0
- package/lib/cli/global-setup-extras.test.mjs +462 -0
- package/lib/cli/inbox.mjs +304 -0
- package/lib/cli/inbox.test.mjs +230 -0
- package/lib/cli/session-ack.mjs +63 -0
- package/lib/cli/session-ack.test.mjs +63 -0
- package/lib/cli/session.mjs +760 -0
- package/lib/cli/session.test.mjs +613 -0
- package/lib/collective/global-config.mjs +209 -6
- package/lib/collective/global-config.test.mjs +145 -0
- package/lib/collective/global-skills.mjs +145 -0
- package/lib/collective/global-skills.test.mjs +126 -0
- package/lib/collective/presence.mjs +4 -3
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/comms/send-gate.mjs +115 -0
- package/lib/comms/send-gate.test.mjs +113 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/feature-init.mjs +2 -2
- package/lib/mcp/server.test.mjs +9 -4
- package/lib/model-router/spawn.test.mjs +21 -0
- package/lib/org/board-mine-cache.mjs +99 -0
- package/lib/org/board-mine-cache.test.mjs +53 -0
- package/lib/org/board.mjs +11 -0
- package/lib/org/board.test.mjs +11 -1
- package/lib/org/client.mjs +36 -0
- package/lib/org/client.test.mjs +46 -0
- package/lib/org/inbound/directedness.mjs +18 -2
- package/lib/org/inbound/directedness.test.mjs +58 -0
- package/lib/org/inbound/index.mjs +8 -1
- package/lib/org/inbound/index.test.mjs +22 -0
- package/lib/org/mesh-directives.test.mjs +110 -0
- package/lib/org/mesh.mjs +61 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +52 -0
- package/lib/org/protocol.test.mjs +12 -1
- package/lib/org/registry.mjs +3 -2
- package/lib/org/tool-surface.mjs +120 -0
- package/lib/org/tool-surface.test.mjs +118 -5
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/lib/security/external-content.mjs +1 -1
- package/lib/security/external-content.test.mjs +17 -0
- package/lib/session/config.mjs +137 -0
- package/lib/session/config.test.mjs +92 -0
- package/lib/session/feed-core.mjs +229 -0
- package/lib/session/feed-core.test.mjs +198 -0
- package/lib/session/first-run.mjs +126 -0
- package/lib/session/first-run.test.mjs +121 -0
- package/lib/session/frontdoor.mjs +266 -0
- package/lib/session/frontdoor.test.mjs +205 -0
- package/lib/session/handoffs.mjs +295 -0
- package/lib/session/handoffs.test.mjs +183 -0
- package/lib/session/identity.mjs +220 -0
- package/lib/session/identity.test.mjs +180 -0
- package/lib/session/inbox-claims.mjs +434 -0
- package/lib/session/inbox-claims.test.mjs +286 -0
- package/lib/session/launch-args.mjs +161 -0
- package/lib/session/launch-args.test.mjs +157 -0
- package/lib/session/liveness.mjs +174 -0
- package/lib/session/liveness.test.mjs +100 -0
- package/lib/session/status-summary.mjs +172 -0
- package/lib/session/status-summary.test.mjs +118 -0
- package/lib/session-permissions.mjs +39 -3
- package/lib/session-permissions.test.mjs +20 -0
- package/lib/setup/claude-probe.mjs +161 -24
- package/lib/setup/claude-probe.test.mjs +187 -0
- package/lib/setup/sections/learning.mjs +2 -1
- package/lib/setup/sections/model.mjs +104 -24
- package/lib/setup/sections/model.test.mjs +240 -0
- package/lib/setup/sections/org.mjs +27 -2
- package/lib/setup/sections/org.test.mjs +35 -2
- package/lib/setup/sections/verify.mjs +5 -0
- package/lib/setup/state.mjs +30 -10
- package/lib/setup/state.test.mjs +24 -1
- package/lib/singleton.js +11 -3
- package/lib/singleton.test.mjs +16 -0
- package/lib/subagents/lock.mjs +1 -1
- package/lib/telemetry/collect.mjs +270 -6
- package/lib/telemetry/collect.test.mjs +196 -1
- package/lib/upgrade/global-refresh.mjs +108 -0
- package/lib/upgrade/global-refresh.test.mjs +65 -0
- package/lib/upgrade/launchd-reconcile.mjs +327 -0
- package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
- package/lib/upgrade/post-steps.mjs +151 -0
- package/lib/upgrade/post-steps.test.mjs +200 -0
- package/lib/upgrade/verify.mjs +215 -0
- package/lib/upgrade/verify.test.mjs +164 -0
- package/lib/voice/outbound.mjs +3 -2
- package/lib/voice/post-call-brief.mjs +2 -1
- package/lib/voice/session-rotation.mjs +6 -1
- package/lib/voice/session-rotation.test.mjs +114 -0
- package/package.json +3 -3
- package/plugins/maestro-skills/plugin.json +25 -1
- package/plugins/maestro-skills/skills/board-work.md +63 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
- package/plugins/maestro-skills/skills/main-session.md +102 -0
- package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
- package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scaffold/CLAUDE.md +24 -0
- package/scripts/ci/check-durable-write-seam.mjs +147 -0
- package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +6 -0
- package/scripts/collective/hook-runner.mjs +39 -4
- package/scripts/collective/hook-runner.test.mjs +85 -2
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
- package/scripts/daemon/agent-daemon.mjs +249 -10
- package/scripts/daemon/agent-daemon.test.mjs +73 -0
- package/scripts/daemon/assurance-e2e.test.mjs +141 -6
- package/scripts/daemon/assurance.mjs +461 -37
- package/scripts/daemon/assurance.test.mjs +408 -43
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
- package/scripts/daemon/cadence-consumer.mjs +289 -89
- package/scripts/daemon/cadence-handlers.mjs +53 -0
- package/scripts/daemon/classifier.mjs +1 -1
- package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
- package/scripts/daemon/dispatcher.mjs +127 -19
- package/scripts/daemon/health.mjs +12 -1
- package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
- package/scripts/daemon/inbox-deferral.mjs +6 -0
- package/scripts/daemon/lib/self-echo.mjs +201 -0
- package/scripts/daemon/lib/self-echo.test.mjs +153 -0
- package/scripts/daemon/maestro-daemon.mjs +3 -0
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/daemon/responder.mjs +51 -40
- package/scripts/daemon/sdk-version.mjs +51 -0
- package/scripts/daemon/sdk-version.test.mjs +31 -0
- package/scripts/hooks/pre-send-audit.sh +97 -4
- package/scripts/hooks/pre-send-audit.test.mjs +140 -1
- package/scripts/local-triggers/autoupdate.sh +243 -19
- package/scripts/local-triggers/autoupdate.test.mjs +518 -0
- package/scripts/local-triggers/generate-plists.sh +24 -1
- package/scripts/local-triggers/generate-plists.test.mjs +49 -11
- package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
- package/scripts/org/send-orgmail.mjs +27 -3
- package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
- package/scripts/poller/slack-poller.mjs +13 -1
- package/scripts/poller/utils.mjs +46 -1
- package/scripts/poller-launchd/install.sh +19 -11
- package/scripts/poller-launchd/install.test.mjs +243 -0
- package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
- package/scripts/poller-launchd/migrate.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +4 -2
- package/scripts/session/feed.mjs +237 -0
- package/scripts/session/feed.test.mjs +196 -0
- package/scripts/session/supervisor-sh.test.mjs +218 -0
- package/scripts/session/supervisor.mjs +328 -0
- package/scripts/session/supervisor.sh +141 -0
- package/scripts/session/supervisor.test.mjs +482 -0
- package/scripts/setup/configure-macos.sh +250 -55
- package/scripts/setup/configure-macos.test.mjs +306 -0
- package/scripts/setup/init-agent.sh +112 -7
- package/scripts/setup/init-agent.test.mjs +220 -1
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
- package/scripts/watchdog/memory-watchdog.sh +37 -1
- package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
- package/scripts/setup/boot-claude-session.sh +0 -94
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* prompt under schedules/triggers/<name>.md.
|
|
17
17
|
* Unknown cadences with a prompt on disk default to escalate; without
|
|
18
18
|
* a prompt they go straight to dlq with a clear error.
|
|
19
|
+
* FRONT DOOR (design §3.4): when the seat's main session is live, an
|
|
20
|
+
* escalate/guarded tick is HANDED to it (state/session/handoffs/<id>.json
|
|
21
|
+
* + the rendered prompt) and processed as "handed-to-session" instead of
|
|
22
|
+
* spawning; a handoff un-acked past its deadline is re-enqueued with
|
|
23
|
+
* metadata.handoffTimedOut=true and then spawned on the legacy lane.
|
|
24
|
+
* A deferred tick (circuit, governance, concurrency, per-cadence
|
|
25
|
+
* in-flight guard) is re-keyed to the TAIL and the drain keeps scanning.
|
|
19
26
|
* 4. Respects .emergency-stop: while present, the loop logs a heartbeat
|
|
20
27
|
* but never spawns a sub-session and never processes events. Existing
|
|
21
28
|
* claims remain on disk so they can be resumed once the stop is lifted.
|
|
@@ -49,7 +56,12 @@
|
|
|
49
56
|
* logger optional fn({ ts, level, …rest }) → void for tests.
|
|
50
57
|
* now injectable clock fn() → ms (tests); used when re-stamping
|
|
51
58
|
* in-flight claim mtimes so the stale-claim sweep can't sweep
|
|
52
|
-
* a live escalate (audit L6).
|
|
59
|
+
* a live escalate (audit L6), and for handoff deadlines.
|
|
60
|
+
* frontDoorState injected fn() → { frontDoor, sessionLive } (tests);
|
|
61
|
+
* defaults to lib/session/frontdoor#readFrontDoorState over
|
|
62
|
+
* config/session.yaml + state/session/heartbeat.json.
|
|
63
|
+
* handoffDeadlineMs how long a handed-off tick may sit un-acked before it
|
|
64
|
+
* is re-enqueued on the legacy lane (default 30 min).
|
|
53
65
|
*/
|
|
54
66
|
|
|
55
67
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, openSync, closeSync, statSync, unlinkSync, utimesSync } from "node:fs";
|
|
@@ -63,6 +75,7 @@ import {
|
|
|
63
75
|
completeTick,
|
|
64
76
|
failTick,
|
|
65
77
|
requeueTick,
|
|
78
|
+
enqueueTick,
|
|
66
79
|
recoverStaleClaims,
|
|
67
80
|
sweepRetention,
|
|
68
81
|
writeHealth,
|
|
@@ -70,12 +83,24 @@ import {
|
|
|
70
83
|
logBusEvent,
|
|
71
84
|
busDepth,
|
|
72
85
|
} from "../../lib/cadence-bus.mjs";
|
|
86
|
+
// FRONT DOOR (design §3.4). When the seat's main session is live, an
|
|
87
|
+
// escalate/guarded tick is HANDED to it (a handoff file + the rendered prompt
|
|
88
|
+
// on disk) instead of spawning a `claude --print` sub-session; the session acks
|
|
89
|
+
// with `maestro session ack <tickId>`. A handoff past its deadline is expired
|
|
90
|
+
// and the tick re-enqueued with metadata.handoffTimedOut=true, which is never
|
|
91
|
+
// handed off again — so a wedged session cannot starve a cadence. Not live →
|
|
92
|
+
// the legacy spawn, unchanged. The decision is pure; the state reader is the
|
|
93
|
+
// one edge and it fails open to "not live".
|
|
94
|
+
import { readFrontDoorState, shouldHandOffTick } from "../../lib/session/frontdoor.mjs";
|
|
95
|
+
import { writeHandoff, listHandoffs, expireHandoffs, pruneHandoffs, handoffPaths } from "../../lib/session/handoffs.mjs";
|
|
96
|
+
import { writeFileAtomic } from "../../lib/fs-atomic.mjs";
|
|
73
97
|
import { resolveClaudeBin as sharedResolveClaude, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
|
|
74
98
|
import { getCadenceDef } from "./cadence-handlers.mjs";
|
|
75
99
|
import { obligationAllowedUnderPosture } from "../../lib/plan/compile.mjs";
|
|
76
100
|
import { isHumanLaneCadence } from "../../lib/cadences.mjs";
|
|
77
101
|
import { sessionPermissionArgs } from "../../lib/session-permissions.mjs";
|
|
78
102
|
import { renderTemplate, buildContext } from "../../lib/render.mjs";
|
|
103
|
+
import { withParallelism } from "../../lib/prompts/parallelism.mjs";
|
|
79
104
|
// Model router (opt-in). A `schema_version: 2` config routes each cadence
|
|
80
105
|
// sub-session's model through resolveChain (SPEC §6.2): the RouteDecision
|
|
81
106
|
// supplies the --model flag, the session-retarget env (third-party backends),
|
|
@@ -291,6 +316,43 @@ export function routeCadenceSpawn(agentRoot, cadence) {
|
|
|
291
316
|
}
|
|
292
317
|
}
|
|
293
318
|
|
|
319
|
+
/**
|
|
320
|
+
* A cadence trigger prompt, as the sub-session actually receives it.
|
|
321
|
+
*
|
|
322
|
+
* TWO transforms, and BOTH lanes must apply them or the two lanes disagree
|
|
323
|
+
* about what the same tick asked for:
|
|
324
|
+
*
|
|
325
|
+
* 1. `{{agent.*}}` / `{{company.*}}` identity tokens are rendered from
|
|
326
|
+
* config/agent.json + config/company.json (lib/render.mjs), so a trigger
|
|
327
|
+
* shipped generically in the package resolves to THIS seat. No-op on
|
|
328
|
+
* token-free text.
|
|
329
|
+
* 2. The fleet parallelism directive LEADS the body (WP-M7 mechanic 5). An
|
|
330
|
+
* escalate/guarded tick is exactly the sub-session the directive exists
|
|
331
|
+
* for — a trigger that asks for six independent audits has, without it,
|
|
332
|
+
* no standing permission to fan out, and runs them one at a time. The
|
|
333
|
+
* inbox/backlog lane gets this from `prompt-builder.mjs#buildPrompt`;
|
|
334
|
+
* cadence prompts never pass through that builder, which is how this lane
|
|
335
|
+
* was missed. `withParallelism` is idempotent, so a trigger file that
|
|
336
|
+
* already carries the text keeps exactly one copy.
|
|
337
|
+
*
|
|
338
|
+
* Pure apart from the two config reads; a missing/invalid config/agent.json
|
|
339
|
+
* leaves the body's tokens verbatim rather than failing the tick.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} agentRoot
|
|
342
|
+
* @param {string} body the raw trigger prompt as read from disk
|
|
343
|
+
* @returns {string}
|
|
344
|
+
*/
|
|
345
|
+
export function renderCadencePromptBody(agentRoot, body) {
|
|
346
|
+
let out = typeof body === "string" ? body : "";
|
|
347
|
+
try {
|
|
348
|
+
const agentCfg = JSON.parse(readFileSync(join(agentRoot, "config/agent.json"), "utf-8"));
|
|
349
|
+
let companyCfg = {};
|
|
350
|
+
try { companyCfg = JSON.parse(readFileSync(join(agentRoot, "config/company.json"), "utf-8")); } catch { /* optional */ }
|
|
351
|
+
out = renderTemplate(out, buildContext(agentCfg, null, companyCfg));
|
|
352
|
+
} catch { /* no/invalid config/agent.json — leave body verbatim */ }
|
|
353
|
+
return withParallelism(out);
|
|
354
|
+
}
|
|
355
|
+
|
|
294
356
|
/**
|
|
295
357
|
* Spawn a sub-session running the cadence's trigger prompt and resolve
|
|
296
358
|
* with { exit_code, durationMs, stderr_tail }. Reads the prompt at call
|
|
@@ -315,17 +377,9 @@ function realSpawnSession({ agentRoot, cadence, promptPath, timeoutMs, log }) {
|
|
|
315
377
|
return;
|
|
316
378
|
}
|
|
317
379
|
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
// No-op on token-free text, so this is behaviour-preserving for any not-yet-
|
|
322
|
-
// tokenised trigger.
|
|
323
|
-
try {
|
|
324
|
-
const agentCfg = JSON.parse(readFileSync(join(agentRoot, "config/agent.json"), "utf-8"));
|
|
325
|
-
let companyCfg = {};
|
|
326
|
-
try { companyCfg = JSON.parse(readFileSync(join(agentRoot, "config/company.json"), "utf-8")); } catch { /* optional */ }
|
|
327
|
-
body = renderTemplate(body, buildContext(agentCfg, null, companyCfg));
|
|
328
|
-
} catch { /* no/invalid config/agent.json — leave body verbatim */ }
|
|
380
|
+
// Identity tokens + the parallelism directive, at spawn time — one seam,
|
|
381
|
+
// shared with the front-door handoff lane. See renderCadencePromptBody.
|
|
382
|
+
body = renderCadencePromptBody(agentRoot, body);
|
|
329
383
|
|
|
330
384
|
const bin = resolveClaudeBin();
|
|
331
385
|
// Permission scoping (audit H1). sessionPermissionArgs() defaults to the
|
|
@@ -549,6 +603,37 @@ export function startConsumer(opts = {}) {
|
|
|
549
603
|
const governor = opts.governor || resourceGovernor;
|
|
550
604
|
const rateGuard = opts.rateGuard || rateGuardModule;
|
|
551
605
|
const budgetGuard = opts.budgetGuard || budgetGuardModule;
|
|
606
|
+
const handoffDeadlineMs = opts.handoffDeadlineMs;
|
|
607
|
+
const frontDoorStateImpl = typeof opts.frontDoorState === "function"
|
|
608
|
+
? opts.frontDoorState
|
|
609
|
+
: () => readFrontDoorState(agentRoot, { now: nowMs() });
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* The front-door state for THIS decision. A throwing reader means "not
|
|
613
|
+
* live" — the legacy lane runs, nothing is dropped.
|
|
614
|
+
*/
|
|
615
|
+
function frontDoorState() {
|
|
616
|
+
try {
|
|
617
|
+
const st = frontDoorStateImpl();
|
|
618
|
+
return st && typeof st === "object" ? st : { frontDoor: "daemon", sessionLive: false };
|
|
619
|
+
} catch {
|
|
620
|
+
return { frontDoor: "daemon", sessionLive: false, reason: "reader-error" };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Render a cadence prompt the way realSpawnSession does — the SAME
|
|
626
|
+
* `renderCadencePromptBody`, so the handed-off prompt is byte-identical to
|
|
627
|
+
* what a sub-session would have been given, parallelism directive included —
|
|
628
|
+
* and land it under state/session/handoffs/prompts/<tickId>.md.
|
|
629
|
+
*/
|
|
630
|
+
function renderHandoffPrompt(tickId, promptPath) {
|
|
631
|
+
const fullPrompt = join(agentRoot, promptPath);
|
|
632
|
+
const body = renderCadencePromptBody(agentRoot, readFileSync(fullPrompt, "utf-8"));
|
|
633
|
+
const out = join(handoffPaths(agentRoot).prompts, `${tickId}.md`);
|
|
634
|
+
writeFileAtomic(out, body);
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
552
637
|
|
|
553
638
|
/**
|
|
554
639
|
* Admission gate for an escalation. Folds the shared 429 breaker + the
|
|
@@ -635,6 +720,11 @@ export function startConsumer(opts = {}) {
|
|
|
635
720
|
dlq: 0,
|
|
636
721
|
retries: 0,
|
|
637
722
|
spawn_failures: 0,
|
|
723
|
+
// Front door (design §3.4): ticks handed to the live main session, handoffs
|
|
724
|
+
// that timed out and were re-enqueued, and deferrals re-keyed to the tail.
|
|
725
|
+
handed_off: 0,
|
|
726
|
+
handoff_timeouts: 0,
|
|
727
|
+
deferred: 0,
|
|
638
728
|
last_event_id: null,
|
|
639
729
|
last_decision: null,
|
|
640
730
|
};
|
|
@@ -660,10 +750,94 @@ export function startConsumer(opts = {}) {
|
|
|
660
750
|
// change recoverStaleClaims' signature (it lives in lib/cadence-bus.mjs,
|
|
661
751
|
// owned by another change-set).
|
|
662
752
|
const inFlightClaimIds = new Set();
|
|
753
|
+
// Per-cadence in-flight guard (audit F9: the same cadence spawned twice
|
|
754
|
+
// back-to-back). A cadence whose sub-session is still running, or whose
|
|
755
|
+
// handoff is still open with the session, must not start again — the second
|
|
756
|
+
// tick is deferred to the tail instead. Names, not ids: two ticks of one
|
|
757
|
+
// cadence are the duplicate this guards against.
|
|
758
|
+
const inFlightCadences = new Set();
|
|
663
759
|
|
|
664
760
|
// Injectable clock so tests can drive the stale window deterministically.
|
|
665
761
|
const nowMs = typeof opts.now === "function" ? opts.now : Date.now;
|
|
666
762
|
|
|
763
|
+
/** Is a cadence in flight — running here, or open as a handoff with the session? */
|
|
764
|
+
function cadenceInFlight(cadence) {
|
|
765
|
+
if (inFlightCadences.has(cadence)) return "sub-session";
|
|
766
|
+
try {
|
|
767
|
+
if (listHandoffs(agentRoot).some((h) => h.cadence === cadence)) return "handoff";
|
|
768
|
+
} catch { /* unreadable handoff dir → not a reason to block */ }
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Re-queue a tick that cannot run YET (an upstream gate, never a per-event
|
|
774
|
+
* failure) to the TAIL under a new id (audit F8). Attempts untouched.
|
|
775
|
+
* `count:false` (the circuit/backoff skip) re-keys without touching the
|
|
776
|
+
* retry/deferral counters or last_decision — a held-back cadence was never
|
|
777
|
+
* attempted, and the skip is already logged and counted as skipped_*; a
|
|
778
|
+
* 30 s poll inside a 60 min circuit window must not read as 120 retries.
|
|
779
|
+
*/
|
|
780
|
+
function deferTick(event, reason, extra = {}, { count = true } = {}) {
|
|
781
|
+
const r = requeueTick(agentRoot, event, { defer: true });
|
|
782
|
+
if (count) {
|
|
783
|
+
stats.retries += 1;
|
|
784
|
+
stats.deferred += 1;
|
|
785
|
+
stats.last_decision = "deferred";
|
|
786
|
+
log({ level: "info", stage: "escalate_deferred", reason, id: event.id, requeued_as: r && r.id, cadence: event.cadence, ...extra });
|
|
787
|
+
}
|
|
788
|
+
return { ok: false, decision: "deferred", reason };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Ledger hygiene runs from the sweep at most this often.
|
|
792
|
+
const HANDOFF_PRUNE_EVERY_MS = 60 * 60_000;
|
|
793
|
+
let lastHandoffPruneAt = 0;
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Expire handoffs past their deadline and re-enqueue each tick on the legacy
|
|
797
|
+
* lane with metadata.handoffTimedOut=true (never handed off again). The
|
|
798
|
+
* re-enqueue runs BEFORE the handoff is retired (expireHandoffs#onExpire):
|
|
799
|
+
* if the bus is unwritable the handoff stays open and the next sweep
|
|
800
|
+
* retries, so the cadence run is never lost. Cheap: one readdir of a small
|
|
801
|
+
* directory. Never throws.
|
|
802
|
+
*/
|
|
803
|
+
function sweepHandoffTimeouts() {
|
|
804
|
+
const now = nowMs();
|
|
805
|
+
let out;
|
|
806
|
+
try {
|
|
807
|
+
out = expireHandoffs(agentRoot, {
|
|
808
|
+
now,
|
|
809
|
+
deadlineMs: handoffDeadlineMs,
|
|
810
|
+
onExpire: (h) => {
|
|
811
|
+
const meta = h.metadata && typeof h.metadata === "object" ? h.metadata : {};
|
|
812
|
+
const r = enqueueTick({
|
|
813
|
+
cadence: h.cadence,
|
|
814
|
+
source: "handoff-timeout",
|
|
815
|
+
agentRoot,
|
|
816
|
+
metadata: { ...meta, handoffTimedOut: true, handoffTickId: h.tickId, reason: `handoff to session timed out (${h.deadlineAt || "no deadline"})` },
|
|
817
|
+
});
|
|
818
|
+
// enqueueTick swallows an unwritable inbox (fallbackOnly, audit row
|
|
819
|
+
// only) — that is NOT back on the bus: keep the handoff open.
|
|
820
|
+
if (!r || !r.path) throw new Error(`tick not enqueued (${r && r.fallbackOnly ? "inbox unwritable" : "no path"})`);
|
|
821
|
+
stats.handoff_timeouts += 1;
|
|
822
|
+
log({ level: "warn", stage: "handoff_timed_out", id: h.tickId, cadence: h.cadence, requeued_as: r.id, deadline_at: h.deadlineAt || null });
|
|
823
|
+
return true;
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
} catch { return 0; }
|
|
827
|
+
for (const f of out.failed) {
|
|
828
|
+
log({ level: "error", stage: "handoff_requeue_failed", id: f.tickId, error: f.error, note: "handoff kept open; retried next sweep" });
|
|
829
|
+
}
|
|
830
|
+
if (now - lastHandoffPruneAt >= HANDOFF_PRUNE_EVERY_MS) {
|
|
831
|
+
lastHandoffPruneAt = now;
|
|
832
|
+
try {
|
|
833
|
+
const pr = pruneHandoffs(agentRoot, { now });
|
|
834
|
+
const n = pr.removed.done.length + pr.removed.prompts.length;
|
|
835
|
+
if (n > 0) log({ level: "info", stage: "handoffs_pruned", done: pr.removed.done.length, prompts: pr.removed.prompts.length });
|
|
836
|
+
} catch { /* hygiene is never a reason to skip the tick */ }
|
|
837
|
+
}
|
|
838
|
+
return out.expired.length;
|
|
839
|
+
}
|
|
840
|
+
|
|
667
841
|
/**
|
|
668
842
|
* Re-stamp the mtime of every in-flight claim file to the current time so
|
|
669
843
|
* the next mtime-based stale sweep treats it as fresh. Best-effort: a missing
|
|
@@ -793,56 +967,22 @@ export function startConsumer(opts = {}) {
|
|
|
793
967
|
});
|
|
794
968
|
if (gate.reason === "circuit-open") stats.skipped_circuit_open += 1;
|
|
795
969
|
else stats.skipped_backoff += 1;
|
|
796
|
-
// Put the event back
|
|
797
|
-
// single-sourced in failTick (audit M5), so we
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
// a bare writeFileSync so a crash can't leave a half-written inbox file.
|
|
801
|
-
requeueTick(agentRoot, event);
|
|
970
|
+
// Put the event back — to the TAIL (F8). Attempt accounting is
|
|
971
|
+
// single-sourced in failTick (audit M5), so we never touch it here: a
|
|
972
|
+
// held-back-by-circuit re-queue is not a failed attempt, nor a retry.
|
|
973
|
+
deferTick(event, gate.reason, { retry_at: new Date(gate.retry_at).toISOString() }, { count: false });
|
|
802
974
|
return { ok: false, decision: gate.reason };
|
|
803
975
|
}
|
|
804
976
|
|
|
805
977
|
// WS4 governance gate — beside the concurrency cap. If the host is under
|
|
806
978
|
// memory/load pressure, the 429 breaker is open, or the daily budget is
|
|
807
|
-
// exhausted (essential-only), DEFER this cadence tick: requeue it
|
|
808
|
-
// and DO NOT call failTick (a governor/rate deferral is an upstream
|
|
809
|
-
// not a per-event failure — burning retry budget here would
|
|
810
|
-
// a perfectly good cadence just because the box was busy).
|
|
979
|
+
// exhausted (essential-only), DEFER this cadence tick: requeue it to the
|
|
980
|
+
// tail and DO NOT call failTick (a governor/rate deferral is an upstream
|
|
981
|
+
// gate, not a per-event failure — burning retry budget here would
|
|
982
|
+
// eventually DLQ a perfectly good cadence just because the box was busy).
|
|
811
983
|
{
|
|
812
984
|
const gov = governanceGate(event.cadence);
|
|
813
|
-
if (!gov.admit)
|
|
814
|
-
log({
|
|
815
|
-
level: "info",
|
|
816
|
-
stage: "escalate_deferred",
|
|
817
|
-
reason: gov.reason,
|
|
818
|
-
id: event.id,
|
|
819
|
-
cadence: event.cadence,
|
|
820
|
-
});
|
|
821
|
-
requeueTick(agentRoot, event);
|
|
822
|
-
stats.retries += 1;
|
|
823
|
-
stats.last_decision = "deferred";
|
|
824
|
-
return { ok: false, decision: "deferred" };
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (activeSubSessions >= MAX_CONCURRENT_SUB_SESSIONS) {
|
|
829
|
-
// Re-queue and try again next tick. Single-owner cadence consumer
|
|
830
|
-
// means this can only happen when a prior tick is still running —
|
|
831
|
-
// queue depth is the right back-pressure signal.
|
|
832
|
-
log({
|
|
833
|
-
level: "info",
|
|
834
|
-
stage: "escalate_deferred",
|
|
835
|
-
id: event.id,
|
|
836
|
-
cadence: event.cadence,
|
|
837
|
-
active_subsessions: activeSubSessions,
|
|
838
|
-
});
|
|
839
|
-
// Re-queue unchanged — concurrent-spawn isn't a per-event failure, so
|
|
840
|
-
// it must not touch the attempt count (single-sourced in failTick,
|
|
841
|
-
// audit M5). Atomic requeueTick (audit M6) replaces the bare
|
|
842
|
-
// writeFileSync that could leave a half-written inbox file.
|
|
843
|
-
requeueTick(agentRoot, event);
|
|
844
|
-
stats.retries += 1;
|
|
845
|
-
return { ok: false, decision: "deferred" };
|
|
985
|
+
if (!gov.admit) return deferTick(event, gov.reason);
|
|
846
986
|
}
|
|
847
987
|
|
|
848
988
|
const def = getCadenceDef(event.cadence);
|
|
@@ -861,7 +1001,61 @@ export function startConsumer(opts = {}) {
|
|
|
861
1001
|
}
|
|
862
1002
|
}
|
|
863
1003
|
|
|
1004
|
+
// Per-cadence in-flight guard (F9): never the same cadence twice at once —
|
|
1005
|
+
// neither as two sub-sessions nor as a sub-session beside an open handoff.
|
|
1006
|
+
{
|
|
1007
|
+
const why = cadenceInFlight(event.cadence);
|
|
1008
|
+
if (why) return deferTick(event, `cadence-in-flight:${why}`);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// FRONT DOOR (§3.4): a live main session takes this tick instead of a
|
|
1012
|
+
// sub-session. Handoff = rendered prompt on disk + the ledger file; the
|
|
1013
|
+
// tick is processed here with decision "handed-to-session". A tick whose
|
|
1014
|
+
// previous handoff timed out (metadata.handoffTimedOut) falls through to
|
|
1015
|
+
// the legacy spawn — shouldHandOffTick is pure and owns that rule.
|
|
1016
|
+
{
|
|
1017
|
+
const fd = frontDoorState();
|
|
1018
|
+
const verdict = shouldHandOffTick({ ...fd, mode: def?.mode, metadata: event.metadata });
|
|
1019
|
+
if (verdict.handOff) {
|
|
1020
|
+
try {
|
|
1021
|
+
const rendered = renderHandoffPrompt(event.id, promptPath);
|
|
1022
|
+
const h = writeHandoff(agentRoot, {
|
|
1023
|
+
tickId: event.id,
|
|
1024
|
+
cadence: event.cadence,
|
|
1025
|
+
mode: def?.mode || "escalate",
|
|
1026
|
+
promptPath: rendered,
|
|
1027
|
+
metadata: { ...(event.metadata || {}), sourcePrompt: promptPath },
|
|
1028
|
+
}, { now: nowMs(), deadlineMs: handoffDeadlineMs });
|
|
1029
|
+
if (!h.ok) throw new Error(h.error || "handoff write failed");
|
|
1030
|
+
completeTick(agentRoot, event.id, {
|
|
1031
|
+
decision: "handed-to-session",
|
|
1032
|
+
cadence: event.cadence,
|
|
1033
|
+
prompt: promptPath,
|
|
1034
|
+
handoff: h.path,
|
|
1035
|
+
deadline_at: h.handoff && h.handoff.deadlineAt,
|
|
1036
|
+
});
|
|
1037
|
+
stats.handed_off += 1;
|
|
1038
|
+
stats.last_decision = "handed-to-session";
|
|
1039
|
+
log({ level: "info", stage: "handed_to_session", id: event.id, cadence: event.cadence, prompt: rendered, deadline_at: h.handoff && h.handoff.deadlineAt });
|
|
1040
|
+
return { ok: true, decision: "handed-to-session" };
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
// A handoff we could not write is not a reason to lose the tick —
|
|
1043
|
+
// fall through to the legacy spawn, loudly.
|
|
1044
|
+
log({ level: "warn", stage: "handoff_failed_spawning_instead", id: event.id, cadence: event.cadence, error: err && err.message });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (activeSubSessions >= MAX_CONCURRENT_SUB_SESSIONS) {
|
|
1050
|
+
// Re-queue (to the tail) and try again next tick. Single-owner cadence
|
|
1051
|
+
// consumer means this can only happen when a prior tick is still
|
|
1052
|
+
// running — queue depth is the right back-pressure signal. Not a
|
|
1053
|
+
// per-event failure: the attempt count (failTick, audit M5) is untouched.
|
|
1054
|
+
return deferTick(event, "concurrency", { active_subsessions: activeSubSessions });
|
|
1055
|
+
}
|
|
1056
|
+
|
|
864
1057
|
activeSubSessions += 1;
|
|
1058
|
+
inFlightCadences.add(event.cadence);
|
|
865
1059
|
// Audit L6: mark this claim in-flight so the periodic stale-claim sweep
|
|
866
1060
|
// (which can run concurrently with a long-running sub-session) does not
|
|
867
1061
|
// treat its claimed/<id>.json as crashed and re-queue it under us.
|
|
@@ -878,6 +1072,7 @@ export function startConsumer(opts = {}) {
|
|
|
878
1072
|
});
|
|
879
1073
|
} finally {
|
|
880
1074
|
activeSubSessions -= 1;
|
|
1075
|
+
inFlightCadences.delete(event.cadence);
|
|
881
1076
|
inFlightClaimIds.delete(event.id);
|
|
882
1077
|
}
|
|
883
1078
|
|
|
@@ -910,14 +1105,11 @@ export function startConsumer(opts = {}) {
|
|
|
910
1105
|
const rec = rateGuard.recordUsageLimit(RATE_PROVIDER, cadenceUl.resetAt, { agentRoot });
|
|
911
1106
|
log({ level: "warn", stage: "subsession_usage_limited", id: event.id, cadence: event.cadence, open_until: rec.openUntil, reset_at: rec.resetAt });
|
|
912
1107
|
} catch { /* */ }
|
|
913
|
-
// Same
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
stats.retries += 1;
|
|
919
|
-
stats.last_decision = "deferred";
|
|
920
|
-
return { ok: false, decision: "deferred" };
|
|
1108
|
+
// Same handling as a 429: a window-usage limit is a shared, provider-side
|
|
1109
|
+
// gate — not evidence THIS cadence is broken — so requeue (to the tail)
|
|
1110
|
+
// without failTick or a circuit trip; the shared breaker (held until
|
|
1111
|
+
// reset) gates re-escalation.
|
|
1112
|
+
return deferTick(event, "usage-limited");
|
|
921
1113
|
}
|
|
922
1114
|
if (rateGuard.classifyStderr(cadenceOut)) {
|
|
923
1115
|
try {
|
|
@@ -929,12 +1121,9 @@ export function startConsumer(opts = {}) {
|
|
|
929
1121
|
// NOT evidence that THIS cadence is broken, so we must NOT call
|
|
930
1122
|
// recordSubsessionFailure here: doing so would advance the per-cadence
|
|
931
1123
|
// circuit toward open and leave a perfectly-healthy cadence circuit-open
|
|
932
|
-
// even after the shared breaker clears. Requeue
|
|
1124
|
+
// even after the shared breaker clears. Requeue to the tail (no failTick,
|
|
933
1125
|
// no circuit trip); the shared breaker gates re-escalation.
|
|
934
|
-
|
|
935
|
-
stats.retries += 1;
|
|
936
|
-
stats.last_decision = "deferred";
|
|
937
|
-
return { ok: false, decision: "deferred" };
|
|
1126
|
+
return deferTick(event, "rate-limited");
|
|
938
1127
|
}
|
|
939
1128
|
|
|
940
1129
|
// Failure path: log + cap retries low. The exact stderr tail comes
|
|
@@ -1040,40 +1229,49 @@ export function startConsumer(opts = {}) {
|
|
|
1040
1229
|
// Routed through the guarded wrapper so an in-flight claim is never swept
|
|
1041
1230
|
// (audit L6).
|
|
1042
1231
|
recoverStaleClaimsGuarded();
|
|
1232
|
+
// Front door: a handed-off tick the session never acked comes back to the
|
|
1233
|
+
// legacy lane here, ahead of the drain, so it is claimable in this tick.
|
|
1234
|
+
sweepHandoffTimeouts();
|
|
1043
1235
|
|
|
1044
1236
|
let processed = 0;
|
|
1045
|
-
let
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1237
|
+
let spawnedThisTick = 0;
|
|
1238
|
+
// Head-of-line (F8): a deferred tick is re-keyed to the tail, so the loop
|
|
1239
|
+
// KEEPS SCANNING past it to the next cadence instead of breaking. This set
|
|
1240
|
+
// (keyed by the tick's original id, which a re-key preserves) is how the
|
|
1241
|
+
// loop notices it has come back round to a tick it already deferred in
|
|
1242
|
+
// this very tick — it puts that one back untouched and stops, so a queue
|
|
1243
|
+
// holding only un-runnable ticks costs one visit per tick, not sixteen.
|
|
1244
|
+
const seenThisTick = new Set();
|
|
1050
1245
|
while (!stopping) {
|
|
1051
1246
|
const claim = claimNextTick(agentRoot);
|
|
1052
1247
|
if (!claim) break;
|
|
1053
1248
|
const event = claim.event;
|
|
1249
|
+
const key = (event.metadata && event.metadata.originalId) || event.id;
|
|
1250
|
+
if (seenThisTick.has(key)) {
|
|
1251
|
+
requeueTick(agentRoot, event);
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
seenThisTick.add(key);
|
|
1054
1255
|
activeTick = event.id;
|
|
1055
|
-
let
|
|
1256
|
+
let didSpawn = false;
|
|
1056
1257
|
try {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
//
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
// signal that an actual sub-session ran this iteration.
|
|
1063
|
-
const before = stats.escalated + stats.spawn_failures + stats.skipped_circuit_open + stats.skipped_backoff;
|
|
1258
|
+
// Did a sub-session actually run (or fail to spawn) this iteration? A
|
|
1259
|
+
// deferral — circuit/backoff skip, governance, concurrency, in-flight
|
|
1260
|
+
// guard — is NOT a spawn and must not end the scan (that was the
|
|
1261
|
+
// head-of-line block). A handoff is not a spawn either: it is cheap.
|
|
1262
|
+
const before = stats.escalated + stats.spawn_failures;
|
|
1064
1263
|
await processEvent(event);
|
|
1065
|
-
const after = stats.escalated + stats.spawn_failures
|
|
1066
|
-
if (after > before)
|
|
1067
|
-
// Silence unused var warning.
|
|
1068
|
-
void willEscalate;
|
|
1264
|
+
const after = stats.escalated + stats.spawn_failures;
|
|
1265
|
+
if (after > before) didSpawn = true;
|
|
1069
1266
|
} finally {
|
|
1070
1267
|
activeTick = null;
|
|
1071
1268
|
}
|
|
1072
1269
|
processed += 1;
|
|
1073
|
-
if (
|
|
1074
|
-
// Hard cap: at most ONE sub-session spawn per tick
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1270
|
+
if (didSpawn) spawnedThisTick += 1;
|
|
1271
|
+
// Hard cap: at most ONE sub-session spawn per tick so a fast-failing
|
|
1272
|
+
// cadence can't burn a whole minute's worth of retries inside a single
|
|
1273
|
+
// poll. Inline ticks and handoffs keep draining freely (they're cheap).
|
|
1274
|
+
if (spawnedThisTick >= 1) break;
|
|
1077
1275
|
if (processed >= 16) break; // soft batch cap
|
|
1078
1276
|
}
|
|
1079
1277
|
return { processed };
|
|
@@ -1216,6 +1414,8 @@ export function startConsumer(opts = {}) {
|
|
|
1216
1414
|
_recoverStaleClaimsGuarded: recoverStaleClaimsGuarded,
|
|
1217
1415
|
_markInFlight: (id) => inFlightClaimIds.add(id),
|
|
1218
1416
|
_clearInFlight: (id) => inFlightClaimIds.delete(id),
|
|
1417
|
+
// Front door: the open handoffs this consumer has written for the session.
|
|
1418
|
+
_handoffs: () => listHandoffs(agentRoot),
|
|
1219
1419
|
};
|
|
1220
1420
|
}
|
|
1221
1421
|
|
|
@@ -679,6 +679,33 @@ const MESSAGING_CURSOR_REL = join("state", "messaging", "inbound-cursor.json");
|
|
|
679
679
|
* (re)install starts at "now" and replays nothing. A real, persisted cursor
|
|
680
680
|
* (always > 0 in practice — see writeMessagingCursor) is returned as-is.
|
|
681
681
|
*/
|
|
682
|
+
/**
|
|
683
|
+
* The seat's own member cuid, as resolved by `agent-daemon.resolveSelfMemberId`
|
|
684
|
+
* and persisted to `config/agent.json`. This is the `actor` hq stamps on every
|
|
685
|
+
* event the seat emits, so it is the ONLY id the wide reader's own-echo and
|
|
686
|
+
* mention joins can match against.
|
|
687
|
+
*
|
|
688
|
+
* Returns "" when unresolved. Also returns "" when the field holds the SLUG
|
|
689
|
+
* (COHORT_AGENT_ID) rather than a cuid — a bad writer putting the slug in the
|
|
690
|
+
* cuid field would otherwise make this fix mask the very bug it fixes, since a
|
|
691
|
+
* slug read from disk is exactly as broken as a slug read from env.
|
|
692
|
+
*
|
|
693
|
+
* @param {string} agentRoot
|
|
694
|
+
* @returns {string} member cuid, or "" if unresolved
|
|
695
|
+
*/
|
|
696
|
+
function persistedMemberId(agentRoot) {
|
|
697
|
+
try {
|
|
698
|
+
const c = JSON.parse(readFileSync(join(agentRoot, "config", "agent.json"), "utf-8"));
|
|
699
|
+
const mid = c && typeof c.memberId === "string" ? c.memberId.trim() : "";
|
|
700
|
+
if (!mid) return "";
|
|
701
|
+
const slug = String(process.env.COHORT_AGENT_ID || "").trim();
|
|
702
|
+
if (slug && mid === slug) return "";
|
|
703
|
+
return mid;
|
|
704
|
+
} catch {
|
|
705
|
+
return "";
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
682
709
|
function readMessagingCursor(agentRoot) {
|
|
683
710
|
try {
|
|
684
711
|
const p = join(agentRoot, MESSAGING_CURSOR_REL);
|
|
@@ -813,11 +840,36 @@ async function guardMessagingInbound({ event, agentRoot, log }, opts = {}) {
|
|
|
813
840
|
// skipped, and the agent re-ingests its own replies as fresh inbound (a
|
|
814
841
|
// conversation with itself). config/org.yaml does not carry an agentId, so
|
|
815
842
|
// env is the reliable source on a real machine.
|
|
843
|
+
//
|
|
844
|
+
// MUST BE THE MEMBER CUID, NOT THE SLUG. hq names the actor on every
|
|
845
|
+
// /v1/events row by member cuid ("cmqh0te…"); COHORT_AGENT_ID is a SLUG
|
|
846
|
+
// ("A028"). Every directedness join in the wide reader — own_echo, mention,
|
|
847
|
+
// assignee, decision proposer, doc owner — compares `me` against a
|
|
848
|
+
// cuid-shaped payload field, so handing it the slug does not throw: it
|
|
849
|
+
// silently answers "no" to every identity question. own_echo then never
|
|
850
|
+
// fires and the seat re-ingests its own outbound, which produces a holding
|
|
851
|
+
// note, which is itself re-ingested — a self-sustaining loop that burns a
|
|
852
|
+
// sub-session per cycle and posts into real rooms.
|
|
853
|
+
//
|
|
854
|
+
// agent-daemon.resolveSelfMemberId already resolves the cuid via whoami and
|
|
855
|
+
// persists it to config/agent.json; prefer that, and fall back to the slug
|
|
856
|
+
// (degraded, but better than no id) only until it has been resolved once.
|
|
816
857
|
const agentId =
|
|
817
858
|
opts.agentId ||
|
|
859
|
+
persistedMemberId(agentRoot) ||
|
|
818
860
|
process.env.COHORT_AGENT_ID ||
|
|
819
861
|
(cfg && cfg.org && cfg.org.cohort && cfg.org.cohort.agentId) ||
|
|
820
862
|
undefined;
|
|
863
|
+
// Belt and braces on top of the id above: hand the reader BOTH namespaces so
|
|
864
|
+
// own-echo suppression cannot silently lapse if `agentId` resolves to the
|
|
865
|
+
// slug after all (persistedMemberId returns "" when no memberId is on record,
|
|
866
|
+
// or when it equals the slug). resolveDirected uses these ONLY to widen the
|
|
867
|
+
// own-echo drop — never to widen what the agent is entitled to read.
|
|
868
|
+
const meAliases = [
|
|
869
|
+
cfg && cfg.memberId,
|
|
870
|
+
process.env.COHORT_AGENT_ID,
|
|
871
|
+
cfg && cfg.org && cfg.org.cohort && cfg.org.cohort.agentId,
|
|
872
|
+
].filter((v) => typeof v === "string" && v.trim());
|
|
821
873
|
// Display names for prose-mention matching ("Isla, can you…" with no @).
|
|
822
874
|
// The wide reader only ever uses these to match text it was ALREADY entitled
|
|
823
875
|
// to read, so this cannot widen the aperture.
|
|
@@ -835,6 +887,7 @@ async function guardMessagingInbound({ event, agentRoot, log }, opts = {}) {
|
|
|
835
887
|
const res = await pull({
|
|
836
888
|
cfg,
|
|
837
889
|
agentId,
|
|
890
|
+
meAliases,
|
|
838
891
|
cursor,
|
|
839
892
|
fetchImpl: opts.fetchImpl,
|
|
840
893
|
myNames,
|
|
@@ -196,7 +196,7 @@ const CLAUDE_CLI_TIMEOUT_MS = 30_000;
|
|
|
196
196
|
* it was a hardcoded example-company literal sitting between two interpolated
|
|
197
197
|
* fields. This
|
|
198
198
|
* is not cosmetic: the classifier's `summary` is carried verbatim into
|
|
199
|
-
*
|
|
199
|
+
* board-mirror's board row title and the needs-attention escalation record,
|
|
200
200
|
* so a fabricated employer in its frame of reference leaks into the org record.
|
|
201
201
|
*
|
|
202
202
|
* @param {{name?:string, role?:string, company?:string, principal?:object}} id
|