harnery 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1220 -588
- 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 +305 -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 +155 -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 +1512 -592
- 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 +351 -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 +165 -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)"}`);
|
|
1260
1506
|
}
|
|
1261
|
-
|
|
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}`);
|
|
1512
|
+
}
|
|
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,7 +1848,7 @@ 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
|
|
@@ -1631,19 +1860,6 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1631
1860
|
: null;
|
|
1632
1861
|
const firstOfSession = suggestedName !== null;
|
|
1633
1862
|
|
|
1634
|
-
emitCanonical({
|
|
1635
|
-
type: "state.task_set",
|
|
1636
|
-
owner: myOwner,
|
|
1637
|
-
session: hb?.session_id ?? myOwner,
|
|
1638
|
-
adapter: normalizeAdapter(hb?.platform),
|
|
1639
|
-
data: {
|
|
1640
|
-
task,
|
|
1641
|
-
cleared: !task || task.length === 0,
|
|
1642
|
-
first_of_session: firstOfSession,
|
|
1643
|
-
suggested_session_name: suggestedName,
|
|
1644
|
-
},
|
|
1645
|
-
});
|
|
1646
|
-
|
|
1647
1863
|
emit.data({
|
|
1648
1864
|
instance_id: myOwner,
|
|
1649
1865
|
name: hb?.name ?? null,
|
|
@@ -1656,7 +1872,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1656
1872
|
// exists; this result is the moment the model holds the string.
|
|
1657
1873
|
...(suggestedName
|
|
1658
1874
|
? {
|
|
1659
|
-
note:
|
|
1875
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1660
1876
|
}
|
|
1661
1877
|
: {}),
|
|
1662
1878
|
});
|
|
@@ -1683,7 +1899,7 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1683
1899
|
return;
|
|
1684
1900
|
}
|
|
1685
1901
|
if (!opts.sessionId) ensureCursorSession(root);
|
|
1686
|
-
const myOwner = opts.sessionId ?? resolveOwner();
|
|
1902
|
+
const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1687
1903
|
if (!myOwner) {
|
|
1688
1904
|
emit.error({
|
|
1689
1905
|
code: "no_pidmap_entry",
|
|
@@ -1694,11 +1910,40 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1694
1910
|
return;
|
|
1695
1911
|
}
|
|
1696
1912
|
|
|
1697
|
-
|
|
1913
|
+
let hb = readCurrentCoordinationRow(myOwner);
|
|
1914
|
+
let reopenedGeneration: ReturnType<typeof reopenLiveCoordinationGenerationV3> | undefined;
|
|
1698
1915
|
if (!hb) {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1916
|
+
if (state !== "active") {
|
|
1917
|
+
emit.error({
|
|
1918
|
+
code: "no_live_generation",
|
|
1919
|
+
message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
|
|
1920
|
+
});
|
|
1921
|
+
process.exitCode = 1;
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
reopenedGeneration = reopenLiveCoordinationGenerationV3({
|
|
1926
|
+
coordRoot: root,
|
|
1927
|
+
owner: myOwner,
|
|
1928
|
+
nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
|
|
1929
|
+
});
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
emit.error({
|
|
1932
|
+
code: "lifecycle_reopen_failed",
|
|
1933
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1934
|
+
});
|
|
1935
|
+
process.exitCode = 1;
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
hb = readCurrentCoordinationRow(myOwner);
|
|
1939
|
+
if (!hb) {
|
|
1940
|
+
emit.error({
|
|
1941
|
+
code: "lifecycle_reopen_failed",
|
|
1942
|
+
message: "the fresh generation was recorded but did not project as live",
|
|
1943
|
+
});
|
|
1944
|
+
process.exitCode = 1;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1702
1947
|
}
|
|
1703
1948
|
if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
|
|
1704
1949
|
emit.error({
|
|
@@ -1729,6 +1974,22 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1729
1974
|
|
|
1730
1975
|
const priorState: TaskState = hb.task_state ?? "active";
|
|
1731
1976
|
const priorReason = hb.task_state_reason || undefined;
|
|
1977
|
+
if (reopenedGeneration) {
|
|
1978
|
+
emit.data({
|
|
1979
|
+
instance_id: myOwner,
|
|
1980
|
+
task_state: "active",
|
|
1981
|
+
prior_state: null,
|
|
1982
|
+
reason: null,
|
|
1983
|
+
changed: true,
|
|
1984
|
+
generation_reopened: true,
|
|
1985
|
+
prior_generation_id: reopenedGeneration.prior_generation_id,
|
|
1986
|
+
generation_id: reopenedGeneration.generation_id,
|
|
1987
|
+
name_reminted: false,
|
|
1988
|
+
suggested_session_name: null,
|
|
1989
|
+
git_finalization_checked: false,
|
|
1990
|
+
});
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1732
1993
|
if (priorState === state && priorReason === reason) {
|
|
1733
1994
|
emit.data({
|
|
1734
1995
|
instance_id: myOwner,
|
|
@@ -1743,7 +2004,11 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1743
2004
|
|
|
1744
2005
|
let finalization: GitFinalizationResult | null = null;
|
|
1745
2006
|
if (state === "done") {
|
|
1746
|
-
const history = readSessionWriteClaims(
|
|
2007
|
+
const history = readSessionWriteClaims(
|
|
2008
|
+
root,
|
|
2009
|
+
hb.instance_id,
|
|
2010
|
+
nativeSessionIdentity(hb, myOwner),
|
|
2011
|
+
);
|
|
1747
2012
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1748
2013
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1749
2014
|
claimHistoryComplete: history.complete,
|
|
@@ -1761,19 +2026,16 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1761
2026
|
}
|
|
1762
2027
|
}
|
|
1763
2028
|
|
|
1764
|
-
const suggestedName = buildLifecycleSuggestedName(hb.
|
|
2029
|
+
const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
|
|
1765
2030
|
const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
|
|
1766
|
-
const emitted =
|
|
1767
|
-
type: "state.task_state",
|
|
2031
|
+
const emitted = emitEventV3({
|
|
1768
2032
|
owner: myOwner,
|
|
1769
|
-
session: hb
|
|
2033
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1770
2034
|
adapter: normalizeAdapter(hb.platform),
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
name_reminted: nameReminted,
|
|
1776
|
-
git_finalization_checked: finalization !== null,
|
|
2035
|
+
observation: {
|
|
2036
|
+
event_type: "coord.lifecycle_changed",
|
|
2037
|
+
new_state: state,
|
|
2038
|
+
...(reason ? { reason } : {}),
|
|
1777
2039
|
...(nameReminted ? { suggested_session_name: suggestedName } : {}),
|
|
1778
2040
|
},
|
|
1779
2041
|
});
|
|
@@ -1797,7 +2059,7 @@ function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: str
|
|
|
1797
2059
|
git_finalization_checked: finalization !== null,
|
|
1798
2060
|
...(nameReminted
|
|
1799
2061
|
? {
|
|
1800
|
-
note:
|
|
2062
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1801
2063
|
}
|
|
1802
2064
|
: {}),
|
|
1803
2065
|
});
|
|
@@ -1829,7 +2091,7 @@ function runSuggestName(
|
|
|
1829
2091
|
process.exit(1);
|
|
1830
2092
|
}
|
|
1831
2093
|
|
|
1832
|
-
const hb =
|
|
2094
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1833
2095
|
const agentName = hb?.name || "unknown";
|
|
1834
2096
|
// Description resolution: an explicit arg wins; with no arg, fall back to the
|
|
1835
2097
|
// agent's current declared task, so a bare `suggest-name` reprints the running
|
|
@@ -1867,7 +2129,12 @@ function runSuggestName(
|
|
|
1867
2129
|
process.stdout.write(`${suggestedName}\n`); // lint-ok-emission: chat-paste path
|
|
1868
2130
|
}
|
|
1869
2131
|
|
|
1870
|
-
function runStatus(opts: {
|
|
2132
|
+
function runStatus(opts: {
|
|
2133
|
+
endTurn?: boolean;
|
|
2134
|
+
endSession?: boolean;
|
|
2135
|
+
json?: boolean;
|
|
2136
|
+
sessionId?: string;
|
|
2137
|
+
}): void {
|
|
1871
2138
|
const root = monorepoRoot();
|
|
1872
2139
|
if (!root) {
|
|
1873
2140
|
emit.error({
|
|
@@ -1876,6 +2143,14 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1876
2143
|
});
|
|
1877
2144
|
process.exit(1);
|
|
1878
2145
|
}
|
|
2146
|
+
if (opts.endSession && !opts.endTurn) {
|
|
2147
|
+
emit.error({
|
|
2148
|
+
code: "end_session_requires_end_turn",
|
|
2149
|
+
message: "--end-session requires --end-turn so Git finalization is verified first",
|
|
2150
|
+
});
|
|
2151
|
+
process.exitCode = 1;
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
1879
2154
|
|
|
1880
2155
|
// Identity resolution: prefer explicit --session-id (hook-friendly), fall
|
|
1881
2156
|
// back to ppid walk for interactive shell usage.
|
|
@@ -1890,18 +2165,30 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1890
2165
|
process.exit(1);
|
|
1891
2166
|
}
|
|
1892
2167
|
|
|
1893
|
-
const hb =
|
|
2168
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1894
2169
|
if (!hb) {
|
|
1895
2170
|
emit.error({
|
|
1896
|
-
code: "
|
|
1897
|
-
message:
|
|
2171
|
+
code: "no_live_generation",
|
|
2172
|
+
message: noLiveGenerationMessage(myOwner),
|
|
1898
2173
|
});
|
|
1899
2174
|
process.exit(1);
|
|
1900
2175
|
}
|
|
1901
2176
|
|
|
2177
|
+
// Report-only run quality: cheap config/cursor due check first, then a
|
|
2178
|
+
// non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
|
|
2179
|
+
const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
|
|
2180
|
+
const quality = qualityForStatus(
|
|
2181
|
+
qualityEvaluation.config.requested_mode,
|
|
2182
|
+
qualityEvaluation.snapshot,
|
|
2183
|
+
);
|
|
2184
|
+
|
|
1902
2185
|
let finalization: GitFinalizationResult | null = null;
|
|
1903
2186
|
if (opts.endTurn) {
|
|
1904
|
-
const history = readSessionWriteClaims(
|
|
2187
|
+
const history = readSessionWriteClaims(
|
|
2188
|
+
root,
|
|
2189
|
+
hb.instance_id,
|
|
2190
|
+
nativeSessionIdentity(hb, myOwner),
|
|
2191
|
+
);
|
|
1905
2192
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1906
2193
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1907
2194
|
claimHistoryComplete: history.complete,
|
|
@@ -1916,48 +2203,60 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1916
2203
|
}
|
|
1917
2204
|
}
|
|
1918
2205
|
|
|
1919
|
-
|
|
1920
|
-
type: "state.status_checked",
|
|
2206
|
+
emitEventV3({
|
|
1921
2207
|
owner: myOwner,
|
|
1922
|
-
session: hb
|
|
2208
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1923
2209
|
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,
|
|
2210
|
+
observation: {
|
|
2211
|
+
event_type: "coord.status_observed",
|
|
2212
|
+
status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
|
|
1929
2213
|
},
|
|
1930
2214
|
});
|
|
1931
2215
|
|
|
1932
|
-
|
|
2216
|
+
let sessionEnd:
|
|
2217
|
+
| { state: "queued" | "already_requested"; request_id: string }
|
|
2218
|
+
| { state: "recorded" | "already_ended"; terminal_event_id?: string }
|
|
2219
|
+
| undefined;
|
|
2220
|
+
if (opts.endSession) {
|
|
2221
|
+
const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
|
|
2222
|
+
const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(
|
|
2223
|
+
({ state }) => state.instance_id === canonicalInstanceId,
|
|
2224
|
+
);
|
|
2225
|
+
if (records.length !== 1 || !records[0]) {
|
|
2226
|
+
emit.error({
|
|
2227
|
+
code: "session_identity_ambiguous",
|
|
2228
|
+
message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
|
|
2229
|
+
});
|
|
2230
|
+
process.exitCode = 1;
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const state = records[0].state;
|
|
2234
|
+
const requested = requestSessionEndExplicitV3({
|
|
2235
|
+
coordRoot: root,
|
|
2236
|
+
instance_id: state.instance_id,
|
|
2237
|
+
generation_id: state.generation_id,
|
|
2238
|
+
outcome: "succeeded",
|
|
2239
|
+
coordination_finalized: true,
|
|
2240
|
+
});
|
|
2241
|
+
if (requested.state === "queued" || requested.state === "already_requested") {
|
|
2242
|
+
sessionEnd = { state: requested.state, request_id: requested.request.request_id };
|
|
2243
|
+
} else if (requested.state === "recorded") {
|
|
2244
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
|
|
2245
|
+
} else if (requested.state === "already_ended") {
|
|
2246
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
|
|
2247
|
+
} else {
|
|
2248
|
+
emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
|
|
2249
|
+
process.exitCode = 1;
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1933
2255
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1934
2256
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1935
2257
|
: 0;
|
|
1936
2258
|
|
|
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
|
-
}
|
|
2259
|
+
const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
|
|
1961
2260
|
|
|
1962
2261
|
// Sort: file-holders first (by file count desc), then idle peers by recency.
|
|
1963
2262
|
livePeers.sort((a, b) => {
|
|
@@ -1977,7 +2276,7 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1977
2276
|
const remoteMachines = readRemoteMachines(root);
|
|
1978
2277
|
const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
|
|
1979
2278
|
|
|
1980
|
-
const ctxUsage = readContextUsage(hb.session_id, hb.platform);
|
|
2279
|
+
const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
|
|
1981
2280
|
let ctxStr: string;
|
|
1982
2281
|
if (!ctxUsage) {
|
|
1983
2282
|
ctxStr = "unavailable";
|
|
@@ -2014,6 +2313,7 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2014
2313
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
2015
2314
|
activity_source: hb.activity_source ?? null,
|
|
2016
2315
|
task_state: taskStateOf(hb),
|
|
2316
|
+
task_state_scope: "current" as const,
|
|
2017
2317
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
2018
2318
|
task_state_reason: hb.task_state_reason ?? null,
|
|
2019
2319
|
files_held: filesHeld,
|
|
@@ -2040,7 +2340,9 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2040
2340
|
context_window: ctxUsage?.window ?? null,
|
|
2041
2341
|
timestamp_iso: new Date().toISOString(),
|
|
2042
2342
|
timestamp_local: timeStr,
|
|
2343
|
+
...(quality ? { quality } : {}),
|
|
2043
2344
|
...(finalization ? { finalization } : {}),
|
|
2345
|
+
...(sessionEnd ? { session_end: sessionEnd } : {}),
|
|
2044
2346
|
};
|
|
2045
2347
|
|
|
2046
2348
|
if (opts.json) {
|
|
@@ -2058,10 +2360,7 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2058
2360
|
["peers", peersStr],
|
|
2059
2361
|
["time", timeStr],
|
|
2060
2362
|
];
|
|
2061
|
-
//
|
|
2062
|
-
if (hb.turn_summary && hb.turn_summary.length > 0) {
|
|
2063
|
-
rows.splice(1, 0, ["last turn", hb.turn_summary]);
|
|
2064
|
-
}
|
|
2363
|
+
// Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
|
|
2065
2364
|
if (hb.task && hb.task.length > 0) {
|
|
2066
2365
|
rows.splice(1, 0, ["task", hb.task]);
|
|
2067
2366
|
}
|
|
@@ -2076,11 +2375,59 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
2076
2375
|
: `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
|
|
2077
2376
|
rows.splice(idx, 0, ["council", summary]);
|
|
2078
2377
|
}
|
|
2378
|
+
if (quality) {
|
|
2379
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
2380
|
+
rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
|
|
2381
|
+
}
|
|
2382
|
+
if (sessionEnd) {
|
|
2383
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
2384
|
+
rows.splice(idx, 0, [
|
|
2385
|
+
"session end",
|
|
2386
|
+
sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
|
|
2387
|
+
? "queued after this turn"
|
|
2388
|
+
: "recorded",
|
|
2389
|
+
]);
|
|
2390
|
+
}
|
|
2079
2391
|
// Box rendering needs predictable stdout regardless of TTY/pipe detection:
|
|
2080
2392
|
// agent runs this via Bash (no TTY) and pastes captured stdout into chat.
|
|
2081
2393
|
process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
|
|
2082
2394
|
}
|
|
2083
2395
|
|
|
2396
|
+
function qualityForStatus(
|
|
2397
|
+
mode: "off" | "shadow" | "report",
|
|
2398
|
+
snapshot: RunQualitySnapshot | null,
|
|
2399
|
+
): {
|
|
2400
|
+
status: RunQualityStatus;
|
|
2401
|
+
signal_ids: string[];
|
|
2402
|
+
evaluated_at: string | null;
|
|
2403
|
+
fresh: boolean;
|
|
2404
|
+
} | null {
|
|
2405
|
+
if (mode !== "report") return null;
|
|
2406
|
+
const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
|
|
2407
|
+
if (!snapshot || !fresh) {
|
|
2408
|
+
return {
|
|
2409
|
+
status: "unknown",
|
|
2410
|
+
signal_ids: [],
|
|
2411
|
+
evaluated_at: snapshot?.evaluated_at ?? null,
|
|
2412
|
+
fresh: false,
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
return {
|
|
2416
|
+
status: snapshot.status,
|
|
2417
|
+
signal_ids: snapshot.signals
|
|
2418
|
+
.filter((signal) => signal.state === "active" && signal.severity !== "none")
|
|
2419
|
+
.map((signal) => signal.id)
|
|
2420
|
+
.sort()
|
|
2421
|
+
.slice(0, 3),
|
|
2422
|
+
evaluated_at: snapshot.evaluated_at,
|
|
2423
|
+
fresh: true,
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function formatRunQuality(status: RunQualityStatus, signalIds: string[]): string {
|
|
2428
|
+
return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2084
2431
|
function formatList(items: string[], cap: number, emptyLabel: string): string {
|
|
2085
2432
|
if (items.length === 0) return emptyLabel;
|
|
2086
2433
|
if (items.length <= cap) return items.join(", ");
|
|
@@ -2089,7 +2436,7 @@ function formatList(items: string[], cap: number, emptyLabel: string): string {
|
|
|
2089
2436
|
}
|
|
2090
2437
|
|
|
2091
2438
|
function formatPeers(
|
|
2092
|
-
peers:
|
|
2439
|
+
peers: StatusPeerHeartbeat[],
|
|
2093
2440
|
cap: number,
|
|
2094
2441
|
staleCount: number,
|
|
2095
2442
|
remoteMachines: RemoteMachine[] = [],
|
|
@@ -2122,6 +2469,38 @@ function formatPeers(
|
|
|
2122
2469
|
return main;
|
|
2123
2470
|
}
|
|
2124
2471
|
|
|
2472
|
+
type StatusPeerHeartbeat = {
|
|
2473
|
+
instance_id: string;
|
|
2474
|
+
last_heartbeat: string;
|
|
2475
|
+
files_touched?: string[];
|
|
2476
|
+
name?: string;
|
|
2477
|
+
platform?: string;
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
export function collectStatusPeerHealth(
|
|
2481
|
+
root: string,
|
|
2482
|
+
myOwner: string,
|
|
2483
|
+
nowMs = Date.now(),
|
|
2484
|
+
): { livePeers: StatusPeerHeartbeat[]; stale: number } {
|
|
2485
|
+
let rows: StatusPeerHeartbeat[] = [];
|
|
2486
|
+
try {
|
|
2487
|
+
rows = readLiveCoordinationRows(root);
|
|
2488
|
+
} catch {
|
|
2489
|
+
// V3 authority failures must not resurrect disposable cache rows.
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
|
|
2493
|
+
const livePeers: StatusPeerHeartbeat[] = [];
|
|
2494
|
+
let stale = 0;
|
|
2495
|
+
for (const peer of rows) {
|
|
2496
|
+
if (peer.instance_id === myOwner) continue;
|
|
2497
|
+
const observedAtMs = Date.parse(peer.last_heartbeat);
|
|
2498
|
+
if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs) livePeers.push(peer);
|
|
2499
|
+
else stale += 1;
|
|
2500
|
+
}
|
|
2501
|
+
return { livePeers, stale };
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2125
2504
|
function fmtTokens(n: number): string {
|
|
2126
2505
|
if (n < 1000) return `${n}`;
|
|
2127
2506
|
if (n < 1000000) return `${Math.round(n / 1000)}K`;
|
|
@@ -2371,46 +2750,69 @@ interface HealEvent {
|
|
|
2371
2750
|
platform: string;
|
|
2372
2751
|
}
|
|
2373
2752
|
|
|
2374
|
-
/**
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
*/
|
|
2378
|
-
interface CanonicalEvent {
|
|
2379
|
-
event_type: string;
|
|
2753
|
+
/** Bounded diagnostic event shape projected from the canonical V3 ledger. */
|
|
2754
|
+
export interface CanonicalEvent {
|
|
2755
|
+
event_type: EventTypeV3;
|
|
2380
2756
|
ts: string;
|
|
2381
2757
|
instance_id?: string;
|
|
2382
2758
|
adapter?: string;
|
|
2383
|
-
|
|
2759
|
+
payload?: Record<string, unknown>;
|
|
2384
2760
|
}
|
|
2385
2761
|
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2762
|
+
export interface AgentDiagnosticEventRead {
|
|
2763
|
+
source: "v3";
|
|
2764
|
+
authoritative: boolean;
|
|
2765
|
+
reason?: string;
|
|
2766
|
+
truncated: boolean;
|
|
2767
|
+
bytes: number;
|
|
2768
|
+
events: CanonicalEvent[];
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
/** Read validated canonical V3 events. */
|
|
2772
|
+
export function readAgentDiagnosticEventsInWindow(
|
|
2773
|
+
root: string,
|
|
2774
|
+
cutoffMs: number,
|
|
2775
|
+
): AgentDiagnosticEventRead {
|
|
2776
|
+
const control = readEventV3ControlState(root);
|
|
2777
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
2778
|
+
const ledger = readLedgerV3(root);
|
|
2779
|
+
if (!ledger.complete) {
|
|
2780
|
+
return {
|
|
2781
|
+
source: "v3",
|
|
2782
|
+
authoritative: false,
|
|
2783
|
+
reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
|
|
2784
|
+
truncated: false,
|
|
2785
|
+
bytes: ledger.bytes,
|
|
2786
|
+
events: [],
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
const events: CanonicalEvent[] = [];
|
|
2790
|
+
for (const { event } of ledger.events) {
|
|
2791
|
+
const tsMs = Date.parse(event.time.recorded_at);
|
|
2407
2792
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs) continue;
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2793
|
+
events.push({
|
|
2794
|
+
event_type: event.event_type,
|
|
2795
|
+
ts: event.time.recorded_at,
|
|
2796
|
+
instance_id: event.scope.instance_id,
|
|
2797
|
+
payload: event.payload as Record<string, unknown>,
|
|
2798
|
+
});
|
|
2411
2799
|
}
|
|
2800
|
+
return {
|
|
2801
|
+
source: "v3",
|
|
2802
|
+
authoritative: true,
|
|
2803
|
+
truncated: false,
|
|
2804
|
+
bytes: ledger.bytes,
|
|
2805
|
+
events,
|
|
2806
|
+
};
|
|
2412
2807
|
}
|
|
2413
|
-
return
|
|
2808
|
+
return {
|
|
2809
|
+
source: "v3",
|
|
2810
|
+
authoritative: false,
|
|
2811
|
+
reason: `V3 control state is ${control.state}`,
|
|
2812
|
+
truncated: false,
|
|
2813
|
+
bytes: 0,
|
|
2814
|
+
events: [],
|
|
2815
|
+
};
|
|
2414
2816
|
}
|
|
2415
2817
|
|
|
2416
2818
|
/** Normalize adapter event data into the heartbeat platform value. */
|
|
@@ -2418,19 +2820,21 @@ function adapterToPlatform(adapter: string | undefined): string {
|
|
|
2418
2820
|
if (adapter === "claude-code") return "claude-code";
|
|
2419
2821
|
if (adapter === "cursor") return "cursor";
|
|
2420
2822
|
if (adapter === "codex") return "codex";
|
|
2421
|
-
return "
|
|
2823
|
+
return "unknown";
|
|
2422
2824
|
}
|
|
2423
2825
|
|
|
2424
2826
|
/** Project a canonical health.* event into the HealEvent shape the aggregators
|
|
2425
2827
|
* already consume. Returns null for non-heal events. instance_id → display name
|
|
2426
2828
|
* via `nameById` (full-UUID keyed). */
|
|
2427
2829
|
function canonicalToHealEvent(ev: CanonicalEvent, nameById: Map<string, string>): HealEvent | null {
|
|
2428
|
-
if (ev.event_type !== "health.
|
|
2830
|
+
if (ev.event_type !== "health.observed") {
|
|
2429
2831
|
return null;
|
|
2430
2832
|
}
|
|
2431
|
-
const
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2833
|
+
const data = ev.payload ?? {};
|
|
2834
|
+
const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
|
|
2835
|
+
if (subsystem !== "pidmap" && subsystem !== "heartbeat") return null;
|
|
2836
|
+
const kind = subsystem;
|
|
2837
|
+
const reason: "missing" | "stale" = data.condition === "stale" ? "stale" : "missing";
|
|
2434
2838
|
const instanceId = ev.instance_id ?? "";
|
|
2435
2839
|
const name = nameById.get(instanceId);
|
|
2436
2840
|
const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
|
|
@@ -2511,13 +2915,12 @@ function runHealEvents(opts: {
|
|
|
2511
2915
|
process.exit(1);
|
|
2512
2916
|
}
|
|
2513
2917
|
|
|
2514
|
-
// Heal telemetry
|
|
2515
|
-
// (health.pidmap_heal / health.heartbeat_heal), emitted by the writer on
|
|
2516
|
-
// actual self-heal writes.
|
|
2918
|
+
// Heal telemetry is read from the canonical V3 ledger.
|
|
2517
2919
|
const cutoffMs = Date.now() - sinceSecs * 1000;
|
|
2518
2920
|
const nameById = buildNameById(root);
|
|
2519
2921
|
const events: HealEvent[] = [];
|
|
2520
|
-
|
|
2922
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
2923
|
+
for (const ev of diagnosticEvents.events) {
|
|
2521
2924
|
const heal = canonicalToHealEvent(ev, nameById);
|
|
2522
2925
|
if (heal) events.push(heal);
|
|
2523
2926
|
}
|
|
@@ -2554,6 +2957,11 @@ function runHealEvents(opts: {
|
|
|
2554
2957
|
|
|
2555
2958
|
const data = {
|
|
2556
2959
|
since: opts.since,
|
|
2960
|
+
telemetry: {
|
|
2961
|
+
source: diagnosticEvents.source,
|
|
2962
|
+
authoritative: diagnosticEvents.authoritative,
|
|
2963
|
+
reason: diagnosticEvents.reason,
|
|
2964
|
+
},
|
|
2557
2965
|
total: events.length,
|
|
2558
2966
|
by_reason: byReason,
|
|
2559
2967
|
by_kind: byKind,
|
|
@@ -2565,7 +2973,17 @@ function runHealEvents(opts: {
|
|
|
2565
2973
|
|
|
2566
2974
|
if (opts.csv) {
|
|
2567
2975
|
emit.config({ format: "csv" });
|
|
2568
|
-
emit.data(
|
|
2976
|
+
emit.data(
|
|
2977
|
+
data.telemetry.authoritative
|
|
2978
|
+
? recent
|
|
2979
|
+
: [
|
|
2980
|
+
{
|
|
2981
|
+
status: "unavailable",
|
|
2982
|
+
source: data.telemetry.source,
|
|
2983
|
+
reason: data.telemetry.reason,
|
|
2984
|
+
},
|
|
2985
|
+
],
|
|
2986
|
+
);
|
|
2569
2987
|
return;
|
|
2570
2988
|
}
|
|
2571
2989
|
if (opts.json) {
|
|
@@ -2580,6 +2998,13 @@ function runHealEvents(opts: {
|
|
|
2580
2998
|
lines.push(
|
|
2581
2999
|
`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`,
|
|
2582
3000
|
);
|
|
3001
|
+
if (!data.telemetry.authoritative) {
|
|
3002
|
+
lines.push(
|
|
3003
|
+
` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`,
|
|
3004
|
+
);
|
|
3005
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
2583
3008
|
lines.push("");
|
|
2584
3009
|
if (events.length === 0) {
|
|
2585
3010
|
lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
|
|
@@ -2631,7 +3056,13 @@ function runHealEvents(opts: {
|
|
|
2631
3056
|
interface HealthReport {
|
|
2632
3057
|
since: string;
|
|
2633
3058
|
generated_at: string;
|
|
3059
|
+
event_telemetry: {
|
|
3060
|
+
source: "v3";
|
|
3061
|
+
authoritative: boolean;
|
|
3062
|
+
reason?: string;
|
|
3063
|
+
};
|
|
2634
3064
|
active_agents: {
|
|
3065
|
+
source: "event-ledger-v3";
|
|
2635
3066
|
total: number;
|
|
2636
3067
|
by_platform: Record<string, number>;
|
|
2637
3068
|
by_kind: Record<string, number>;
|
|
@@ -2661,11 +3092,19 @@ interface HealthReport {
|
|
|
2661
3092
|
// bug (e.g. a stop-projection crash that caused ~200 errors/day until it was fixed).
|
|
2662
3093
|
hook_errors: {
|
|
2663
3094
|
total: number;
|
|
3095
|
+
last_1h: number;
|
|
3096
|
+
latest_at: string | null;
|
|
2664
3097
|
by_phase: Record<string, number>;
|
|
3098
|
+
by_error: Record<string, number>;
|
|
2665
3099
|
top: Array<{ phase: string; count: number; sample: string }>;
|
|
3100
|
+
top_errors: Array<{ error: string; count: number; phase: string }>;
|
|
3101
|
+
recent_top_errors: Array<{ error: string; count: number; phase: string }>;
|
|
2666
3102
|
};
|
|
2667
3103
|
// Canonical event stream growth + drain lag.
|
|
2668
3104
|
stream: {
|
|
3105
|
+
source: "v3";
|
|
3106
|
+
authoritative: boolean;
|
|
3107
|
+
reason?: string;
|
|
2669
3108
|
bytes: number;
|
|
2670
3109
|
lines: number;
|
|
2671
3110
|
cursor_backlog: number;
|
|
@@ -2677,31 +3116,269 @@ interface HealthReport {
|
|
|
2677
3116
|
count: number;
|
|
2678
3117
|
samples: string[];
|
|
2679
3118
|
};
|
|
3119
|
+
// V3 event-ledger producer health: open tool spans, pending finalization
|
|
3120
|
+
// requests, intake/diagnostics spool depth, span-count pressure.
|
|
3121
|
+
event_ledger: EventLedgerHealthV3;
|
|
2680
3122
|
anomalies: string[];
|
|
2681
3123
|
}
|
|
2682
3124
|
|
|
3125
|
+
/** Open-span soft watermark per producer state. The producer-state reader
|
|
3126
|
+
* hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
|
|
3127
|
+
* pressure at half that gives room to act before reads start failing. */
|
|
3128
|
+
const SPAN_PRESSURE_SOFT_WATERMARK = 128;
|
|
3129
|
+
|
|
3130
|
+
export type EventLedgerHealthV3 =
|
|
3131
|
+
| { state: "unavailable"; reason: string }
|
|
3132
|
+
| {
|
|
3133
|
+
state: "live";
|
|
3134
|
+
mode: "candidate" | "active";
|
|
3135
|
+
open_spans: {
|
|
3136
|
+
total: number;
|
|
3137
|
+
generations: Array<{
|
|
3138
|
+
instance_id: string;
|
|
3139
|
+
generation_id: string;
|
|
3140
|
+
adapter: string;
|
|
3141
|
+
span_count: number;
|
|
3142
|
+
/** False = spans are open with NO open turn: the orphan signature
|
|
3143
|
+
* (a turn ended without its tool spans being closed). */
|
|
3144
|
+
turn_open: boolean;
|
|
3145
|
+
}>;
|
|
3146
|
+
};
|
|
3147
|
+
pending_finalizations: Array<{
|
|
3148
|
+
request_id: string;
|
|
3149
|
+
trigger: string;
|
|
3150
|
+
generation_id: string;
|
|
3151
|
+
age_ms: number;
|
|
3152
|
+
allowed_open_span_count: number;
|
|
3153
|
+
}>;
|
|
3154
|
+
intake_spool: {
|
|
3155
|
+
total: number;
|
|
3156
|
+
groups: Array<{ adapter: string; session_hash: string; count: number }>;
|
|
3157
|
+
};
|
|
3158
|
+
diagnostics_spool: {
|
|
3159
|
+
total: number;
|
|
3160
|
+
last_24h: number;
|
|
3161
|
+
last_1h: number;
|
|
3162
|
+
latest_at: string | null;
|
|
3163
|
+
by_category: Record<string, { total: number; last_24h: number }>;
|
|
3164
|
+
recent_by_category: Record<string, { last_1h: number; latest_at: string | null }>;
|
|
3165
|
+
};
|
|
3166
|
+
span_pressure: Array<{ instance_id: string; generation_id: string; span_count: number }>;
|
|
3167
|
+
collection_errors: string[];
|
|
3168
|
+
};
|
|
3169
|
+
|
|
3170
|
+
function errorText(error: unknown): string {
|
|
3171
|
+
return error instanceof Error ? error.message : String(error);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/**
|
|
3175
|
+
* Read-only health counters for the V3 event ledger's producer surfaces. Never
|
|
3176
|
+
* mutates ledger state (no control repair, no spool drain) and never throws:
|
|
3177
|
+
* a non-live route returns `{ state: "unavailable" }`, and each sub-surface
|
|
3178
|
+
* that fails to read lands in `collection_errors` instead of aborting the rest.
|
|
3179
|
+
*/
|
|
3180
|
+
export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): EventLedgerHealthV3 {
|
|
3181
|
+
let control: ReturnType<typeof readEventV3ControlState>;
|
|
3182
|
+
try {
|
|
3183
|
+
control = readEventV3ControlState(root);
|
|
3184
|
+
} catch (error) {
|
|
3185
|
+
return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
|
|
3186
|
+
}
|
|
3187
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
3188
|
+
return {
|
|
3189
|
+
state: "unavailable",
|
|
3190
|
+
reason: `${control.state}: ${control.reason}`,
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
const collectionErrors: string[] = [];
|
|
3195
|
+
|
|
3196
|
+
// 1) Open tool spans per live (non-terminal) generation + span-count pressure.
|
|
3197
|
+
const generations: Extract<EventLedgerHealthV3, { state: "live" }>["open_spans"]["generations"] =
|
|
3198
|
+
[];
|
|
3199
|
+
const spanPressure: Extract<EventLedgerHealthV3, { state: "live" }>["span_pressure"] = [];
|
|
3200
|
+
try {
|
|
3201
|
+
for (const { state } of listHookProducerStateRecordsV3(root)) {
|
|
3202
|
+
if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
|
|
3203
|
+
spanPressure.push({
|
|
3204
|
+
instance_id: state.instance_id,
|
|
3205
|
+
generation_id: state.generation_id,
|
|
3206
|
+
span_count: state.spans.length,
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
if (state.spans.length === 0) continue;
|
|
3210
|
+
generations.push({
|
|
3211
|
+
instance_id: state.instance_id,
|
|
3212
|
+
generation_id: state.generation_id,
|
|
3213
|
+
adapter: state.adapter,
|
|
3214
|
+
span_count: state.spans.length,
|
|
3215
|
+
turn_open: Boolean(state.current_turn_id),
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// 2) Pending finalization requests with age + trigger.
|
|
3223
|
+
const pending: Extract<EventLedgerHealthV3, { state: "live" }>["pending_finalizations"] = [];
|
|
3224
|
+
try {
|
|
3225
|
+
for (const request of listSessionFinalizationRequestsV3(root)) {
|
|
3226
|
+
if (request.status !== "pending") continue;
|
|
3227
|
+
const observedMs = Date.parse(request.observed_at);
|
|
3228
|
+
pending.push({
|
|
3229
|
+
request_id: request.request_id,
|
|
3230
|
+
trigger: request.trigger,
|
|
3231
|
+
generation_id: request.generation_id,
|
|
3232
|
+
age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
|
|
3233
|
+
allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
} catch (error) {
|
|
3237
|
+
collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
|
|
3241
|
+
let intakeTotal = 0;
|
|
3242
|
+
const intakeGroups: Extract<EventLedgerHealthV3, { state: "live" }>["intake_spool"]["groups"] =
|
|
3243
|
+
[];
|
|
3244
|
+
try {
|
|
3245
|
+
for (const group of listHookIntakeGroupsV3(root)) {
|
|
3246
|
+
const count = listHookIntakeRecordsV3(group.directory).length;
|
|
3247
|
+
if (count === 0) continue;
|
|
3248
|
+
intakeTotal += count;
|
|
3249
|
+
intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
|
|
3250
|
+
}
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// 4) Diagnostics spool counts by category. Filenames only (never the
|
|
3256
|
+
// contents): `<category>-<orderkey>.json` where the orderkey leads with a
|
|
3257
|
+
// zero-padded epoch-ms, which also gives the last-24h split for free.
|
|
3258
|
+
const byCategory: Record<string, { total: number; last_24h: number }> = {};
|
|
3259
|
+
const recentByCategory: Extract<
|
|
3260
|
+
EventLedgerHealthV3,
|
|
3261
|
+
{ state: "live" }
|
|
3262
|
+
>["diagnostics_spool"]["recent_by_category"] = {};
|
|
3263
|
+
let diagnosticsTotal = 0;
|
|
3264
|
+
let diagnostics24h = 0;
|
|
3265
|
+
let diagnostics1h = 0;
|
|
3266
|
+
let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
|
|
3267
|
+
try {
|
|
3268
|
+
const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
|
|
3269
|
+
if (existsSync(diagnosticsDir)) {
|
|
3270
|
+
const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
|
|
3271
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
3272
|
+
for (const name of readdirSync(diagnosticsDir)) {
|
|
3273
|
+
const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
|
|
3274
|
+
const category = match?.[1];
|
|
3275
|
+
const epochMs = Number(match?.[2]);
|
|
3276
|
+
if (!category) continue;
|
|
3277
|
+
const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
|
|
3278
|
+
const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
|
|
3279
|
+
byCategory[category] = entry;
|
|
3280
|
+
recentByCategory[category] = recent;
|
|
3281
|
+
entry.total += 1;
|
|
3282
|
+
diagnosticsTotal += 1;
|
|
3283
|
+
if (Number.isFinite(epochMs)) {
|
|
3284
|
+
const recordedAt = new Date(epochMs).toISOString();
|
|
3285
|
+
if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
|
|
3286
|
+
recent.latest_at = recordedAt;
|
|
3287
|
+
}
|
|
3288
|
+
latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
|
|
3289
|
+
if (epochMs >= dayAgoMs) {
|
|
3290
|
+
entry.last_24h += 1;
|
|
3291
|
+
diagnostics24h += 1;
|
|
3292
|
+
}
|
|
3293
|
+
if (epochMs >= hourAgoMs) {
|
|
3294
|
+
recent.last_1h += 1;
|
|
3295
|
+
diagnostics1h += 1;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
} catch (error) {
|
|
3301
|
+
collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
return {
|
|
3305
|
+
state: "live",
|
|
3306
|
+
mode: control.state,
|
|
3307
|
+
open_spans: {
|
|
3308
|
+
total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
|
|
3309
|
+
generations,
|
|
3310
|
+
},
|
|
3311
|
+
pending_finalizations: pending,
|
|
3312
|
+
intake_spool: { total: intakeTotal, groups: intakeGroups },
|
|
3313
|
+
diagnostics_spool: {
|
|
3314
|
+
total: diagnosticsTotal,
|
|
3315
|
+
last_24h: diagnostics24h,
|
|
3316
|
+
last_1h: diagnostics1h,
|
|
3317
|
+
latest_at: Number.isFinite(latestDiagnosticMs)
|
|
3318
|
+
? new Date(latestDiagnosticMs).toISOString()
|
|
3319
|
+
: null,
|
|
3320
|
+
by_category: byCategory,
|
|
3321
|
+
recent_by_category: recentByCategory,
|
|
3322
|
+
},
|
|
3323
|
+
span_pressure: spanPressure,
|
|
3324
|
+
collection_errors: collectionErrors,
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
|
|
2683
3328
|
/** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
|
|
2684
|
-
* window
|
|
2685
|
-
*
|
|
2686
|
-
function readHookErrors(
|
|
3329
|
+
* window by exact error and phase, while separating current-hour failures from
|
|
3330
|
+
* historical rows. Each line is {ts, error, phase, ...}. */
|
|
3331
|
+
export function readHookErrors(
|
|
2687
3332
|
root: string,
|
|
2688
3333
|
cutoffMs: number,
|
|
3334
|
+
nowMs = Date.now(),
|
|
2689
3335
|
): {
|
|
2690
3336
|
total: number;
|
|
3337
|
+
last1h: number;
|
|
3338
|
+
latestAt: string | null;
|
|
2691
3339
|
byPhase: Record<string, number>;
|
|
3340
|
+
byError: Record<string, number>;
|
|
2692
3341
|
top: Array<{ phase: string; count: number; sample: string }>;
|
|
3342
|
+
topErrors: Array<{ error: string; count: number; phase: string }>;
|
|
3343
|
+
recentTopErrors: Array<{ error: string; count: number; phase: string }>;
|
|
2693
3344
|
} {
|
|
2694
3345
|
const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
|
|
2695
3346
|
const byPhase: Record<string, number> = {};
|
|
3347
|
+
const byError: Record<string, number> = {};
|
|
2696
3348
|
const sampleByPhase: Record<string, string> = {};
|
|
3349
|
+
const errorPhaseCounts = new Map<string, { error: string; phase: string; count: number }>();
|
|
3350
|
+
const recentErrorPhaseCounts = new Map<string, { error: string; phase: string; count: number }>();
|
|
2697
3351
|
let total = 0;
|
|
2698
|
-
|
|
3352
|
+
let last1h = 0;
|
|
3353
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
3354
|
+
if (!existsSync(p)) {
|
|
3355
|
+
return {
|
|
3356
|
+
total: 0,
|
|
3357
|
+
last1h: 0,
|
|
3358
|
+
latestAt: null,
|
|
3359
|
+
byPhase,
|
|
3360
|
+
byError,
|
|
3361
|
+
top: [],
|
|
3362
|
+
topErrors: [],
|
|
3363
|
+
recentTopErrors: [],
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
2699
3366
|
let raw: string;
|
|
2700
3367
|
try {
|
|
2701
3368
|
raw = readFileSync(p, "utf8");
|
|
2702
3369
|
} catch {
|
|
2703
|
-
return {
|
|
3370
|
+
return {
|
|
3371
|
+
total: 0,
|
|
3372
|
+
last1h: 0,
|
|
3373
|
+
latestAt: null,
|
|
3374
|
+
byPhase,
|
|
3375
|
+
byError,
|
|
3376
|
+
top: [],
|
|
3377
|
+
topErrors: [],
|
|
3378
|
+
recentTopErrors: [],
|
|
3379
|
+
};
|
|
2704
3380
|
}
|
|
3381
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2705
3382
|
for (const line of raw.split("\n")) {
|
|
2706
3383
|
if (!line.trim()) continue;
|
|
2707
3384
|
try {
|
|
@@ -2709,8 +3386,21 @@ function readHookErrors(
|
|
|
2709
3386
|
const tsMs = e.ts ? Date.parse(e.ts) : Number.NaN;
|
|
2710
3387
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs) continue;
|
|
2711
3388
|
const phase = e.phase ?? "(unknown)";
|
|
3389
|
+
const error = e.error ?? "(unknown)";
|
|
2712
3390
|
byPhase[phase] = (byPhase[phase] ?? 0) + 1;
|
|
3391
|
+
byError[error] = (byError[error] ?? 0) + 1;
|
|
2713
3392
|
if (!sampleByPhase[phase] && e.error) sampleByPhase[phase] = e.error;
|
|
3393
|
+
const errorPhaseKey = `${error}\0${phase}`;
|
|
3394
|
+
const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
3395
|
+
errorPhase.count += 1;
|
|
3396
|
+
errorPhaseCounts.set(errorPhaseKey, errorPhase);
|
|
3397
|
+
latestMs = Math.max(latestMs, tsMs);
|
|
3398
|
+
if (tsMs >= hourAgoMs) {
|
|
3399
|
+
last1h += 1;
|
|
3400
|
+
const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
3401
|
+
recent.count += 1;
|
|
3402
|
+
recentErrorPhaseCounts.set(errorPhaseKey, recent);
|
|
3403
|
+
}
|
|
2714
3404
|
total++;
|
|
2715
3405
|
} catch {
|
|
2716
3406
|
/* skip malformed */
|
|
@@ -2720,130 +3410,181 @@ function readHookErrors(
|
|
|
2720
3410
|
.sort((a, b) => b[1] - a[1])
|
|
2721
3411
|
.slice(0, 5)
|
|
2722
3412
|
.map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
|
|
2723
|
-
|
|
3413
|
+
const topErrors = [...errorPhaseCounts.values()]
|
|
3414
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
3415
|
+
.slice(0, 5);
|
|
3416
|
+
const recentTopErrors = [...recentErrorPhaseCounts.values()]
|
|
3417
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
3418
|
+
.slice(0, 5);
|
|
3419
|
+
return {
|
|
3420
|
+
total,
|
|
3421
|
+
last1h,
|
|
3422
|
+
latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
|
|
3423
|
+
byPhase,
|
|
3424
|
+
byError,
|
|
3425
|
+
top,
|
|
3426
|
+
topErrors,
|
|
3427
|
+
recentTopErrors,
|
|
3428
|
+
};
|
|
2724
3429
|
}
|
|
2725
3430
|
|
|
2726
3431
|
/** 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 };
|
|
3432
|
+
function readStreamStats(root: string): HealthReport["stream"] {
|
|
3433
|
+
const read = readAgentDiagnosticEventsInWindow(root, 0);
|
|
3434
|
+
return {
|
|
3435
|
+
source: "v3",
|
|
3436
|
+
authoritative: read.authoritative,
|
|
3437
|
+
...(read.reason ? { reason: read.reason } : {}),
|
|
3438
|
+
bytes: read.bytes,
|
|
3439
|
+
lines: read.events.length,
|
|
3440
|
+
cursor_backlog: 0,
|
|
3441
|
+
};
|
|
2767
3442
|
}
|
|
2768
3443
|
|
|
2769
3444
|
/** One rendered line in a trace. */
|
|
2770
|
-
interface TraceEntry {
|
|
3445
|
+
export interface TraceEntry {
|
|
2771
3446
|
ts: string;
|
|
2772
3447
|
event_type: string;
|
|
2773
3448
|
detail: string;
|
|
2774
3449
|
}
|
|
2775
3450
|
|
|
3451
|
+
export function traceInstanceIdsForEventSource(
|
|
3452
|
+
nativeInstanceIds: readonly string[],
|
|
3453
|
+
source: "v3",
|
|
3454
|
+
): string[] {
|
|
3455
|
+
void source;
|
|
3456
|
+
return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
|
|
3457
|
+
}
|
|
3458
|
+
|
|
2776
3459
|
/** 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.
|
|
3460
|
+
export function traceLine(ev: CanonicalEvent, allTools: boolean): TraceEntry | null {
|
|
3461
|
+
const d = (ev.payload ?? {}) as Record<string, unknown>;
|
|
2779
3462
|
const s = (k: string): string => (typeof d[k] === "string" ? (d[k] as string) : "");
|
|
3463
|
+
const object = (value: unknown): Record<string, unknown> =>
|
|
3464
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
3465
|
+
? (value as Record<string, unknown>)
|
|
3466
|
+
: {};
|
|
2780
3467
|
const clip = (v: string, n = 70): string => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
|
|
3468
|
+
const recovery =
|
|
3469
|
+
typeof d.recovery === "object" && d.recovery !== null
|
|
3470
|
+
? (d.recovery as { reason?: unknown })
|
|
3471
|
+
: null;
|
|
3472
|
+
const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
|
|
3473
|
+
const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
|
|
2781
3474
|
let detail = "";
|
|
2782
3475
|
switch (ev.event_type) {
|
|
2783
|
-
case "session.
|
|
2784
|
-
detail =
|
|
3476
|
+
case "session.started":
|
|
3477
|
+
detail = "generation started";
|
|
3478
|
+
break;
|
|
3479
|
+
case "session.resumed":
|
|
3480
|
+
detail = "generation resumed";
|
|
2785
3481
|
break;
|
|
2786
|
-
case "session.
|
|
2787
|
-
detail = `
|
|
3482
|
+
case "session.ended":
|
|
3483
|
+
detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2788
3484
|
break;
|
|
2789
|
-
case "
|
|
2790
|
-
detail =
|
|
3485
|
+
case "session.termination_observed":
|
|
3486
|
+
detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2791
3487
|
break;
|
|
2792
|
-
case "
|
|
2793
|
-
detail = `
|
|
3488
|
+
case "agent.delegated":
|
|
3489
|
+
detail = `delegated child=${s("child_generation_id") || "unknown"}`;
|
|
2794
3490
|
break;
|
|
2795
|
-
case "
|
|
2796
|
-
detail =
|
|
3491
|
+
case "agent.started":
|
|
3492
|
+
detail = `child started=${s("child_generation_id") || "unknown"}`;
|
|
2797
3493
|
break;
|
|
2798
|
-
case "
|
|
2799
|
-
detail = `
|
|
3494
|
+
case "agent.completed":
|
|
3495
|
+
detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
|
|
2800
3496
|
break;
|
|
2801
|
-
case "
|
|
2802
|
-
detail =
|
|
3497
|
+
case "turn.started":
|
|
3498
|
+
detail = `intent=${s("intent_kind") || "unknown"}`;
|
|
2803
3499
|
break;
|
|
2804
|
-
case "
|
|
2805
|
-
detail =
|
|
3500
|
+
case "turn.completed":
|
|
3501
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2806
3502
|
break;
|
|
2807
|
-
case "
|
|
2808
|
-
|
|
3503
|
+
case "tool.requested": {
|
|
3504
|
+
const tool = object(d.tool);
|
|
3505
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
|
|
2809
3506
|
break;
|
|
2810
|
-
|
|
2811
|
-
|
|
3507
|
+
}
|
|
3508
|
+
case "tool.completed": {
|
|
3509
|
+
const tool = object(d.tool);
|
|
3510
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
3511
|
+
break;
|
|
3512
|
+
}
|
|
3513
|
+
case "command.started":
|
|
3514
|
+
if (!allTools) return null;
|
|
3515
|
+
detail = "command started (content omitted)";
|
|
3516
|
+
break;
|
|
3517
|
+
case "command.completed":
|
|
3518
|
+
if (!allTools && !recoveryReason) return null;
|
|
3519
|
+
detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
3520
|
+
break;
|
|
3521
|
+
case "coord.task_changed":
|
|
3522
|
+
case "coord.lifecycle_changed":
|
|
3523
|
+
case "coord.presence_changed":
|
|
3524
|
+
detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
|
|
3525
|
+
break;
|
|
3526
|
+
case "coord.status_observed":
|
|
3527
|
+
detail = s("status") || "status observed";
|
|
3528
|
+
break;
|
|
3529
|
+
case "coord.claim_changed": {
|
|
3530
|
+
const target = object(d.target);
|
|
3531
|
+
detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
|
|
3532
|
+
break;
|
|
3533
|
+
}
|
|
3534
|
+
case "coord.message_observed":
|
|
3535
|
+
detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
|
|
3536
|
+
break;
|
|
3537
|
+
case "coord.identity_attested":
|
|
3538
|
+
detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
|
|
3539
|
+
break;
|
|
3540
|
+
case "wait.started":
|
|
3541
|
+
detail = s("kind") || "wait started";
|
|
3542
|
+
break;
|
|
3543
|
+
case "wait.ended":
|
|
3544
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
3545
|
+
break;
|
|
3546
|
+
case "artifact.observed": {
|
|
3547
|
+
const artifact = object(d.artifact);
|
|
3548
|
+
detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
|
|
2812
3549
|
break;
|
|
2813
|
-
|
|
2814
|
-
|
|
3550
|
+
}
|
|
3551
|
+
case "progress.observed":
|
|
3552
|
+
detail = s("kind") || "progress";
|
|
3553
|
+
break;
|
|
3554
|
+
case "health.observed":
|
|
3555
|
+
detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
|
|
2815
3556
|
break;
|
|
2816
|
-
case "
|
|
2817
|
-
detail = `${s("
|
|
3557
|
+
case "council.state_changed":
|
|
3558
|
+
detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2818
3559
|
break;
|
|
2819
|
-
case "
|
|
2820
|
-
|
|
2821
|
-
case "claim.conflict":
|
|
2822
|
-
detail = clip(s("path"));
|
|
3560
|
+
case "decision.state_changed":
|
|
3561
|
+
detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2823
3562
|
break;
|
|
2824
|
-
case "
|
|
2825
|
-
|
|
2826
|
-
detail = `reason=${s("reason")}`;
|
|
3563
|
+
case "lifecycle.recovered":
|
|
3564
|
+
detail = `recovered=${s("recovery_kind") || "unknown"}`;
|
|
2827
3565
|
break;
|
|
2828
|
-
case "
|
|
2829
|
-
detail =
|
|
3566
|
+
case "lifecycle.sweep_observed":
|
|
3567
|
+
detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
|
|
2830
3568
|
break;
|
|
2831
3569
|
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
3570
|
return null;
|
|
2843
3571
|
}
|
|
2844
3572
|
return { ts: ev.ts, event_type: ev.event_type, detail };
|
|
2845
3573
|
}
|
|
2846
3574
|
|
|
3575
|
+
export function pendingFinalizationTraceEntries(
|
|
3576
|
+
requests: readonly SessionFinalizationRequestV3[],
|
|
3577
|
+
instanceId: string,
|
|
3578
|
+
): TraceEntry[] {
|
|
3579
|
+
return requests
|
|
3580
|
+
.filter((request) => request.status === "pending" && request.instance_id === instanceId)
|
|
3581
|
+
.map((request) => ({
|
|
3582
|
+
ts: request.observed_at,
|
|
3583
|
+
event_type: "session.finalization_pending",
|
|
3584
|
+
detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
|
|
3585
|
+
}));
|
|
3586
|
+
}
|
|
3587
|
+
|
|
2847
3588
|
function runTrace(
|
|
2848
3589
|
name: string,
|
|
2849
3590
|
opts: { since?: string; limit: string; allTools?: boolean; json?: boolean },
|
|
@@ -2872,7 +3613,7 @@ function runTrace(
|
|
|
2872
3613
|
if (candidates.length === 1) targetId = candidates[0]![0];
|
|
2873
3614
|
else if (candidates.length > 1)
|
|
2874
3615
|
targetId = candidates.map(([id]) => id).join("\x00"); // sentinel; resolved below
|
|
2875
|
-
else if (/^[0-9a-
|
|
3616
|
+
else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted)) targetId = wanted;
|
|
2876
3617
|
}
|
|
2877
3618
|
if (!targetId) {
|
|
2878
3619
|
emit.error({
|
|
@@ -2884,37 +3625,16 @@ function runTrace(
|
|
|
2884
3625
|
|
|
2885
3626
|
const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
|
|
2886
3627
|
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");
|
|
3628
|
+
const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
|
|
3629
|
+
const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
|
|
3630
|
+
const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
|
|
2894
3631
|
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
|
-
}
|
|
3632
|
+
for (const ev of diagnosticRead.events) {
|
|
3633
|
+
if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
|
|
3634
|
+
const arr = byId.get(ev.instance_id) ?? [];
|
|
3635
|
+
arr.push(ev);
|
|
3636
|
+
byId.set(ev.instance_id, arr);
|
|
2911
3637
|
}
|
|
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
3638
|
// If the name mapped to multiple instances, trace the one with the latest event.
|
|
2919
3639
|
let resolvedId = candidateIds[0]!;
|
|
2920
3640
|
if (candidateIds.length > 1) {
|
|
@@ -2930,25 +3650,84 @@ function runTrace(
|
|
|
2930
3650
|
}
|
|
2931
3651
|
|
|
2932
3652
|
const events = byId.get(resolvedId) ?? [];
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3653
|
+
let state = foldSessionState(events, { instance_id: resolvedId });
|
|
3654
|
+
let sessionState: "live" | "ended" = events.some((event) => event.event_type === "session.ended")
|
|
3655
|
+
? "ended"
|
|
3656
|
+
: "live";
|
|
3657
|
+
if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
|
|
3658
|
+
try {
|
|
3659
|
+
const view = projectCoordinationViewV3(readLedgerV3(root));
|
|
3660
|
+
const liveGeneration = view.instances[resolvedId];
|
|
3661
|
+
const terminalGeneration = Object.values(view.terminal_generations).find(
|
|
3662
|
+
(candidate) => candidate.instance_id === resolvedId,
|
|
3663
|
+
);
|
|
3664
|
+
const generation = liveGeneration ?? terminalGeneration;
|
|
3665
|
+
if (generation) {
|
|
3666
|
+
sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
|
|
3667
|
+
const lifecycleState = generation.lifecycle_state;
|
|
3668
|
+
state = {
|
|
3669
|
+
activity: generation.activity === "terminal" ? "idle" : generation.activity,
|
|
3670
|
+
activity_updated_at: generation.last_observed_at,
|
|
3671
|
+
activity_source: "event-v3-coordination-view",
|
|
3672
|
+
task_state:
|
|
3673
|
+
lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
|
|
3674
|
+
? lifecycleState
|
|
3675
|
+
: "active",
|
|
3676
|
+
task_state_updated_at: generation.lifecycle_state_updated_at,
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
} catch {
|
|
3680
|
+
diagnosticRead.authoritative = false;
|
|
3681
|
+
diagnosticRead.reason = "V3 coordination projection is unavailable";
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
let pendingFinalizations: SessionFinalizationRequestV3[] = [];
|
|
3685
|
+
if (diagnosticRead.source === "v3") {
|
|
3686
|
+
try {
|
|
3687
|
+
pendingFinalizations = listSessionFinalizationRequestsV3(root).filter(
|
|
3688
|
+
(request) => request.status === "pending" && request.instance_id === resolvedId,
|
|
3689
|
+
);
|
|
3690
|
+
} catch {
|
|
3691
|
+
diagnosticRead.authoritative = false;
|
|
3692
|
+
diagnosticRead.reason = "V3 finalization requests are unreadable";
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
const lines = [
|
|
3696
|
+
...events
|
|
3697
|
+
.map((ev) => traceLine(ev, !!opts.allTools))
|
|
3698
|
+
.filter((l): l is TraceEntry => l !== null),
|
|
3699
|
+
...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
|
|
3700
|
+
]
|
|
2937
3701
|
// Sort by timestamp, not file order: codex replays events (original ts,
|
|
2938
3702
|
// appended later), so append-order ≠ chronological order.
|
|
2939
3703
|
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
2940
3704
|
const shown = lines.slice(-limit);
|
|
2941
|
-
const
|
|
3705
|
+
const nativeResolvedId =
|
|
3706
|
+
diagnosticRead.source === "v3"
|
|
3707
|
+
? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
|
|
3708
|
+
: resolvedId;
|
|
3709
|
+
const displayName =
|
|
3710
|
+
nameById.get(resolvedId) ??
|
|
3711
|
+
(nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
|
|
3712
|
+
resolvedId.slice(0, 8);
|
|
2942
3713
|
|
|
2943
3714
|
const result = {
|
|
3715
|
+
event_source: {
|
|
3716
|
+
contract: diagnosticRead.source,
|
|
3717
|
+
authoritative: diagnosticRead.authoritative,
|
|
3718
|
+
reason: diagnosticRead.reason ?? null,
|
|
3719
|
+
},
|
|
2944
3720
|
name: displayName,
|
|
2945
3721
|
instance_id: resolvedId,
|
|
2946
3722
|
other_instances: candidateIds.filter((id) => id !== resolvedId),
|
|
3723
|
+
pending_finalizations: pendingFinalizations,
|
|
2947
3724
|
total_events: events.length,
|
|
3725
|
+
session_state: sessionState,
|
|
2948
3726
|
activity: state.activity,
|
|
2949
3727
|
activity_updated_at: state.activity_updated_at ?? null,
|
|
2950
3728
|
activity_source: state.activity_source ?? null,
|
|
2951
3729
|
task_state: state.task_state,
|
|
3730
|
+
task_state_scope: sessionState === "ended" ? "historical" : "current",
|
|
2952
3731
|
task_state_updated_at: state.task_state_updated_at ?? null,
|
|
2953
3732
|
task_state_reason: state.task_state_reason ?? null,
|
|
2954
3733
|
shown: shown.length,
|
|
@@ -2963,8 +3742,11 @@ function runTrace(
|
|
|
2963
3742
|
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
3743
|
process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
|
|
2965
3744
|
process.stdout.write(
|
|
2966
|
-
` activity=${state.activity} · lifecycle=${state.task_state}${state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`,
|
|
3745
|
+
` 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
3746
|
); // lint-ok-emission: human trace view
|
|
3747
|
+
if (!diagnosticRead.authoritative) {
|
|
3748
|
+
process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
|
|
3749
|
+
}
|
|
2968
3750
|
if (shown.length === 0) {
|
|
2969
3751
|
process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
|
|
2970
3752
|
return;
|
|
@@ -2975,6 +3757,73 @@ function runTrace(
|
|
|
2975
3757
|
}
|
|
2976
3758
|
}
|
|
2977
3759
|
|
|
3760
|
+
export interface ActiveAgentHealthSummary {
|
|
3761
|
+
source: "event-ledger-v3";
|
|
3762
|
+
total: number;
|
|
3763
|
+
by_platform: Record<string, number>;
|
|
3764
|
+
by_kind: Record<string, number>;
|
|
3765
|
+
by_schema_version: Record<string, number>;
|
|
3766
|
+
stale: number;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
type ActiveHealthHeartbeat = {
|
|
3770
|
+
instance_id: string;
|
|
3771
|
+
platform?: string;
|
|
3772
|
+
kind?: string;
|
|
3773
|
+
schema_version?: number;
|
|
3774
|
+
last_heartbeat?: string;
|
|
3775
|
+
};
|
|
3776
|
+
|
|
3777
|
+
/**
|
|
3778
|
+
* V3 generations, not disposable heartbeat caches, are the active-agent authority.
|
|
3779
|
+
*/
|
|
3780
|
+
export function collectActiveAgentHealth(
|
|
3781
|
+
root: string,
|
|
3782
|
+
nowMs = Date.now(),
|
|
3783
|
+
): ActiveAgentHealthSummary {
|
|
3784
|
+
const source: ActiveAgentHealthSummary["source"] = "event-ledger-v3";
|
|
3785
|
+
let heartbeats: ActiveHealthHeartbeat[] = [];
|
|
3786
|
+
try {
|
|
3787
|
+
const control = readEventV3ControlState(root);
|
|
3788
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
3789
|
+
heartbeats = readLiveCoordinationRows(root);
|
|
3790
|
+
}
|
|
3791
|
+
} catch {
|
|
3792
|
+
// The event-ledger section reports the authority read failure. Do not
|
|
3793
|
+
// substitute stale cache rows for a live V3 route.
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
const byPlatform: Record<string, number> = {};
|
|
3797
|
+
const byKind: Record<string, number> = {};
|
|
3798
|
+
const bySchema: Record<string, number> = {};
|
|
3799
|
+
let stale = 0;
|
|
3800
|
+
for (const heartbeat of heartbeats) {
|
|
3801
|
+
const platform = formatPlatformLabel(heartbeat.platform);
|
|
3802
|
+
byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
|
|
3803
|
+
const kind = heartbeat.kind ?? "unknown";
|
|
3804
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
3805
|
+
// These rows exist only after an authority-safe V3 projection. The
|
|
3806
|
+
// disposable heartbeat file has its own cache-format version, which must
|
|
3807
|
+
// not be reported as the ledger schema version.
|
|
3808
|
+
bySchema.v3 = (bySchema.v3 ?? 0) + 1;
|
|
3809
|
+
const lastObservedMs = heartbeat.last_heartbeat
|
|
3810
|
+
? Date.parse(heartbeat.last_heartbeat)
|
|
3811
|
+
: Number.NaN;
|
|
3812
|
+
const ageMs = Number.isFinite(lastObservedMs)
|
|
3813
|
+
? nowMs - lastObservedMs
|
|
3814
|
+
: Number.POSITIVE_INFINITY;
|
|
3815
|
+
if (ageMs > freshnessCutoffSecs() * 1000) stale += 1;
|
|
3816
|
+
}
|
|
3817
|
+
return {
|
|
3818
|
+
source,
|
|
3819
|
+
total: heartbeats.length,
|
|
3820
|
+
by_platform: byPlatform,
|
|
3821
|
+
by_kind: byKind,
|
|
3822
|
+
by_schema_version: bySchema,
|
|
3823
|
+
stale,
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
|
|
2978
3827
|
function runHealth(opts: { since: string; json?: boolean }): void {
|
|
2979
3828
|
if (opts.json) emit.config({ format: "json" });
|
|
2980
3829
|
|
|
@@ -2996,12 +3845,13 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2996
3845
|
process.exit(1);
|
|
2997
3846
|
}
|
|
2998
3847
|
|
|
2999
|
-
const
|
|
3848
|
+
const nowMs = Date.now();
|
|
3849
|
+
const cutoffMs = nowMs - sinceSecs * 1000;
|
|
3000
3850
|
const activeDir = resolve(root, ".harnery/active");
|
|
3001
3851
|
const councilsDir = resolve(root, ".harnery/councils");
|
|
3002
3852
|
|
|
3003
|
-
// Coordination telemetry reads from the canonical
|
|
3004
|
-
// Heals come from health
|
|
3853
|
+
// Coordination telemetry reads from the canonical V3 ledger.
|
|
3854
|
+
// Heals come from health.observed; council activity from council.state_changed.
|
|
3005
3855
|
const heal: HealEvent[] = [];
|
|
3006
3856
|
let councilAdvanced = 0;
|
|
3007
3857
|
let councilClosed = 0;
|
|
@@ -3010,33 +3860,33 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3010
3860
|
const sweptByReason: Record<string, number> = {};
|
|
3011
3861
|
|
|
3012
3862
|
const nameById = buildNameById(root);
|
|
3013
|
-
|
|
3863
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
3864
|
+
for (const ev of diagnosticEvents.events) {
|
|
3014
3865
|
const healEv = canonicalToHealEvent(ev, nameById);
|
|
3015
3866
|
if (healEv) {
|
|
3016
3867
|
heal.push(healEv);
|
|
3017
3868
|
continue;
|
|
3018
3869
|
}
|
|
3870
|
+
if (ev.event_type === "council.state_changed") {
|
|
3871
|
+
const state = String(ev.payload?.new_state ?? "");
|
|
3872
|
+
if (state === "round_open") councilAdvanced++;
|
|
3873
|
+
if (state === "closed") councilClosed++;
|
|
3874
|
+
if (state === "archived") councilArchived++;
|
|
3875
|
+
continue;
|
|
3876
|
+
}
|
|
3019
3877
|
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": {
|
|
3878
|
+
case "lifecycle.sweep_observed": {
|
|
3030
3879
|
sweptTotal++;
|
|
3031
|
-
const reason = String(
|
|
3880
|
+
const reason = String(ev.payload?.observation ?? "unknown");
|
|
3032
3881
|
sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
|
|
3033
3882
|
break;
|
|
3034
3883
|
}
|
|
3035
3884
|
}
|
|
3036
3885
|
}
|
|
3037
3886
|
|
|
3038
|
-
const hookErrors = readHookErrors(root, cutoffMs);
|
|
3887
|
+
const hookErrors = readHookErrors(root, cutoffMs, nowMs);
|
|
3039
3888
|
const stream = readStreamStats(root);
|
|
3889
|
+
const eventLedger = collectEventLedgerHealthV3(root);
|
|
3040
3890
|
|
|
3041
3891
|
// Canonical health.* events carry the full instance_id, already resolved to
|
|
3042
3892
|
// `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
|
|
@@ -3056,19 +3906,13 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3056
3906
|
.slice(0, 5)
|
|
3057
3907
|
.map(([agent, count]) => ({ agent, count }));
|
|
3058
3908
|
|
|
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;
|
|
3909
|
+
const activeAgents = collectActiveAgentHealth(root, nowMs);
|
|
3065
3910
|
// Zombies: files in active/ that are broken: unparseable, nameless, or an
|
|
3066
3911
|
// absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
|
|
3067
3912
|
// mean dead files the sweep isn't reaping.
|
|
3068
3913
|
let zombieCount = 0;
|
|
3069
3914
|
const zombieSamples: string[] = [];
|
|
3070
3915
|
const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
|
|
3071
|
-
const nowMs = Date.now();
|
|
3072
3916
|
if (existsSync(activeDir)) {
|
|
3073
3917
|
for (const file of readdirSync(activeDir)) {
|
|
3074
3918
|
if (!file.endsWith(".json")) continue;
|
|
@@ -3085,17 +3929,8 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3085
3929
|
zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
|
|
3086
3930
|
continue;
|
|
3087
3931
|
}
|
|
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
3932
|
const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
|
|
3097
3933
|
const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
|
|
3098
|
-
if (ageMs > freshnessCutoffSecs() * 1000) staleHeartbeats++;
|
|
3099
3934
|
// Zombie heuristics on a parseable heartbeat: no name, or an age so large
|
|
3100
3935
|
// it can only be a broken/epoch timestamp (a real agent would have healed).
|
|
3101
3936
|
if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
|
|
@@ -3127,37 +3962,38 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3127
3962
|
);
|
|
3128
3963
|
}
|
|
3129
3964
|
}
|
|
3130
|
-
if (
|
|
3965
|
+
if (activeAgents.stale > 0) {
|
|
3966
|
+
const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
|
|
3967
|
+
const action =
|
|
3968
|
+
activeAgents.source === "event-ledger-v3"
|
|
3969
|
+
? "lifecycle reconciliation may be delayed"
|
|
3970
|
+
: "heal mechanism may not be firing";
|
|
3131
3971
|
anomalies.push(
|
|
3132
|
-
`${
|
|
3972
|
+
`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`,
|
|
3133
3973
|
);
|
|
3134
3974
|
}
|
|
3135
|
-
const
|
|
3975
|
+
const expectedSchema = "v3";
|
|
3976
|
+
const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter(
|
|
3977
|
+
(schema) => schema !== expectedSchema,
|
|
3978
|
+
);
|
|
3136
3979
|
if (unexpectedSchemas.length > 0) {
|
|
3137
3980
|
anomalies.push(
|
|
3138
|
-
`Unexpected
|
|
3981
|
+
`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`,
|
|
3139
3982
|
);
|
|
3140
3983
|
}
|
|
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}`);
|
|
3984
|
+
// Only recent hook failures are active anomalies. Historical rows remain in
|
|
3985
|
+
// the report with exact error signatures and their latest timestamp.
|
|
3986
|
+
if (hookErrors.last1h > 0) {
|
|
3987
|
+
const top = hookErrors.recentTopErrors[0];
|
|
3988
|
+
const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
|
|
3989
|
+
anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
|
|
3150
3990
|
}
|
|
3151
3991
|
if (stream.cursor_backlog > 500) {
|
|
3152
3992
|
anomalies.push(
|
|
3153
3993
|
`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`,
|
|
3154
3994
|
);
|
|
3155
3995
|
}
|
|
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.
|
|
3996
|
+
// Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
|
|
3161
3997
|
if ((sweptByReason.unparseable ?? 0) > 0) {
|
|
3162
3998
|
anomalies.push(
|
|
3163
3999
|
`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`,
|
|
@@ -3168,16 +4004,46 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3168
4004
|
`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`,
|
|
3169
4005
|
);
|
|
3170
4006
|
}
|
|
4007
|
+
if (eventLedger.state === "live") {
|
|
4008
|
+
// Open spans whose generation has no open turn: the turn ended without its
|
|
4009
|
+
// tool spans closing, so an explicit end can only queue, never finalize.
|
|
4010
|
+
const orphanGenerations = eventLedger.open_spans.generations.filter(
|
|
4011
|
+
(generation) => !generation.turn_open,
|
|
4012
|
+
);
|
|
4013
|
+
if (orphanGenerations.length > 0) {
|
|
4014
|
+
const orphanSpans = orphanGenerations.reduce(
|
|
4015
|
+
(sum, generation) => sum + generation.span_count,
|
|
4016
|
+
0,
|
|
4017
|
+
);
|
|
4018
|
+
anomalies.push(
|
|
4019
|
+
`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`,
|
|
4020
|
+
);
|
|
4021
|
+
}
|
|
4022
|
+
for (const pressure of eventLedger.span_pressure) {
|
|
4023
|
+
anomalies.push(
|
|
4024
|
+
`${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)`,
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
for (const collectionError of eventLedger.collection_errors) {
|
|
4028
|
+
anomalies.push(`event-ledger health: ${collectionError}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
3171
4031
|
|
|
3172
4032
|
const report: HealthReport = {
|
|
3173
4033
|
since: opts.since,
|
|
3174
4034
|
generated_at: new Date().toISOString(),
|
|
4035
|
+
event_telemetry: {
|
|
4036
|
+
source: diagnosticEvents.source,
|
|
4037
|
+
authoritative: diagnosticEvents.authoritative,
|
|
4038
|
+
...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
|
|
4039
|
+
},
|
|
3175
4040
|
active_agents: {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
4041
|
+
source: activeAgents.source,
|
|
4042
|
+
total: activeAgents.total,
|
|
4043
|
+
by_platform: activeAgents.by_platform,
|
|
4044
|
+
by_kind: activeAgents.by_kind,
|
|
4045
|
+
by_schema_version: activeAgents.by_schema_version,
|
|
4046
|
+
stale: activeAgents.stale,
|
|
3181
4047
|
},
|
|
3182
4048
|
heal_events: {
|
|
3183
4049
|
total: heal.length,
|
|
@@ -3193,9 +4059,19 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
3193
4059
|
closed_in_window: councilClosed,
|
|
3194
4060
|
},
|
|
3195
4061
|
swept_events: { total: sweptTotal, by_reason: sweptByReason },
|
|
3196
|
-
hook_errors: {
|
|
4062
|
+
hook_errors: {
|
|
4063
|
+
total: hookErrors.total,
|
|
4064
|
+
last_1h: hookErrors.last1h,
|
|
4065
|
+
latest_at: hookErrors.latestAt,
|
|
4066
|
+
by_phase: hookErrors.byPhase,
|
|
4067
|
+
by_error: hookErrors.byError,
|
|
4068
|
+
top: hookErrors.top,
|
|
4069
|
+
top_errors: hookErrors.topErrors,
|
|
4070
|
+
recent_top_errors: hookErrors.recentTopErrors,
|
|
4071
|
+
},
|
|
3197
4072
|
stream,
|
|
3198
4073
|
zombies: { count: zombieCount, samples: zombieSamples },
|
|
4074
|
+
event_ledger: eventLedger,
|
|
3199
4075
|
anomalies,
|
|
3200
4076
|
};
|
|
3201
4077
|
|
|
@@ -3237,7 +4113,9 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3237
4113
|
if (report.councils.archived_in_window > 0)
|
|
3238
4114
|
councilParts.push(`${report.councils.archived_in_window} archived`);
|
|
3239
4115
|
|
|
3240
|
-
const
|
|
4116
|
+
const activeSource =
|
|
4117
|
+
report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
|
|
4118
|
+
const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
|
|
3241
4119
|
|
|
3242
4120
|
const sweptReasonStr = Object.entries(report.swept_events.by_reason)
|
|
3243
4121
|
.map(([r, n]) => `${r} ${n}`)
|
|
@@ -3245,11 +4123,36 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3245
4123
|
const hookErrStr =
|
|
3246
4124
|
report.hook_errors.total === 0
|
|
3247
4125
|
? "0"
|
|
3248
|
-
: `${report.hook_errors.total}
|
|
4126
|
+
: `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
|
|
3249
4127
|
const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
|
|
3250
4128
|
|
|
4129
|
+
const ledger = report.event_ledger;
|
|
4130
|
+
let ledgerStr: string;
|
|
4131
|
+
if (ledger.state !== "live") {
|
|
4132
|
+
ledgerStr = `unavailable (${ledger.reason})`;
|
|
4133
|
+
} else {
|
|
4134
|
+
const orphanGenerations = ledger.open_spans.generations.filter(
|
|
4135
|
+
(generation) => !generation.turn_open,
|
|
4136
|
+
).length;
|
|
4137
|
+
const pendingAges = ledger.pending_finalizations
|
|
4138
|
+
.map((request) =>
|
|
4139
|
+
request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)),
|
|
4140
|
+
)
|
|
4141
|
+
.join(", ");
|
|
4142
|
+
ledgerStr = [
|
|
4143
|
+
`open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
|
|
4144
|
+
`pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
|
|
4145
|
+
`intake ${ledger.intake_spool.total}`,
|
|
4146
|
+
`diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
|
|
4147
|
+
].join(" · ");
|
|
4148
|
+
}
|
|
4149
|
+
|
|
3251
4150
|
const rows: Array<[string, string]> = [
|
|
3252
4151
|
["window", `last ${report.since}`],
|
|
4152
|
+
[
|
|
4153
|
+
"event source",
|
|
4154
|
+
`${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
|
|
4155
|
+
],
|
|
3253
4156
|
["active", activeStr],
|
|
3254
4157
|
[
|
|
3255
4158
|
"heals",
|
|
@@ -3259,6 +4162,7 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3259
4162
|
["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
|
|
3260
4163
|
["hook errors", hookErrStr],
|
|
3261
4164
|
["stream", streamStr],
|
|
4165
|
+
["event ledger", ledgerStr],
|
|
3262
4166
|
[
|
|
3263
4167
|
"zombies",
|
|
3264
4168
|
report.zombies.count === 0
|
|
@@ -3383,6 +4287,7 @@ function runAdapterProbe(
|
|
|
3383
4287
|
replayExitCode = result.exitCode;
|
|
3384
4288
|
data.samples = samples;
|
|
3385
4289
|
data.samples_summary = result.summary;
|
|
4290
|
+
if (result.note) data.samples_note = result.note;
|
|
3386
4291
|
}
|
|
3387
4292
|
|
|
3388
4293
|
emit.data(data);
|
|
@@ -3402,7 +4307,9 @@ function runAdapterProbe(
|
|
|
3402
4307
|
| undefined;
|
|
3403
4308
|
lines.push("");
|
|
3404
4309
|
if (samples.length === 0) {
|
|
3405
|
-
lines.push(
|
|
4310
|
+
lines.push(
|
|
4311
|
+
` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`,
|
|
4312
|
+
);
|
|
3406
4313
|
} else {
|
|
3407
4314
|
lines.push(
|
|
3408
4315
|
` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`,
|
|
@@ -3459,6 +4366,7 @@ function replayAdapterSamples(
|
|
|
3459
4366
|
samples: SampleReplayResult[];
|
|
3460
4367
|
exitCode: number;
|
|
3461
4368
|
summary: { total: number; pass: number; fail: number; skipped: number };
|
|
4369
|
+
note?: string;
|
|
3462
4370
|
} {
|
|
3463
4371
|
const sampleDir = resolve(root, relativeSampleDir);
|
|
3464
4372
|
if (!existsSync(sampleDir)) {
|
|
@@ -3466,6 +4374,7 @@ function replayAdapterSamples(
|
|
|
3466
4374
|
samples: [],
|
|
3467
4375
|
exitCode: 0,
|
|
3468
4376
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
4377
|
+
note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
|
|
3469
4378
|
};
|
|
3470
4379
|
}
|
|
3471
4380
|
|
|
@@ -3479,6 +4388,7 @@ function replayAdapterSamples(
|
|
|
3479
4388
|
samples: [],
|
|
3480
4389
|
exitCode: 0,
|
|
3481
4390
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
4391
|
+
note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
|
|
3482
4392
|
};
|
|
3483
4393
|
}
|
|
3484
4394
|
|
|
@@ -3704,11 +4614,26 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
|
|
|
3704
4614
|
});
|
|
3705
4615
|
process.exit(1);
|
|
3706
4616
|
}
|
|
3707
|
-
const myHb =
|
|
4617
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3708
4618
|
const fromName = myHb?.name ?? "anonymous";
|
|
3709
4619
|
const body = `from agent-${fromName}: ${message.trim()}`;
|
|
3710
4620
|
const doc = appendEntry(peerOwner, "handoff", body);
|
|
3711
4621
|
|
|
4622
|
+
// Canonical delivery record: sender is the envelope owner, recipient rides
|
|
4623
|
+
// in data, so read-only observers can render the communication without
|
|
4624
|
+
// joining journal files.
|
|
4625
|
+
emitEventV3({
|
|
4626
|
+
owner: myOwner,
|
|
4627
|
+
session: nativeSessionIdentity(myHb, myOwner),
|
|
4628
|
+
adapter: normalizeAdapter(myHb?.platform),
|
|
4629
|
+
observation: {
|
|
4630
|
+
event_type: "coord.message_observed",
|
|
4631
|
+
direction: "sent",
|
|
4632
|
+
subject: peerOwner,
|
|
4633
|
+
body: message.trim(),
|
|
4634
|
+
},
|
|
4635
|
+
});
|
|
4636
|
+
|
|
3712
4637
|
const data = {
|
|
3713
4638
|
peer: name,
|
|
3714
4639
|
peer_instance_id: peerOwner,
|
|
@@ -3766,7 +4691,7 @@ async function runWait(
|
|
|
3766
4691
|
|
|
3767
4692
|
let lastProgressMs = 0;
|
|
3768
4693
|
while (true) {
|
|
3769
|
-
const hb =
|
|
4694
|
+
const hb = readCurrentCoordinationRow(peerOwner);
|
|
3770
4695
|
const now = Date.now();
|
|
3771
4696
|
const elapsedMs = now - startMs;
|
|
3772
4697
|
|
|
@@ -3806,9 +4731,8 @@ async function runWait(
|
|
|
3806
4731
|
const progressGapMs = Math.max(pollMs, 30_000);
|
|
3807
4732
|
if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
|
|
3808
4733
|
lastProgressMs = now;
|
|
3809
|
-
const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
|
|
3810
4734
|
const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
|
|
3811
|
-
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking
|
|
4735
|
+
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
|
|
3812
4736
|
process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
|
|
3813
4737
|
}
|
|
3814
4738
|
|
|
@@ -3849,6 +4773,7 @@ function runHeal(opts: {
|
|
|
3849
4773
|
owner: string;
|
|
3850
4774
|
kind: string;
|
|
3851
4775
|
sessionId?: string;
|
|
4776
|
+
adapter?: string;
|
|
3852
4777
|
pid?: string;
|
|
3853
4778
|
json?: boolean;
|
|
3854
4779
|
}): void {
|
|
@@ -3860,10 +4785,22 @@ function runHeal(opts: {
|
|
|
3860
4785
|
emit.error({ code: "missing_owner", message: "--owner is required" });
|
|
3861
4786
|
process.exit(1);
|
|
3862
4787
|
}
|
|
3863
|
-
if (kind !== "pidmap" && kind !== "
|
|
4788
|
+
if (kind !== "pidmap" && kind !== "cache") {
|
|
3864
4789
|
emit.error({
|
|
3865
4790
|
code: "bad_kind",
|
|
3866
|
-
message: "--kind must be one of: pidmap,
|
|
4791
|
+
message: "--kind must be one of: pidmap, cache",
|
|
4792
|
+
});
|
|
4793
|
+
process.exit(1);
|
|
4794
|
+
}
|
|
4795
|
+
if (
|
|
4796
|
+
opts.adapter &&
|
|
4797
|
+
opts.adapter !== "claude-code" &&
|
|
4798
|
+
opts.adapter !== "cursor" &&
|
|
4799
|
+
opts.adapter !== "codex"
|
|
4800
|
+
) {
|
|
4801
|
+
emit.error({
|
|
4802
|
+
code: "bad_adapter",
|
|
4803
|
+
message: "--adapter must be one of: claude-code, cursor, codex",
|
|
3867
4804
|
});
|
|
3868
4805
|
process.exit(1);
|
|
3869
4806
|
}
|
|
@@ -3877,10 +4814,9 @@ function runHeal(opts: {
|
|
|
3877
4814
|
process.exit(1);
|
|
3878
4815
|
}
|
|
3879
4816
|
|
|
3880
|
-
const action =
|
|
3881
|
-
kind === "pidmap" ? "heal-pidmap" : kind === "heartbeat" ? "heal-heartbeat" : "kill-heartbeat";
|
|
4817
|
+
const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
|
|
3882
4818
|
|
|
3883
|
-
// Refuse to
|
|
4819
|
+
// Refuse to materialize a V3 cache at a truncated owner id.
|
|
3884
4820
|
//
|
|
3885
4821
|
// Heartbeats are keyed by the whole instance_id, so healing at an
|
|
3886
4822
|
// abbreviated id writes `.harnery/active/<prefix>.json` while every reader
|
|
@@ -3900,19 +4836,18 @@ function runHeal(opts: {
|
|
|
3900
4836
|
// whose instance_id this id is a prefix of settles it just as well, and is
|
|
3901
4837
|
// present in precisely the case that matters (the session the reader was
|
|
3902
4838
|
// trying to heal is registered, just not under the abbreviated name).
|
|
3903
|
-
if (kind === "
|
|
4839
|
+
if (kind === "cache" && !readHeartbeatCache(root, owner)) {
|
|
3904
4840
|
const canonical =
|
|
3905
4841
|
opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
|
|
3906
4842
|
? opts.sessionId.trim()
|
|
3907
4843
|
: liveIdWithPrefix(root, owner);
|
|
3908
4844
|
if (canonical) {
|
|
3909
|
-
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live
|
|
4845
|
+
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
|
|
3910
4846
|
emit.error({
|
|
3911
4847
|
code: "truncated_owner",
|
|
3912
4848
|
message:
|
|
3913
|
-
`--owner ${owner} is a prefix of ${canonical} (${source})
|
|
3914
|
-
`
|
|
3915
|
-
`resolves. Re-run with --owner ${canonical}.`,
|
|
4849
|
+
`--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
|
|
4850
|
+
`the canonical instance id; re-run with --owner ${canonical}.`,
|
|
3916
4851
|
});
|
|
3917
4852
|
process.exit(1);
|
|
3918
4853
|
}
|
|
@@ -3920,14 +4855,13 @@ function runHeal(opts: {
|
|
|
3920
4855
|
|
|
3921
4856
|
// Build positional args. agent-coord's arg layout:
|
|
3922
4857
|
// heal-pidmap <instance_id> [<pid>]
|
|
3923
|
-
//
|
|
3924
|
-
// kill-heartbeat <instance_id>
|
|
4858
|
+
// repair-coordination-cache <instance_id> [<session_id>]
|
|
3925
4859
|
const helperArgs: string[] = [action, owner];
|
|
3926
4860
|
if (kind === "pidmap" && opts.pid) helperArgs.push(opts.pid);
|
|
3927
|
-
if (kind === "
|
|
4861
|
+
if (kind === "cache" && opts.sessionId) helperArgs.push(opts.sessionId);
|
|
4862
|
+
if (kind === "cache" && opts.adapter) helperArgs.push(`--adapter=${opts.adapter}`);
|
|
3928
4863
|
|
|
3929
|
-
//
|
|
3930
|
-
// bundled agent-coord binary.
|
|
4864
|
+
// Both recovery actions are handled by the bundled agent-coord binary.
|
|
3931
4865
|
const helper = agentCoordOrExit(root);
|
|
3932
4866
|
const proc = spawnSync(helper, helperArgs, {
|
|
3933
4867
|
encoding: "utf8",
|
|
@@ -3942,27 +4876,22 @@ function runHeal(opts: {
|
|
|
3942
4876
|
process.exit(1);
|
|
3943
4877
|
}
|
|
3944
4878
|
|
|
3945
|
-
//
|
|
4879
|
+
// Read the derived cache to surface post-action state.
|
|
3946
4880
|
let after: Heartbeat | null = null;
|
|
3947
4881
|
try {
|
|
3948
|
-
after =
|
|
4882
|
+
after = readHeartbeatCache(root, owner);
|
|
3949
4883
|
} catch {
|
|
3950
4884
|
after = null;
|
|
3951
4885
|
}
|
|
3952
4886
|
|
|
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
4887
|
const outcome =
|
|
3959
4888
|
kind === "pidmap"
|
|
3960
4889
|
? proc.status === 0
|
|
3961
4890
|
? "ok"
|
|
3962
4891
|
: "failed"
|
|
3963
4892
|
: after
|
|
3964
|
-
? "
|
|
3965
|
-
: "
|
|
4893
|
+
? "cache_present"
|
|
4894
|
+
: "cache_absent";
|
|
3966
4895
|
|
|
3967
4896
|
emit.data({
|
|
3968
4897
|
rows: [
|
|
@@ -3985,36 +4914,40 @@ function runHeal(opts: {
|
|
|
3985
4914
|
if (kind === "pidmap") {
|
|
3986
4915
|
emit.text(`agent-coord ${action} ok\n`);
|
|
3987
4916
|
} else {
|
|
3988
|
-
emit.text(`agent-coord ${action} ok:
|
|
4917
|
+
emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
|
|
3989
4918
|
}
|
|
3990
4919
|
}
|
|
3991
4920
|
|
|
3992
|
-
// Canonical health.* emission is owned by the writer
|
|
3993
|
-
//
|
|
4921
|
+
// Canonical health.* emission is owned by the cache writer, so it fires
|
|
4922
|
+
// inside the agent-coord subprocess
|
|
3994
4923
|
// above on actual writes only: write-only telemetry, no double-emit, no
|
|
3995
4924
|
// event when an already-correct heal no-ops. (Previously emitted here
|
|
3996
4925
|
// unconditionally on every `harn agents heal`, which over-counted no-op heals.)
|
|
3997
4926
|
}
|
|
3998
4927
|
|
|
3999
4928
|
/**
|
|
4000
|
-
*
|
|
4001
|
-
*
|
|
4002
|
-
* through silently if no session (CI / direct invocation).
|
|
4929
|
+
* Emit one V3 council audit transition keyed to the exact durable manifest.
|
|
4930
|
+
* Falls through silently when no live session can attest the observation.
|
|
4003
4931
|
*/
|
|
4004
4932
|
function emitCouncilStateEvent(
|
|
4005
|
-
type: string,
|
|
4006
4933
|
manifest: CouncilManifest,
|
|
4007
|
-
|
|
4934
|
+
newState: string,
|
|
4935
|
+
priorState?: string,
|
|
4008
4936
|
): void {
|
|
4009
4937
|
const myOwner = resolveOwner();
|
|
4010
4938
|
if (!myOwner) return;
|
|
4011
|
-
const hb =
|
|
4012
|
-
|
|
4013
|
-
type,
|
|
4939
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
4940
|
+
emitEventV3({
|
|
4014
4941
|
owner: myOwner,
|
|
4015
|
-
session: hb
|
|
4942
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
4016
4943
|
adapter: normalizeAdapter(hb?.platform),
|
|
4017
|
-
|
|
4944
|
+
observation: {
|
|
4945
|
+
event_type: "council.state_changed",
|
|
4946
|
+
council_id: manifest.council_id,
|
|
4947
|
+
...(priorState ? { prior_state: priorState } : {}),
|
|
4948
|
+
new_state: newState,
|
|
4949
|
+
record: manifest,
|
|
4950
|
+
},
|
|
4018
4951
|
});
|
|
4019
4952
|
}
|
|
4020
4953
|
|
|
@@ -4071,7 +5004,7 @@ function runCouncilCreate(
|
|
|
4071
5004
|
if (opts.createdBy?.trim()) {
|
|
4072
5005
|
createdBy = normalizeAgentName(opts.createdBy);
|
|
4073
5006
|
} else if (myOwner) {
|
|
4074
|
-
const myHb =
|
|
5007
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4075
5008
|
if (myHb?.name) {
|
|
4076
5009
|
createdBy = normalizeAgentName(myHb.name);
|
|
4077
5010
|
}
|
|
@@ -4132,17 +5065,16 @@ function runCouncilCreate(
|
|
|
4132
5065
|
writeManifest(manifest);
|
|
4133
5066
|
|
|
4134
5067
|
if (myOwner) {
|
|
4135
|
-
const myHbForEmit =
|
|
4136
|
-
|
|
4137
|
-
type: "council.open",
|
|
5068
|
+
const myHbForEmit = readCurrentCoordinationRow(myOwner);
|
|
5069
|
+
emitEventV3({
|
|
4138
5070
|
owner: myOwner,
|
|
4139
|
-
session: myHbForEmit
|
|
5071
|
+
session: nativeSessionIdentity(myHbForEmit, myOwner),
|
|
4140
5072
|
adapter: normalizeAdapter(myHbForEmit?.platform),
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
5073
|
+
observation: {
|
|
5074
|
+
event_type: "council.state_changed",
|
|
5075
|
+
council_id: manifest.council_id,
|
|
5076
|
+
new_state: "active",
|
|
5077
|
+
record: manifest,
|
|
4146
5078
|
},
|
|
4147
5079
|
});
|
|
4148
5080
|
}
|
|
@@ -4155,7 +5087,7 @@ function runCouncilCreate(
|
|
|
4155
5087
|
const skippedMembers: string[] = [];
|
|
4156
5088
|
for (const memberName of members) {
|
|
4157
5089
|
const bareName = memberName.replace(/^agent-/, "");
|
|
4158
|
-
if (myOwner &&
|
|
5090
|
+
if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
|
|
4159
5091
|
// Convener is themselves a member; skip the self-ping
|
|
4160
5092
|
continue;
|
|
4161
5093
|
}
|
|
@@ -4220,7 +5152,7 @@ function runCouncilList(opts: { status?: string; mine?: boolean; json?: boolean
|
|
|
4220
5152
|
if (opts.mine) {
|
|
4221
5153
|
const myOwner = resolveOwner();
|
|
4222
5154
|
if (myOwner) {
|
|
4223
|
-
const myHb =
|
|
5155
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4224
5156
|
if (myHb?.name) myName = normalizeAgentName(myHb.name);
|
|
4225
5157
|
}
|
|
4226
5158
|
if (!myName) {
|
|
@@ -4397,7 +5329,7 @@ function runCouncilClose(id: string, opts: { json?: boolean }): void {
|
|
|
4397
5329
|
closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
4398
5330
|
};
|
|
4399
5331
|
writeManifest(next);
|
|
4400
|
-
emitCouncilStateEvent("
|
|
5332
|
+
emitCouncilStateEvent(next, "closed", manifest.status);
|
|
4401
5333
|
|
|
4402
5334
|
// Build the transcript: every round's contributions in order.
|
|
4403
5335
|
const transcript = buildTranscript(next);
|
|
@@ -4442,7 +5374,7 @@ function runCouncilArchive(id: string, opts: { json?: boolean }): void {
|
|
|
4442
5374
|
// updated status. moveToArchive then physically relocates the artifacts.
|
|
4443
5375
|
writeManifest(next);
|
|
4444
5376
|
moveToArchive(next.council_id);
|
|
4445
|
-
emitCouncilStateEvent("
|
|
5377
|
+
emitCouncilStateEvent(next, "archived", manifest.status);
|
|
4446
5378
|
|
|
4447
5379
|
emit.data({
|
|
4448
5380
|
rows: [
|
|
@@ -4501,7 +5433,7 @@ function runCouncilUnarchive(id: string, opts: { json?: boolean }): void {
|
|
|
4501
5433
|
// (allows re-running for testing).
|
|
4502
5434
|
moveFromArchive(next.council_id);
|
|
4503
5435
|
writeManifest(next);
|
|
4504
|
-
emitCouncilStateEvent(
|
|
5436
|
+
emitCouncilStateEvent(next, restoredStatus, "archived");
|
|
4505
5437
|
|
|
4506
5438
|
emit.data({
|
|
4507
5439
|
rows: [
|
|
@@ -4563,7 +5495,7 @@ function runCouncilDelete(id: string, opts: { yes?: boolean; json?: boolean }):
|
|
|
4563
5495
|
|
|
4564
5496
|
const removed = deleteArchivedCouncil(manifest.council_id);
|
|
4565
5497
|
if (removed) {
|
|
4566
|
-
emitCouncilStateEvent("
|
|
5498
|
+
emitCouncilStateEvent(manifest, "deleted", "archived");
|
|
4567
5499
|
}
|
|
4568
5500
|
|
|
4569
5501
|
emit.data({
|
|
@@ -4753,7 +5685,7 @@ function runCouncilContribute(
|
|
|
4753
5685
|
try {
|
|
4754
5686
|
const myOwner = resolveOwner();
|
|
4755
5687
|
if (myOwner) {
|
|
4756
|
-
const myHb =
|
|
5688
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4757
5689
|
if (myHb?.name) actualName = normalizeAgentName(myHb.name);
|
|
4758
5690
|
}
|
|
4759
5691
|
} catch {
|
|
@@ -4769,7 +5701,7 @@ function runCouncilContribute(
|
|
|
4769
5701
|
});
|
|
4770
5702
|
process.exit(1);
|
|
4771
5703
|
}
|
|
4772
|
-
const myHb =
|
|
5704
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4773
5705
|
if (!myHb?.name) {
|
|
4774
5706
|
emit.error({
|
|
4775
5707
|
code: "no_self_name",
|
|
@@ -4822,11 +5754,7 @@ function runCouncilContribute(
|
|
|
4822
5754
|
}
|
|
4823
5755
|
|
|
4824
5756
|
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
|
-
});
|
|
5757
|
+
emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
|
|
4830
5758
|
|
|
4831
5759
|
// Update manifest: if all members have now contributed, flip round_status.
|
|
4832
5760
|
const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
|
|
@@ -4836,14 +5764,10 @@ function runCouncilContribute(
|
|
|
4836
5764
|
if (allIn) {
|
|
4837
5765
|
nextManifest = { ...manifest, round_status: "collected" };
|
|
4838
5766
|
writeManifest(nextManifest);
|
|
4839
|
-
emitCouncilStateEvent("
|
|
4840
|
-
round_no: nextManifest.current_round,
|
|
4841
|
-
});
|
|
5767
|
+
emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
|
|
4842
5768
|
if (manifest.auto_advance) {
|
|
4843
5769
|
nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
|
|
4844
|
-
emitCouncilStateEvent("
|
|
4845
|
-
round_no: nextManifest.current_round,
|
|
4846
|
-
});
|
|
5770
|
+
emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
|
|
4847
5771
|
autoAdvanced = true;
|
|
4848
5772
|
}
|
|
4849
5773
|
}
|
|
@@ -4934,7 +5858,7 @@ function runCouncilPrompt(
|
|
|
4934
5858
|
try {
|
|
4935
5859
|
const myOwner = resolveOwner();
|
|
4936
5860
|
if (myOwner) {
|
|
4937
|
-
const myHb =
|
|
5861
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4938
5862
|
if (myHb?.name) actualName = normalizeAgentName(myHb.name);
|
|
4939
5863
|
}
|
|
4940
5864
|
} catch {
|
|
@@ -4950,7 +5874,7 @@ function runCouncilPrompt(
|
|
|
4950
5874
|
});
|
|
4951
5875
|
process.exit(1);
|
|
4952
5876
|
}
|
|
4953
|
-
const myHb =
|
|
5877
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4954
5878
|
if (!myHb?.name) {
|
|
4955
5879
|
emit.error({
|
|
4956
5880
|
code: "no_self_name",
|
|
@@ -5115,12 +6039,8 @@ function runCouncilAdvance(id: string, opts: { force?: boolean; json?: boolean }
|
|
|
5115
6039
|
}
|
|
5116
6040
|
|
|
5117
6041
|
const next = advanceCouncil(manifest, !!opts.force);
|
|
5118
|
-
emitCouncilStateEvent("
|
|
5119
|
-
|
|
5120
|
-
});
|
|
5121
|
-
emitCouncilStateEvent("council.round_open", next, {
|
|
5122
|
-
round_no: next.current_round,
|
|
5123
|
-
});
|
|
6042
|
+
emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
|
|
6043
|
+
emitCouncilStateEvent(next, "round_open", "round_closed");
|
|
5124
6044
|
|
|
5125
6045
|
emit.data({
|
|
5126
6046
|
rows: [
|
|
@@ -5166,7 +6086,7 @@ function advanceCouncil(manifest: CouncilManifest, force: boolean): CouncilManif
|
|
|
5166
6086
|
// (Convener already knows; we skip pinging them if they convened it from
|
|
5167
6087
|
// their own session.)
|
|
5168
6088
|
const myOwner = resolveOwner();
|
|
5169
|
-
const myName = myOwner ? normalizeAgentName(
|
|
6089
|
+
const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
|
|
5170
6090
|
for (const memberName of next.members) {
|
|
5171
6091
|
if (memberName === myName) continue;
|
|
5172
6092
|
const bareName = memberName.replace(/^agent-/, "");
|
|
@@ -5190,7 +6110,7 @@ function advanceCouncil(manifest: CouncilManifest, force: boolean): CouncilManif
|
|
|
5190
6110
|
// Hard cap on box width. Values longer than the per-row budget word-wrap to
|
|
5191
6111
|
// continuation lines (blank key column, value resumes indented). Picked to
|
|
5192
6112
|
// stay readable in narrow terminals + chat clients while giving long
|
|
5193
|
-
//
|
|
6113
|
+
// Task values need room to breathe.
|
|
5194
6114
|
const MAX_BOX_CONTENT_WIDTH = 100;
|
|
5195
6115
|
|
|
5196
6116
|
function formatBox(title: string, rows: Array<[string, string]>): string {
|