harnery 0.33.0 → 0.34.1
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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/adapter.js +2 -2
- package/dist/commands/agents.d.ts +144 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +1237 -590
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/backup.d.ts +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +29 -16
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +234 -12
- package/dist/commands/checkpoint.d.ts.map +1 -1
- package/dist/commands/checkpoint.js +3 -2
- package/dist/commands/claude-desktop.d.ts +1 -1
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +3 -74
- package/dist/commands/context.js +1 -6
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +22 -38
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +91 -28
- package/dist/commands/doctor.js +10 -1
- package/dist/commands/events.d.ts +5 -0
- package/dist/commands/events.d.ts.map +1 -0
- package/dist/commands/events.js +189 -0
- package/dist/commands/governor.d.ts.map +1 -1
- package/dist/commands/governor.js +13 -4
- package/dist/commands/init.d.ts +9 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +223 -51
- package/dist/commands/journal.d.ts.map +1 -1
- package/dist/commands/journal.js +4 -13
- package/dist/commands/ledger-v3.d.ts +5 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -0
- package/dist/commands/ledger-v3.js +91 -0
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +11 -5
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/web.d.ts +6 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +67 -7
- package/dist/commands/work.d.ts.map +1 -1
- package/dist/commands/work.js +22 -5
- package/dist/core/adapter.d.ts +5 -0
- package/dist/core/adapter.d.ts.map +1 -0
- package/dist/core/adapter.js +1 -0
- package/dist/core/adapters/attest.d.ts +5 -2
- package/dist/core/adapters/attest.d.ts.map +1 -1
- package/dist/core/adapters/attest.js +8 -3
- package/dist/core/adapters/profiles.d.ts +6 -6
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +4 -4
- package/dist/core/agents/canonical-emit.d.ts +17 -15
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +22 -22
- package/dist/core/agents/cli.js +207 -234
- package/dist/core/agents/codex-archive-v3.d.ts +14 -0
- package/dist/core/agents/codex-archive-v3.d.ts.map +1 -0
- package/dist/core/agents/codex-archive-v3.js +110 -0
- package/dist/core/agents/coord-client.d.ts +19 -81
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +65 -155
- package/dist/core/agents/finalization.d.ts +4 -1
- package/dist/core/agents/finalization.d.ts.map +1 -1
- package/dist/core/agents/finalization.js +89 -32
- package/dist/core/agents/index.d.ts +4 -5
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +4 -5
- package/dist/core/agents/live-authority-v3.d.ts +59 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -0
- package/dist/core/agents/live-authority-v3.js +233 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +20 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -0
- package/dist/core/agents/live-lifecycle-v3.js +91 -0
- package/dist/core/agents/live-observation-v3.d.ts +25 -0
- package/dist/core/agents/live-observation-v3.d.ts.map +1 -0
- package/dist/core/agents/live-observation-v3.js +157 -0
- package/dist/core/agents/render/prompt-context.d.ts +4 -4
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +23 -58
- package/dist/core/agents/render/session-context.d.ts +1 -1
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +8 -32
- package/dist/core/agents/rules/claim-conflict.d.ts +3 -3
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +79 -127
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +3 -18
- package/dist/core/agents/rules/stop-hook.d.ts +9 -50
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +193 -236
- package/dist/core/agents/session-events.d.ts +5 -7
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +127 -95
- package/dist/core/agents/session-finalization-state-v3.d.ts +30 -0
- package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -0
- package/dist/core/agents/session-finalization-state-v3.js +40 -0
- package/dist/core/agents/session-finalizer-v3.d.ts +107 -0
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -0
- package/dist/core/agents/session-finalizer-v3.js +734 -0
- package/dist/core/agents/session-name-display.d.ts +40 -0
- package/dist/core/agents/session-name-display.d.ts.map +1 -0
- package/dist/core/agents/session-name-display.js +119 -0
- package/dist/core/agents/state/activity-log.d.ts +3 -3
- package/dist/core/agents/state/activity-log.d.ts.map +1 -1
- package/dist/core/agents/state/activity-log.js +7 -16
- package/dist/core/agents/state/heartbeat-writer.d.ts +23 -55
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +56 -228
- package/dist/core/agents/state/live-coordination-view.d.ts +31 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -0
- package/dist/core/agents/state/live-coordination-view.js +209 -0
- package/dist/core/agents/state/session-state.d.ts +4 -3
- package/dist/core/agents/state/session-state.d.ts.map +1 -1
- package/dist/core/agents/state/session-state.js +18 -9
- package/dist/core/agents/state/stale-sweep.d.ts +1 -1
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +26 -15
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +5 -7
- package/dist/core/config.d.ts +33 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +98 -2
- package/dist/core/context/index.d.ts +1 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +9 -9
- package/dist/core/events/span-parent.d.ts +15 -0
- package/dist/core/events/span-parent.d.ts.map +1 -0
- package/dist/core/events/span-parent.js +34 -0
- package/dist/core/events/v3/advance.d.ts +14 -0
- package/dist/core/events/v3/advance.d.ts.map +1 -0
- package/dist/core/events/v3/advance.js +134 -0
- package/dist/core/events/v3/authority-outbox.d.ts +78 -0
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -0
- package/dist/core/events/v3/authority-outbox.js +399 -0
- package/dist/core/events/v3/base-builder.d.ts +20 -0
- package/dist/core/events/v3/base-builder.d.ts.map +1 -0
- package/dist/core/events/v3/base-builder.js +29 -0
- package/dist/core/events/v3/base-contract.d.ts +7576 -0
- package/dist/core/events/v3/base-contract.d.ts.map +1 -0
- package/dist/core/events/v3/base-contract.js +647 -0
- package/dist/core/events/v3/bootstrap.d.ts +34 -0
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -0
- package/dist/core/events/v3/bootstrap.js +175 -0
- package/dist/core/events/v3/builder.d.ts +20 -0
- package/dist/core/events/v3/builder.d.ts.map +1 -0
- package/dist/core/events/v3/builder.js +28 -0
- package/dist/core/events/v3/canonical.d.ts +31 -0
- package/dist/core/events/v3/canonical.d.ts.map +1 -0
- package/dist/core/events/v3/canonical.js +100 -0
- package/dist/core/events/v3/capabilities.d.ts +31 -0
- package/dist/core/events/v3/capabilities.d.ts.map +1 -0
- package/dist/core/events/v3/capabilities.js +107 -0
- package/dist/core/events/v3/capability-drift.d.ts +22 -0
- package/dist/core/events/v3/capability-drift.d.ts.map +1 -0
- package/dist/core/events/v3/capability-drift.js +134 -0
- package/dist/core/events/v3/catalog.d.ts +38 -0
- package/dist/core/events/v3/catalog.d.ts.map +1 -0
- package/dist/core/events/v3/catalog.js +135 -0
- package/dist/core/events/v3/clock-order.d.ts +34 -0
- package/dist/core/events/v3/clock-order.d.ts.map +1 -0
- package/dist/core/events/v3/clock-order.js +35 -0
- package/dist/core/events/v3/contract.d.ts +171 -0
- package/dist/core/events/v3/contract.d.ts.map +1 -0
- package/dist/core/events/v3/contract.js +180 -0
- package/dist/core/events/v3/control.d.ts +111 -0
- package/dist/core/events/v3/control.d.ts.map +1 -0
- package/dist/core/events/v3/control.js +419 -0
- package/dist/core/events/v3/coordination-view.d.ts +69 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -0
- package/dist/core/events/v3/coordination-view.js +119 -0
- package/dist/core/events/v3/economics.d.ts +77 -0
- package/dist/core/events/v3/economics.d.ts.map +1 -0
- package/dist/core/events/v3/economics.js +274 -0
- package/dist/core/events/v3/finalization-view.d.ts +26 -0
- package/dist/core/events/v3/finalization-view.d.ts.map +1 -0
- package/dist/core/events/v3/finalization-view.js +37 -0
- package/dist/core/events/v3/fingerprint-keys.d.ts +25 -0
- package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -0
- package/dist/core/events/v3/fingerprint-keys.js +217 -0
- package/dist/core/events/v3/generated.d.ts +4 -0
- package/dist/core/events/v3/generated.d.ts.map +1 -0
- package/dist/core/events/v3/generated.js +47 -0
- package/dist/core/events/v3/ids.d.ts +9 -0
- package/dist/core/events/v3/ids.d.ts.map +1 -0
- package/dist/core/events/v3/ids.js +25 -0
- package/dist/core/events/v3/index.d.ts +35 -0
- package/dist/core/events/v3/index.d.ts.map +1 -0
- package/dist/core/events/v3/index.js +33 -0
- package/dist/core/events/v3/latency.d.ts +63 -0
- package/dist/core/events/v3/latency.d.ts.map +1 -0
- package/dist/core/events/v3/latency.js +412 -0
- package/dist/core/events/v3/live-feed.d.ts +45 -0
- package/dist/core/events/v3/live-feed.d.ts.map +1 -0
- package/dist/core/events/v3/live-feed.js +246 -0
- package/dist/core/events/v3/live-observation.d.ts +35 -0
- package/dist/core/events/v3/live-observation.d.ts.map +1 -0
- package/dist/core/events/v3/live-observation.js +1 -0
- package/dist/core/events/v3/live-routing.d.ts +57 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -0
- package/dist/core/events/v3/live-routing.js +107 -0
- package/dist/core/events/v3/producers/command-recorder.d.ts +67 -0
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -0
- package/dist/core/events/v3/producers/command-recorder.js +477 -0
- package/dist/core/events/v3/producers/command.d.ts +50 -0
- package/dist/core/events/v3/producers/command.d.ts.map +1 -0
- package/dist/core/events/v3/producers/command.js +135 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +53 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.js +376 -0
- package/dist/core/events/v3/producers/coordination.d.ts +90 -0
- package/dist/core/events/v3/producers/coordination.d.ts.map +1 -0
- package/dist/core/events/v3/producers/coordination.js +229 -0
- package/dist/core/events/v3/producers/hook-base.d.ts +54 -0
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -0
- package/dist/core/events/v3/producers/hook-base.js +424 -0
- package/dist/core/events/v3/producers/hook.d.ts +29 -0
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -0
- package/dist/core/events/v3/producers/hook.js +71 -0
- package/dist/core/events/v3/producers/intake.d.ts +65 -0
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -0
- package/dist/core/events/v3/producers/intake.js +244 -0
- package/dist/core/events/v3/producers/recorder.d.ts +240 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -0
- package/dist/core/events/v3/producers/recorder.js +2176 -0
- package/dist/core/events/v3/projection.d.ts +101 -0
- package/dist/core/events/v3/projection.d.ts.map +1 -0
- package/dist/core/events/v3/projection.js +479 -0
- package/dist/core/events/v3/reader.d.ts +75 -0
- package/dist/core/events/v3/reader.d.ts.map +1 -0
- package/dist/core/events/v3/reader.js +778 -0
- package/dist/core/events/v3/recovery-record.d.ts +47 -0
- package/dist/core/events/v3/recovery-record.d.ts.map +1 -0
- package/dist/core/events/v3/recovery-record.js +167 -0
- package/dist/core/events/v3/recovery.d.ts +22 -0
- package/dist/core/events/v3/recovery.d.ts.map +1 -0
- package/dist/core/events/v3/recovery.js +227 -0
- package/dist/core/events/v3/span-state.d.ts +52 -0
- package/dist/core/events/v3/span-state.d.ts.map +1 -0
- package/dist/core/events/v3/span-state.js +142 -0
- package/dist/core/events/v3/targets.d.ts +23 -0
- package/dist/core/events/v3/targets.d.ts.map +1 -0
- package/dist/core/events/v3/targets.js +134 -0
- package/dist/core/events/v3/turn-telemetry.d.ts +41 -0
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -0
- package/dist/core/events/v3/turn-telemetry.js +168 -0
- package/dist/core/events/v3/validate.d.ts +9 -0
- package/dist/core/events/v3/validate.d.ts.map +1 -0
- package/dist/core/events/v3/validate.js +281 -0
- package/dist/core/events/v3/writer.d.ts +56 -0
- package/dist/core/events/v3/writer.d.ts.map +1 -0
- package/dist/core/events/v3/writer.js +335 -0
- package/dist/core/governor/index.d.ts +1 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +1 -1
- package/dist/core/governor/plan-api.d.ts +2 -1
- package/dist/core/governor/plan-api.d.ts.map +1 -1
- package/dist/core/governor/plan-api.js +6 -3
- package/dist/core/governor/read.d.ts +1 -1
- package/dist/core/governor/read.d.ts.map +1 -1
- package/dist/core/governor/read.js +1 -1
- package/dist/core/governor/state.d.ts +9 -0
- package/dist/core/governor/state.d.ts.map +1 -1
- package/dist/core/governor/state.js +40 -3
- package/dist/core/guard/config.d.ts +5 -0
- package/dist/core/guard/config.d.ts.map +1 -0
- package/dist/core/guard/config.js +157 -0
- package/dist/core/guard/coordinator.d.ts +14 -0
- package/dist/core/guard/coordinator.d.ts.map +1 -0
- package/dist/core/guard/coordinator.js +173 -0
- package/dist/core/guard/evaluator.d.ts +4 -0
- package/dist/core/guard/evaluator.d.ts.map +1 -0
- package/dist/core/guard/evaluator.js +242 -0
- package/dist/core/guard/evidence-v3.d.ts +15 -0
- package/dist/core/guard/evidence-v3.d.ts.map +1 -0
- package/dist/core/guard/evidence-v3.js +171 -0
- package/dist/core/guard/index.d.ts +10 -0
- package/dist/core/guard/index.d.ts.map +1 -0
- package/dist/core/guard/index.js +6 -0
- package/dist/core/guard/live-source-v3.d.ts +40 -0
- package/dist/core/guard/live-source-v3.d.ts.map +1 -0
- package/dist/core/guard/live-source-v3.js +128 -0
- package/dist/core/guard/storage.d.ts +27 -0
- package/dist/core/guard/storage.d.ts.map +1 -0
- package/dist/core/guard/storage.js +140 -0
- package/dist/core/guard/types.d.ts +119 -0
- package/dist/core/guard/types.d.ts.map +1 -0
- package/dist/core/guard/types.js +1 -0
- package/dist/core/hooks/adapter/detect.d.ts +18 -1
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +29 -0
- package/dist/core/hooks/adapter/events.d.ts +7 -4
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +12 -4
- package/dist/core/hooks/adapter/output.d.ts +2 -2
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.d.ts +7 -4
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +63 -25
- package/dist/core/hooks/adapter/wiring.d.ts +30 -4
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +89 -11
- package/dist/core/hooks/cli.d.ts +1 -1
- package/dist/core/hooks/cli.js +306 -444
- package/dist/core/hooks/effects/image-capture.d.ts +20 -34
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +212 -221
- package/dist/core/hooks/effects/index.d.ts +1 -9
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +1 -24
- package/dist/core/hooks/events/input-hash.d.ts +7 -0
- package/dist/core/hooks/events/input-hash.d.ts.map +1 -0
- package/dist/core/hooks/events/input-hash.js +43 -0
- package/dist/core/hooks/index.d.ts +4 -3
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +2 -3
- package/dist/core/hooks/resolve/coord-root.d.ts +1 -1
- package/dist/core/hooks/resolve/coord-root.js +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +16 -15
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +51 -42
- package/dist/core/hooks/resolve/transcript.d.ts +33 -3
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +161 -14
- package/dist/core/hooks/session-name-presence.d.ts +9 -10
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +15 -15
- package/dist/core/hooks/unsafe-cross-shell.d.ts +1 -1
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -1
- package/dist/core/journal/index.d.ts +2 -2
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +9 -22
- package/dist/core/presence/blob.d.ts +4 -3
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +45 -44
- package/dist/core/presence/codec-digest.d.ts +29 -0
- package/dist/core/presence/codec-digest.d.ts.map +1 -0
- package/dist/core/presence/codec-digest.js +170 -0
- package/dist/core/presence/relay-client.d.ts +1 -1
- package/dist/core/presence/relay-client.d.ts.map +1 -1
- package/dist/core/presence/relay-client.js +5 -5
- package/dist/core/work/index.d.ts +1 -1
- package/dist/core/work/index.d.ts.map +1 -1
- package/dist/core/work/index.js +1 -1
- package/dist/core/work/read.d.ts +1 -1
- package/dist/core/work/read.d.ts.map +1 -1
- package/dist/core/work/read.js +1 -1
- package/dist/core/work/runner.d.ts.map +1 -1
- package/dist/core/work/runner.js +6 -0
- package/dist/core/work/state.d.ts +9 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +17 -3
- package/dist/core/workflow/engine.js +24 -25
- package/dist/core/workflow/live-session-v3.d.ts +19 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -0
- package/dist/core/workflow/live-session-v3.js +86 -0
- package/dist/core/workflow/scope-id.d.ts +3 -0
- package/dist/core/workflow/scope-id.d.ts.map +1 -0
- package/dist/core/workflow/scope-id.js +5 -0
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +53 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/browser/client.d.ts +41 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +203 -0
- package/dist/lib/browser/critique.d.ts +28 -2
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +74 -18
- package/dist/lib/browser/geometry.d.ts +1 -1
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +130 -6
- package/dist/lib/browser/index.d.ts +3 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +3 -0
- package/dist/lib/browser/qa-plan.d.ts +174 -0
- package/dist/lib/browser/qa-plan.d.ts.map +1 -0
- package/dist/lib/browser/qa-plan.js +321 -0
- package/dist/lib/browser/qa-reuse.d.ts +99 -0
- package/dist/lib/browser/qa-reuse.d.ts.map +1 -0
- package/dist/lib/browser/qa-reuse.js +222 -0
- package/dist/lib/browser/qa-snapshot.d.ts +64 -0
- package/dist/lib/browser/qa-snapshot.d.ts.map +1 -0
- package/dist/lib/browser/qa-snapshot.js +171 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +22 -1
- package/dist/lib/browser/tiling.d.ts +71 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -0
- package/dist/lib/browser/tiling.js +118 -0
- package/dist/lib/claude-desktop.d.ts +0 -47
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +1 -169
- package/dist/lib/context/index.d.ts +0 -3
- package/dist/lib/context/index.d.ts.map +1 -1
- package/dist/lib/context/index.js +20 -42
- package/dist/lib/council/index.d.ts +2 -2
- package/dist/lib/council/index.d.ts.map +1 -1
- package/dist/lib/council/index.js +9 -20
- package/dist/lib/docs-frontmatter.d.ts +1 -6
- package/dist/lib/docs-frontmatter.d.ts.map +1 -1
- package/dist/lib/docs-frontmatter.js +12 -78
- package/dist/lib/docs-index.d.ts +9 -0
- package/dist/lib/docs-index.d.ts.map +1 -1
- package/dist/lib/docs-index.js +32 -3
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +14 -30
- package/dist/lib/docs-metadata-audit.d.ts +19 -0
- package/dist/lib/docs-metadata-audit.d.ts.map +1 -0
- package/dist/lib/docs-metadata-audit.js +132 -0
- package/dist/lib/docs-metadata-managed.d.ts +23 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -0
- package/dist/lib/docs-metadata-managed.js +78 -0
- package/dist/lib/docs-metadata-sync.d.ts +20 -0
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -0
- package/dist/lib/docs-metadata-sync.js +222 -0
- package/dist/lib/docs-metadata-v2-schema.d.ts +273 -0
- package/dist/lib/docs-metadata-v2-schema.d.ts.map +1 -0
- package/dist/lib/docs-metadata-v2-schema.js +134 -0
- package/dist/lib/docs-metadata-v2.d.ts +21 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -0
- package/dist/lib/docs-metadata-v2.js +424 -0
- package/dist/lib/docs-new.d.ts +11 -0
- package/dist/lib/docs-new.d.ts.map +1 -0
- package/dist/lib/docs-new.js +52 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -1
- package/dist/lib/docs-sweep.js +45 -29
- package/dist/lib/headless/index.d.ts +98 -0
- package/dist/lib/headless/index.d.ts.map +1 -0
- package/dist/lib/headless/index.js +211 -0
- package/dist/lib/identities/assume.d.ts +4 -4
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +52 -68
- package/dist/lib/instructions/apply.d.ts +5 -4
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +32 -20
- package/dist/lib/instructions/bundle.d.ts +1 -1
- package/dist/lib/instructions/bundle.d.ts.map +1 -1
- package/dist/lib/instructions/bundle.js +1 -1
- package/dist/lib/instructions/splice.d.ts +0 -1
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +22 -6
- package/dist/lib/instructions/templates.d.ts +7 -5
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +79 -9
- package/package.json +26 -5
- package/schemas/config.schema.json +120 -2
- package/schemas/docs-metadata-v2.schema.json +441 -0
- package/schemas/event-v3.schema.json +24472 -0
- package/src/commander.ts +4 -0
- package/src/commands/adapter.ts +2 -2
- package/src/commands/agents.ts +1530 -593
- package/src/commands/artifacts.ts +7 -6
- package/src/commands/backup.ts +29 -21
- package/src/commands/browse.ts +349 -8
- package/src/commands/checkpoint.ts +4 -3
- package/src/commands/claude-desktop.ts +2 -101
- package/src/commands/context.ts +1 -6
- package/src/commands/decision.ts +27 -44
- package/src/commands/docs.ts +100 -33
- package/src/commands/doctor.ts +14 -1
- package/src/commands/events.ts +256 -0
- package/src/commands/governor.ts +18 -4
- package/src/commands/init.ts +253 -57
- package/src/commands/instructions.ts +1 -1
- package/src/commands/journal.ts +4 -13
- package/src/commands/ledger-v3.ts +111 -0
- package/src/commands/presence.ts +11 -10
- package/src/commands/sync.ts +1 -1
- package/src/commands/web.ts +67 -8
- package/src/commands/work.ts +28 -5
- package/src/core/adapter.ts +5 -0
- package/src/core/adapters/attest.ts +8 -3
- package/src/core/adapters/profiles.ts +4 -6
- package/src/core/agents/canonical-emit.ts +34 -25
- package/src/core/agents/cli.ts +236 -262
- package/src/core/agents/codex-archive-v3.ts +138 -0
- package/src/core/agents/coord-client.ts +75 -188
- package/src/core/agents/finalization.ts +120 -38
- package/src/core/agents/index.ts +4 -5
- package/src/core/agents/live-authority-v3.ts +375 -0
- package/src/core/agents/live-lifecycle-v3.ts +140 -0
- package/src/core/agents/live-observation-v3.ts +216 -0
- package/src/core/agents/render/prompt-context.ts +29 -60
- package/src/core/agents/render/session-context.ts +10 -31
- package/src/core/agents/rules/claim-conflict.ts +85 -126
- package/src/core/agents/rules/commit-conflict.ts +3 -14
- package/src/core/agents/rules/stop-hook.ts +239 -293
- package/src/core/agents/session-events.ts +137 -104
- package/src/core/agents/session-finalization-state-v3.ts +90 -0
- package/src/core/agents/session-finalizer-v3.ts +953 -0
- package/src/core/agents/session-name-display.ts +149 -0
- package/src/core/agents/state/activity-log.ts +6 -14
- package/src/core/agents/state/heartbeat-writer.ts +82 -277
- package/src/core/agents/state/live-coordination-view.ts +290 -0
- package/src/core/agents/state/session-state.ts +24 -12
- package/src/core/agents/state/stale-sweep.ts +40 -17
- package/src/core/artifacts/index.ts +4 -5
- package/src/core/config.ts +148 -3
- package/src/core/context/index.ts +9 -17
- package/src/core/events/span-parent.ts +46 -0
- package/src/core/events/v3/advance.ts +168 -0
- package/src/core/events/v3/authority-outbox.ts +548 -0
- package/src/core/events/v3/base-builder.ts +60 -0
- package/src/core/events/v3/base-contract.ts +882 -0
- package/src/core/events/v3/bootstrap.ts +226 -0
- package/src/core/events/v3/builder.ts +59 -0
- package/src/core/events/v3/canonical.ts +153 -0
- package/src/core/events/v3/capabilities.ts +171 -0
- package/src/core/events/v3/capability-drift.ts +177 -0
- package/src/core/events/v3/catalog.ts +202 -0
- package/src/core/events/v3/clock-order.ts +41 -0
- package/src/core/events/v3/contract.ts +365 -0
- package/src/core/events/v3/control.ts +576 -0
- package/src/core/events/v3/coordination-view.ts +193 -0
- package/src/core/events/v3/economics.ts +384 -0
- package/src/core/events/v3/finalization-view.ts +67 -0
- package/src/core/events/v3/fingerprint-keys.ts +260 -0
- package/src/core/events/v3/generated.ts +48 -0
- package/src/core/events/v3/ids.ts +33 -0
- package/src/core/events/v3/index.ts +322 -0
- package/src/core/events/v3/latency.ts +551 -0
- package/src/core/events/v3/live-feed.ts +319 -0
- package/src/core/events/v3/live-observation.ts +39 -0
- package/src/core/events/v3/live-routing.ts +135 -0
- package/src/core/events/v3/producers/command-recorder.ts +631 -0
- package/src/core/events/v3/producers/command.ts +201 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +538 -0
- package/src/core/events/v3/producers/coordination.ts +355 -0
- package/src/core/events/v3/producers/hook-base.ts +578 -0
- package/src/core/events/v3/producers/hook.ts +119 -0
- package/src/core/events/v3/producers/intake.ts +319 -0
- package/src/core/events/v3/producers/recorder.ts +2805 -0
- package/src/core/events/v3/projection.ts +653 -0
- package/src/core/events/v3/reader.ts +1127 -0
- package/src/core/events/v3/recovery-record.ts +244 -0
- package/src/core/events/v3/recovery.ts +271 -0
- package/src/core/events/v3/span-state.ts +187 -0
- package/src/core/events/v3/targets.ts +182 -0
- package/src/core/events/v3/turn-telemetry.ts +259 -0
- package/src/core/events/v3/validate.ts +361 -0
- package/src/core/events/v3/writer.ts +419 -0
- package/src/core/governor/index.ts +3 -0
- package/src/core/governor/plan-api.ts +10 -2
- package/src/core/governor/read.ts +3 -0
- package/src/core/governor/state.ts +52 -5
- package/src/core/guard/config.ts +206 -0
- package/src/core/guard/coordinator.ts +227 -0
- package/src/core/guard/evaluator.ts +343 -0
- package/src/core/guard/evidence-v3.ts +227 -0
- package/src/core/guard/index.ts +35 -0
- package/src/core/guard/live-source-v3.ts +204 -0
- package/src/core/guard/storage.ts +188 -0
- package/src/core/guard/types.ts +152 -0
- package/src/core/hooks/adapter/detect.ts +28 -1
- package/src/core/hooks/adapter/events.ts +14 -6
- package/src/core/hooks/adapter/output.ts +7 -2
- package/src/core/hooks/adapter/parse.ts +80 -38
- package/src/core/hooks/adapter/wiring.ts +119 -13
- package/src/core/hooks/cli.ts +352 -466
- package/src/core/hooks/effects/image-capture.ts +228 -238
- package/src/core/hooks/effects/index.ts +1 -36
- package/src/core/hooks/events/input-hash.ts +47 -0
- package/src/core/hooks/index.ts +4 -11
- package/src/core/hooks/resolve/coord-root.ts +1 -1
- package/src/core/hooks/resolve/owner.ts +52 -45
- package/src/core/hooks/resolve/transcript.ts +171 -14
- package/src/core/hooks/session-name-presence.ts +14 -15
- package/src/core/hooks/unsafe-cross-shell.ts +1 -1
- package/src/core/journal/index.ts +15 -22
- package/src/core/presence/blob.ts +48 -41
- package/src/core/presence/codec-digest.ts +215 -0
- package/src/core/presence/relay-client.ts +5 -5
- package/src/core/work/index.ts +3 -0
- package/src/core/work/read.ts +3 -0
- package/src/core/work/runner.ts +6 -0
- package/src/core/work/state.ts +27 -3
- package/src/core/workflow/engine.ts +24 -24
- package/src/core/workflow/live-session-v3.ts +126 -0
- package/src/core/workflow/scope-id.ts +6 -0
- package/src/core/workflow/workspaces/leases.ts +56 -19
- package/src/index.ts +1 -1
- package/src/lib/browser/client.ts +230 -0
- package/src/lib/browser/critique.ts +104 -27
- package/src/lib/browser/geometry.ts +143 -14
- package/src/lib/browser/index.ts +34 -0
- package/src/lib/browser/qa-plan.ts +501 -0
- package/src/lib/browser/qa-reuse.ts +306 -0
- package/src/lib/browser/qa-snapshot.ts +223 -0
- package/src/lib/browser/runts.ts +27 -2
- package/src/lib/browser/tiling.ts +164 -0
- package/src/lib/claude-desktop.ts +1 -232
- package/src/lib/context/index.ts +22 -46
- package/src/lib/council/index.ts +9 -21
- package/src/lib/docs-frontmatter.ts +8 -79
- package/src/lib/docs-index.ts +33 -3
- package/src/lib/docs-lint.ts +15 -29
- package/src/lib/docs-metadata-audit.ts +166 -0
- package/src/lib/docs-metadata-managed.ts +83 -0
- package/src/lib/docs-metadata-sync.ts +262 -0
- package/src/lib/docs-metadata-v2-schema.ts +138 -0
- package/src/lib/docs-metadata-v2.ts +646 -0
- package/src/lib/docs-new.ts +63 -0
- package/src/lib/docs-sweep.ts +42 -28
- package/src/lib/headless/index.ts +287 -0
- package/src/lib/identities/assume.ts +67 -86
- package/src/lib/instructions/apply.ts +33 -20
- package/src/lib/instructions/bundle.ts +2 -2
- package/src/lib/instructions/splice.ts +21 -7
- package/src/lib/instructions/templates.ts +85 -13
- package/dist/core/agents/cli-emit.d.ts +0 -27
- package/dist/core/agents/cli-emit.d.ts.map +0 -1
- package/dist/core/agents/cli-emit.js +0 -57
- package/dist/core/agents/codex-replay.d.ts +0 -29
- package/dist/core/agents/codex-replay.d.ts.map +0 -1
- package/dist/core/agents/codex-replay.js +0 -138
- package/dist/core/agents/events/consume.d.ts +0 -82
- package/dist/core/agents/events/consume.d.ts.map +0 -1
- package/dist/core/agents/events/consume.js +0 -195
- package/dist/core/agents/events/emit.d.ts +0 -42
- package/dist/core/agents/events/emit.d.ts.map +0 -1
- package/dist/core/agents/events/emit.js +0 -75
- package/dist/core/agents/events/ulid.d.ts +0 -11
- package/dist/core/agents/events/ulid.d.ts.map +0 -1
- package/dist/core/agents/events/ulid.js +0 -47
- package/dist/core/agents/state/heartbeat-projector.d.ts +0 -76
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +0 -1
- package/dist/core/agents/state/heartbeat-projector.js +0 -581
- package/dist/core/hooks/events/emit.d.ts +0 -31
- package/dist/core/hooks/events/emit.d.ts.map +0 -1
- package/dist/core/hooks/events/emit.js +0 -93
- package/dist/core/hooks/events/rotate.d.ts +0 -43
- package/dist/core/hooks/events/rotate.d.ts.map +0 -1
- package/dist/core/hooks/events/rotate.js +0 -142
- package/dist/core/hooks/events/schema.d.ts +0 -341
- package/dist/core/hooks/events/schema.d.ts.map +0 -1
- package/dist/core/hooks/events/schema.js +0 -12
- package/dist/core/hooks/events/ulid.d.ts +0 -10
- package/dist/core/hooks/events/ulid.d.ts.map +0 -1
- package/dist/core/hooks/events/ulid.js +0 -47
- package/dist/lib/docs-frontmatter-migrate.d.ts +0 -33
- package/dist/lib/docs-frontmatter-migrate.d.ts.map +0 -1
- package/dist/lib/docs-frontmatter-migrate.js +0 -364
- package/src/core/agents/cli-emit.ts +0 -64
- package/src/core/agents/codex-replay.ts +0 -163
- package/src/core/agents/events/consume.ts +0 -254
- package/src/core/agents/events/emit.ts +0 -115
- package/src/core/agents/events/ulid.ts +0 -51
- package/src/core/agents/state/heartbeat-projector.ts +0 -645
- package/src/core/hooks/events/emit.ts +0 -125
- package/src/core/hooks/events/rotate.ts +0 -151
- package/src/core/hooks/events/schema.ts +0 -582
- package/src/core/hooks/events/ulid.ts +0 -51
- package/src/lib/docs-frontmatter-migrate.ts +0 -427
package/src/commands/agents.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* harn agents whoami current agent's name + instance_id + claims
|
|
5
5
|
* harn agents list all active agents (default: fold transients)
|
|
6
6
|
* harn agents list --all include raw kind=transient rows
|
|
7
|
-
* harn agents list --stale include
|
|
7
|
+
* harn agents list --stale include generations older than the freshness window
|
|
8
8
|
* harn agents list --json JSON output (alias for --format json)
|
|
9
9
|
* harn agents status end-of-turn status box (name + age + files + peers)
|
|
10
10
|
* harn agents heal-events PIDMAP_HEAL telemetry (pid-map self-heal frequency)
|
|
@@ -29,7 +29,6 @@ import { basename, join, resolve } from "node:path";
|
|
|
29
29
|
import type { Command } from "commander";
|
|
30
30
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
31
31
|
import { coordBinPath } from "../core/agents/coord-bin.ts";
|
|
32
|
-
import { readStreamTailBounded } from "../core/agents/events/consume.ts";
|
|
33
32
|
import {
|
|
34
33
|
checkGitFinalization,
|
|
35
34
|
formatGitFinalizationFailure,
|
|
@@ -37,32 +36,72 @@ import {
|
|
|
37
36
|
readSessionWriteClaims,
|
|
38
37
|
} from "../core/agents/finalization.ts";
|
|
39
38
|
import {
|
|
40
|
-
|
|
41
|
-
type Heartbeat,
|
|
39
|
+
emitEventV3,
|
|
42
40
|
monorepoRoot,
|
|
43
41
|
normalizeAdapter,
|
|
44
|
-
readHeartbeat,
|
|
45
42
|
resolveOwner,
|
|
46
43
|
resolveOwnerBySessionEnv,
|
|
47
44
|
resolveOwnerWithSource,
|
|
45
|
+
sessionIdentityFromEnv,
|
|
48
46
|
} from "../core/agents/index.ts";
|
|
47
|
+
import { reopenLiveCoordinationGenerationV3 } from "../core/agents/live-authority-v3.ts";
|
|
48
|
+
import {
|
|
49
|
+
listSessionFinalizationRequestsV3,
|
|
50
|
+
observeHostDisappearedV3,
|
|
51
|
+
reconcileSessionFinalizationV3,
|
|
52
|
+
requestSessionEndExplicitV3,
|
|
53
|
+
type SessionFinalizationRequestV3,
|
|
54
|
+
} from "../core/agents/session-finalizer-v3.ts";
|
|
55
|
+
import { SESSION_NAME_DISPLAY_NOTE } from "../core/agents/session-name-display.ts";
|
|
49
56
|
import {
|
|
50
57
|
buildLifecycleSuggestedName,
|
|
51
58
|
buildSuggestedName,
|
|
59
|
+
type Heartbeat,
|
|
60
|
+
readHeartbeat as readHeartbeatCache,
|
|
52
61
|
} from "../core/agents/state/heartbeat-writer.ts";
|
|
62
|
+
import {
|
|
63
|
+
readLiveCoordinationRow,
|
|
64
|
+
readLiveCoordinationRows,
|
|
65
|
+
} from "../core/agents/state/live-coordination-view.ts";
|
|
53
66
|
import {
|
|
54
67
|
type AgentActivity,
|
|
55
68
|
foldSessionState,
|
|
56
69
|
type TaskState,
|
|
57
70
|
} from "../core/agents/state/session-state.ts";
|
|
58
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
coordFreshnessSeconds,
|
|
73
|
+
resolveBinName,
|
|
74
|
+
sessionFinalizationConfig,
|
|
75
|
+
} from "../core/config.ts";
|
|
76
|
+
import type { EventTypeV3 } from "../core/events/v3/contract.ts";
|
|
77
|
+
import { readEventV3ControlState } from "../core/events/v3/control.ts";
|
|
78
|
+
import { projectCoordinationViewV3 } from "../core/events/v3/coordination-view.ts";
|
|
79
|
+
import { liveInstanceIdV3 } from "../core/events/v3/live-routing.ts";
|
|
80
|
+
import {
|
|
81
|
+
listHookIntakeGroupsV3,
|
|
82
|
+
listHookIntakeRecordsV3,
|
|
83
|
+
} from "../core/events/v3/producers/intake.ts";
|
|
84
|
+
import {
|
|
85
|
+
listHookProducerStateRecordsV3,
|
|
86
|
+
readHookProducerStateV3,
|
|
87
|
+
} from "../core/events/v3/producers/recorder.ts";
|
|
88
|
+
import { readLedgerV3 } from "../core/events/v3/reader.ts";
|
|
89
|
+
import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../core/events/v3/writer.ts";
|
|
90
|
+
import type { RunQualitySnapshot, RunQualityStatus } from "../core/guard/index.ts";
|
|
91
|
+
import { evaluateRunQualityIfDue } from "../core/guard/index.ts";
|
|
59
92
|
import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
|
|
60
93
|
import { registerContextCommand } from "./context.ts";
|
|
61
94
|
|
|
95
|
+
type LiveCoordinationRow = ReturnType<typeof readLiveCoordinationRows>[number];
|
|
96
|
+
|
|
97
|
+
function readCurrentCoordinationRow(instanceId: string): LiveCoordinationRow | null {
|
|
98
|
+
const root = monorepoRoot();
|
|
99
|
+
return root ? readLiveCoordinationRow(root, instanceId) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
62
102
|
/** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
|
|
63
103
|
* under V8's ~512MB max string length so a `readFileSync` of the whole file can
|
|
64
104
|
* never throw; covers ample recent history for a diagnostic scan. */
|
|
65
|
-
const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
|
|
66
105
|
|
|
67
106
|
import { parsePsChainLine } from "../core/hooks/resolve/anchor.ts";
|
|
68
107
|
import { appendEntry, resolveOwnerByName } from "../core/journal/index.ts";
|
|
@@ -113,7 +152,7 @@ function freshnessCutoffSecs(): number {
|
|
|
113
152
|
}
|
|
114
153
|
|
|
115
154
|
const SUBAGENT_NOTE =
|
|
116
|
-
"Bash identity is process-level
|
|
155
|
+
"Bash identity is process-level; if you're running inside a subagent, " +
|
|
117
156
|
"this resolves to the parent group's name, not the subagent's. A subagent-aware " +
|
|
118
157
|
"bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
|
|
119
158
|
|
|
@@ -187,17 +226,14 @@ interface Row {
|
|
|
187
226
|
started_at: string;
|
|
188
227
|
last_heartbeat: string;
|
|
189
228
|
files_touched: string[];
|
|
190
|
-
last_tool?: string | null;
|
|
191
|
-
last_tool_target?: string | null;
|
|
192
229
|
task?: string | null;
|
|
193
230
|
activity: AgentActivity;
|
|
194
231
|
activity_updated_at?: string | null;
|
|
195
232
|
activity_source?: string | null;
|
|
196
233
|
task_state: TaskState;
|
|
234
|
+
task_state_scope: "current" | "historical";
|
|
197
235
|
task_state_updated_at?: string | null;
|
|
198
236
|
task_state_reason?: string | null;
|
|
199
|
-
turn_summary?: string | null;
|
|
200
|
-
turn_summary_updated_at?: string | null;
|
|
201
237
|
platform?: string | null;
|
|
202
238
|
/** Set on relation=remote rows: the machine label the row arrived from
|
|
203
239
|
* via the cross-machine presence transport (ADR 0016). */
|
|
@@ -212,6 +248,20 @@ function taskStateOf(hb: Pick<Heartbeat, "task_state">): TaskState {
|
|
|
212
248
|
return hb.task_state ?? "active";
|
|
213
249
|
}
|
|
214
250
|
|
|
251
|
+
/** Producer joins use the private native session ID; `session_id` is the canonical V3 fingerprint. */
|
|
252
|
+
export function nativeSessionIdentity(
|
|
253
|
+
row: Pick<Heartbeat, "native_session_id" | "session_id"> | null | undefined,
|
|
254
|
+
fallback: string,
|
|
255
|
+
): string {
|
|
256
|
+
if (row?.native_session_id) return row.native_session_id;
|
|
257
|
+
// A projection-only row carries the privacy-safe canonical session
|
|
258
|
+
// fingerprint, not the adapter's native session ID needed to join the hook
|
|
259
|
+
// producer. Until a disposable cache exists, the resolved owner is the only
|
|
260
|
+
// native identity available to command surfaces such as `agents status`.
|
|
261
|
+
if (row?.session_id && !/^sid_[a-f0-9]{64}$/.test(row.session_id)) return row.session_id;
|
|
262
|
+
return fallback;
|
|
263
|
+
}
|
|
264
|
+
|
|
215
265
|
function lifecycleLabel(hb: Pick<Heartbeat, "task_state" | "task_state_reason">): string {
|
|
216
266
|
const state = taskStateOf(hb);
|
|
217
267
|
return state === "blocked" && hb.task_state_reason ? `${state}: ${hb.task_state_reason}` : state;
|
|
@@ -245,7 +295,7 @@ export function registerAgentsCommand(
|
|
|
245
295
|
.command("list")
|
|
246
296
|
.description("List all active agents (folds kind=transient by default)")
|
|
247
297
|
.option("--all", "Include raw kind=transient rows (no fold)")
|
|
248
|
-
.option("--stale", "Include
|
|
298
|
+
.option("--stale", "Include generations older than the freshness window")
|
|
249
299
|
.option("--json", "JSON output (alias for --format json)")
|
|
250
300
|
.action((opts: { all?: boolean; stale?: boolean; json?: boolean }) => {
|
|
251
301
|
runList(opts);
|
|
@@ -259,15 +309,21 @@ export function registerAgentsCommand(
|
|
|
259
309
|
"--end-turn",
|
|
260
310
|
"Treat this as the turn's closing status: issue the box only when this session's held paths are committed and their repositories are pushed",
|
|
261
311
|
)
|
|
312
|
+
.option(
|
|
313
|
+
"--end-session",
|
|
314
|
+
"After the closing status, durably request an authoritative session end as soon as this exact turn closes (requires --end-turn)",
|
|
315
|
+
)
|
|
262
316
|
.option(
|
|
263
317
|
"--session-id <id>",
|
|
264
|
-
"Lookup
|
|
318
|
+
"Lookup the V3 generation by session_id directly, bypassing the ppid walk. " +
|
|
265
319
|
"Use this when calling from a hook (the hook's process tree may not lead back to Claude Code's session pid). " +
|
|
266
320
|
"The Stop hook payload includes session_id; pass it through.",
|
|
267
321
|
)
|
|
268
|
-
.action(
|
|
269
|
-
|
|
270
|
-
|
|
322
|
+
.action(
|
|
323
|
+
(opts: { endTurn?: boolean; endSession?: boolean; json?: boolean; sessionId?: string }) => {
|
|
324
|
+
runStatus(opts);
|
|
325
|
+
},
|
|
326
|
+
);
|
|
271
327
|
|
|
272
328
|
cmd
|
|
273
329
|
.command("suggest-name [description...]")
|
|
@@ -280,7 +336,7 @@ export function registerAgentsCommand(
|
|
|
280
336
|
.option("--json", "JSON output instead of the bare name")
|
|
281
337
|
.option(
|
|
282
338
|
"--session-id <id>",
|
|
283
|
-
"Lookup
|
|
339
|
+
"Lookup the V3 generation by session_id directly, bypassing the ppid walk.",
|
|
284
340
|
)
|
|
285
341
|
.action((description: string[], opts: { json?: boolean; sessionId?: string }) => {
|
|
286
342
|
runSuggestName(description, opts);
|
|
@@ -289,7 +345,7 @@ export function registerAgentsCommand(
|
|
|
289
345
|
cmd
|
|
290
346
|
.command("watch")
|
|
291
347
|
.description(
|
|
292
|
-
"Stream peer state changes
|
|
348
|
+
"Stream peer state changes from the authoritative V3 coordination projection. " +
|
|
293
349
|
"Prints one line per delta: started / ended / activity / file claim / task change.",
|
|
294
350
|
)
|
|
295
351
|
.option("--poll-ms <n>", "Debounce window after a change event", "200")
|
|
@@ -311,15 +367,15 @@ export function registerAgentsCommand(
|
|
|
311
367
|
cmd
|
|
312
368
|
.command("trace <name>")
|
|
313
369
|
.description(
|
|
314
|
-
"Reconstruct one agent's coordination lifecycle from
|
|
315
|
-
"session.
|
|
370
|
+
"Reconstruct one agent's coordination lifecycle from the active canonical event ledger: " +
|
|
371
|
+
"session.started → prompts → turns → tools → observations → session.ended, " +
|
|
316
372
|
"in chronological order. The answer to 'what happened to this agent / why did " +
|
|
317
373
|
"it vanish?' without hand-grepping the stream. Accepts a name (agent-Foo or Foo) " +
|
|
318
374
|
"or an instance_id.",
|
|
319
375
|
)
|
|
320
376
|
.option("--since <window>", "Only events newer than Nh|Nd (default: all)")
|
|
321
377
|
.option("--limit <n>", "Show at most N most-recent events. Default: 200.", "200")
|
|
322
|
-
.option("--all-tools", "Include tool.
|
|
378
|
+
.option("--all-tools", "Include tool.completed + command.* (default: hidden as noise)")
|
|
323
379
|
.option("--json", "JSON envelope output")
|
|
324
380
|
.action(
|
|
325
381
|
(
|
|
@@ -356,6 +412,60 @@ export function registerAgentsCommand(
|
|
|
356
412
|
runLifecycle(state, opts);
|
|
357
413
|
});
|
|
358
414
|
|
|
415
|
+
cmd
|
|
416
|
+
.command("end")
|
|
417
|
+
.description(
|
|
418
|
+
"Finalize the current V3 session explicitly, or durably queue finalization until the current turn and tool spans close.",
|
|
419
|
+
)
|
|
420
|
+
.option("--session-id <id>", "Native adapter session id to finalize")
|
|
421
|
+
.option("--instance-id <id>", "Canonical V3 instance id to finalize")
|
|
422
|
+
.option(
|
|
423
|
+
"--outcome <outcome>",
|
|
424
|
+
"succeeded | failed | cancelled | timed_out | denied | interrupted | unknown",
|
|
425
|
+
"succeeded",
|
|
426
|
+
)
|
|
427
|
+
.action((opts: { sessionId?: string; instanceId?: string; outcome: string }) => {
|
|
428
|
+
runEndSession(opts);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
cmd
|
|
432
|
+
.command("reconcile")
|
|
433
|
+
.description(
|
|
434
|
+
"Reconcile archive, idle, parent/run completion, stale, superseded, and host lifecycle signals into V3 session finalization.",
|
|
435
|
+
)
|
|
436
|
+
.option("--watch", "Keep reconciling until interrupted")
|
|
437
|
+
.option("--interval-seconds <n>", "Watch interval in seconds")
|
|
438
|
+
.option("--json", "JSON output")
|
|
439
|
+
.action(async (opts: { watch?: boolean; intervalSeconds?: string; json?: boolean }) => {
|
|
440
|
+
await runSessionReconcile(opts);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
cmd
|
|
444
|
+
.command("observe-archive")
|
|
445
|
+
.description(
|
|
446
|
+
"Record an adapter archive or unarchive observation and reconcile it through the canonical V3 finalizer.",
|
|
447
|
+
)
|
|
448
|
+
.requiredOption("--adapter <id>", "claude-code | codex | cursor")
|
|
449
|
+
.requiredOption("--session-id <id>", "Native adapter session id")
|
|
450
|
+
.option("--unarchived", "Cancel a pending archive finalization")
|
|
451
|
+
.option("--observed-at <iso>", "Observation time (defaults to now)")
|
|
452
|
+
.action(
|
|
453
|
+
(opts: { adapter: string; sessionId: string; unarchived?: boolean; observedAt?: string }) =>
|
|
454
|
+
runObserveArchive(opts),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
cmd
|
|
458
|
+
.command("observe-host-disappeared")
|
|
459
|
+
.description(
|
|
460
|
+
"Record a provisional host-loss observation; finalization follows only after the configured cascade grace period.",
|
|
461
|
+
)
|
|
462
|
+
.requiredOption("--instance-id <id>", "Canonical V3 instance id")
|
|
463
|
+
.requiredOption("--generation-id <id>", "Canonical V3 generation id")
|
|
464
|
+
.option("--observed-at <iso>", "Observation time (defaults to now)")
|
|
465
|
+
.action((opts: { instanceId: string; generationId: string; observedAt?: string }) => {
|
|
466
|
+
runObserveHostDisappeared(opts);
|
|
467
|
+
});
|
|
468
|
+
|
|
359
469
|
cmd
|
|
360
470
|
.command("release-claim <path>")
|
|
361
471
|
.description(
|
|
@@ -467,18 +577,19 @@ export function registerAgentsCommand(
|
|
|
467
577
|
.command("heal")
|
|
468
578
|
.description(
|
|
469
579
|
"Force a coord-layer recovery action on a specific agent. " +
|
|
470
|
-
"Kinds: pidmap (
|
|
471
|
-
"
|
|
472
|
-
"the live hooks use, so it governs the operation safely.",
|
|
580
|
+
"Kinds: pidmap (repair process attribution) and cache (rebuild the " +
|
|
581
|
+
"disposable V3 coordination cache from the authoritative ledger).",
|
|
473
582
|
)
|
|
474
583
|
.requiredOption("--owner <id>", "Target agent's instance_id")
|
|
475
|
-
.requiredOption("--kind <kind>", "pidmap |
|
|
584
|
+
.requiredOption("--kind <kind>", "pidmap | cache")
|
|
476
585
|
.option(
|
|
477
586
|
"--session-id <id>",
|
|
478
|
-
"(--kind
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
587
|
+
"(--kind cache) native session id used to join the authoritative V3 generation.",
|
|
588
|
+
)
|
|
589
|
+
.option(
|
|
590
|
+
"--adapter <id>",
|
|
591
|
+
"(--kind cache) adapter used to validate the authoritative V3 generation: " +
|
|
592
|
+
"claude-code | cursor | codex.",
|
|
482
593
|
)
|
|
483
594
|
.option(
|
|
484
595
|
"--pid <pid>",
|
|
@@ -489,13 +600,218 @@ export function registerAgentsCommand(
|
|
|
489
600
|
)
|
|
490
601
|
.option("--json", "JSON envelope output")
|
|
491
602
|
.action(
|
|
492
|
-
(opts: {
|
|
603
|
+
(opts: {
|
|
604
|
+
owner: string;
|
|
605
|
+
kind: string;
|
|
606
|
+
sessionId?: string;
|
|
607
|
+
adapter?: string;
|
|
608
|
+
pid?: string;
|
|
609
|
+
json?: boolean;
|
|
610
|
+
}) => {
|
|
493
611
|
runHeal(opts);
|
|
494
612
|
},
|
|
495
613
|
);
|
|
496
614
|
registerIdentityCommands(cmd);
|
|
497
615
|
}
|
|
498
616
|
|
|
617
|
+
const SESSION_OUTCOMES = new Set([
|
|
618
|
+
"succeeded",
|
|
619
|
+
"failed",
|
|
620
|
+
"cancelled",
|
|
621
|
+
"timed_out",
|
|
622
|
+
"denied",
|
|
623
|
+
"interrupted",
|
|
624
|
+
"unknown",
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
function runEndSession(opts: { sessionId?: string; instanceId?: string; outcome: string }) {
|
|
628
|
+
const root = monorepoRoot();
|
|
629
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
630
|
+
if (!SESSION_OUTCOMES.has(opts.outcome)) {
|
|
631
|
+
return failCommand("invalid_outcome", "invalid session outcome");
|
|
632
|
+
}
|
|
633
|
+
const target = opts.instanceId ?? opts.sessionId ?? resolveOwner();
|
|
634
|
+
if (!target) {
|
|
635
|
+
return failCommand(
|
|
636
|
+
"session_identity_missing",
|
|
637
|
+
"could not resolve the current session; pass --session-id or --instance-id",
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const byInstance = listHookProducerStateRecordsV3(root, { includeTerminal: false }).filter(
|
|
641
|
+
({ state }) => state.instance_id === target,
|
|
642
|
+
);
|
|
643
|
+
const byNative = (["claude-code", "codex", "cursor"] as const).flatMap((adapter) => {
|
|
644
|
+
const state = readHookProducerStateV3(root, adapter, target);
|
|
645
|
+
return state && !state.terminal ? [{ path: "", modified_at_ms: 0, state }] : [];
|
|
646
|
+
});
|
|
647
|
+
const matches = byInstance.length > 0 ? byInstance : byNative;
|
|
648
|
+
if (matches.length !== 1) {
|
|
649
|
+
return failCommand(
|
|
650
|
+
"session_identity_ambiguous",
|
|
651
|
+
`expected one live V3 generation for the target; found ${matches.length}`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
const record = matches[0];
|
|
655
|
+
if (!record) {
|
|
656
|
+
return failCommand("session_identity_missing", "live V3 generation disappeared");
|
|
657
|
+
}
|
|
658
|
+
const state = record.state;
|
|
659
|
+
if (state.delegations.length > 0) {
|
|
660
|
+
return failCommand(
|
|
661
|
+
"session_work_open",
|
|
662
|
+
`cannot finalize while ${state.delegations.length} delegated child(ren) remain open`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const history = readSessionWriteClaims(root, state.instance_id, state.session_id);
|
|
666
|
+
const finalized = checkGitFinalization(root, history.paths, {
|
|
667
|
+
claimHistoryComplete: history.complete,
|
|
668
|
+
});
|
|
669
|
+
if (!finalized.ok) {
|
|
670
|
+
return failCommand(
|
|
671
|
+
"git_not_finalized",
|
|
672
|
+
formatGitFinalizationFailure(finalized, resolveBinName(root)),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const result = requestSessionEndExplicitV3({
|
|
676
|
+
coordRoot: root,
|
|
677
|
+
instance_id: state.instance_id,
|
|
678
|
+
generation_id: state.generation_id,
|
|
679
|
+
outcome: opts.outcome as
|
|
680
|
+
| "succeeded"
|
|
681
|
+
| "failed"
|
|
682
|
+
| "cancelled"
|
|
683
|
+
| "timed_out"
|
|
684
|
+
| "denied"
|
|
685
|
+
| "interrupted"
|
|
686
|
+
| "unknown",
|
|
687
|
+
coordination_finalized: true,
|
|
688
|
+
});
|
|
689
|
+
if (
|
|
690
|
+
result.state !== "recorded" &&
|
|
691
|
+
result.state !== "already_ended" &&
|
|
692
|
+
result.state !== "queued" &&
|
|
693
|
+
result.state !== "already_requested"
|
|
694
|
+
) {
|
|
695
|
+
return failCommand("session_end_failed", JSON.stringify(result));
|
|
696
|
+
}
|
|
697
|
+
const terminalEventId =
|
|
698
|
+
result.state === "recorded"
|
|
699
|
+
? result.event.event_id
|
|
700
|
+
: result.state === "already_ended"
|
|
701
|
+
? result.event_id
|
|
702
|
+
: undefined;
|
|
703
|
+
const requestId =
|
|
704
|
+
result.state === "queued" || result.state === "already_requested"
|
|
705
|
+
? result.request.request_id
|
|
706
|
+
: undefined;
|
|
707
|
+
emit.data({
|
|
708
|
+
ok: true,
|
|
709
|
+
state: result.state,
|
|
710
|
+
instance_id: state.instance_id,
|
|
711
|
+
generation_id: state.generation_id,
|
|
712
|
+
...(terminalEventId ? { terminal_event_id: terminalEventId } : {}),
|
|
713
|
+
...(requestId ? { request_id: requestId } : {}),
|
|
714
|
+
authority: "approved",
|
|
715
|
+
reason: "approved_explicit_end",
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function runSessionReconcile(opts: {
|
|
720
|
+
watch?: boolean;
|
|
721
|
+
intervalSeconds?: string;
|
|
722
|
+
json?: boolean;
|
|
723
|
+
}): Promise<void> {
|
|
724
|
+
const root = monorepoRoot();
|
|
725
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
726
|
+
if (opts.json) emit.config({ format: "json" });
|
|
727
|
+
const interval = opts.intervalSeconds
|
|
728
|
+
? Number.parseInt(opts.intervalSeconds, 10)
|
|
729
|
+
: sessionFinalizationConfig(root).reconcileIntervalSeconds;
|
|
730
|
+
if (!Number.isSafeInteger(interval) || interval < 1) {
|
|
731
|
+
return failCommand("invalid_interval", "interval-seconds must be a positive integer");
|
|
732
|
+
}
|
|
733
|
+
let stopped = false;
|
|
734
|
+
do {
|
|
735
|
+
emit.data(reconcileSessionFinalizationV3(root));
|
|
736
|
+
if (!opts.watch || stopped) return;
|
|
737
|
+
await new Promise<void>((resolvePromise) => {
|
|
738
|
+
const timer = setTimeout(resolvePromise, interval * 1_000);
|
|
739
|
+
const stop = () => {
|
|
740
|
+
stopped = true;
|
|
741
|
+
clearTimeout(timer);
|
|
742
|
+
process.off("SIGINT", stop);
|
|
743
|
+
process.off("SIGTERM", stop);
|
|
744
|
+
resolvePromise();
|
|
745
|
+
};
|
|
746
|
+
process.once("SIGINT", stop);
|
|
747
|
+
process.once("SIGTERM", stop);
|
|
748
|
+
});
|
|
749
|
+
} while (opts.watch && !stopped);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function runObserveArchive(opts: {
|
|
753
|
+
adapter: string;
|
|
754
|
+
sessionId: string;
|
|
755
|
+
unarchived?: boolean;
|
|
756
|
+
observedAt?: string;
|
|
757
|
+
}) {
|
|
758
|
+
const root = monorepoRoot();
|
|
759
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
760
|
+
if (!(["claude-code", "codex", "cursor"] as string[]).includes(opts.adapter)) {
|
|
761
|
+
return failCommand("invalid_adapter", "adapter must be claude-code, codex, or cursor");
|
|
762
|
+
}
|
|
763
|
+
const adapter = opts.adapter as "claude-code" | "codex" | "cursor";
|
|
764
|
+
const observedAt = opts.observedAt ?? new Date().toISOString();
|
|
765
|
+
if (Number.isNaN(Date.parse(observedAt))) {
|
|
766
|
+
return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
|
|
767
|
+
}
|
|
768
|
+
emit.data(
|
|
769
|
+
reconcileSessionFinalizationV3(root, {
|
|
770
|
+
now: new Date(observedAt),
|
|
771
|
+
archive_observations: [
|
|
772
|
+
{
|
|
773
|
+
adapter,
|
|
774
|
+
native_session_id: opts.sessionId,
|
|
775
|
+
archived: !opts.unarchived,
|
|
776
|
+
observed_at: observedAt,
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
}),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function runObserveHostDisappeared(opts: {
|
|
784
|
+
instanceId: string;
|
|
785
|
+
generationId: string;
|
|
786
|
+
observedAt?: string;
|
|
787
|
+
}) {
|
|
788
|
+
const root = monorepoRoot();
|
|
789
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
790
|
+
if (!/^inst_[A-Za-z0-9._-]+$/.test(opts.instanceId)) {
|
|
791
|
+
return failCommand("invalid_instance_id", "invalid V3 instance id");
|
|
792
|
+
}
|
|
793
|
+
if (!/^gen_[A-Za-z0-9._-]+$/.test(opts.generationId)) {
|
|
794
|
+
return failCommand("invalid_generation_id", "invalid V3 generation id");
|
|
795
|
+
}
|
|
796
|
+
const observedAt = opts.observedAt ?? new Date().toISOString();
|
|
797
|
+
if (Number.isNaN(Date.parse(observedAt))) {
|
|
798
|
+
return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
|
|
799
|
+
}
|
|
800
|
+
emit.data(
|
|
801
|
+
observeHostDisappearedV3({
|
|
802
|
+
coordRoot: root,
|
|
803
|
+
instance_id: opts.instanceId as `inst_${string}`,
|
|
804
|
+
generation_id: opts.generationId as `gen_${string}`,
|
|
805
|
+
observed_at: observedAt,
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function failCommand(code: string, message: string): void {
|
|
811
|
+
emit.error({ code, message });
|
|
812
|
+
process.exitCode = 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
499
815
|
function registerIdentityCommands(parent: Command): void {
|
|
500
816
|
const identity = parent
|
|
501
817
|
.command("identity")
|
|
@@ -779,7 +1095,7 @@ export function registerCouncilCommands(parent: Command): void {
|
|
|
779
1095
|
"archive it first (trash-can pattern). Without --yes this prints " +
|
|
780
1096
|
"the paths that would be removed and exits 0 without touching " +
|
|
781
1097
|
"anything. Does NOT touch target_doc, close_handoff_path, or " +
|
|
782
|
-
"
|
|
1098
|
+
"the canonical V3 event ledger, which is owned independently.",
|
|
783
1099
|
)
|
|
784
1100
|
.option("-y, --yes", "Required to actually delete; without this, dry-run")
|
|
785
1101
|
.option("--json", "JSON envelope output")
|
|
@@ -918,11 +1234,11 @@ function runWhoami(opts: { json?: boolean }): void {
|
|
|
918
1234
|
process.exit(1);
|
|
919
1235
|
}
|
|
920
1236
|
|
|
921
|
-
const hb =
|
|
1237
|
+
const hb = readLiveCoordinationRow(root, myOwner);
|
|
922
1238
|
if (!hb) {
|
|
923
1239
|
emit.error({
|
|
924
|
-
code: "
|
|
925
|
-
message:
|
|
1240
|
+
code: "no_live_generation",
|
|
1241
|
+
message: noLiveGenerationMessage(myOwner),
|
|
926
1242
|
});
|
|
927
1243
|
process.exit(1);
|
|
928
1244
|
}
|
|
@@ -934,20 +1250,17 @@ function runWhoami(opts: { json?: boolean }): void {
|
|
|
934
1250
|
session_id: hb.session_id,
|
|
935
1251
|
kind: normalizeKind(hb.kind),
|
|
936
1252
|
relation: "self",
|
|
937
|
-
started_at: hb.started_at,
|
|
1253
|
+
started_at: hb.started_at ?? hb.last_heartbeat,
|
|
938
1254
|
last_heartbeat: hb.last_heartbeat,
|
|
939
1255
|
files_touched: hb.files_touched ?? [],
|
|
940
|
-
last_tool: hb.last_tool ?? null,
|
|
941
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
942
1256
|
task: hb.task ?? null,
|
|
943
1257
|
activity: activityOf(hb),
|
|
944
1258
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
945
1259
|
activity_source: hb.activity_source ?? null,
|
|
946
1260
|
task_state: taskStateOf(hb),
|
|
1261
|
+
task_state_scope: "current",
|
|
947
1262
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
948
1263
|
task_state_reason: hb.task_state_reason ?? null,
|
|
949
|
-
turn_summary: hb.turn_summary ?? null,
|
|
950
|
-
turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
|
|
951
1264
|
platform: hb.platform ?? "claude-code",
|
|
952
1265
|
};
|
|
953
1266
|
|
|
@@ -971,38 +1284,19 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
971
1284
|
process.exit(1);
|
|
972
1285
|
}
|
|
973
1286
|
|
|
974
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
975
|
-
if (!existsSync(activeDir)) {
|
|
976
|
-
emit.data({ rows: [], note: SUBAGENT_NOTE });
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
1287
|
// Resolve self for relation column; best-effort, missing → "unknown" on every row.
|
|
981
1288
|
const myOwner = resolveOwner();
|
|
982
|
-
const myHb = myOwner ?
|
|
1289
|
+
const myHb = myOwner ? readLiveCoordinationRow(root, myOwner) : null;
|
|
983
1290
|
const mySession = myHb?.session_id ?? null;
|
|
984
1291
|
|
|
985
|
-
|
|
986
|
-
const heartbeats: Heartbeat[] = [];
|
|
987
|
-
for (const file of readdirSync(activeDir)) {
|
|
988
|
-
if (!file.endsWith(".json")) continue;
|
|
989
|
-
try {
|
|
990
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
991
|
-
const parsed = JSON.parse(raw);
|
|
992
|
-
if (parsed && typeof parsed.instance_id === "string") {
|
|
993
|
-
heartbeats.push(parsed as Heartbeat);
|
|
994
|
-
}
|
|
995
|
-
} catch {
|
|
996
|
-
// skip malformed
|
|
997
|
-
}
|
|
998
|
-
}
|
|
1292
|
+
const generations = readLiveCoordinationRows(root);
|
|
999
1293
|
|
|
1000
1294
|
// Apply staleness filter unless --stale.
|
|
1001
1295
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1002
1296
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1003
1297
|
const live = opts.stale
|
|
1004
|
-
?
|
|
1005
|
-
:
|
|
1298
|
+
? generations
|
|
1299
|
+
: generations.filter((h) => {
|
|
1006
1300
|
const ts = Date.parse(h.last_heartbeat);
|
|
1007
1301
|
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
1008
1302
|
});
|
|
@@ -1038,20 +1332,17 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1038
1332
|
session_id: h.session_id,
|
|
1039
1333
|
kind: "transient",
|
|
1040
1334
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
1041
|
-
started_at: h.started_at,
|
|
1335
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
1042
1336
|
last_heartbeat: h.last_heartbeat,
|
|
1043
1337
|
files_touched: [...(h.files_touched ?? [])].sort(),
|
|
1044
|
-
last_tool: h.last_tool ?? null,
|
|
1045
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
1046
1338
|
task: h.task ?? null,
|
|
1047
1339
|
activity: activityOf(h),
|
|
1048
1340
|
activity_updated_at: h.activity_updated_at ?? null,
|
|
1049
1341
|
activity_source: h.activity_source ?? null,
|
|
1050
1342
|
task_state: taskStateOf(h),
|
|
1343
|
+
task_state_scope: "current",
|
|
1051
1344
|
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
1052
1345
|
task_state_reason: h.task_state_reason ?? null,
|
|
1053
|
-
turn_summary: h.turn_summary ?? null,
|
|
1054
|
-
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
1055
1346
|
platform: h.platform ?? "claude-code",
|
|
1056
1347
|
});
|
|
1057
1348
|
continue;
|
|
@@ -1069,32 +1360,28 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1069
1360
|
session_id: h.session_id,
|
|
1070
1361
|
kind: normalizeKind(h.kind),
|
|
1071
1362
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
1072
|
-
started_at: h.started_at,
|
|
1363
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
1073
1364
|
last_heartbeat: h.last_heartbeat,
|
|
1074
1365
|
files_touched: files,
|
|
1075
|
-
last_tool: h.last_tool ?? null,
|
|
1076
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
1077
1366
|
task: h.task ?? null,
|
|
1078
1367
|
activity: activityOf(h),
|
|
1079
1368
|
activity_updated_at: h.activity_updated_at ?? null,
|
|
1080
1369
|
activity_source: h.activity_source ?? null,
|
|
1081
1370
|
task_state: taskStateOf(h),
|
|
1371
|
+
task_state_scope: "current",
|
|
1082
1372
|
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
1083
1373
|
task_state_reason: h.task_state_reason ?? null,
|
|
1084
|
-
turn_summary: h.turn_summary ?? null,
|
|
1085
|
-
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
1086
1374
|
platform: h.platform ?? "claude-code",
|
|
1087
1375
|
});
|
|
1088
1376
|
}
|
|
1089
1377
|
|
|
1090
|
-
// Guard missing started_at
|
|
1091
|
-
//
|
|
1092
|
-
// what made `harn agents list --all --stale` crash.
|
|
1378
|
+
// Guard missing started_at so a partial observational projection cannot make
|
|
1379
|
+
// `harn agents list --all --stale` throw while sorting.
|
|
1093
1380
|
rows.sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? ""));
|
|
1094
1381
|
|
|
1095
1382
|
// Cross-machine presence (ADR 0016): append sessions on OTHER machines from
|
|
1096
1383
|
// the locally-fetched presence refs. Advisory rows (relation=remote) — they
|
|
1097
|
-
//
|
|
1384
|
+
// never participate in local claim blocking.
|
|
1098
1385
|
for (const rm of readRemoteMachines(root)) {
|
|
1099
1386
|
for (const a of rm.agents) {
|
|
1100
1387
|
rows.push({
|
|
@@ -1106,17 +1393,14 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1106
1393
|
started_at: a.started_at ?? "",
|
|
1107
1394
|
last_heartbeat: a.last_heartbeat ?? "",
|
|
1108
1395
|
files_touched: [...(a.files_touched ?? [])].sort(),
|
|
1109
|
-
last_tool: a.last_tool ?? null,
|
|
1110
|
-
last_tool_target: null,
|
|
1111
1396
|
task: a.task ?? null,
|
|
1112
1397
|
activity: a.activity ?? "unknown",
|
|
1113
1398
|
activity_updated_at: null,
|
|
1114
1399
|
activity_source: null,
|
|
1115
1400
|
task_state: a.task_state ?? "active",
|
|
1401
|
+
task_state_scope: "current",
|
|
1116
1402
|
task_state_updated_at: null,
|
|
1117
1403
|
task_state_reason: a.task_state_reason ?? null,
|
|
1118
|
-
turn_summary: a.turn_summary ?? null,
|
|
1119
|
-
turn_summary_updated_at: null,
|
|
1120
1404
|
platform: a.platform ?? "claude-code",
|
|
1121
1405
|
machine: rm.machine,
|
|
1122
1406
|
});
|
|
@@ -1131,7 +1415,7 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1131
1415
|
}
|
|
1132
1416
|
|
|
1133
1417
|
function relationOf(
|
|
1134
|
-
peer:
|
|
1418
|
+
peer: Pick<LiveCoordinationRow, "instance_id" | "session_id">,
|
|
1135
1419
|
myOwner: string,
|
|
1136
1420
|
mySession: string | null,
|
|
1137
1421
|
): "self" | "group" | "blocks" | "unknown" {
|
|
@@ -1147,22 +1431,18 @@ function normalizeKind(kind: string | undefined | null): string {
|
|
|
1147
1431
|
}
|
|
1148
1432
|
|
|
1149
1433
|
/**
|
|
1150
|
-
* The `
|
|
1434
|
+
* The `no_live_generation` diagnostic, quoting the owner id in full.
|
|
1151
1435
|
*
|
|
1152
|
-
* This id is actionable: the reader
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
* here reads as complete, so it gets copy-pasted into `--owner` and mints a
|
|
1156
|
-
* heartbeat at a filename no reader resolves — an orphan that leaves the
|
|
1157
|
-
* session looking unhealable. Abbreviate ids for display elsewhere, never in
|
|
1158
|
-
* a message whose whole purpose is to hand the reader an id to pass back.
|
|
1436
|
+
* This id is actionable: the reader may pass it to cache repair or explicit
|
|
1437
|
+
* finalization. Abbreviate ids for display elsewhere, never in a diagnostic
|
|
1438
|
+
* whose purpose is to hand the caller a canonical identity.
|
|
1159
1439
|
*/
|
|
1160
|
-
function
|
|
1161
|
-
return `resolved owner=${owner} but no
|
|
1440
|
+
function noLiveGenerationMessage(owner: string): string {
|
|
1441
|
+
return `resolved owner=${owner} but no authority-safe live V3 generation exists for it`;
|
|
1162
1442
|
}
|
|
1163
1443
|
|
|
1164
1444
|
/**
|
|
1165
|
-
* The instance_id of a live
|
|
1445
|
+
* The instance_id of a live V3 generation that `prefix` strictly prefixes, or null.
|
|
1166
1446
|
*
|
|
1167
1447
|
* Used to recognize an abbreviated id handed back by a reader when the caller
|
|
1168
1448
|
* supplied no canonical id of their own. Ambiguity is treated as "no answer":
|
|
@@ -1170,12 +1450,9 @@ function noHeartbeatMessage(owner: string): string {
|
|
|
1170
1450
|
* and refusing with the wrong id in the message would be worse than the orphan.
|
|
1171
1451
|
*/
|
|
1172
1452
|
function liveIdWithPrefix(root: string, prefix: string): string | null {
|
|
1173
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
1174
|
-
if (!existsSync(activeDir)) return null;
|
|
1175
1453
|
const matches = new Set<string>();
|
|
1176
|
-
for (const
|
|
1177
|
-
|
|
1178
|
-
const id = file.slice(0, -".json".length);
|
|
1454
|
+
for (const row of readLiveCoordinationRows(root)) {
|
|
1455
|
+
const id = row.instance_id;
|
|
1179
1456
|
if (id !== prefix && id.startsWith(prefix)) matches.add(id);
|
|
1180
1457
|
}
|
|
1181
1458
|
return matches.size === 1 ? (matches.values().next().value as string) : null;
|
|
@@ -1190,82 +1467,60 @@ async function runWatch(pollMs: number): Promise<void> {
|
|
|
1190
1467
|
});
|
|
1191
1468
|
process.exit(1);
|
|
1192
1469
|
}
|
|
1193
|
-
const
|
|
1194
|
-
if (!existsSync(activeDir)) {
|
|
1195
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
1196
|
-
process.exit(1);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
const fs = await import("node:fs");
|
|
1200
|
-
const cache = new Map<string, Heartbeat>();
|
|
1470
|
+
const cache = new Map<string, LiveCoordinationRow>();
|
|
1201
1471
|
|
|
1202
1472
|
// Seed cache + print an initial roster line per live peer.
|
|
1203
|
-
const initial =
|
|
1204
|
-
process.stderr.write(
|
|
1473
|
+
const initial = listLiveGenerations(root);
|
|
1474
|
+
process.stderr.write("watching authoritative V3 coordination state (Ctrl-C to exit)\n"); // lint-ok-emission: banner goes to stderr, stdout is the live stream
|
|
1205
1475
|
for (const h of initial) {
|
|
1206
1476
|
cache.set(h.instance_id, h);
|
|
1207
1477
|
emitWatchLine(
|
|
1208
|
-
`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at))} old${h.task ? `, task: "${h.task}"` : ""})`,
|
|
1478
|
+
`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`,
|
|
1209
1479
|
);
|
|
1210
1480
|
}
|
|
1211
1481
|
|
|
1212
|
-
let scheduled: NodeJS.Timeout | null = null;
|
|
1213
1482
|
const rescan = () => {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
if (!current.has(id)) {
|
|
1223
|
-
emitWatchLine(`agent-${old.name ?? "?"} ended`);
|
|
1224
|
-
cache.delete(id);
|
|
1225
|
-
}
|
|
1483
|
+
const current = new Map<string, LiveCoordinationRow>();
|
|
1484
|
+
for (const h of listLiveGenerations(root)) current.set(h.instance_id, h);
|
|
1485
|
+
|
|
1486
|
+
// Removed agents.
|
|
1487
|
+
for (const [id, old] of cache) {
|
|
1488
|
+
if (!current.has(id)) {
|
|
1489
|
+
emitWatchLine(`agent-${old.name ?? "?"} ended`);
|
|
1490
|
+
cache.delete(id);
|
|
1226
1491
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
)
|
|
1234
|
-
|
|
1235
|
-
continue;
|
|
1236
|
-
}
|
|
1237
|
-
// Diff fields we care about.
|
|
1238
|
-
if ((prev.task ?? "") !== (h.task ?? "")) {
|
|
1239
|
-
emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
|
|
1240
|
-
}
|
|
1241
|
-
if (
|
|
1242
|
-
(prev.last_tool ?? "") !== (h.last_tool ?? "") ||
|
|
1243
|
-
(prev.last_tool_target ?? "") !== (h.last_tool_target ?? "")
|
|
1244
|
-
) {
|
|
1245
|
-
if (h.last_tool) {
|
|
1246
|
-
const target = h.last_tool_target ? ` ${truncate(h.last_tool_target, 80)}` : "";
|
|
1247
|
-
emitWatchLine(`agent-${h.name ?? "?"} ${h.last_tool}${target}`);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
// File additions/removals.
|
|
1251
|
-
const prevFiles = new Set(prev.files_touched ?? []);
|
|
1252
|
-
const currFiles = new Set(h.files_touched ?? []);
|
|
1253
|
-
for (const f of currFiles) {
|
|
1254
|
-
if (!prevFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
|
|
1255
|
-
}
|
|
1256
|
-
for (const f of prevFiles) {
|
|
1257
|
-
if (!currFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
|
|
1258
|
-
}
|
|
1492
|
+
}
|
|
1493
|
+
// Added or changed agents.
|
|
1494
|
+
for (const [id, h] of current) {
|
|
1495
|
+
const prev = cache.get(id);
|
|
1496
|
+
if (!prev) {
|
|
1497
|
+
emitWatchLine(
|
|
1498
|
+
`agent-${h.name ?? "?"} started (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`,
|
|
1499
|
+
);
|
|
1259
1500
|
cache.set(id, h);
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
// Diff fields we care about.
|
|
1504
|
+
if ((prev.task ?? "") !== (h.task ?? "")) {
|
|
1505
|
+
emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
|
|
1506
|
+
}
|
|
1507
|
+
// File additions/removals.
|
|
1508
|
+
const prevFiles = new Set(prev.files_touched ?? []);
|
|
1509
|
+
const currFiles = new Set(h.files_touched ?? []);
|
|
1510
|
+
for (const f of currFiles) {
|
|
1511
|
+
if (!prevFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
|
|
1260
1512
|
}
|
|
1261
|
-
|
|
1513
|
+
for (const f of prevFiles) {
|
|
1514
|
+
if (!currFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
|
|
1515
|
+
}
|
|
1516
|
+
cache.set(id, h);
|
|
1517
|
+
}
|
|
1262
1518
|
};
|
|
1263
1519
|
|
|
1264
|
-
const watcher = fs.watch(activeDir, { persistent: true }, () => rescan());
|
|
1265
|
-
|
|
1266
1520
|
await new Promise<void>((resolveP) => {
|
|
1521
|
+
const timer = setInterval(rescan, Math.max(100, pollMs));
|
|
1267
1522
|
const stop = () => {
|
|
1268
|
-
|
|
1523
|
+
clearInterval(timer);
|
|
1269
1524
|
resolveP();
|
|
1270
1525
|
};
|
|
1271
1526
|
process.on("SIGINT", stop);
|
|
@@ -1273,23 +1528,13 @@ async function runWatch(pollMs: number): Promise<void> {
|
|
|
1273
1528
|
});
|
|
1274
1529
|
}
|
|
1275
1530
|
|
|
1276
|
-
function
|
|
1277
|
-
const out: Heartbeat[] = [];
|
|
1531
|
+
function listLiveGenerations(root: string): LiveCoordinationRow[] {
|
|
1278
1532
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1279
1533
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const parsed = JSON.parse(raw) as Heartbeat;
|
|
1285
|
-
if (!parsed || typeof parsed.instance_id !== "string") continue;
|
|
1286
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
1287
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) out.push(parsed);
|
|
1288
|
-
} catch {
|
|
1289
|
-
// skip
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
return out;
|
|
1534
|
+
return readLiveCoordinationRows(root).filter((row) => {
|
|
1535
|
+
const ts = Date.parse(row.last_heartbeat);
|
|
1536
|
+
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
1537
|
+
});
|
|
1293
1538
|
}
|
|
1294
1539
|
|
|
1295
1540
|
function secondsSince(iso: string): number {
|
|
@@ -1311,29 +1556,14 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1311
1556
|
});
|
|
1312
1557
|
process.exit(1);
|
|
1313
1558
|
}
|
|
1314
|
-
|
|
1315
|
-
if (!existsSync(activeDir)) {
|
|
1316
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
1317
|
-
process.exit(1);
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// Read all heartbeats; match by name (case-insensitive). Apply freshness filter.
|
|
1321
|
-
const matches: Heartbeat[] = [];
|
|
1559
|
+
// Match authority-safe V3 generations by resolved display name.
|
|
1322
1560
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1323
1561
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1324
|
-
|
|
1325
|
-
if (
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (!parsed || typeof parsed.instance_id !== "string") continue;
|
|
1330
|
-
if ((parsed.name ?? "").toLowerCase() !== name.toLowerCase()) continue;
|
|
1331
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
1332
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) matches.push(parsed);
|
|
1333
|
-
} catch {
|
|
1334
|
-
// skip malformed
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1562
|
+
const matches = readLiveCoordinationRows(root).filter((row) => {
|
|
1563
|
+
if ((row.name ?? "").toLowerCase() !== name.toLowerCase()) return false;
|
|
1564
|
+
const ts = Date.parse(row.last_heartbeat);
|
|
1565
|
+
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
1566
|
+
});
|
|
1337
1567
|
|
|
1338
1568
|
if (matches.length === 0) {
|
|
1339
1569
|
emit.error({
|
|
@@ -1365,7 +1595,7 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1365
1595
|
const report = null as PeerReport | null;
|
|
1366
1596
|
const bqError = null as string | null;
|
|
1367
1597
|
|
|
1368
|
-
const startedAtMs = Date.parse(hb.started_at);
|
|
1598
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1369
1599
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1370
1600
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1371
1601
|
: 0;
|
|
@@ -1386,14 +1616,11 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1386
1616
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1387
1617
|
activity_source: hb.activity_source ?? null,
|
|
1388
1618
|
task_state: taskStateOf(hb),
|
|
1619
|
+
task_state_scope: "current" as const,
|
|
1389
1620
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1390
1621
|
task_state_reason: hb.task_state_reason ?? null,
|
|
1391
|
-
turn_summary: hb.turn_summary ?? null,
|
|
1392
|
-
turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
|
|
1393
1622
|
title: report?.title ?? null,
|
|
1394
1623
|
files_held: hb.files_touched ?? [],
|
|
1395
|
-
last_tool: hb.last_tool ?? null,
|
|
1396
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
1397
1624
|
recent_prompts: report?.recent_prompts ?? [],
|
|
1398
1625
|
recent_tools: report?.recent_tools ?? [],
|
|
1399
1626
|
tool_counts: report?.tool_counts ?? [],
|
|
@@ -1423,10 +1650,6 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1423
1650
|
lines.push(
|
|
1424
1651
|
` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`,
|
|
1425
1652
|
);
|
|
1426
|
-
if (data.last_tool) {
|
|
1427
|
-
const target = data.last_tool_target ? ` ${truncate(data.last_tool_target, 80)}` : "";
|
|
1428
|
-
lines.push(` last activity: ${data.last_tool}${target}`);
|
|
1429
|
-
}
|
|
1430
1653
|
if (data.files_held.length > 0) {
|
|
1431
1654
|
lines.push(` holds ${data.files_held.length} file(s):`);
|
|
1432
1655
|
for (const f of data.files_held.slice(0, 10)) lines.push(` ${f}`);
|
|
@@ -1581,13 +1804,19 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1581
1804
|
// Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
|
|
1582
1805
|
// mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
|
|
1583
1806
|
// don't descend from a pid-map-registered anchor, so the walk can miss there.
|
|
1807
|
+
// Final fallback: the adapter/connector-stamped session id from the
|
|
1808
|
+
// environment. set-task is the session's REGISTRATION point — a fresh
|
|
1809
|
+
// (bridge) session has no heartbeat yet, so the heartbeat-validated resolver
|
|
1810
|
+
// returns null by design, and erroring here orphans the session's first
|
|
1811
|
+
// ritual command. The env id carries the same trust as an explicit
|
|
1812
|
+
// --session-id, and set-task mints the heartbeat exactly as that path does.
|
|
1584
1813
|
if (!opts?.sessionId) ensureCursorSession(root);
|
|
1585
|
-
const myOwner = opts?.sessionId ?? resolveOwner();
|
|
1814
|
+
const myOwner = opts?.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1586
1815
|
if (!myOwner) {
|
|
1587
1816
|
emit.error({
|
|
1588
1817
|
code: "no_pidmap_entry",
|
|
1589
1818
|
message:
|
|
1590
|
-
"not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1819
|
+
"not in an agent session; no session-id environment identity and the ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1591
1820
|
});
|
|
1592
1821
|
process.exit(1);
|
|
1593
1822
|
}
|
|
@@ -1598,7 +1827,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1598
1827
|
// naming window, and subagent/workflow kinds are never named — see
|
|
1599
1828
|
// heartbeat-writer.setTask. This call is the naming call exactly when the
|
|
1600
1829
|
// stamp appears across the mutation.
|
|
1601
|
-
const priorHb =
|
|
1830
|
+
const priorHb = readCurrentCoordinationRow(myOwner);
|
|
1602
1831
|
const normalizedTask = task.length > 0 ? task : undefined;
|
|
1603
1832
|
const lifecycleWarning =
|
|
1604
1833
|
priorHb && (priorHb.task_state ?? "active") !== "active" && priorHb.task !== normalizedTask
|
|
@@ -1619,30 +1848,33 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1619
1848
|
process.exit(1);
|
|
1620
1849
|
}
|
|
1621
1850
|
|
|
1622
|
-
const hb =
|
|
1851
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1623
1852
|
|
|
1624
1853
|
// The naming call: the mutation just stamped `suggested_session_name` for
|
|
1625
1854
|
// the first time. `first_of_session: true` means exactly "this call produced
|
|
1626
1855
|
// the session name" — it is never true with a null name (an empty first
|
|
1627
1856
|
// declaration or a `--session-id` relay both keep the window open instead).
|
|
1628
|
-
const
|
|
1857
|
+
const firstSuggestedName =
|
|
1629
1858
|
!priorHb?.suggested_session_name && hb?.suggested_session_name
|
|
1630
1859
|
? hb.suggested_session_name
|
|
1631
1860
|
: null;
|
|
1632
|
-
const firstOfSession = suggestedName !== null;
|
|
1633
1861
|
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1862
|
+
// A readable but misclassified first display would otherwise leave the
|
|
1863
|
+
// session permanently latched: set-task is allowed through the gate, but a
|
|
1864
|
+
// normal repeat used to return no name and therefore could not establish a
|
|
1865
|
+
// fresh ordered transcript boundary. Re-emit the unchanged pending name as
|
|
1866
|
+
// an explicit retry. This does not rename the session or claim that the
|
|
1867
|
+
// display was seen; it only gives the next exact block new mint evidence.
|
|
1868
|
+
const retriedSuggestedName =
|
|
1869
|
+
!firstSuggestedName &&
|
|
1870
|
+
priorHb?.suggested_session_name &&
|
|
1871
|
+
priorHb.session_name_seen_for !== priorHb.suggested_session_name &&
|
|
1872
|
+
hb?.suggested_session_name === priorHb.suggested_session_name
|
|
1873
|
+
? hb.suggested_session_name
|
|
1874
|
+
: null;
|
|
1875
|
+
const suggestedName = firstSuggestedName ?? retriedSuggestedName;
|
|
1876
|
+
const firstOfSession = firstSuggestedName !== null;
|
|
1877
|
+
const sessionNameRetry = retriedSuggestedName !== null;
|
|
1646
1878
|
|
|
1647
1879
|
emit.data({
|
|
1648
1880
|
instance_id: myOwner,
|
|
@@ -1650,13 +1882,14 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1650
1882
|
task: hb?.task ?? null,
|
|
1651
1883
|
cleared: !task || task.length === 0,
|
|
1652
1884
|
first_of_session: firstOfSession,
|
|
1885
|
+
session_name_retry: sessionNameRetry,
|
|
1653
1886
|
suggested_session_name: suggestedName,
|
|
1654
1887
|
...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
|
|
1655
1888
|
// Right-time instruction: the UserPromptSubmit nudge fires before the name
|
|
1656
1889
|
// exists; this result is the moment the model holds the string.
|
|
1657
1890
|
...(suggestedName
|
|
1658
1891
|
? {
|
|
1659
|
-
note:
|
|
1892
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1660
1893
|
}
|
|
1661
1894
|
: {}),
|
|
1662
1895
|
});
|
|
@@ -1683,7 +1916,7 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1683
1916
|
return;
|
|
1684
1917
|
}
|
|
1685
1918
|
if (!opts.sessionId) ensureCursorSession(root);
|
|
1686
|
-
const myOwner = opts.sessionId ?? resolveOwner();
|
|
1919
|
+
const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1687
1920
|
if (!myOwner) {
|
|
1688
1921
|
emit.error({
|
|
1689
1922
|
code: "no_pidmap_entry",
|
|
@@ -1694,11 +1927,40 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1694
1927
|
return;
|
|
1695
1928
|
}
|
|
1696
1929
|
|
|
1697
|
-
|
|
1930
|
+
let hb = readCurrentCoordinationRow(myOwner);
|
|
1931
|
+
let reopenedGeneration: ReturnType<typeof reopenLiveCoordinationGenerationV3> | undefined;
|
|
1698
1932
|
if (!hb) {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1933
|
+
if (state !== "active") {
|
|
1934
|
+
emit.error({
|
|
1935
|
+
code: "no_live_generation",
|
|
1936
|
+
message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
|
|
1937
|
+
});
|
|
1938
|
+
process.exitCode = 1;
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
try {
|
|
1942
|
+
reopenedGeneration = reopenLiveCoordinationGenerationV3({
|
|
1943
|
+
coordRoot: root,
|
|
1944
|
+
owner: myOwner,
|
|
1945
|
+
nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
|
|
1946
|
+
});
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
emit.error({
|
|
1949
|
+
code: "lifecycle_reopen_failed",
|
|
1950
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1951
|
+
});
|
|
1952
|
+
process.exitCode = 1;
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
hb = readCurrentCoordinationRow(myOwner);
|
|
1956
|
+
if (!hb) {
|
|
1957
|
+
emit.error({
|
|
1958
|
+
code: "lifecycle_reopen_failed",
|
|
1959
|
+
message: "the fresh generation was recorded but did not project as live",
|
|
1960
|
+
});
|
|
1961
|
+
process.exitCode = 1;
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1702
1964
|
}
|
|
1703
1965
|
if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
|
|
1704
1966
|
emit.error({
|
|
@@ -1729,6 +1991,22 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1729
1991
|
|
|
1730
1992
|
const priorState: TaskState = hb.task_state ?? "active";
|
|
1731
1993
|
const priorReason = hb.task_state_reason || undefined;
|
|
1994
|
+
if (reopenedGeneration) {
|
|
1995
|
+
emit.data({
|
|
1996
|
+
instance_id: myOwner,
|
|
1997
|
+
task_state: "active",
|
|
1998
|
+
prior_state: null,
|
|
1999
|
+
reason: null,
|
|
2000
|
+
changed: true,
|
|
2001
|
+
generation_reopened: true,
|
|
2002
|
+
prior_generation_id: reopenedGeneration.prior_generation_id,
|
|
2003
|
+
generation_id: reopenedGeneration.generation_id,
|
|
2004
|
+
name_reminted: false,
|
|
2005
|
+
suggested_session_name: null,
|
|
2006
|
+
git_finalization_checked: false,
|
|
2007
|
+
});
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
1732
2010
|
if (priorState === state && priorReason === reason) {
|
|
1733
2011
|
emit.data({
|
|
1734
2012
|
instance_id: myOwner,
|
|
@@ -1743,7 +2021,11 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1743
2021
|
|
|
1744
2022
|
let finalization: GitFinalizationResult | null = null;
|
|
1745
2023
|
if (state === "done") {
|
|
1746
|
-
const history = readSessionWriteClaims(
|
|
2024
|
+
const history = readSessionWriteClaims(
|
|
2025
|
+
root,
|
|
2026
|
+
hb.instance_id,
|
|
2027
|
+
nativeSessionIdentity(hb, myOwner),
|
|
2028
|
+
);
|
|
1747
2029
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1748
2030
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1749
2031
|
claimHistoryComplete: history.complete,
|
|
@@ -1761,19 +2043,16 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1761
2043
|
}
|
|
1762
2044
|
}
|
|
1763
2045
|
|
|
1764
|
-
const suggestedName = buildLifecycleSuggestedName(hb.
|
|
2046
|
+
const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
|
|
1765
2047
|
const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
|
|
1766
|
-
const emitted =
|
|
1767
|
-
type: "state.task_state",
|
|
2048
|
+
const emitted = emitEventV3({
|
|
1768
2049
|
owner: myOwner,
|
|
1769
|
-
session: hb
|
|
2050
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1770
2051
|
adapter: normalizeAdapter(hb.platform),
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
name_reminted: nameReminted,
|
|
1776
|
-
git_finalization_checked: finalization !== null,
|
|
2052
|
+
observation: {
|
|
2053
|
+
event_type: "coord.lifecycle_changed",
|
|
2054
|
+
new_state: state,
|
|
2055
|
+
...(reason ? { reason } : {}),
|
|
1777
2056
|
...(nameReminted ? { suggested_session_name: suggestedName } : {}),
|
|
1778
2057
|
},
|
|
1779
2058
|
});
|
|
@@ -1797,7 +2076,7 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1797
2076
|
git_finalization_checked: finalization !== null,
|
|
1798
2077
|
...(nameReminted
|
|
1799
2078
|
? {
|
|
1800
|
-
note:
|
|
2079
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1801
2080
|
}
|
|
1802
2081
|
: {}),
|
|
1803
2082
|
});
|
|
@@ -1829,7 +2108,7 @@ function runSuggestName(
|
|
|
1829
2108
|
process.exit(1);
|
|
1830
2109
|
}
|
|
1831
2110
|
|
|
1832
|
-
const hb =
|
|
2111
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1833
2112
|
const agentName = hb?.name || "unknown";
|
|
1834
2113
|
// Description resolution: an explicit arg wins; with no arg, fall back to the
|
|
1835
2114
|
// agent's current declared task, so a bare `suggest-name` reprints the running
|
|
@@ -1867,7 +2146,12 @@ function runSuggestName(
|
|
|
1867
2146
|
process.stdout.write(`${suggestedName}\n`); // lint-ok-emission: chat-paste path
|
|
1868
2147
|
}
|
|
1869
2148
|
|
|
1870
|
-
function runStatus(opts: {
|
|
2149
|
+
function runStatus(opts: {
|
|
2150
|
+
endTurn?: boolean;
|
|
2151
|
+
endSession?: boolean;
|
|
2152
|
+
json?: boolean;
|
|
2153
|
+
sessionId?: string;
|
|
2154
|
+
}): void {
|
|
1871
2155
|
const root = monorepoRoot();
|
|
1872
2156
|
if (!root) {
|
|
1873
2157
|
emit.error({
|
|
@@ -1876,6 +2160,14 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1876
2160
|
});
|
|
1877
2161
|
process.exit(1);
|
|
1878
2162
|
}
|
|
2163
|
+
if (opts.endSession && !opts.endTurn) {
|
|
2164
|
+
emit.error({
|
|
2165
|
+
code: "end_session_requires_end_turn",
|
|
2166
|
+
message: "--end-session requires --end-turn so Git finalization is verified first",
|
|
2167
|
+
});
|
|
2168
|
+
process.exitCode = 1;
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
1879
2171
|
|
|
1880
2172
|
// Identity resolution: prefer explicit --session-id (hook-friendly), fall
|
|
1881
2173
|
// back to ppid walk for interactive shell usage.
|
|
@@ -1890,18 +2182,30 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1890
2182
|
process.exit(1);
|
|
1891
2183
|
}
|
|
1892
2184
|
|
|
1893
|
-
const hb =
|
|
2185
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1894
2186
|
if (!hb) {
|
|
1895
2187
|
emit.error({
|
|
1896
|
-
code: "
|
|
1897
|
-
message:
|
|
2188
|
+
code: "no_live_generation",
|
|
2189
|
+
message: noLiveGenerationMessage(myOwner),
|
|
1898
2190
|
});
|
|
1899
2191
|
process.exit(1);
|
|
1900
2192
|
}
|
|
1901
2193
|
|
|
2194
|
+
// Report-only run quality: cheap config/cursor due check first, then a
|
|
2195
|
+
// non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
|
|
2196
|
+
const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
|
|
2197
|
+
const quality = qualityForStatus(
|
|
2198
|
+
qualityEvaluation.config.requested_mode,
|
|
2199
|
+
qualityEvaluation.snapshot,
|
|
2200
|
+
);
|
|
2201
|
+
|
|
1902
2202
|
let finalization: GitFinalizationResult | null = null;
|
|
1903
2203
|
if (opts.endTurn) {
|
|
1904
|
-
const history = readSessionWriteClaims(
|
|
2204
|
+
const history = readSessionWriteClaims(
|
|
2205
|
+
root,
|
|
2206
|
+
hb.instance_id,
|
|
2207
|
+
nativeSessionIdentity(hb, myOwner),
|
|
2208
|
+
);
|
|
1905
2209
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1906
2210
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1907
2211
|
claimHistoryComplete: history.complete,
|
|
@@ -1916,48 +2220,60 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1916
2220
|
}
|
|
1917
2221
|
}
|
|
1918
2222
|
|
|
1919
|
-
|
|
1920
|
-
type: "state.status_checked",
|
|
2223
|
+
emitEventV3({
|
|
1921
2224
|
owner: myOwner,
|
|
1922
|
-
session: hb
|
|
2225
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1923
2226
|
adapter: normalizeAdapter(hb.platform),
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
agent_count: 0, // computed below, not yet available here; Phase 5 verdict reads owner-scope only
|
|
1928
|
-
included_self: true,
|
|
2227
|
+
observation: {
|
|
2228
|
+
event_type: "coord.status_observed",
|
|
2229
|
+
status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
|
|
1929
2230
|
},
|
|
1930
2231
|
});
|
|
1931
2232
|
|
|
1932
|
-
|
|
2233
|
+
let sessionEnd:
|
|
2234
|
+
| { state: "queued" | "already_requested"; request_id: string }
|
|
2235
|
+
| { state: "recorded" | "already_ended"; terminal_event_id?: string }
|
|
2236
|
+
| undefined;
|
|
2237
|
+
if (opts.endSession) {
|
|
2238
|
+
const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
|
|
2239
|
+
const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(
|
|
2240
|
+
({ state }) => state.instance_id === canonicalInstanceId,
|
|
2241
|
+
);
|
|
2242
|
+
if (records.length !== 1 || !records[0]) {
|
|
2243
|
+
emit.error({
|
|
2244
|
+
code: "session_identity_ambiguous",
|
|
2245
|
+
message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
|
|
2246
|
+
});
|
|
2247
|
+
process.exitCode = 1;
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
const state = records[0].state;
|
|
2251
|
+
const requested = requestSessionEndExplicitV3({
|
|
2252
|
+
coordRoot: root,
|
|
2253
|
+
instance_id: state.instance_id,
|
|
2254
|
+
generation_id: state.generation_id,
|
|
2255
|
+
outcome: "succeeded",
|
|
2256
|
+
coordination_finalized: true,
|
|
2257
|
+
});
|
|
2258
|
+
if (requested.state === "queued" || requested.state === "already_requested") {
|
|
2259
|
+
sessionEnd = { state: requested.state, request_id: requested.request.request_id };
|
|
2260
|
+
} else if (requested.state === "recorded") {
|
|
2261
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
|
|
2262
|
+
} else if (requested.state === "already_ended") {
|
|
2263
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
|
|
2264
|
+
} else {
|
|
2265
|
+
emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
|
|
2266
|
+
process.exitCode = 1;
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1933
2272
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1934
2273
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1935
2274
|
: 0;
|
|
1936
2275
|
|
|
1937
|
-
const
|
|
1938
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1939
|
-
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1940
|
-
const livePeers: Heartbeat[] = [];
|
|
1941
|
-
let peersStale = 0;
|
|
1942
|
-
if (existsSync(activeDir)) {
|
|
1943
|
-
for (const file of readdirSync(activeDir)) {
|
|
1944
|
-
if (!file.endsWith(".json")) continue;
|
|
1945
|
-
try {
|
|
1946
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
1947
|
-
const peer = JSON.parse(raw) as Heartbeat;
|
|
1948
|
-
if (!peer || typeof peer.instance_id !== "string") continue;
|
|
1949
|
-
if (peer.instance_id === myOwner) continue;
|
|
1950
|
-
const ts = Date.parse(peer.last_heartbeat);
|
|
1951
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) {
|
|
1952
|
-
livePeers.push(peer);
|
|
1953
|
-
} else {
|
|
1954
|
-
peersStale++;
|
|
1955
|
-
}
|
|
1956
|
-
} catch {
|
|
1957
|
-
// skip malformed
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
2276
|
+
const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
|
|
1961
2277
|
|
|
1962
2278
|
// Sort: file-holders first (by file count desc), then idle peers by recency.
|
|
1963
2279
|
livePeers.sort((a, b) => {
|
|
@@ -1977,7 +2293,7 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1977
2293
|
const remoteMachines = readRemoteMachines(root);
|
|
1978
2294
|
const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
|
|
1979
2295
|
|
|
1980
|
-
const ctxUsage = readContextUsage(hb.session_id, hb.platform);
|
|
2296
|
+
const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
|
|
1981
2297
|
let ctxStr: string;
|
|
1982
2298
|
if (!ctxUsage) {
|
|
1983
2299
|
ctxStr = "unavailable";
|
|
@@ -2014,6 +2330,7 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2014
2330
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
2015
2331
|
activity_source: hb.activity_source ?? null,
|
|
2016
2332
|
task_state: taskStateOf(hb),
|
|
2333
|
+
task_state_scope: "current" as const,
|
|
2017
2334
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
2018
2335
|
task_state_reason: hb.task_state_reason ?? null,
|
|
2019
2336
|
files_held: filesHeld,
|
|
@@ -2040,7 +2357,9 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2040
2357
|
context_window: ctxUsage?.window ?? null,
|
|
2041
2358
|
timestamp_iso: new Date().toISOString(),
|
|
2042
2359
|
timestamp_local: timeStr,
|
|
2360
|
+
...(quality ? { quality } : {}),
|
|
2043
2361
|
...(finalization ? { finalization } : {}),
|
|
2362
|
+
...(sessionEnd ? { session_end: sessionEnd } : {}),
|
|
2044
2363
|
};
|
|
2045
2364
|
|
|
2046
2365
|
if (opts.json) {
|
|
@@ -2058,10 +2377,7 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2058
2377
|
["peers", peersStr],
|
|
2059
2378
|
["time", timeStr],
|
|
2060
2379
|
];
|
|
2061
|
-
//
|
|
2062
|
-
if (hb.turn_summary && hb.turn_summary.length > 0) {
|
|
2063
|
-
rows.splice(1, 0, ["last turn", hb.turn_summary]);
|
|
2064
|
-
}
|
|
2380
|
+
// Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
|
|
2065
2381
|
if (hb.task && hb.task.length > 0) {
|
|
2066
2382
|
rows.splice(1, 0, ["task", hb.task]);
|
|
2067
2383
|
}
|
|
@@ -2076,11 +2392,59 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2076
2392
|
: `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
|
|
2077
2393
|
rows.splice(idx, 0, ["council", summary]);
|
|
2078
2394
|
}
|
|
2395
|
+
if (quality) {
|
|
2396
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
2397
|
+
rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
|
|
2398
|
+
}
|
|
2399
|
+
if (sessionEnd) {
|
|
2400
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
2401
|
+
rows.splice(idx, 0, [
|
|
2402
|
+
"session end",
|
|
2403
|
+
sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
|
|
2404
|
+
? "queued after this turn"
|
|
2405
|
+
: "recorded",
|
|
2406
|
+
]);
|
|
2407
|
+
}
|
|
2079
2408
|
// Box rendering needs predictable stdout regardless of TTY/pipe detection:
|
|
2080
2409
|
// agent runs this via Bash (no TTY) and pastes captured stdout into chat.
|
|
2081
2410
|
process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
|
|
2082
2411
|
}
|
|
2083
2412
|
|
|
2413
|
+
function qualityForStatus(
|
|
2414
|
+
mode: "off" | "shadow" | "report",
|
|
2415
|
+
snapshot: RunQualitySnapshot | null,
|
|
2416
|
+
): {
|
|
2417
|
+
status: RunQualityStatus;
|
|
2418
|
+
signal_ids: string[];
|
|
2419
|
+
evaluated_at: string | null;
|
|
2420
|
+
fresh: boolean;
|
|
2421
|
+
} | null {
|
|
2422
|
+
if (mode !== "report") return null;
|
|
2423
|
+
const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
|
|
2424
|
+
if (!snapshot || !fresh) {
|
|
2425
|
+
return {
|
|
2426
|
+
status: "unknown",
|
|
2427
|
+
signal_ids: [],
|
|
2428
|
+
evaluated_at: snapshot?.evaluated_at ?? null,
|
|
2429
|
+
fresh: false,
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
return {
|
|
2433
|
+
status: snapshot.status,
|
|
2434
|
+
signal_ids: snapshot.signals
|
|
2435
|
+
.filter((signal) => signal.state === "active" && signal.severity !== "none")
|
|
2436
|
+
.map((signal) => signal.id)
|
|
2437
|
+
.sort()
|
|
2438
|
+
.slice(0, 3),
|
|
2439
|
+
evaluated_at: snapshot.evaluated_at,
|
|
2440
|
+
fresh: true,
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function formatRunQuality(status: RunQualityStatus, signalIds: string[]): string {
|
|
2445
|
+
return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2084
2448
|
function formatList(items: string[], cap: number, emptyLabel: string): string {
|
|
2085
2449
|
if (items.length === 0) return emptyLabel;
|
|
2086
2450
|
if (items.length <= cap) return items.join(", ");
|
|
@@ -2089,7 +2453,7 @@ function formatList(items: string[], cap: number, emptyLabel: string): string {
|
|
|
2089
2453
|
}
|
|
2090
2454
|
|
|
2091
2455
|
function formatPeers(
|
|
2092
|
-
peers:
|
|
2456
|
+
peers: StatusPeerHeartbeat[],
|
|
2093
2457
|
cap: number,
|
|
2094
2458
|
staleCount: number,
|
|
2095
2459
|
remoteMachines: RemoteMachine[] = [],
|
|
@@ -2122,6 +2486,38 @@ function formatPeers(
|
|
|
2122
2486
|
return main;
|
|
2123
2487
|
}
|
|
2124
2488
|
|
|
2489
|
+
type StatusPeerHeartbeat = {
|
|
2490
|
+
instance_id: string;
|
|
2491
|
+
last_heartbeat: string;
|
|
2492
|
+
files_touched?: string[];
|
|
2493
|
+
name?: string;
|
|
2494
|
+
platform?: string;
|
|
2495
|
+
};
|
|
2496
|
+
|
|
2497
|
+
export function collectStatusPeerHealth(
|
|
2498
|
+
root: string,
|
|
2499
|
+
myOwner: string,
|
|
2500
|
+
nowMs = Date.now(),
|
|
2501
|
+
): { livePeers: StatusPeerHeartbeat[]; stale: number } {
|
|
2502
|
+
let rows: StatusPeerHeartbeat[] = [];
|
|
2503
|
+
try {
|
|
2504
|
+
rows = readLiveCoordinationRows(root);
|
|
2505
|
+
} catch {
|
|
2506
|
+
// V3 authority failures must not resurrect disposable cache rows.
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
|
|
2510
|
+
const livePeers: StatusPeerHeartbeat[] = [];
|
|
2511
|
+
let stale = 0;
|
|
2512
|
+
for (const peer of rows) {
|
|
2513
|
+
if (peer.instance_id === myOwner) continue;
|
|
2514
|
+
const observedAtMs = Date.parse(peer.last_heartbeat);
|
|
2515
|
+
if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs) livePeers.push(peer);
|
|
2516
|
+
else stale += 1;
|
|
2517
|
+
}
|
|
2518
|
+
return { livePeers, stale };
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2125
2521
|
function fmtTokens(n: number): string {
|
|
2126
2522
|
if (n < 1000) return `${n}`;
|
|
2127
2523
|
if (n < 1000000) return `${Math.round(n / 1000)}K`;
|
|
@@ -2371,46 +2767,69 @@ interface HealEvent {
|
|
|
2371
2767
|
platform: string;
|
|
2372
2768
|
}
|
|
2373
2769
|
|
|
2374
|
-
/**
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
*/
|
|
2378
|
-
interface CanonicalEvent {
|
|
2379
|
-
event_type: string;
|
|
2770
|
+
/** Bounded diagnostic event shape projected from the canonical V3 ledger. */
|
|
2771
|
+
export interface CanonicalEvent {
|
|
2772
|
+
event_type: EventTypeV3;
|
|
2380
2773
|
ts: string;
|
|
2381
2774
|
instance_id?: string;
|
|
2382
2775
|
adapter?: string;
|
|
2383
|
-
|
|
2776
|
+
payload?: Record<string, unknown>;
|
|
2384
2777
|
}
|
|
2385
2778
|
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2779
|
+
export interface AgentDiagnosticEventRead {
|
|
2780
|
+
source: "v3";
|
|
2781
|
+
authoritative: boolean;
|
|
2782
|
+
reason?: string;
|
|
2783
|
+
truncated: boolean;
|
|
2784
|
+
bytes: number;
|
|
2785
|
+
events: CanonicalEvent[];
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
/** Read validated canonical V3 events. */
|
|
2789
|
+
export function readAgentDiagnosticEventsInWindow(
|
|
2790
|
+
root: string,
|
|
2791
|
+
cutoffMs: number,
|
|
2792
|
+
): AgentDiagnosticEventRead {
|
|
2793
|
+
const control = readEventV3ControlState(root);
|
|
2794
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
2795
|
+
const ledger = readLedgerV3(root);
|
|
2796
|
+
if (!ledger.complete) {
|
|
2797
|
+
return {
|
|
2798
|
+
source: "v3",
|
|
2799
|
+
authoritative: false,
|
|
2800
|
+
reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
|
|
2801
|
+
truncated: false,
|
|
2802
|
+
bytes: ledger.bytes,
|
|
2803
|
+
events: [],
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
const events: CanonicalEvent[] = [];
|
|
2807
|
+
for (const { event } of ledger.events) {
|
|
2808
|
+
const tsMs = Date.parse(event.time.recorded_at);
|
|
2407
2809
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs) continue;
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2810
|
+
events.push({
|
|
2811
|
+
event_type: event.event_type,
|
|
2812
|
+
ts: event.time.recorded_at,
|
|
2813
|
+
instance_id: event.scope.instance_id,
|
|
2814
|
+
payload: event.payload as Record<string, unknown>,
|
|
2815
|
+
});
|
|
2411
2816
|
}
|
|
2817
|
+
return {
|
|
2818
|
+
source: "v3",
|
|
2819
|
+
authoritative: true,
|
|
2820
|
+
truncated: false,
|
|
2821
|
+
bytes: ledger.bytes,
|
|
2822
|
+
events,
|
|
2823
|
+
};
|
|
2412
2824
|
}
|
|
2413
|
-
return
|
|
2825
|
+
return {
|
|
2826
|
+
source: "v3",
|
|
2827
|
+
authoritative: false,
|
|
2828
|
+
reason: `V3 control state is ${control.state}`,
|
|
2829
|
+
truncated: false,
|
|
2830
|
+
bytes: 0,
|
|
2831
|
+
events: [],
|
|
2832
|
+
};
|
|
2414
2833
|
}
|
|
2415
2834
|
|
|
2416
2835
|
/** Normalize adapter event data into the heartbeat platform value. */
|
|
@@ -2418,19 +2837,21 @@ function adapterToPlatform(adapter: string | undefined): string {
|
|
|
2418
2837
|
if (adapter === "claude-code") return "claude-code";
|
|
2419
2838
|
if (adapter === "cursor") return "cursor";
|
|
2420
2839
|
if (adapter === "codex") return "codex";
|
|
2421
|
-
return "
|
|
2840
|
+
return "unknown";
|
|
2422
2841
|
}
|
|
2423
2842
|
|
|
2424
2843
|
/** Project a canonical health.* event into the HealEvent shape the aggregators
|
|
2425
2844
|
* already consume. Returns null for non-heal events. instance_id → display name
|
|
2426
2845
|
* via `nameById` (full-UUID keyed). */
|
|
2427
2846
|
function canonicalToHealEvent(ev: CanonicalEvent, nameById: Map<string, string>): HealEvent | null {
|
|
2428
|
-
if (ev.event_type !== "health.
|
|
2847
|
+
if (ev.event_type !== "health.observed") {
|
|
2429
2848
|
return null;
|
|
2430
2849
|
}
|
|
2431
|
-
const
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2850
|
+
const data = ev.payload ?? {};
|
|
2851
|
+
const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
|
|
2852
|
+
if (subsystem !== "pidmap" && subsystem !== "heartbeat") return null;
|
|
2853
|
+
const kind = subsystem;
|
|
2854
|
+
const reason: "missing" | "stale" = data.condition === "stale" ? "stale" : "missing";
|
|
2434
2855
|
const instanceId = ev.instance_id ?? "";
|
|
2435
2856
|
const name = nameById.get(instanceId);
|
|
2436
2857
|
const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
|
|
@@ -2511,13 +2932,12 @@ function runHealEvents(opts: {
|
|
|
2511
2932
|
process.exit(1);
|
|
2512
2933
|
}
|
|
2513
2934
|
|
|
2514
|
-
// Heal telemetry
|
|
2515
|
-
// (health.pidmap_heal / health.heartbeat_heal), emitted by the writer on
|
|
2516
|
-
// actual self-heal writes.
|
|
2935
|
+
// Heal telemetry is read from the canonical V3 ledger.
|
|
2517
2936
|
const cutoffMs = Date.now() - sinceSecs * 1000;
|
|
2518
2937
|
const nameById = buildNameById(root);
|
|
2519
2938
|
const events: HealEvent[] = [];
|
|
2520
|
-
|
|
2939
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
2940
|
+
for (const ev of diagnosticEvents.events) {
|
|
2521
2941
|
const heal = canonicalToHealEvent(ev, nameById);
|
|
2522
2942
|
if (heal) events.push(heal);
|
|
2523
2943
|
}
|
|
@@ -2554,6 +2974,11 @@ function runHealEvents(opts: {
|
|
|
2554
2974
|
|
|
2555
2975
|
const data = {
|
|
2556
2976
|
since: opts.since,
|
|
2977
|
+
telemetry: {
|
|
2978
|
+
source: diagnosticEvents.source,
|
|
2979
|
+
authoritative: diagnosticEvents.authoritative,
|
|
2980
|
+
reason: diagnosticEvents.reason,
|
|
2981
|
+
},
|
|
2557
2982
|
total: events.length,
|
|
2558
2983
|
by_reason: byReason,
|
|
2559
2984
|
by_kind: byKind,
|
|
@@ -2565,7 +2990,17 @@ function runHealEvents(opts: {
|
|
|
2565
2990
|
|
|
2566
2991
|
if (opts.csv) {
|
|
2567
2992
|
emit.config({ format: "csv" });
|
|
2568
|
-
emit.data(
|
|
2993
|
+
emit.data(
|
|
2994
|
+
data.telemetry.authoritative
|
|
2995
|
+
? recent
|
|
2996
|
+
: [
|
|
2997
|
+
{
|
|
2998
|
+
status: "unavailable",
|
|
2999
|
+
source: data.telemetry.source,
|
|
3000
|
+
reason: data.telemetry.reason,
|
|
3001
|
+
},
|
|
3002
|
+
],
|
|
3003
|
+
);
|
|
2569
3004
|
return;
|
|
2570
3005
|
}
|
|
2571
3006
|
if (opts.json) {
|
|
@@ -2580,6 +3015,13 @@ function runHealEvents(opts: {
|
|
|
2580
3015
|
lines.push(
|
|
2581
3016
|
`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`,
|
|
2582
3017
|
);
|
|
3018
|
+
if (!data.telemetry.authoritative) {
|
|
3019
|
+
lines.push(
|
|
3020
|
+
` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`,
|
|
3021
|
+
);
|
|
3022
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
2583
3025
|
lines.push("");
|
|
2584
3026
|
if (events.length === 0) {
|
|
2585
3027
|
lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
|
|
@@ -2631,7 +3073,13 @@ function runHealEvents(opts: {
|
|
|
2631
3073
|
interface HealthReport {
|
|
2632
3074
|
since: string;
|
|
2633
3075
|
generated_at: string;
|
|
3076
|
+
event_telemetry: {
|
|
3077
|
+
source: "v3";
|
|
3078
|
+
authoritative: boolean;
|
|
3079
|
+
reason?: string;
|
|
3080
|
+
};
|
|
2634
3081
|
active_agents: {
|
|
3082
|
+
source: "event-ledger-v3";
|
|
2635
3083
|
total: number;
|
|
2636
3084
|
by_platform: Record<string, number>;
|
|
2637
3085
|
by_kind: Record<string, number>;
|
|
@@ -2661,11 +3109,19 @@ interface HealthReport {
|
|
|
2661
3109
|
// bug (e.g. a stop-projection crash that caused ~200 errors/day until it was fixed).
|
|
2662
3110
|
hook_errors: {
|
|
2663
3111
|
total: number;
|
|
3112
|
+
last_1h: number;
|
|
3113
|
+
latest_at: string | null;
|
|
2664
3114
|
by_phase: Record<string, number>;
|
|
3115
|
+
by_error: Record<string, number>;
|
|
2665
3116
|
top: Array<{ phase: string; count: number; sample: string }>;
|
|
3117
|
+
top_errors: Array<{ error: string; count: number; phase: string }>;
|
|
3118
|
+
recent_top_errors: Array<{ error: string; count: number; phase: string }>;
|
|
2666
3119
|
};
|
|
2667
3120
|
// Canonical event stream growth + drain lag.
|
|
2668
3121
|
stream: {
|
|
3122
|
+
source: "v3";
|
|
3123
|
+
authoritative: boolean;
|
|
3124
|
+
reason?: string;
|
|
2669
3125
|
bytes: number;
|
|
2670
3126
|
lines: number;
|
|
2671
3127
|
cursor_backlog: number;
|
|
@@ -2677,31 +3133,269 @@ interface HealthReport {
|
|
|
2677
3133
|
count: number;
|
|
2678
3134
|
samples: string[];
|
|
2679
3135
|
};
|
|
3136
|
+
// V3 event-ledger producer health: open tool spans, pending finalization
|
|
3137
|
+
// requests, intake/diagnostics spool depth, span-count pressure.
|
|
3138
|
+
event_ledger: EventLedgerHealthV3;
|
|
2680
3139
|
anomalies: string[];
|
|
2681
3140
|
}
|
|
2682
3141
|
|
|
3142
|
+
/** Open-span soft watermark per producer state. The producer-state reader
|
|
3143
|
+
* hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
|
|
3144
|
+
* pressure at half that gives room to act before reads start failing. */
|
|
3145
|
+
const SPAN_PRESSURE_SOFT_WATERMARK = 128;
|
|
3146
|
+
|
|
3147
|
+
export type EventLedgerHealthV3 =
|
|
3148
|
+
| { state: "unavailable"; reason: string }
|
|
3149
|
+
| {
|
|
3150
|
+
state: "live";
|
|
3151
|
+
mode: "candidate" | "active";
|
|
3152
|
+
open_spans: {
|
|
3153
|
+
total: number;
|
|
3154
|
+
generations: Array<{
|
|
3155
|
+
instance_id: string;
|
|
3156
|
+
generation_id: string;
|
|
3157
|
+
adapter: string;
|
|
3158
|
+
span_count: number;
|
|
3159
|
+
/** False = spans are open with NO open turn: the orphan signature
|
|
3160
|
+
* (a turn ended without its tool spans being closed). */
|
|
3161
|
+
turn_open: boolean;
|
|
3162
|
+
}>;
|
|
3163
|
+
};
|
|
3164
|
+
pending_finalizations: Array<{
|
|
3165
|
+
request_id: string;
|
|
3166
|
+
trigger: string;
|
|
3167
|
+
generation_id: string;
|
|
3168
|
+
age_ms: number;
|
|
3169
|
+
allowed_open_span_count: number;
|
|
3170
|
+
}>;
|
|
3171
|
+
intake_spool: {
|
|
3172
|
+
total: number;
|
|
3173
|
+
groups: Array<{ adapter: string; session_hash: string; count: number }>;
|
|
3174
|
+
};
|
|
3175
|
+
diagnostics_spool: {
|
|
3176
|
+
total: number;
|
|
3177
|
+
last_24h: number;
|
|
3178
|
+
last_1h: number;
|
|
3179
|
+
latest_at: string | null;
|
|
3180
|
+
by_category: Record<string, { total: number; last_24h: number }>;
|
|
3181
|
+
recent_by_category: Record<string, { last_1h: number; latest_at: string | null }>;
|
|
3182
|
+
};
|
|
3183
|
+
span_pressure: Array<{ instance_id: string; generation_id: string; span_count: number }>;
|
|
3184
|
+
collection_errors: string[];
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
function errorText(error: unknown): string {
|
|
3188
|
+
return error instanceof Error ? error.message : String(error);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
/**
|
|
3192
|
+
* Read-only health counters for the V3 event ledger's producer surfaces. Never
|
|
3193
|
+
* mutates ledger state (no control repair, no spool drain) and never throws:
|
|
3194
|
+
* a non-live route returns `{ state: "unavailable" }`, and each sub-surface
|
|
3195
|
+
* that fails to read lands in `collection_errors` instead of aborting the rest.
|
|
3196
|
+
*/
|
|
3197
|
+
export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): EventLedgerHealthV3 {
|
|
3198
|
+
let control: ReturnType<typeof readEventV3ControlState>;
|
|
3199
|
+
try {
|
|
3200
|
+
control = readEventV3ControlState(root);
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
|
|
3203
|
+
}
|
|
3204
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
3205
|
+
return {
|
|
3206
|
+
state: "unavailable",
|
|
3207
|
+
reason: `${control.state}: ${control.reason}`,
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
const collectionErrors: string[] = [];
|
|
3212
|
+
|
|
3213
|
+
// 1) Open tool spans per live (non-terminal) generation + span-count pressure.
|
|
3214
|
+
const generations: Extract<EventLedgerHealthV3, { state: "live" }>["open_spans"]["generations"] =
|
|
3215
|
+
[];
|
|
3216
|
+
const spanPressure: Extract<EventLedgerHealthV3, { state: "live" }>["span_pressure"] = [];
|
|
3217
|
+
try {
|
|
3218
|
+
for (const { state } of listHookProducerStateRecordsV3(root)) {
|
|
3219
|
+
if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
|
|
3220
|
+
spanPressure.push({
|
|
3221
|
+
instance_id: state.instance_id,
|
|
3222
|
+
generation_id: state.generation_id,
|
|
3223
|
+
span_count: state.spans.length,
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
if (state.spans.length === 0) continue;
|
|
3227
|
+
generations.push({
|
|
3228
|
+
instance_id: state.instance_id,
|
|
3229
|
+
generation_id: state.generation_id,
|
|
3230
|
+
adapter: state.adapter,
|
|
3231
|
+
span_count: state.spans.length,
|
|
3232
|
+
turn_open: Boolean(state.current_turn_id),
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
} catch (error) {
|
|
3236
|
+
collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// 2) Pending finalization requests with age + trigger.
|
|
3240
|
+
const pending: Extract<EventLedgerHealthV3, { state: "live" }>["pending_finalizations"] = [];
|
|
3241
|
+
try {
|
|
3242
|
+
for (const request of listSessionFinalizationRequestsV3(root)) {
|
|
3243
|
+
if (request.status !== "pending") continue;
|
|
3244
|
+
const observedMs = Date.parse(request.observed_at);
|
|
3245
|
+
pending.push({
|
|
3246
|
+
request_id: request.request_id,
|
|
3247
|
+
trigger: request.trigger,
|
|
3248
|
+
generation_id: request.generation_id,
|
|
3249
|
+
age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
|
|
3250
|
+
allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
|
|
3258
|
+
let intakeTotal = 0;
|
|
3259
|
+
const intakeGroups: Extract<EventLedgerHealthV3, { state: "live" }>["intake_spool"]["groups"] =
|
|
3260
|
+
[];
|
|
3261
|
+
try {
|
|
3262
|
+
for (const group of listHookIntakeGroupsV3(root)) {
|
|
3263
|
+
const count = listHookIntakeRecordsV3(group.directory).length;
|
|
3264
|
+
if (count === 0) continue;
|
|
3265
|
+
intakeTotal += count;
|
|
3266
|
+
intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
|
|
3267
|
+
}
|
|
3268
|
+
} catch (error) {
|
|
3269
|
+
collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// 4) Diagnostics spool counts by category. Filenames only (never the
|
|
3273
|
+
// contents): `<category>-<orderkey>.json` where the orderkey leads with a
|
|
3274
|
+
// zero-padded epoch-ms, which also gives the last-24h split for free.
|
|
3275
|
+
const byCategory: Record<string, { total: number; last_24h: number }> = {};
|
|
3276
|
+
const recentByCategory: Extract<
|
|
3277
|
+
EventLedgerHealthV3,
|
|
3278
|
+
{ state: "live" }
|
|
3279
|
+
>["diagnostics_spool"]["recent_by_category"] = {};
|
|
3280
|
+
let diagnosticsTotal = 0;
|
|
3281
|
+
let diagnostics24h = 0;
|
|
3282
|
+
let diagnostics1h = 0;
|
|
3283
|
+
let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
|
|
3284
|
+
try {
|
|
3285
|
+
const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
|
|
3286
|
+
if (existsSync(diagnosticsDir)) {
|
|
3287
|
+
const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
|
|
3288
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
3289
|
+
for (const name of readdirSync(diagnosticsDir)) {
|
|
3290
|
+
const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
|
|
3291
|
+
const category = match?.[1];
|
|
3292
|
+
const epochMs = Number(match?.[2]);
|
|
3293
|
+
if (!category) continue;
|
|
3294
|
+
const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
|
|
3295
|
+
const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
|
|
3296
|
+
byCategory[category] = entry;
|
|
3297
|
+
recentByCategory[category] = recent;
|
|
3298
|
+
entry.total += 1;
|
|
3299
|
+
diagnosticsTotal += 1;
|
|
3300
|
+
if (Number.isFinite(epochMs)) {
|
|
3301
|
+
const recordedAt = new Date(epochMs).toISOString();
|
|
3302
|
+
if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
|
|
3303
|
+
recent.latest_at = recordedAt;
|
|
3304
|
+
}
|
|
3305
|
+
latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
|
|
3306
|
+
if (epochMs >= dayAgoMs) {
|
|
3307
|
+
entry.last_24h += 1;
|
|
3308
|
+
diagnostics24h += 1;
|
|
3309
|
+
}
|
|
3310
|
+
if (epochMs >= hourAgoMs) {
|
|
3311
|
+
recent.last_1h += 1;
|
|
3312
|
+
diagnostics1h += 1;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
} catch (error) {
|
|
3318
|
+
collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
return {
|
|
3322
|
+
state: "live",
|
|
3323
|
+
mode: control.state,
|
|
3324
|
+
open_spans: {
|
|
3325
|
+
total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
|
|
3326
|
+
generations,
|
|
3327
|
+
},
|
|
3328
|
+
pending_finalizations: pending,
|
|
3329
|
+
intake_spool: { total: intakeTotal, groups: intakeGroups },
|
|
3330
|
+
diagnostics_spool: {
|
|
3331
|
+
total: diagnosticsTotal,
|
|
3332
|
+
last_24h: diagnostics24h,
|
|
3333
|
+
last_1h: diagnostics1h,
|
|
3334
|
+
latest_at: Number.isFinite(latestDiagnosticMs)
|
|
3335
|
+
? new Date(latestDiagnosticMs).toISOString()
|
|
3336
|
+
: null,
|
|
3337
|
+
by_category: byCategory,
|
|
3338
|
+
recent_by_category: recentByCategory,
|
|
3339
|
+
},
|
|
3340
|
+
span_pressure: spanPressure,
|
|
3341
|
+
collection_errors: collectionErrors,
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
|
|
2683
3345
|
/** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
|
|
2684
|
-
* window
|
|
2685
|
-
*
|
|
2686
|
-
function readHookErrors(
|
|
3346
|
+
* window by exact error and phase, while separating current-hour failures from
|
|
3347
|
+
* historical rows. Each line is {ts, error, phase, ...}. */
|
|
3348
|
+
export function readHookErrors(
|
|
2687
3349
|
root: string,
|
|
2688
3350
|
cutoffMs: number,
|
|
3351
|
+
nowMs = Date.now(),
|
|
2689
3352
|
): {
|
|
2690
3353
|
total: number;
|
|
3354
|
+
last1h: number;
|
|
3355
|
+
latestAt: string | null;
|
|
2691
3356
|
byPhase: Record<string, number>;
|
|
3357
|
+
byError: Record<string, number>;
|
|
2692
3358
|
top: Array<{ phase: string; count: number; sample: string }>;
|
|
3359
|
+
topErrors: Array<{ error: string; count: number; phase: string }>;
|
|
3360
|
+
recentTopErrors: Array<{ error: string; count: number; phase: string }>;
|
|
2693
3361
|
} {
|
|
2694
3362
|
const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
|
|
2695
3363
|
const byPhase: Record<string, number> = {};
|
|
3364
|
+
const byError: Record<string, number> = {};
|
|
2696
3365
|
const sampleByPhase: Record<string, string> = {};
|
|
3366
|
+
const errorPhaseCounts = new Map<string, { error: string; phase: string; count: number }>();
|
|
3367
|
+
const recentErrorPhaseCounts = new Map<string, { error: string; phase: string; count: number }>();
|
|
2697
3368
|
let total = 0;
|
|
2698
|
-
|
|
3369
|
+
let last1h = 0;
|
|
3370
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
3371
|
+
if (!existsSync(p)) {
|
|
3372
|
+
return {
|
|
3373
|
+
total: 0,
|
|
3374
|
+
last1h: 0,
|
|
3375
|
+
latestAt: null,
|
|
3376
|
+
byPhase,
|
|
3377
|
+
byError,
|
|
3378
|
+
top: [],
|
|
3379
|
+
topErrors: [],
|
|
3380
|
+
recentTopErrors: [],
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
2699
3383
|
let raw: string;
|
|
2700
3384
|
try {
|
|
2701
3385
|
raw = readFileSync(p, "utf8");
|
|
2702
3386
|
} catch {
|
|
2703
|
-
return {
|
|
3387
|
+
return {
|
|
3388
|
+
total: 0,
|
|
3389
|
+
last1h: 0,
|
|
3390
|
+
latestAt: null,
|
|
3391
|
+
byPhase,
|
|
3392
|
+
byError,
|
|
3393
|
+
top: [],
|
|
3394
|
+
topErrors: [],
|
|
3395
|
+
recentTopErrors: [],
|
|
3396
|
+
};
|
|
2704
3397
|
}
|
|
3398
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2705
3399
|
for (const line of raw.split("\n")) {
|
|
2706
3400
|
if (!line.trim()) continue;
|
|
2707
3401
|
try {
|
|
@@ -2709,8 +3403,21 @@ function readHookErrors(
|
|
|
2709
3403
|
const tsMs = e.ts ? Date.parse(e.ts) : Number.NaN;
|
|
2710
3404
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs) continue;
|
|
2711
3405
|
const phase = e.phase ?? "(unknown)";
|
|
3406
|
+
const error = e.error ?? "(unknown)";
|
|
2712
3407
|
byPhase[phase] = (byPhase[phase] ?? 0) + 1;
|
|
3408
|
+
byError[error] = (byError[error] ?? 0) + 1;
|
|
2713
3409
|
if (!sampleByPhase[phase] && e.error) sampleByPhase[phase] = e.error;
|
|
3410
|
+
const errorPhaseKey = `${error}\0${phase}`;
|
|
3411
|
+
const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
3412
|
+
errorPhase.count += 1;
|
|
3413
|
+
errorPhaseCounts.set(errorPhaseKey, errorPhase);
|
|
3414
|
+
latestMs = Math.max(latestMs, tsMs);
|
|
3415
|
+
if (tsMs >= hourAgoMs) {
|
|
3416
|
+
last1h += 1;
|
|
3417
|
+
const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
3418
|
+
recent.count += 1;
|
|
3419
|
+
recentErrorPhaseCounts.set(errorPhaseKey, recent);
|
|
3420
|
+
}
|
|
2714
3421
|
total++;
|
|
2715
3422
|
} catch {
|
|
2716
3423
|
/* skip malformed */
|
|
@@ -2720,130 +3427,181 @@ function readHookErrors(
|
|
|
2720
3427
|
.sort((a, b) => b[1] - a[1])
|
|
2721
3428
|
.slice(0, 5)
|
|
2722
3429
|
.map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
|
|
2723
|
-
|
|
3430
|
+
const topErrors = [...errorPhaseCounts.values()]
|
|
3431
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
3432
|
+
.slice(0, 5);
|
|
3433
|
+
const recentTopErrors = [...recentErrorPhaseCounts.values()]
|
|
3434
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
3435
|
+
.slice(0, 5);
|
|
3436
|
+
return {
|
|
3437
|
+
total,
|
|
3438
|
+
last1h,
|
|
3439
|
+
latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
|
|
3440
|
+
byPhase,
|
|
3441
|
+
byError,
|
|
3442
|
+
top,
|
|
3443
|
+
topErrors,
|
|
3444
|
+
recentTopErrors,
|
|
3445
|
+
};
|
|
2724
3446
|
}
|
|
2725
3447
|
|
|
2726
3448
|
/** Canonical event stream size + drain lag (events appended after the cursor). */
|
|
2727
|
-
function readStreamStats(root: string):
|
|
2728
|
-
const
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
const cursorPath = resolve(root, ".harnery", ".events-cursor");
|
|
2738
|
-
if (existsSync(cursorPath)) {
|
|
2739
|
-
try {
|
|
2740
|
-
cursor = readFileSync(cursorPath, "utf8").trim() || null;
|
|
2741
|
-
} catch {
|
|
2742
|
-
/* ignore */
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
let lines = 0;
|
|
2746
|
-
let backlog = 0;
|
|
2747
|
-
let seenCursor = cursor === null; // no cursor → everything is "backlog"
|
|
2748
|
-
try {
|
|
2749
|
-
// Bounded tail read: the ledger grows without bound and a whole-file
|
|
2750
|
-
// readFileSync throws past V8's ~512MB string limit. lines/backlog are then
|
|
2751
|
-
// scoped to the scanned window; `bytes` (full size, via statSync above) still
|
|
2752
|
-
// reflects the true stream size for the health rollup.
|
|
2753
|
-
const { text } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
|
|
2754
|
-
for (const line of text.split("\n")) {
|
|
2755
|
-
if (!line.trim()) continue;
|
|
2756
|
-
lines++;
|
|
2757
|
-
if (seenCursor) {
|
|
2758
|
-
backlog++;
|
|
2759
|
-
} else if (cursor && line.includes(`"event_id":"${cursor}"`)) {
|
|
2760
|
-
seenCursor = true;
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
} catch {
|
|
2764
|
-
/* ignore */
|
|
2765
|
-
}
|
|
2766
|
-
return { bytes, lines, cursor_backlog: backlog };
|
|
3449
|
+
function readStreamStats(root: string): HealthReport["stream"] {
|
|
3450
|
+
const read = readAgentDiagnosticEventsInWindow(root, 0);
|
|
3451
|
+
return {
|
|
3452
|
+
source: "v3",
|
|
3453
|
+
authoritative: read.authoritative,
|
|
3454
|
+
...(read.reason ? { reason: read.reason } : {}),
|
|
3455
|
+
bytes: read.bytes,
|
|
3456
|
+
lines: read.events.length,
|
|
3457
|
+
cursor_backlog: 0,
|
|
3458
|
+
};
|
|
2767
3459
|
}
|
|
2768
3460
|
|
|
2769
3461
|
/** One rendered line in a trace. */
|
|
2770
|
-
interface TraceEntry {
|
|
3462
|
+
export interface TraceEntry {
|
|
2771
3463
|
ts: string;
|
|
2772
3464
|
event_type: string;
|
|
2773
3465
|
detail: string;
|
|
2774
3466
|
}
|
|
2775
3467
|
|
|
3468
|
+
export function traceInstanceIdsForEventSource(
|
|
3469
|
+
nativeInstanceIds: readonly string[],
|
|
3470
|
+
source: "v3",
|
|
3471
|
+
): string[] {
|
|
3472
|
+
void source;
|
|
3473
|
+
return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
|
|
3474
|
+
}
|
|
3475
|
+
|
|
2776
3476
|
/** Map a canonical event to a concise trace line, or null to drop it. */
|
|
2777
|
-
function traceLine(ev: CanonicalEvent, allTools: boolean): TraceEntry | null {
|
|
2778
|
-
const d = (ev.
|
|
3477
|
+
export function traceLine(ev: CanonicalEvent, allTools: boolean): TraceEntry | null {
|
|
3478
|
+
const d = (ev.payload ?? {}) as Record<string, unknown>;
|
|
2779
3479
|
const s = (k: string): string => (typeof d[k] === "string" ? (d[k] as string) : "");
|
|
3480
|
+
const object = (value: unknown): Record<string, unknown> =>
|
|
3481
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
3482
|
+
? (value as Record<string, unknown>)
|
|
3483
|
+
: {};
|
|
2780
3484
|
const clip = (v: string, n = 70): string => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
|
|
3485
|
+
const recovery =
|
|
3486
|
+
typeof d.recovery === "object" && d.recovery !== null
|
|
3487
|
+
? (d.recovery as { reason?: unknown })
|
|
3488
|
+
: null;
|
|
3489
|
+
const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
|
|
3490
|
+
const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
|
|
2781
3491
|
let detail = "";
|
|
2782
3492
|
switch (ev.event_type) {
|
|
2783
|
-
case "session.
|
|
2784
|
-
detail =
|
|
3493
|
+
case "session.started":
|
|
3494
|
+
detail = "generation started";
|
|
3495
|
+
break;
|
|
3496
|
+
case "session.resumed":
|
|
3497
|
+
detail = "generation resumed";
|
|
2785
3498
|
break;
|
|
2786
|
-
case "session.
|
|
2787
|
-
detail = `
|
|
3499
|
+
case "session.ended":
|
|
3500
|
+
detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2788
3501
|
break;
|
|
2789
|
-
case "
|
|
2790
|
-
detail =
|
|
3502
|
+
case "session.termination_observed":
|
|
3503
|
+
detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2791
3504
|
break;
|
|
2792
|
-
case "
|
|
2793
|
-
detail = `
|
|
3505
|
+
case "agent.delegated":
|
|
3506
|
+
detail = `delegated child=${s("child_generation_id") || "unknown"}`;
|
|
2794
3507
|
break;
|
|
2795
|
-
case "
|
|
2796
|
-
detail =
|
|
3508
|
+
case "agent.started":
|
|
3509
|
+
detail = `child started=${s("child_generation_id") || "unknown"}`;
|
|
2797
3510
|
break;
|
|
2798
|
-
case "
|
|
2799
|
-
detail = `
|
|
3511
|
+
case "agent.completed":
|
|
3512
|
+
detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
|
|
2800
3513
|
break;
|
|
2801
|
-
case "
|
|
2802
|
-
detail =
|
|
3514
|
+
case "turn.started":
|
|
3515
|
+
detail = `intent=${s("intent_kind") || "unknown"}`;
|
|
2803
3516
|
break;
|
|
2804
|
-
case "
|
|
2805
|
-
detail =
|
|
3517
|
+
case "turn.completed":
|
|
3518
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2806
3519
|
break;
|
|
2807
|
-
case "
|
|
2808
|
-
|
|
3520
|
+
case "tool.requested": {
|
|
3521
|
+
const tool = object(d.tool);
|
|
3522
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
|
|
2809
3523
|
break;
|
|
2810
|
-
|
|
2811
|
-
|
|
3524
|
+
}
|
|
3525
|
+
case "tool.completed": {
|
|
3526
|
+
const tool = object(d.tool);
|
|
3527
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
3528
|
+
break;
|
|
3529
|
+
}
|
|
3530
|
+
case "command.started":
|
|
3531
|
+
if (!allTools) return null;
|
|
3532
|
+
detail = "command started (content omitted)";
|
|
3533
|
+
break;
|
|
3534
|
+
case "command.completed":
|
|
3535
|
+
if (!allTools && !recoveryReason) return null;
|
|
3536
|
+
detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
3537
|
+
break;
|
|
3538
|
+
case "coord.task_changed":
|
|
3539
|
+
case "coord.lifecycle_changed":
|
|
3540
|
+
case "coord.presence_changed":
|
|
3541
|
+
detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
|
|
3542
|
+
break;
|
|
3543
|
+
case "coord.status_observed":
|
|
3544
|
+
detail = s("status") || "status observed";
|
|
3545
|
+
break;
|
|
3546
|
+
case "coord.claim_changed": {
|
|
3547
|
+
const target = object(d.target);
|
|
3548
|
+
detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
|
|
3549
|
+
break;
|
|
3550
|
+
}
|
|
3551
|
+
case "coord.message_observed":
|
|
3552
|
+
detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
|
|
3553
|
+
break;
|
|
3554
|
+
case "coord.identity_attested":
|
|
3555
|
+
detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
|
|
3556
|
+
break;
|
|
3557
|
+
case "wait.started":
|
|
3558
|
+
detail = s("kind") || "wait started";
|
|
3559
|
+
break;
|
|
3560
|
+
case "wait.ended":
|
|
3561
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
3562
|
+
break;
|
|
3563
|
+
case "artifact.observed": {
|
|
3564
|
+
const artifact = object(d.artifact);
|
|
3565
|
+
detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
|
|
2812
3566
|
break;
|
|
2813
|
-
|
|
2814
|
-
|
|
3567
|
+
}
|
|
3568
|
+
case "progress.observed":
|
|
3569
|
+
detail = s("kind") || "progress";
|
|
3570
|
+
break;
|
|
3571
|
+
case "health.observed":
|
|
3572
|
+
detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
|
|
2815
3573
|
break;
|
|
2816
|
-
case "
|
|
2817
|
-
detail = `${s("
|
|
3574
|
+
case "council.state_changed":
|
|
3575
|
+
detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2818
3576
|
break;
|
|
2819
|
-
case "
|
|
2820
|
-
|
|
2821
|
-
case "claim.conflict":
|
|
2822
|
-
detail = clip(s("path"));
|
|
3577
|
+
case "decision.state_changed":
|
|
3578
|
+
detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2823
3579
|
break;
|
|
2824
|
-
case "
|
|
2825
|
-
|
|
2826
|
-
detail = `reason=${s("reason")}`;
|
|
3580
|
+
case "lifecycle.recovered":
|
|
3581
|
+
detail = `recovered=${s("recovery_kind") || "unknown"}`;
|
|
2827
3582
|
break;
|
|
2828
|
-
case "
|
|
2829
|
-
detail =
|
|
3583
|
+
case "lifecycle.sweep_observed":
|
|
3584
|
+
detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
|
|
2830
3585
|
break;
|
|
2831
3586
|
default:
|
|
2832
|
-
// Noise unless --all-tools: per-line command.* + tool.post_use.
|
|
2833
|
-
if (!allTools) return null;
|
|
2834
|
-
if (
|
|
2835
|
-
ev.event_type === "tool.post_use" ||
|
|
2836
|
-
ev.event_type === "tool.post_use_failure" ||
|
|
2837
|
-
ev.event_type.startsWith("command.")
|
|
2838
|
-
) {
|
|
2839
|
-
detail = s("tool_name") || "";
|
|
2840
|
-
break;
|
|
2841
|
-
}
|
|
2842
3587
|
return null;
|
|
2843
3588
|
}
|
|
2844
3589
|
return { ts: ev.ts, event_type: ev.event_type, detail };
|
|
2845
3590
|
}
|
|
2846
3591
|
|
|
3592
|
+
export function pendingFinalizationTraceEntries(
|
|
3593
|
+
requests: readonly SessionFinalizationRequestV3[],
|
|
3594
|
+
instanceId: string,
|
|
3595
|
+
): TraceEntry[] {
|
|
3596
|
+
return requests
|
|
3597
|
+
.filter((request) => request.status === "pending" && request.instance_id === instanceId)
|
|
3598
|
+
.map((request) => ({
|
|
3599
|
+
ts: request.observed_at,
|
|
3600
|
+
event_type: "session.finalization_pending",
|
|
3601
|
+
detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
|
|
3602
|
+
}));
|
|
3603
|
+
}
|
|
3604
|
+
|
|
2847
3605
|
function runTrace(
|
|
2848
3606
|
name: string,
|
|
2849
3607
|
opts: { since?: string; limit: string; allTools?: boolean; json?: boolean },
|
|
@@ -2872,7 +3630,7 @@ function runTrace(
|
|
|
2872
3630
|
if (candidates.length === 1) targetId = candidates[0]![0];
|
|
2873
3631
|
else if (candidates.length > 1)
|
|
2874
3632
|
targetId = candidates.map(([id]) => id).join("\x00"); // sentinel; resolved below
|
|
2875
|
-
else if (/^[0-9a-
|
|
3633
|
+
else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted)) targetId = wanted;
|
|
2876
3634
|
}
|
|
2877
3635
|
if (!targetId) {
|
|
2878
3636
|
emit.error({
|
|
@@ -2884,37 +3642,16 @@ function runTrace(
|
|
|
2884
3642
|
|
|
2885
3643
|
const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
|
|
2886
3644
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 200);
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
// Bounded read: the ledger grows without bound and a whole-file readFileSync
|
|
2891
|
-
// throws past V8's ~512MB string limit. A trace of an agent whose events all
|
|
2892
|
-
// predate the window is reported as truncated rather than crashing.
|
|
2893
|
-
const streamPath = resolve(root, ".harnery", "events.ndjson");
|
|
3645
|
+
const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
|
|
3646
|
+
const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
|
|
3647
|
+
const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
|
|
2894
3648
|
const byId = new Map<string, CanonicalEvent[]>();
|
|
2895
|
-
const
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
if (!line) continue;
|
|
2901
|
-
try {
|
|
2902
|
-
const ev = JSON.parse(line) as CanonicalEvent;
|
|
2903
|
-
if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
|
|
2904
|
-
if (sinceMs && Date.parse(ev.ts) < sinceMs) continue;
|
|
2905
|
-
const arr = byId.get(ev.instance_id) ?? [];
|
|
2906
|
-
arr.push(ev);
|
|
2907
|
-
byId.set(ev.instance_id, arr);
|
|
2908
|
-
} catch {
|
|
2909
|
-
/* skip */
|
|
2910
|
-
}
|
|
3649
|
+
for (const ev of diagnosticRead.events) {
|
|
3650
|
+
if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
|
|
3651
|
+
const arr = byId.get(ev.instance_id) ?? [];
|
|
3652
|
+
arr.push(ev);
|
|
3653
|
+
byId.set(ev.instance_id, arr);
|
|
2911
3654
|
}
|
|
2912
|
-
if (streamTruncated) {
|
|
2913
|
-
process.stderr.write(
|
|
2914
|
-
`note: event ledger exceeds ${Math.round(STREAM_SCAN_CAP_BYTES / 1024 / 1024)}MB; traced only the most recent window (older events omitted)\n`,
|
|
2915
|
-
);
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
3655
|
// If the name mapped to multiple instances, trace the one with the latest event.
|
|
2919
3656
|
let resolvedId = candidateIds[0]!;
|
|
2920
3657
|
if (candidateIds.length > 1) {
|
|
@@ -2930,25 +3667,84 @@ function runTrace(
|
|
|
2930
3667
|
}
|
|
2931
3668
|
|
|
2932
3669
|
const events = byId.get(resolvedId) ?? [];
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3670
|
+
let state = foldSessionState(events, { instance_id: resolvedId });
|
|
3671
|
+
let sessionState: "live" | "ended" = events.some((event) => event.event_type === "session.ended")
|
|
3672
|
+
? "ended"
|
|
3673
|
+
: "live";
|
|
3674
|
+
if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
|
|
3675
|
+
try {
|
|
3676
|
+
const view = projectCoordinationViewV3(readLedgerV3(root));
|
|
3677
|
+
const liveGeneration = view.instances[resolvedId];
|
|
3678
|
+
const terminalGeneration = Object.values(view.terminal_generations).find(
|
|
3679
|
+
(candidate) => candidate.instance_id === resolvedId,
|
|
3680
|
+
);
|
|
3681
|
+
const generation = liveGeneration ?? terminalGeneration;
|
|
3682
|
+
if (generation) {
|
|
3683
|
+
sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
|
|
3684
|
+
const lifecycleState = generation.lifecycle_state;
|
|
3685
|
+
state = {
|
|
3686
|
+
activity: generation.activity === "terminal" ? "idle" : generation.activity,
|
|
3687
|
+
activity_updated_at: generation.last_observed_at,
|
|
3688
|
+
activity_source: "event-v3-coordination-view",
|
|
3689
|
+
task_state:
|
|
3690
|
+
lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
|
|
3691
|
+
? lifecycleState
|
|
3692
|
+
: "active",
|
|
3693
|
+
task_state_updated_at: generation.lifecycle_state_updated_at,
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
} catch {
|
|
3697
|
+
diagnosticRead.authoritative = false;
|
|
3698
|
+
diagnosticRead.reason = "V3 coordination projection is unavailable";
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
let pendingFinalizations: SessionFinalizationRequestV3[] = [];
|
|
3702
|
+
if (diagnosticRead.source === "v3") {
|
|
3703
|
+
try {
|
|
3704
|
+
pendingFinalizations = listSessionFinalizationRequestsV3(root).filter(
|
|
3705
|
+
(request) => request.status === "pending" && request.instance_id === resolvedId,
|
|
3706
|
+
);
|
|
3707
|
+
} catch {
|
|
3708
|
+
diagnosticRead.authoritative = false;
|
|
3709
|
+
diagnosticRead.reason = "V3 finalization requests are unreadable";
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
const lines = [
|
|
3713
|
+
...events
|
|
3714
|
+
.map((ev) => traceLine(ev, !!opts.allTools))
|
|
3715
|
+
.filter((l): l is TraceEntry => l !== null),
|
|
3716
|
+
...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
|
|
3717
|
+
]
|
|
2937
3718
|
// Sort by timestamp, not file order: codex replays events (original ts,
|
|
2938
3719
|
// appended later), so append-order ≠ chronological order.
|
|
2939
3720
|
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
2940
3721
|
const shown = lines.slice(-limit);
|
|
2941
|
-
const
|
|
3722
|
+
const nativeResolvedId =
|
|
3723
|
+
diagnosticRead.source === "v3"
|
|
3724
|
+
? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
|
|
3725
|
+
: resolvedId;
|
|
3726
|
+
const displayName =
|
|
3727
|
+
nameById.get(resolvedId) ??
|
|
3728
|
+
(nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
|
|
3729
|
+
resolvedId.slice(0, 8);
|
|
2942
3730
|
|
|
2943
3731
|
const result = {
|
|
3732
|
+
event_source: {
|
|
3733
|
+
contract: diagnosticRead.source,
|
|
3734
|
+
authoritative: diagnosticRead.authoritative,
|
|
3735
|
+
reason: diagnosticRead.reason ?? null,
|
|
3736
|
+
},
|
|
2944
3737
|
name: displayName,
|
|
2945
3738
|
instance_id: resolvedId,
|
|
2946
3739
|
other_instances: candidateIds.filter((id) => id !== resolvedId),
|
|
3740
|
+
pending_finalizations: pendingFinalizations,
|
|
2947
3741
|
total_events: events.length,
|
|
3742
|
+
session_state: sessionState,
|
|
2948
3743
|
activity: state.activity,
|
|
2949
3744
|
activity_updated_at: state.activity_updated_at ?? null,
|
|
2950
3745
|
activity_source: state.activity_source ?? null,
|
|
2951
3746
|
task_state: state.task_state,
|
|
3747
|
+
task_state_scope: sessionState === "ended" ? "historical" : "current",
|
|
2952
3748
|
task_state_updated_at: state.task_state_updated_at ?? null,
|
|
2953
3749
|
task_state_reason: state.task_state_reason ?? null,
|
|
2954
3750
|
shown: shown.length,
|
|
@@ -2963,8 +3759,11 @@ function runTrace(
|
|
|
2963
3759
|
const header = `Trace: agent-${displayName} (${resolvedId.slice(0, 8)}…) ${events.length} events${result.other_instances.length ? ` · ${result.other_instances.length} older instance(s) of this name` : ""}`;
|
|
2964
3760
|
process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
|
|
2965
3761
|
process.stdout.write(
|
|
2966
|
-
` activity=${state.activity} · lifecycle=${state.task_state}${state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`,
|
|
3762
|
+
` activity=${state.activity} · session=${sessionState} · lifecycle=${sessionState === "ended" ? `historical(${state.task_state})` : state.task_state}${sessionState !== "ended" && state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`,
|
|
2967
3763
|
); // lint-ok-emission: human trace view
|
|
3764
|
+
if (!diagnosticRead.authoritative) {
|
|
3765
|
+
process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
|
|
3766
|
+
}
|
|
2968
3767
|
if (shown.length === 0) {
|
|
2969
3768
|
process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
|
|
2970
3769
|
return;
|
|
@@ -2975,6 +3774,73 @@ function runTrace(
|
|
|
2975
3774
|
}
|
|
2976
3775
|
}
|
|
2977
3776
|
|
|
3777
|
+
export interface ActiveAgentHealthSummary {
|
|
3778
|
+
source: "event-ledger-v3";
|
|
3779
|
+
total: number;
|
|
3780
|
+
by_platform: Record<string, number>;
|
|
3781
|
+
by_kind: Record<string, number>;
|
|
3782
|
+
by_schema_version: Record<string, number>;
|
|
3783
|
+
stale: number;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
type ActiveHealthHeartbeat = {
|
|
3787
|
+
instance_id: string;
|
|
3788
|
+
platform?: string;
|
|
3789
|
+
kind?: string;
|
|
3790
|
+
schema_version?: number;
|
|
3791
|
+
last_heartbeat?: string;
|
|
3792
|
+
};
|
|
3793
|
+
|
|
3794
|
+
/**
|
|
3795
|
+
* V3 generations, not disposable heartbeat caches, are the active-agent authority.
|
|
3796
|
+
*/
|
|
3797
|
+
export function collectActiveAgentHealth(
|
|
3798
|
+
root: string,
|
|
3799
|
+
nowMs = Date.now(),
|
|
3800
|
+
): ActiveAgentHealthSummary {
|
|
3801
|
+
const source: ActiveAgentHealthSummary["source"] = "event-ledger-v3";
|
|
3802
|
+
let heartbeats: ActiveHealthHeartbeat[] = [];
|
|
3803
|
+
try {
|
|
3804
|
+
const control = readEventV3ControlState(root);
|
|
3805
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
3806
|
+
heartbeats = readLiveCoordinationRows(root);
|
|
3807
|
+
}
|
|
3808
|
+
} catch {
|
|
3809
|
+
// The event-ledger section reports the authority read failure. Do not
|
|
3810
|
+
// substitute stale cache rows for a live V3 route.
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
const byPlatform: Record<string, number> = {};
|
|
3814
|
+
const byKind: Record<string, number> = {};
|
|
3815
|
+
const bySchema: Record<string, number> = {};
|
|
3816
|
+
let stale = 0;
|
|
3817
|
+
for (const heartbeat of heartbeats) {
|
|
3818
|
+
const platform = formatPlatformLabel(heartbeat.platform);
|
|
3819
|
+
byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
|
|
3820
|
+
const kind = heartbeat.kind ?? "unknown";
|
|
3821
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
3822
|
+
// These rows exist only after an authority-safe V3 projection. The
|
|
3823
|
+
// disposable heartbeat file has its own cache-format version, which must
|
|
3824
|
+
// not be reported as the ledger schema version.
|
|
3825
|
+
bySchema.v3 = (bySchema.v3 ?? 0) + 1;
|
|
3826
|
+
const lastObservedMs = heartbeat.last_heartbeat
|
|
3827
|
+
? Date.parse(heartbeat.last_heartbeat)
|
|
3828
|
+
: Number.NaN;
|
|
3829
|
+
const ageMs = Number.isFinite(lastObservedMs)
|
|
3830
|
+
? nowMs - lastObservedMs
|
|
3831
|
+
: Number.POSITIVE_INFINITY;
|
|
3832
|
+
if (ageMs > freshnessCutoffSecs() * 1000) stale += 1;
|
|
3833
|
+
}
|
|
3834
|
+
return {
|
|
3835
|
+
source,
|
|
3836
|
+
total: heartbeats.length,
|
|
3837
|
+
by_platform: byPlatform,
|
|
3838
|
+
by_kind: byKind,
|
|
3839
|
+
by_schema_version: bySchema,
|
|
3840
|
+
stale,
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
|
|
2978
3844
|
function runHealth(opts: { since: string; json?: boolean }): void {
|
|
2979
3845
|
if (opts.json) emit.config({ format: "json" });
|
|
2980
3846
|
|
|
@@ -2996,12 +3862,13 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2996
3862
|
process.exit(1);
|
|
2997
3863
|
}
|
|
2998
3864
|
|
|
2999
|
-
const
|
|
3865
|
+
const nowMs = Date.now();
|
|
3866
|
+
const cutoffMs = nowMs - sinceSecs * 1000;
|
|
3000
3867
|
const activeDir = resolve(root, ".harnery/active");
|
|
3001
3868
|
const councilsDir = resolve(root, ".harnery/councils");
|
|
3002
3869
|
|
|
3003
|
-
// Coordination telemetry reads from the canonical
|
|
3004
|
-
// Heals come from health
|
|
3870
|
+
// Coordination telemetry reads from the canonical V3 ledger.
|
|
3871
|
+
// Heals come from health.observed; council activity from council.state_changed.
|
|
3005
3872
|
const heal: HealEvent[] = [];
|
|
3006
3873
|
let councilAdvanced = 0;
|
|
3007
3874
|
let councilClosed = 0;
|
|
@@ -3010,33 +3877,33 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3010
3877
|
const sweptByReason: Record<string, number> = {};
|
|
3011
3878
|
|
|
3012
3879
|
const nameById = buildNameById(root);
|
|
3013
|
-
|
|
3880
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
3881
|
+
for (const ev of diagnosticEvents.events) {
|
|
3014
3882
|
const healEv = canonicalToHealEvent(ev, nameById);
|
|
3015
3883
|
if (healEv) {
|
|
3016
3884
|
heal.push(healEv);
|
|
3017
3885
|
continue;
|
|
3018
3886
|
}
|
|
3887
|
+
if (ev.event_type === "council.state_changed") {
|
|
3888
|
+
const state = String(ev.payload?.new_state ?? "");
|
|
3889
|
+
if (state === "round_open") councilAdvanced++;
|
|
3890
|
+
if (state === "closed") councilClosed++;
|
|
3891
|
+
if (state === "archived") councilArchived++;
|
|
3892
|
+
continue;
|
|
3893
|
+
}
|
|
3019
3894
|
switch (ev.event_type) {
|
|
3020
|
-
case "
|
|
3021
|
-
councilAdvanced++;
|
|
3022
|
-
break;
|
|
3023
|
-
case "council.close":
|
|
3024
|
-
councilClosed++;
|
|
3025
|
-
break;
|
|
3026
|
-
case "council.archive":
|
|
3027
|
-
councilArchived++;
|
|
3028
|
-
break;
|
|
3029
|
-
case "health.heartbeat_swept": {
|
|
3895
|
+
case "lifecycle.sweep_observed": {
|
|
3030
3896
|
sweptTotal++;
|
|
3031
|
-
const reason = String(
|
|
3897
|
+
const reason = String(ev.payload?.observation ?? "unknown");
|
|
3032
3898
|
sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
|
|
3033
3899
|
break;
|
|
3034
3900
|
}
|
|
3035
3901
|
}
|
|
3036
3902
|
}
|
|
3037
3903
|
|
|
3038
|
-
const hookErrors = readHookErrors(root, cutoffMs);
|
|
3904
|
+
const hookErrors = readHookErrors(root, cutoffMs, nowMs);
|
|
3039
3905
|
const stream = readStreamStats(root);
|
|
3906
|
+
const eventLedger = collectEventLedgerHealthV3(root);
|
|
3040
3907
|
|
|
3041
3908
|
// Canonical health.* events carry the full instance_id, already resolved to
|
|
3042
3909
|
// `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
|
|
@@ -3056,19 +3923,13 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3056
3923
|
.slice(0, 5)
|
|
3057
3924
|
.map(([agent, count]) => ({ agent, count }));
|
|
3058
3925
|
|
|
3059
|
-
|
|
3060
|
-
const activeByPlatform: Record<string, number> = {};
|
|
3061
|
-
const activeByKind: Record<string, number> = {};
|
|
3062
|
-
const activeBySchema: Record<string, number> = {};
|
|
3063
|
-
let activeTotal = 0;
|
|
3064
|
-
let staleHeartbeats = 0;
|
|
3926
|
+
const activeAgents = collectActiveAgentHealth(root, nowMs);
|
|
3065
3927
|
// Zombies: files in active/ that are broken: unparseable, nameless, or an
|
|
3066
3928
|
// absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
|
|
3067
3929
|
// mean dead files the sweep isn't reaping.
|
|
3068
3930
|
let zombieCount = 0;
|
|
3069
3931
|
const zombieSamples: string[] = [];
|
|
3070
3932
|
const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
|
|
3071
|
-
const nowMs = Date.now();
|
|
3072
3933
|
if (existsSync(activeDir)) {
|
|
3073
3934
|
for (const file of readdirSync(activeDir)) {
|
|
3074
3935
|
if (!file.endsWith(".json")) continue;
|
|
@@ -3085,17 +3946,8 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3085
3946
|
zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
|
|
3086
3947
|
continue;
|
|
3087
3948
|
}
|
|
3088
|
-
activeTotal++;
|
|
3089
|
-
const platform = formatPlatformLabel(hb.platform);
|
|
3090
|
-
activeByPlatform[platform] = (activeByPlatform[platform] ?? 0) + 1;
|
|
3091
|
-
const kind = hb.kind ?? "unknown";
|
|
3092
|
-
activeByKind[kind] = (activeByKind[kind] ?? 0) + 1;
|
|
3093
|
-
const sv = (hb as { schema_version?: number }).schema_version;
|
|
3094
|
-
const schemaKey = sv === undefined ? "v0" : `v${sv}`;
|
|
3095
|
-
activeBySchema[schemaKey] = (activeBySchema[schemaKey] ?? 0) + 1;
|
|
3096
3949
|
const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
|
|
3097
3950
|
const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
|
|
3098
|
-
if (ageMs > freshnessCutoffSecs() * 1000) staleHeartbeats++;
|
|
3099
3951
|
// Zombie heuristics on a parseable heartbeat: no name, or an age so large
|
|
3100
3952
|
// it can only be a broken/epoch timestamp (a real agent would have healed).
|
|
3101
3953
|
if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
|
|
@@ -3127,37 +3979,38 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3127
3979
|
);
|
|
3128
3980
|
}
|
|
3129
3981
|
}
|
|
3130
|
-
if (
|
|
3982
|
+
if (activeAgents.stale > 0) {
|
|
3983
|
+
const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
|
|
3984
|
+
const action =
|
|
3985
|
+
activeAgents.source === "event-ledger-v3"
|
|
3986
|
+
? "lifecycle reconciliation may be delayed"
|
|
3987
|
+
: "heal mechanism may not be firing";
|
|
3131
3988
|
anomalies.push(
|
|
3132
|
-
`${
|
|
3989
|
+
`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`,
|
|
3133
3990
|
);
|
|
3134
3991
|
}
|
|
3135
|
-
const
|
|
3992
|
+
const expectedSchema = "v3";
|
|
3993
|
+
const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter(
|
|
3994
|
+
(schema) => schema !== expectedSchema,
|
|
3995
|
+
);
|
|
3136
3996
|
if (unexpectedSchemas.length > 0) {
|
|
3137
3997
|
anomalies.push(
|
|
3138
|
-
`Unexpected
|
|
3998
|
+
`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`,
|
|
3139
3999
|
);
|
|
3140
4000
|
}
|
|
3141
|
-
//
|
|
3142
|
-
//
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
const
|
|
3146
|
-
|
|
3147
|
-
? `: top phase '${top.phase}' x${top.count}${top.sample ? ` (${top.sample.slice(0, 80)})` : ""}`
|
|
3148
|
-
: "";
|
|
3149
|
-
anomalies.push(`agent-hook errored ${hookErrors.total}x in ${opts.since}${detail}`);
|
|
4001
|
+
// Only recent hook failures are active anomalies. Historical rows remain in
|
|
4002
|
+
// the report with exact error signatures and their latest timestamp.
|
|
4003
|
+
if (hookErrors.last1h > 0) {
|
|
4004
|
+
const top = hookErrors.recentTopErrors[0];
|
|
4005
|
+
const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
|
|
4006
|
+
anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
|
|
3150
4007
|
}
|
|
3151
4008
|
if (stream.cursor_backlog > 500) {
|
|
3152
4009
|
anomalies.push(
|
|
3153
4010
|
`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`,
|
|
3154
4011
|
);
|
|
3155
4012
|
}
|
|
3156
|
-
//
|
|
3157
|
-
// append-only ledger (names + forensics for the life of the log), and
|
|
3158
|
-
// consumeSince tail-reads it, so size no longer drives latency. The
|
|
3159
|
-
// bytes/lines still surface in the summary line for visibility;
|
|
3160
|
-
// cursor_backlog above is the real drain-lag signal.
|
|
4013
|
+
// Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
|
|
3161
4014
|
if ((sweptByReason.unparseable ?? 0) > 0) {
|
|
3162
4015
|
anomalies.push(
|
|
3163
4016
|
`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`,
|
|
@@ -3168,16 +4021,46 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3168
4021
|
`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`,
|
|
3169
4022
|
);
|
|
3170
4023
|
}
|
|
4024
|
+
if (eventLedger.state === "live") {
|
|
4025
|
+
// Open spans whose generation has no open turn: the turn ended without its
|
|
4026
|
+
// tool spans closing, so an explicit end can only queue, never finalize.
|
|
4027
|
+
const orphanGenerations = eventLedger.open_spans.generations.filter(
|
|
4028
|
+
(generation) => !generation.turn_open,
|
|
4029
|
+
);
|
|
4030
|
+
if (orphanGenerations.length > 0) {
|
|
4031
|
+
const orphanSpans = orphanGenerations.reduce(
|
|
4032
|
+
(sum, generation) => sum + generation.span_count,
|
|
4033
|
+
0,
|
|
4034
|
+
);
|
|
4035
|
+
anomalies.push(
|
|
4036
|
+
`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`,
|
|
4037
|
+
);
|
|
4038
|
+
}
|
|
4039
|
+
for (const pressure of eventLedger.span_pressure) {
|
|
4040
|
+
anomalies.push(
|
|
4041
|
+
`${pressure.instance_id} (${pressure.generation_id}) holds ${pressure.span_count} open spans (soft watermark ${SPAN_PRESSURE_SOFT_WATERMARK}; the reader's 256-span cap makes the state unreadable)`,
|
|
4042
|
+
);
|
|
4043
|
+
}
|
|
4044
|
+
for (const collectionError of eventLedger.collection_errors) {
|
|
4045
|
+
anomalies.push(`event-ledger health: ${collectionError}`);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
3171
4048
|
|
|
3172
4049
|
const report: HealthReport = {
|
|
3173
4050
|
since: opts.since,
|
|
3174
4051
|
generated_at: new Date().toISOString(),
|
|
4052
|
+
event_telemetry: {
|
|
4053
|
+
source: diagnosticEvents.source,
|
|
4054
|
+
authoritative: diagnosticEvents.authoritative,
|
|
4055
|
+
...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
|
|
4056
|
+
},
|
|
3175
4057
|
active_agents: {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
4058
|
+
source: activeAgents.source,
|
|
4059
|
+
total: activeAgents.total,
|
|
4060
|
+
by_platform: activeAgents.by_platform,
|
|
4061
|
+
by_kind: activeAgents.by_kind,
|
|
4062
|
+
by_schema_version: activeAgents.by_schema_version,
|
|
4063
|
+
stale: activeAgents.stale,
|
|
3181
4064
|
},
|
|
3182
4065
|
heal_events: {
|
|
3183
4066
|
total: heal.length,
|
|
@@ -3193,9 +4076,19 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3193
4076
|
closed_in_window: councilClosed,
|
|
3194
4077
|
},
|
|
3195
4078
|
swept_events: { total: sweptTotal, by_reason: sweptByReason },
|
|
3196
|
-
hook_errors: {
|
|
4079
|
+
hook_errors: {
|
|
4080
|
+
total: hookErrors.total,
|
|
4081
|
+
last_1h: hookErrors.last1h,
|
|
4082
|
+
latest_at: hookErrors.latestAt,
|
|
4083
|
+
by_phase: hookErrors.byPhase,
|
|
4084
|
+
by_error: hookErrors.byError,
|
|
4085
|
+
top: hookErrors.top,
|
|
4086
|
+
top_errors: hookErrors.topErrors,
|
|
4087
|
+
recent_top_errors: hookErrors.recentTopErrors,
|
|
4088
|
+
},
|
|
3197
4089
|
stream,
|
|
3198
4090
|
zombies: { count: zombieCount, samples: zombieSamples },
|
|
4091
|
+
event_ledger: eventLedger,
|
|
3199
4092
|
anomalies,
|
|
3200
4093
|
};
|
|
3201
4094
|
|
|
@@ -3237,7 +4130,9 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3237
4130
|
if (report.councils.archived_in_window > 0)
|
|
3238
4131
|
councilParts.push(`${report.councils.archived_in_window} archived`);
|
|
3239
4132
|
|
|
3240
|
-
const
|
|
4133
|
+
const activeSource =
|
|
4134
|
+
report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
|
|
4135
|
+
const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
|
|
3241
4136
|
|
|
3242
4137
|
const sweptReasonStr = Object.entries(report.swept_events.by_reason)
|
|
3243
4138
|
.map(([r, n]) => `${r} ${n}`)
|
|
@@ -3245,11 +4140,36 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3245
4140
|
const hookErrStr =
|
|
3246
4141
|
report.hook_errors.total === 0
|
|
3247
4142
|
? "0"
|
|
3248
|
-
: `${report.hook_errors.total}
|
|
4143
|
+
: `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
|
|
3249
4144
|
const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
|
|
3250
4145
|
|
|
4146
|
+
const ledger = report.event_ledger;
|
|
4147
|
+
let ledgerStr: string;
|
|
4148
|
+
if (ledger.state !== "live") {
|
|
4149
|
+
ledgerStr = `unavailable (${ledger.reason})`;
|
|
4150
|
+
} else {
|
|
4151
|
+
const orphanGenerations = ledger.open_spans.generations.filter(
|
|
4152
|
+
(generation) => !generation.turn_open,
|
|
4153
|
+
).length;
|
|
4154
|
+
const pendingAges = ledger.pending_finalizations
|
|
4155
|
+
.map((request) =>
|
|
4156
|
+
request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)),
|
|
4157
|
+
)
|
|
4158
|
+
.join(", ");
|
|
4159
|
+
ledgerStr = [
|
|
4160
|
+
`open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
|
|
4161
|
+
`pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
|
|
4162
|
+
`intake ${ledger.intake_spool.total}`,
|
|
4163
|
+
`diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
|
|
4164
|
+
].join(" · ");
|
|
4165
|
+
}
|
|
4166
|
+
|
|
3251
4167
|
const rows: Array<[string, string]> = [
|
|
3252
4168
|
["window", `last ${report.since}`],
|
|
4169
|
+
[
|
|
4170
|
+
"event source",
|
|
4171
|
+
`${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
|
|
4172
|
+
],
|
|
3253
4173
|
["active", activeStr],
|
|
3254
4174
|
[
|
|
3255
4175
|
"heals",
|
|
@@ -3259,6 +4179,7 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3259
4179
|
["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
|
|
3260
4180
|
["hook errors", hookErrStr],
|
|
3261
4181
|
["stream", streamStr],
|
|
4182
|
+
["event ledger", ledgerStr],
|
|
3262
4183
|
[
|
|
3263
4184
|
"zombies",
|
|
3264
4185
|
report.zombies.count === 0
|
|
@@ -3383,6 +4304,7 @@ function runAdapterProbe(
|
|
|
3383
4304
|
replayExitCode = result.exitCode;
|
|
3384
4305
|
data.samples = samples;
|
|
3385
4306
|
data.samples_summary = result.summary;
|
|
4307
|
+
if (result.note) data.samples_note = result.note;
|
|
3386
4308
|
}
|
|
3387
4309
|
|
|
3388
4310
|
emit.data(data);
|
|
@@ -3402,7 +4324,9 @@ function runAdapterProbe(
|
|
|
3402
4324
|
| undefined;
|
|
3403
4325
|
lines.push("");
|
|
3404
4326
|
if (samples.length === 0) {
|
|
3405
|
-
lines.push(
|
|
4327
|
+
lines.push(
|
|
4328
|
+
` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`,
|
|
4329
|
+
);
|
|
3406
4330
|
} else {
|
|
3407
4331
|
lines.push(
|
|
3408
4332
|
` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`,
|
|
@@ -3459,6 +4383,7 @@ function replayAdapterSamples(
|
|
|
3459
4383
|
samples: SampleReplayResult[];
|
|
3460
4384
|
exitCode: number;
|
|
3461
4385
|
summary: { total: number; pass: number; fail: number; skipped: number };
|
|
4386
|
+
note?: string;
|
|
3462
4387
|
} {
|
|
3463
4388
|
const sampleDir = resolve(root, relativeSampleDir);
|
|
3464
4389
|
if (!existsSync(sampleDir)) {
|
|
@@ -3466,6 +4391,7 @@ function replayAdapterSamples(
|
|
|
3466
4391
|
samples: [],
|
|
3467
4392
|
exitCode: 0,
|
|
3468
4393
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
4394
|
+
note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
|
|
3469
4395
|
};
|
|
3470
4396
|
}
|
|
3471
4397
|
|
|
@@ -3479,6 +4405,7 @@ function replayAdapterSamples(
|
|
|
3479
4405
|
samples: [],
|
|
3480
4406
|
exitCode: 0,
|
|
3481
4407
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
4408
|
+
note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
|
|
3482
4409
|
};
|
|
3483
4410
|
}
|
|
3484
4411
|
|
|
@@ -3704,11 +4631,26 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
|
|
|
3704
4631
|
});
|
|
3705
4632
|
process.exit(1);
|
|
3706
4633
|
}
|
|
3707
|
-
const myHb =
|
|
4634
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3708
4635
|
const fromName = myHb?.name ?? "anonymous";
|
|
3709
4636
|
const body = `from agent-${fromName}: ${message.trim()}`;
|
|
3710
4637
|
const doc = appendEntry(peerOwner, "handoff", body);
|
|
3711
4638
|
|
|
4639
|
+
// Canonical delivery record: sender is the envelope owner, recipient rides
|
|
4640
|
+
// in data, so read-only observers can render the communication without
|
|
4641
|
+
// joining journal files.
|
|
4642
|
+
emitEventV3({
|
|
4643
|
+
owner: myOwner,
|
|
4644
|
+
session: nativeSessionIdentity(myHb, myOwner),
|
|
4645
|
+
adapter: normalizeAdapter(myHb?.platform),
|
|
4646
|
+
observation: {
|
|
4647
|
+
event_type: "coord.message_observed",
|
|
4648
|
+
direction: "sent",
|
|
4649
|
+
subject: peerOwner,
|
|
4650
|
+
body: message.trim(),
|
|
4651
|
+
},
|
|
4652
|
+
});
|
|
4653
|
+
|
|
3712
4654
|
const data = {
|
|
3713
4655
|
peer: name,
|
|
3714
4656
|
peer_instance_id: peerOwner,
|
|
@@ -3766,7 +4708,7 @@ async function runWait(
|
|
|
3766
4708
|
|
|
3767
4709
|
let lastProgressMs = 0;
|
|
3768
4710
|
while (true) {
|
|
3769
|
-
const hb =
|
|
4711
|
+
const hb = readCurrentCoordinationRow(peerOwner);
|
|
3770
4712
|
const now = Date.now();
|
|
3771
4713
|
const elapsedMs = now - startMs;
|
|
3772
4714
|
|
|
@@ -3806,9 +4748,8 @@ async function runWait(
|
|
|
3806
4748
|
const progressGapMs = Math.max(pollMs, 30_000);
|
|
3807
4749
|
if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
|
|
3808
4750
|
lastProgressMs = now;
|
|
3809
|
-
const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
|
|
3810
4751
|
const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
|
|
3811
|
-
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking
|
|
4752
|
+
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
|
|
3812
4753
|
process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
|
|
3813
4754
|
}
|
|
3814
4755
|
|
|
@@ -3849,6 +4790,7 @@ function runHeal(opts: {
|
|
|
3849
4790
|
owner: string;
|
|
3850
4791
|
kind: string;
|
|
3851
4792
|
sessionId?: string;
|
|
4793
|
+
adapter?: string;
|
|
3852
4794
|
pid?: string;
|
|
3853
4795
|
json?: boolean;
|
|
3854
4796
|
}): void {
|
|
@@ -3860,10 +4802,22 @@ function runHeal(opts: {
|
|
|
3860
4802
|
emit.error({ code: "missing_owner", message: "--owner is required" });
|
|
3861
4803
|
process.exit(1);
|
|
3862
4804
|
}
|
|
3863
|
-
if (kind !== "pidmap" && kind !== "
|
|
4805
|
+
if (kind !== "pidmap" && kind !== "cache") {
|
|
3864
4806
|
emit.error({
|
|
3865
4807
|
code: "bad_kind",
|
|
3866
|
-
message: "--kind must be one of: pidmap,
|
|
4808
|
+
message: "--kind must be one of: pidmap, cache",
|
|
4809
|
+
});
|
|
4810
|
+
process.exit(1);
|
|
4811
|
+
}
|
|
4812
|
+
if (
|
|
4813
|
+
opts.adapter &&
|
|
4814
|
+
opts.adapter !== "claude-code" &&
|
|
4815
|
+
opts.adapter !== "cursor" &&
|
|
4816
|
+
opts.adapter !== "codex"
|
|
4817
|
+
) {
|
|
4818
|
+
emit.error({
|
|
4819
|
+
code: "bad_adapter",
|
|
4820
|
+
message: "--adapter must be one of: claude-code, cursor, codex",
|
|
3867
4821
|
});
|
|
3868
4822
|
process.exit(1);
|
|
3869
4823
|
}
|
|
@@ -3877,10 +4831,9 @@ function runHeal(opts: {
|
|
|
3877
4831
|
process.exit(1);
|
|
3878
4832
|
}
|
|
3879
4833
|
|
|
3880
|
-
const action =
|
|
3881
|
-
kind === "pidmap" ? "heal-pidmap" : kind === "heartbeat" ? "heal-heartbeat" : "kill-heartbeat";
|
|
4834
|
+
const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
|
|
3882
4835
|
|
|
3883
|
-
// Refuse to
|
|
4836
|
+
// Refuse to materialize a V3 cache at a truncated owner id.
|
|
3884
4837
|
//
|
|
3885
4838
|
// Heartbeats are keyed by the whole instance_id, so healing at an
|
|
3886
4839
|
// abbreviated id writes `.harnery/active/<prefix>.json` while every reader
|
|
@@ -3900,19 +4853,18 @@ function runHeal(opts: {
|
|
|
3900
4853
|
// whose instance_id this id is a prefix of settles it just as well, and is
|
|
3901
4854
|
// present in precisely the case that matters (the session the reader was
|
|
3902
4855
|
// trying to heal is registered, just not under the abbreviated name).
|
|
3903
|
-
if (kind === "
|
|
4856
|
+
if (kind === "cache" && !readHeartbeatCache(root, owner)) {
|
|
3904
4857
|
const canonical =
|
|
3905
4858
|
opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
|
|
3906
4859
|
? opts.sessionId.trim()
|
|
3907
4860
|
: liveIdWithPrefix(root, owner);
|
|
3908
4861
|
if (canonical) {
|
|
3909
|
-
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live
|
|
4862
|
+
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
|
|
3910
4863
|
emit.error({
|
|
3911
4864
|
code: "truncated_owner",
|
|
3912
4865
|
message:
|
|
3913
|
-
`--owner ${owner} is a prefix of ${canonical} (${source})
|
|
3914
|
-
`
|
|
3915
|
-
`resolves. Re-run with --owner ${canonical}.`,
|
|
4866
|
+
`--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
|
|
4867
|
+
`the canonical instance id; re-run with --owner ${canonical}.`,
|
|
3916
4868
|
});
|
|
3917
4869
|
process.exit(1);
|
|
3918
4870
|
}
|
|
@@ -3920,14 +4872,13 @@ function runHeal(opts: {
|
|
|
3920
4872
|
|
|
3921
4873
|
// Build positional args. agent-coord's arg layout:
|
|
3922
4874
|
// heal-pidmap <instance_id> [<pid>]
|
|
3923
|
-
//
|
|
3924
|
-
// kill-heartbeat <instance_id>
|
|
4875
|
+
// repair-coordination-cache <instance_id> [<session_id>]
|
|
3925
4876
|
const helperArgs: string[] = [action, owner];
|
|
3926
4877
|
if (kind === "pidmap" && opts.pid) helperArgs.push(opts.pid);
|
|
3927
|
-
if (kind === "
|
|
4878
|
+
if (kind === "cache" && opts.sessionId) helperArgs.push(opts.sessionId);
|
|
4879
|
+
if (kind === "cache" && opts.adapter) helperArgs.push(`--adapter=${opts.adapter}`);
|
|
3928
4880
|
|
|
3929
|
-
//
|
|
3930
|
-
// bundled agent-coord binary.
|
|
4881
|
+
// Both recovery actions are handled by the bundled agent-coord binary.
|
|
3931
4882
|
const helper = agentCoordOrExit(root);
|
|
3932
4883
|
const proc = spawnSync(helper, helperArgs, {
|
|
3933
4884
|
encoding: "utf8",
|
|
@@ -3942,27 +4893,22 @@ function runHeal(opts: {
|
|
|
3942
4893
|
process.exit(1);
|
|
3943
4894
|
}
|
|
3944
4895
|
|
|
3945
|
-
//
|
|
4896
|
+
// Read the derived cache to surface post-action state.
|
|
3946
4897
|
let after: Heartbeat | null = null;
|
|
3947
4898
|
try {
|
|
3948
|
-
after =
|
|
4899
|
+
after = readHeartbeatCache(root, owner);
|
|
3949
4900
|
} catch {
|
|
3950
4901
|
after = null;
|
|
3951
4902
|
}
|
|
3952
4903
|
|
|
3953
|
-
// Outcome semantics differ per kind. kill-heartbeat targets the heartbeat
|
|
3954
|
-
// file directly; heal-heartbeat upserts it; heal-pidmap touches a pid-map
|
|
3955
|
-
// row whose existence is independent of the heartbeat. Reporting on
|
|
3956
|
-
// "heartbeat present after" for the pidmap path was misleading: it's
|
|
3957
|
-
// unrelated to whether the heal succeeded.
|
|
3958
4904
|
const outcome =
|
|
3959
4905
|
kind === "pidmap"
|
|
3960
4906
|
? proc.status === 0
|
|
3961
4907
|
? "ok"
|
|
3962
4908
|
: "failed"
|
|
3963
4909
|
: after
|
|
3964
|
-
? "
|
|
3965
|
-
: "
|
|
4910
|
+
? "cache_present"
|
|
4911
|
+
: "cache_absent";
|
|
3966
4912
|
|
|
3967
4913
|
emit.data({
|
|
3968
4914
|
rows: [
|
|
@@ -3985,36 +4931,40 @@ function runHeal(opts: {
|
|
|
3985
4931
|
if (kind === "pidmap") {
|
|
3986
4932
|
emit.text(`agent-coord ${action} ok\n`);
|
|
3987
4933
|
} else {
|
|
3988
|
-
emit.text(`agent-coord ${action} ok:
|
|
4934
|
+
emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
|
|
3989
4935
|
}
|
|
3990
4936
|
}
|
|
3991
4937
|
|
|
3992
|
-
// Canonical health.* emission is owned by the writer
|
|
3993
|
-
//
|
|
4938
|
+
// Canonical health.* emission is owned by the cache writer, so it fires
|
|
4939
|
+
// inside the agent-coord subprocess
|
|
3994
4940
|
// above on actual writes only: write-only telemetry, no double-emit, no
|
|
3995
4941
|
// event when an already-correct heal no-ops. (Previously emitted here
|
|
3996
4942
|
// unconditionally on every `harn agents heal`, which over-counted no-op heals.)
|
|
3997
4943
|
}
|
|
3998
4944
|
|
|
3999
4945
|
/**
|
|
4000
|
-
*
|
|
4001
|
-
*
|
|
4002
|
-
* through silently if no session (CI / direct invocation).
|
|
4946
|
+
* Emit one V3 council audit transition keyed to the exact durable manifest.
|
|
4947
|
+
* Falls through silently when no live session can attest the observation.
|
|
4003
4948
|
*/
|
|
4004
4949
|
function emitCouncilStateEvent(
|
|
4005
|
-
type: string,
|
|
4006
4950
|
manifest: CouncilManifest,
|
|
4007
|
-
|
|
4951
|
+
newState: string,
|
|
4952
|
+
priorState?: string,
|
|
4008
4953
|
): void {
|
|
4009
4954
|
const myOwner = resolveOwner();
|
|
4010
4955
|
if (!myOwner) return;
|
|
4011
|
-
const hb =
|
|
4012
|
-
|
|
4013
|
-
type,
|
|
4956
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
4957
|
+
emitEventV3({
|
|
4014
4958
|
owner: myOwner,
|
|
4015
|
-
session: hb
|
|
4959
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
4016
4960
|
adapter: normalizeAdapter(hb?.platform),
|
|
4017
|
-
|
|
4961
|
+
observation: {
|
|
4962
|
+
event_type: "council.state_changed",
|
|
4963
|
+
council_id: manifest.council_id,
|
|
4964
|
+
...(priorState ? { prior_state: priorState } : {}),
|
|
4965
|
+
new_state: newState,
|
|
4966
|
+
record: manifest,
|
|
4967
|
+
},
|
|
4018
4968
|
});
|
|
4019
4969
|
}
|
|
4020
4970
|
|
|
@@ -4071,7 +5021,7 @@ function runCouncilCreate(
|
|
|
4071
5021
|
if (opts.createdBy?.trim()) {
|
|
4072
5022
|
createdBy = normalizeAgentName(opts.createdBy);
|
|
4073
5023
|
} else if (myOwner) {
|
|
4074
|
-
const myHb =
|
|
5024
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4075
5025
|
if (myHb?.name) {
|
|
4076
5026
|
createdBy = normalizeAgentName(myHb.name);
|
|
4077
5027
|
}
|
|
@@ -4132,17 +5082,16 @@ function runCouncilCreate(
|
|
|
4132
5082
|
writeManifest(manifest);
|
|
4133
5083
|
|
|
4134
5084
|
if (myOwner) {
|
|
4135
|
-
const myHbForEmit =
|
|
4136
|
-
|
|
4137
|
-
type: "council.open",
|
|
5085
|
+
const myHbForEmit = readCurrentCoordinationRow(myOwner);
|
|
5086
|
+
emitEventV3({
|
|
4138
5087
|
owner: myOwner,
|
|
4139
|
-
session: myHbForEmit
|
|
5088
|
+
session: nativeSessionIdentity(myHbForEmit, myOwner),
|
|
4140
5089
|
adapter: normalizeAdapter(myHbForEmit?.platform),
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
5090
|
+
observation: {
|
|
5091
|
+
event_type: "council.state_changed",
|
|
5092
|
+
council_id: manifest.council_id,
|
|
5093
|
+
new_state: "active",
|
|
5094
|
+
record: manifest,
|
|
4146
5095
|
},
|
|
4147
5096
|
});
|
|
4148
5097
|
}
|
|
@@ -4155,7 +5104,7 @@ function runCouncilCreate(
|
|
|
4155
5104
|
const skippedMembers: string[] = [];
|
|
4156
5105
|
for (const memberName of members) {
|
|
4157
5106
|
const bareName = memberName.replace(/^agent-/, "");
|
|
4158
|
-
if (myOwner &&
|
|
5107
|
+
if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
|
|
4159
5108
|
// Convener is themselves a member; skip the self-ping
|
|
4160
5109
|
continue;
|
|
4161
5110
|
}
|
|
@@ -4220,7 +5169,7 @@ function runCouncilList(opts: { status?: string; mine?: boolean; json?: boolean
|
|
|
4220
5169
|
if (opts.mine) {
|
|
4221
5170
|
const myOwner = resolveOwner();
|
|
4222
5171
|
if (myOwner) {
|
|
4223
|
-
const myHb =
|
|
5172
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4224
5173
|
if (myHb?.name) myName = normalizeAgentName(myHb.name);
|
|
4225
5174
|
}
|
|
4226
5175
|
if (!myName) {
|
|
@@ -4397,7 +5346,7 @@ function runCouncilClose(id: string, opts: { json?: boolean }): void {
|
|
|
4397
5346
|
closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
4398
5347
|
};
|
|
4399
5348
|
writeManifest(next);
|
|
4400
|
-
emitCouncilStateEvent("
|
|
5349
|
+
emitCouncilStateEvent(next, "closed", manifest.status);
|
|
4401
5350
|
|
|
4402
5351
|
// Build the transcript: every round's contributions in order.
|
|
4403
5352
|
const transcript = buildTranscript(next);
|
|
@@ -4442,7 +5391,7 @@ function runCouncilArchive(id: string, opts: { json?: boolean }): void {
|
|
|
4442
5391
|
// updated status. moveToArchive then physically relocates the artifacts.
|
|
4443
5392
|
writeManifest(next);
|
|
4444
5393
|
moveToArchive(next.council_id);
|
|
4445
|
-
emitCouncilStateEvent("
|
|
5394
|
+
emitCouncilStateEvent(next, "archived", manifest.status);
|
|
4446
5395
|
|
|
4447
5396
|
emit.data({
|
|
4448
5397
|
rows: [
|
|
@@ -4501,7 +5450,7 @@ function runCouncilUnarchive(id: string, opts: { json?: boolean }): void {
|
|
|
4501
5450
|
// (allows re-running for testing).
|
|
4502
5451
|
moveFromArchive(next.council_id);
|
|
4503
5452
|
writeManifest(next);
|
|
4504
|
-
emitCouncilStateEvent(
|
|
5453
|
+
emitCouncilStateEvent(next, restoredStatus, "archived");
|
|
4505
5454
|
|
|
4506
5455
|
emit.data({
|
|
4507
5456
|
rows: [
|
|
@@ -4563,7 +5512,7 @@ function runCouncilDelete(id: string, opts: { yes?: boolean; json?: boolean }):
|
|
|
4563
5512
|
|
|
4564
5513
|
const removed = deleteArchivedCouncil(manifest.council_id);
|
|
4565
5514
|
if (removed) {
|
|
4566
|
-
emitCouncilStateEvent("
|
|
5515
|
+
emitCouncilStateEvent(manifest, "deleted", "archived");
|
|
4567
5516
|
}
|
|
4568
5517
|
|
|
4569
5518
|
emit.data({
|
|
@@ -4753,7 +5702,7 @@ function runCouncilContribute(
|
|
|
4753
5702
|
try {
|
|
4754
5703
|
const myOwner = resolveOwner();
|
|
4755
5704
|
if (myOwner) {
|
|
4756
|
-
const myHb =
|
|
5705
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4757
5706
|
if (myHb?.name) actualName = normalizeAgentName(myHb.name);
|
|
4758
5707
|
}
|
|
4759
5708
|
} catch {
|
|
@@ -4769,7 +5718,7 @@ function runCouncilContribute(
|
|
|
4769
5718
|
});
|
|
4770
5719
|
process.exit(1);
|
|
4771
5720
|
}
|
|
4772
|
-
const myHb =
|
|
5721
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4773
5722
|
if (!myHb?.name) {
|
|
4774
5723
|
emit.error({
|
|
4775
5724
|
code: "no_self_name",
|
|
@@ -4822,11 +5771,7 @@ function runCouncilContribute(
|
|
|
4822
5771
|
}
|
|
4823
5772
|
|
|
4824
5773
|
const path = writeContribution(manifest.council_id, manifest.current_round, myName, body);
|
|
4825
|
-
emitCouncilStateEvent("
|
|
4826
|
-
round_no: manifest.current_round,
|
|
4827
|
-
member: myName,
|
|
4828
|
-
body_summary: body.length > 1000 ? `${body.slice(0, 997)}...` : body,
|
|
4829
|
-
});
|
|
5774
|
+
emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
|
|
4830
5775
|
|
|
4831
5776
|
// Update manifest: if all members have now contributed, flip round_status.
|
|
4832
5777
|
const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
|
|
@@ -4836,14 +5781,10 @@ function runCouncilContribute(
|
|
|
4836
5781
|
if (allIn) {
|
|
4837
5782
|
nextManifest = { ...manifest, round_status: "collected" };
|
|
4838
5783
|
writeManifest(nextManifest);
|
|
4839
|
-
emitCouncilStateEvent("
|
|
4840
|
-
round_no: nextManifest.current_round,
|
|
4841
|
-
});
|
|
5784
|
+
emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
|
|
4842
5785
|
if (manifest.auto_advance) {
|
|
4843
5786
|
nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
|
|
4844
|
-
emitCouncilStateEvent("
|
|
4845
|
-
round_no: nextManifest.current_round,
|
|
4846
|
-
});
|
|
5787
|
+
emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
|
|
4847
5788
|
autoAdvanced = true;
|
|
4848
5789
|
}
|
|
4849
5790
|
}
|
|
@@ -4934,7 +5875,7 @@ function runCouncilPrompt(
|
|
|
4934
5875
|
try {
|
|
4935
5876
|
const myOwner = resolveOwner();
|
|
4936
5877
|
if (myOwner) {
|
|
4937
|
-
const myHb =
|
|
5878
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4938
5879
|
if (myHb?.name) actualName = normalizeAgentName(myHb.name);
|
|
4939
5880
|
}
|
|
4940
5881
|
} catch {
|
|
@@ -4950,7 +5891,7 @@ function runCouncilPrompt(
|
|
|
4950
5891
|
});
|
|
4951
5892
|
process.exit(1);
|
|
4952
5893
|
}
|
|
4953
|
-
const myHb =
|
|
5894
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4954
5895
|
if (!myHb?.name) {
|
|
4955
5896
|
emit.error({
|
|
4956
5897
|
code: "no_self_name",
|
|
@@ -5115,12 +6056,8 @@ function runCouncilAdvance(id: string, opts: { force?: boolean; json?: boolean }
|
|
|
5115
6056
|
}
|
|
5116
6057
|
|
|
5117
6058
|
const next = advanceCouncil(manifest, !!opts.force);
|
|
5118
|
-
emitCouncilStateEvent("
|
|
5119
|
-
|
|
5120
|
-
});
|
|
5121
|
-
emitCouncilStateEvent("council.round_open", next, {
|
|
5122
|
-
round_no: next.current_round,
|
|
5123
|
-
});
|
|
6059
|
+
emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
|
|
6060
|
+
emitCouncilStateEvent(next, "round_open", "round_closed");
|
|
5124
6061
|
|
|
5125
6062
|
emit.data({
|
|
5126
6063
|
rows: [
|
|
@@ -5166,7 +6103,7 @@ function advanceCouncil(manifest: CouncilManifest, force: boolean): CouncilManif
|
|
|
5166
6103
|
// (Convener already knows; we skip pinging them if they convened it from
|
|
5167
6104
|
// their own session.)
|
|
5168
6105
|
const myOwner = resolveOwner();
|
|
5169
|
-
const myName = myOwner ? normalizeAgentName(
|
|
6106
|
+
const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
|
|
5170
6107
|
for (const memberName of next.members) {
|
|
5171
6108
|
if (memberName === myName) continue;
|
|
5172
6109
|
const bareName = memberName.replace(/^agent-/, "");
|
|
@@ -5190,7 +6127,7 @@ function advanceCouncil(manifest: CouncilManifest, force: boolean): CouncilManif
|
|
|
5190
6127
|
// Hard cap on box width. Values longer than the per-row budget word-wrap to
|
|
5191
6128
|
// continuation lines (blank key column, value resumes indented). Picked to
|
|
5192
6129
|
// stay readable in narrow terminals + chat clients while giving long
|
|
5193
|
-
//
|
|
6130
|
+
// Task values need room to breathe.
|
|
5194
6131
|
const MAX_BOX_CONTENT_WIDTH = 100;
|
|
5195
6132
|
|
|
5196
6133
|
function formatBox(title: string, rows: Array<[string, string]>): string {
|