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/dist/commands/agents.js
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)
|
|
@@ -17,18 +17,32 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync,
|
|
|
17
17
|
import { homedir, tmpdir } from "node:os";
|
|
18
18
|
import { basename, join, resolve } from "node:path";
|
|
19
19
|
import { coordBinPath } from "../core/agents/coord-bin.js";
|
|
20
|
-
import { readStreamTailBounded } from "../core/agents/events/consume.js";
|
|
21
20
|
import { checkGitFinalization, formatGitFinalizationFailure, readSessionWriteClaims, } from "../core/agents/finalization.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
21
|
+
import { emitEventV3, monorepoRoot, normalizeAdapter, resolveOwner, resolveOwnerBySessionEnv, resolveOwnerWithSource, sessionIdentityFromEnv, } from "../core/agents/index.js";
|
|
22
|
+
import { reopenLiveCoordinationGenerationV3 } from "../core/agents/live-authority-v3.js";
|
|
23
|
+
import { listSessionFinalizationRequestsV3, observeHostDisappearedV3, reconcileSessionFinalizationV3, requestSessionEndExplicitV3, } from "../core/agents/session-finalizer-v3.js";
|
|
24
|
+
import { SESSION_NAME_DISPLAY_NOTE } from "../core/agents/session-name-display.js";
|
|
25
|
+
import { buildLifecycleSuggestedName, buildSuggestedName, readHeartbeat as readHeartbeatCache, } from "../core/agents/state/heartbeat-writer.js";
|
|
26
|
+
import { readLiveCoordinationRow, readLiveCoordinationRows, } from "../core/agents/state/live-coordination-view.js";
|
|
24
27
|
import { foldSessionState, } from "../core/agents/state/session-state.js";
|
|
25
|
-
import { coordFreshnessSeconds, resolveBinName } from "../core/config.js";
|
|
28
|
+
import { coordFreshnessSeconds, resolveBinName, sessionFinalizationConfig, } from "../core/config.js";
|
|
29
|
+
import { readEventV3ControlState } from "../core/events/v3/control.js";
|
|
30
|
+
import { projectCoordinationViewV3 } from "../core/events/v3/coordination-view.js";
|
|
31
|
+
import { liveInstanceIdV3 } from "../core/events/v3/live-routing.js";
|
|
32
|
+
import { listHookIntakeGroupsV3, listHookIntakeRecordsV3, } from "../core/events/v3/producers/intake.js";
|
|
33
|
+
import { listHookProducerStateRecordsV3, readHookProducerStateV3, } from "../core/events/v3/producers/recorder.js";
|
|
34
|
+
import { readLedgerV3 } from "../core/events/v3/reader.js";
|
|
35
|
+
import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../core/events/v3/writer.js";
|
|
36
|
+
import { evaluateRunQualityIfDue } from "../core/guard/index.js";
|
|
26
37
|
import { readRemoteMachines } from "../core/presence/index.js";
|
|
27
38
|
import { registerContextCommand } from "./context.js";
|
|
39
|
+
function readCurrentCoordinationRow(instanceId) {
|
|
40
|
+
const root = monorepoRoot();
|
|
41
|
+
return root ? readLiveCoordinationRow(root, instanceId) : null;
|
|
42
|
+
}
|
|
28
43
|
/** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
|
|
29
44
|
* under V8's ~512MB max string length so a `readFileSync` of the whole file can
|
|
30
45
|
* never throw; covers ample recent history for a diagnostic scan. */
|
|
31
|
-
const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
|
|
32
46
|
import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
|
|
33
47
|
import { appendEntry, resolveOwnerByName } from "../core/journal/index.js";
|
|
34
48
|
import { buildCouncilId, buildInviteMarkdown, COUNCIL_SCHEMA_VERSION, contributorsInRound, councilBodyDir, councilsArchiveDir, deleteArchivedCouncil, effectiveSteward, findManifestByPartialId, listKnownAgents, listManifests, moveFromArchive, moveToArchive, normalizeAgentName, pendingCouncilsForMember, readArchivedManifest, readManifest, readRoundPrompts, roundDir, setCouncilSteward, writeContribution, writeManifest, writePrompt, } from "../lib/council/index.js";
|
|
@@ -43,7 +57,7 @@ import { displayName as displayAgentName, ensureIdentity, listIdentities, lookup
|
|
|
43
57
|
function freshnessCutoffSecs() {
|
|
44
58
|
return coordFreshnessSeconds();
|
|
45
59
|
}
|
|
46
|
-
const SUBAGENT_NOTE = "Bash identity is process-level
|
|
60
|
+
const SUBAGENT_NOTE = "Bash identity is process-level; if you're running inside a subagent, " +
|
|
47
61
|
"this resolves to the parent group's name, not the subagent's. A subagent-aware " +
|
|
48
62
|
"bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
|
|
49
63
|
/**
|
|
@@ -108,6 +122,18 @@ function activityOf(hb) {
|
|
|
108
122
|
function taskStateOf(hb) {
|
|
109
123
|
return hb.task_state ?? "active";
|
|
110
124
|
}
|
|
125
|
+
/** Producer joins use the private native session ID; `session_id` is the canonical V3 fingerprint. */
|
|
126
|
+
export function nativeSessionIdentity(row, fallback) {
|
|
127
|
+
if (row?.native_session_id)
|
|
128
|
+
return row.native_session_id;
|
|
129
|
+
// A projection-only row carries the privacy-safe canonical session
|
|
130
|
+
// fingerprint, not the adapter's native session ID needed to join the hook
|
|
131
|
+
// producer. Until a disposable cache exists, the resolved owner is the only
|
|
132
|
+
// native identity available to command surfaces such as `agents status`.
|
|
133
|
+
if (row?.session_id && !/^sid_[a-f0-9]{64}$/.test(row.session_id))
|
|
134
|
+
return row.session_id;
|
|
135
|
+
return fallback;
|
|
136
|
+
}
|
|
111
137
|
function lifecycleLabel(hb) {
|
|
112
138
|
const state = taskStateOf(hb);
|
|
113
139
|
return state === "blocked" && hb.task_state_reason ? `${state}: ${hb.task_state_reason}` : state;
|
|
@@ -130,7 +156,7 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
130
156
|
.command("list")
|
|
131
157
|
.description("List all active agents (folds kind=transient by default)")
|
|
132
158
|
.option("--all", "Include raw kind=transient rows (no fold)")
|
|
133
|
-
.option("--stale", "Include
|
|
159
|
+
.option("--stale", "Include generations older than the freshness window")
|
|
134
160
|
.option("--json", "JSON output (alias for --format json)")
|
|
135
161
|
.action((opts) => {
|
|
136
162
|
runList(opts);
|
|
@@ -140,7 +166,8 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
140
166
|
.description("End-of-turn status box (name + session age + files held + peer count)")
|
|
141
167
|
.option("--json", "JSON output instead of the box")
|
|
142
168
|
.option("--end-turn", "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")
|
|
143
|
-
.option("--session
|
|
169
|
+
.option("--end-session", "After the closing status, durably request an authoritative session end as soon as this exact turn closes (requires --end-turn)")
|
|
170
|
+
.option("--session-id <id>", "Lookup the V3 generation by session_id directly, bypassing the ppid walk. " +
|
|
144
171
|
"Use this when calling from a hook (the hook's process tree may not lead back to Claude Code's session pid). " +
|
|
145
172
|
"The Stop hook payload includes session_id; pass it through.")
|
|
146
173
|
.action((opts) => {
|
|
@@ -153,13 +180,13 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
153
180
|
"is set-task (it suggests a name on the first focus declaration); reach for this to reprint " +
|
|
154
181
|
"or re-suggest after a topic pivot. Read-only.")
|
|
155
182
|
.option("--json", "JSON output instead of the bare name")
|
|
156
|
-
.option("--session-id <id>", "Lookup
|
|
183
|
+
.option("--session-id <id>", "Lookup the V3 generation by session_id directly, bypassing the ppid walk.")
|
|
157
184
|
.action((description, opts) => {
|
|
158
185
|
runSuggestName(description, opts);
|
|
159
186
|
});
|
|
160
187
|
cmd
|
|
161
188
|
.command("watch")
|
|
162
|
-
.description("Stream peer state changes
|
|
189
|
+
.description("Stream peer state changes from the authoritative V3 coordination projection. " +
|
|
163
190
|
"Prints one line per delta: started / ended / activity / file claim / task change.")
|
|
164
191
|
.option("--poll-ms <n>", "Debounce window after a change event", "200")
|
|
165
192
|
.action(async (opts) => {
|
|
@@ -175,14 +202,14 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
175
202
|
});
|
|
176
203
|
cmd
|
|
177
204
|
.command("trace <name>")
|
|
178
|
-
.description("Reconstruct one agent's coordination lifecycle from
|
|
179
|
-
"session.
|
|
205
|
+
.description("Reconstruct one agent's coordination lifecycle from the active canonical event ledger: " +
|
|
206
|
+
"session.started → prompts → turns → tools → observations → session.ended, " +
|
|
180
207
|
"in chronological order. The answer to 'what happened to this agent / why did " +
|
|
181
208
|
"it vanish?' without hand-grepping the stream. Accepts a name (agent-Foo or Foo) " +
|
|
182
209
|
"or an instance_id.")
|
|
183
210
|
.option("--since <window>", "Only events newer than Nh|Nd (default: all)")
|
|
184
211
|
.option("--limit <n>", "Show at most N most-recent events. Default: 200.", "200")
|
|
185
|
-
.option("--all-tools", "Include tool.
|
|
212
|
+
.option("--all-tools", "Include tool.completed + command.* (default: hidden as noise)")
|
|
186
213
|
.option("--json", "JSON envelope output")
|
|
187
214
|
.action((name, opts) => {
|
|
188
215
|
runTrace(name, opts);
|
|
@@ -203,6 +230,41 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
203
230
|
.action((state, opts) => {
|
|
204
231
|
runLifecycle(state, opts);
|
|
205
232
|
});
|
|
233
|
+
cmd
|
|
234
|
+
.command("end")
|
|
235
|
+
.description("Finalize the current V3 session explicitly, or durably queue finalization until the current turn and tool spans close.")
|
|
236
|
+
.option("--session-id <id>", "Native adapter session id to finalize")
|
|
237
|
+
.option("--instance-id <id>", "Canonical V3 instance id to finalize")
|
|
238
|
+
.option("--outcome <outcome>", "succeeded | failed | cancelled | timed_out | denied | interrupted | unknown", "succeeded")
|
|
239
|
+
.action((opts) => {
|
|
240
|
+
runEndSession(opts);
|
|
241
|
+
});
|
|
242
|
+
cmd
|
|
243
|
+
.command("reconcile")
|
|
244
|
+
.description("Reconcile archive, idle, parent/run completion, stale, superseded, and host lifecycle signals into V3 session finalization.")
|
|
245
|
+
.option("--watch", "Keep reconciling until interrupted")
|
|
246
|
+
.option("--interval-seconds <n>", "Watch interval in seconds")
|
|
247
|
+
.option("--json", "JSON output")
|
|
248
|
+
.action(async (opts) => {
|
|
249
|
+
await runSessionReconcile(opts);
|
|
250
|
+
});
|
|
251
|
+
cmd
|
|
252
|
+
.command("observe-archive")
|
|
253
|
+
.description("Record an adapter archive or unarchive observation and reconcile it through the canonical V3 finalizer.")
|
|
254
|
+
.requiredOption("--adapter <id>", "claude-code | codex | cursor")
|
|
255
|
+
.requiredOption("--session-id <id>", "Native adapter session id")
|
|
256
|
+
.option("--unarchived", "Cancel a pending archive finalization")
|
|
257
|
+
.option("--observed-at <iso>", "Observation time (defaults to now)")
|
|
258
|
+
.action((opts) => runObserveArchive(opts));
|
|
259
|
+
cmd
|
|
260
|
+
.command("observe-host-disappeared")
|
|
261
|
+
.description("Record a provisional host-loss observation; finalization follows only after the configured cascade grace period.")
|
|
262
|
+
.requiredOption("--instance-id <id>", "Canonical V3 instance id")
|
|
263
|
+
.requiredOption("--generation-id <id>", "Canonical V3 generation id")
|
|
264
|
+
.option("--observed-at <iso>", "Observation time (defaults to now)")
|
|
265
|
+
.action((opts) => {
|
|
266
|
+
runObserveHostDisappeared(opts);
|
|
267
|
+
});
|
|
206
268
|
cmd
|
|
207
269
|
.command("release-claim <path>")
|
|
208
270
|
.description("Drop a file claim from your heartbeat. Operator escape hatch when a " +
|
|
@@ -269,15 +331,13 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
269
331
|
cmd
|
|
270
332
|
.command("heal")
|
|
271
333
|
.description("Force a coord-layer recovery action on a specific agent. " +
|
|
272
|
-
"Kinds: pidmap (
|
|
273
|
-
"
|
|
274
|
-
"the live hooks use, so it governs the operation safely.")
|
|
334
|
+
"Kinds: pidmap (repair process attribution) and cache (rebuild the " +
|
|
335
|
+
"disposable V3 coordination cache from the authoritative ledger).")
|
|
275
336
|
.requiredOption("--owner <id>", "Target agent's instance_id")
|
|
276
|
-
.requiredOption("--kind <kind>", "pidmap |
|
|
277
|
-
.option("--session-id <id>", "(--kind
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"session_id fails schema validation and pollutes the audit trail).")
|
|
337
|
+
.requiredOption("--kind <kind>", "pidmap | cache")
|
|
338
|
+
.option("--session-id <id>", "(--kind cache) native session id used to join the authoritative V3 generation.")
|
|
339
|
+
.option("--adapter <id>", "(--kind cache) adapter used to validate the authoritative V3 generation: " +
|
|
340
|
+
"claude-code | cursor | codex.")
|
|
281
341
|
.option("--pid <pid>", "(--kind pidmap) PID to register in pid-map. Default: walk " +
|
|
282
342
|
"this shell's ppid chain for a claude process. Pass explicitly " +
|
|
283
343
|
"when calling from outside Claude Code's Bash tool tree (e.g. " +
|
|
@@ -288,6 +348,162 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
288
348
|
});
|
|
289
349
|
registerIdentityCommands(cmd);
|
|
290
350
|
}
|
|
351
|
+
const SESSION_OUTCOMES = new Set([
|
|
352
|
+
"succeeded",
|
|
353
|
+
"failed",
|
|
354
|
+
"cancelled",
|
|
355
|
+
"timed_out",
|
|
356
|
+
"denied",
|
|
357
|
+
"interrupted",
|
|
358
|
+
"unknown",
|
|
359
|
+
]);
|
|
360
|
+
function runEndSession(opts) {
|
|
361
|
+
const root = monorepoRoot();
|
|
362
|
+
if (!root)
|
|
363
|
+
return failCommand("not_in_repo", "not in an agent session");
|
|
364
|
+
if (!SESSION_OUTCOMES.has(opts.outcome)) {
|
|
365
|
+
return failCommand("invalid_outcome", "invalid session outcome");
|
|
366
|
+
}
|
|
367
|
+
const target = opts.instanceId ?? opts.sessionId ?? resolveOwner();
|
|
368
|
+
if (!target) {
|
|
369
|
+
return failCommand("session_identity_missing", "could not resolve the current session; pass --session-id or --instance-id");
|
|
370
|
+
}
|
|
371
|
+
const byInstance = listHookProducerStateRecordsV3(root, { includeTerminal: false }).filter(({ state }) => state.instance_id === target);
|
|
372
|
+
const byNative = ["claude-code", "codex", "cursor"].flatMap((adapter) => {
|
|
373
|
+
const state = readHookProducerStateV3(root, adapter, target);
|
|
374
|
+
return state && !state.terminal ? [{ path: "", modified_at_ms: 0, state }] : [];
|
|
375
|
+
});
|
|
376
|
+
const matches = byInstance.length > 0 ? byInstance : byNative;
|
|
377
|
+
if (matches.length !== 1) {
|
|
378
|
+
return failCommand("session_identity_ambiguous", `expected one live V3 generation for the target; found ${matches.length}`);
|
|
379
|
+
}
|
|
380
|
+
const record = matches[0];
|
|
381
|
+
if (!record) {
|
|
382
|
+
return failCommand("session_identity_missing", "live V3 generation disappeared");
|
|
383
|
+
}
|
|
384
|
+
const state = record.state;
|
|
385
|
+
if (state.delegations.length > 0) {
|
|
386
|
+
return failCommand("session_work_open", `cannot finalize while ${state.delegations.length} delegated child(ren) remain open`);
|
|
387
|
+
}
|
|
388
|
+
const history = readSessionWriteClaims(root, state.instance_id, state.session_id);
|
|
389
|
+
const finalized = checkGitFinalization(root, history.paths, {
|
|
390
|
+
claimHistoryComplete: history.complete,
|
|
391
|
+
});
|
|
392
|
+
if (!finalized.ok) {
|
|
393
|
+
return failCommand("git_not_finalized", formatGitFinalizationFailure(finalized, resolveBinName(root)));
|
|
394
|
+
}
|
|
395
|
+
const result = requestSessionEndExplicitV3({
|
|
396
|
+
coordRoot: root,
|
|
397
|
+
instance_id: state.instance_id,
|
|
398
|
+
generation_id: state.generation_id,
|
|
399
|
+
outcome: opts.outcome,
|
|
400
|
+
coordination_finalized: true,
|
|
401
|
+
});
|
|
402
|
+
if (result.state !== "recorded" &&
|
|
403
|
+
result.state !== "already_ended" &&
|
|
404
|
+
result.state !== "queued" &&
|
|
405
|
+
result.state !== "already_requested") {
|
|
406
|
+
return failCommand("session_end_failed", JSON.stringify(result));
|
|
407
|
+
}
|
|
408
|
+
const terminalEventId = result.state === "recorded"
|
|
409
|
+
? result.event.event_id
|
|
410
|
+
: result.state === "already_ended"
|
|
411
|
+
? result.event_id
|
|
412
|
+
: undefined;
|
|
413
|
+
const requestId = result.state === "queued" || result.state === "already_requested"
|
|
414
|
+
? result.request.request_id
|
|
415
|
+
: undefined;
|
|
416
|
+
emit.data({
|
|
417
|
+
ok: true,
|
|
418
|
+
state: result.state,
|
|
419
|
+
instance_id: state.instance_id,
|
|
420
|
+
generation_id: state.generation_id,
|
|
421
|
+
...(terminalEventId ? { terminal_event_id: terminalEventId } : {}),
|
|
422
|
+
...(requestId ? { request_id: requestId } : {}),
|
|
423
|
+
authority: "approved",
|
|
424
|
+
reason: "approved_explicit_end",
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async function runSessionReconcile(opts) {
|
|
428
|
+
const root = monorepoRoot();
|
|
429
|
+
if (!root)
|
|
430
|
+
return failCommand("not_in_repo", "not in an agent session");
|
|
431
|
+
if (opts.json)
|
|
432
|
+
emit.config({ format: "json" });
|
|
433
|
+
const interval = opts.intervalSeconds
|
|
434
|
+
? Number.parseInt(opts.intervalSeconds, 10)
|
|
435
|
+
: sessionFinalizationConfig(root).reconcileIntervalSeconds;
|
|
436
|
+
if (!Number.isSafeInteger(interval) || interval < 1) {
|
|
437
|
+
return failCommand("invalid_interval", "interval-seconds must be a positive integer");
|
|
438
|
+
}
|
|
439
|
+
let stopped = false;
|
|
440
|
+
do {
|
|
441
|
+
emit.data(reconcileSessionFinalizationV3(root));
|
|
442
|
+
if (!opts.watch || stopped)
|
|
443
|
+
return;
|
|
444
|
+
await new Promise((resolvePromise) => {
|
|
445
|
+
const timer = setTimeout(resolvePromise, interval * 1_000);
|
|
446
|
+
const stop = () => {
|
|
447
|
+
stopped = true;
|
|
448
|
+
clearTimeout(timer);
|
|
449
|
+
process.off("SIGINT", stop);
|
|
450
|
+
process.off("SIGTERM", stop);
|
|
451
|
+
resolvePromise();
|
|
452
|
+
};
|
|
453
|
+
process.once("SIGINT", stop);
|
|
454
|
+
process.once("SIGTERM", stop);
|
|
455
|
+
});
|
|
456
|
+
} while (opts.watch && !stopped);
|
|
457
|
+
}
|
|
458
|
+
function runObserveArchive(opts) {
|
|
459
|
+
const root = monorepoRoot();
|
|
460
|
+
if (!root)
|
|
461
|
+
return failCommand("not_in_repo", "not in an agent session");
|
|
462
|
+
if (!["claude-code", "codex", "cursor"].includes(opts.adapter)) {
|
|
463
|
+
return failCommand("invalid_adapter", "adapter must be claude-code, codex, or cursor");
|
|
464
|
+
}
|
|
465
|
+
const adapter = opts.adapter;
|
|
466
|
+
const observedAt = opts.observedAt ?? new Date().toISOString();
|
|
467
|
+
if (Number.isNaN(Date.parse(observedAt))) {
|
|
468
|
+
return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
|
|
469
|
+
}
|
|
470
|
+
emit.data(reconcileSessionFinalizationV3(root, {
|
|
471
|
+
now: new Date(observedAt),
|
|
472
|
+
archive_observations: [
|
|
473
|
+
{
|
|
474
|
+
adapter,
|
|
475
|
+
native_session_id: opts.sessionId,
|
|
476
|
+
archived: !opts.unarchived,
|
|
477
|
+
observed_at: observedAt,
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
function runObserveHostDisappeared(opts) {
|
|
483
|
+
const root = monorepoRoot();
|
|
484
|
+
if (!root)
|
|
485
|
+
return failCommand("not_in_repo", "not in an agent session");
|
|
486
|
+
if (!/^inst_[A-Za-z0-9._-]+$/.test(opts.instanceId)) {
|
|
487
|
+
return failCommand("invalid_instance_id", "invalid V3 instance id");
|
|
488
|
+
}
|
|
489
|
+
if (!/^gen_[A-Za-z0-9._-]+$/.test(opts.generationId)) {
|
|
490
|
+
return failCommand("invalid_generation_id", "invalid V3 generation id");
|
|
491
|
+
}
|
|
492
|
+
const observedAt = opts.observedAt ?? new Date().toISOString();
|
|
493
|
+
if (Number.isNaN(Date.parse(observedAt))) {
|
|
494
|
+
return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
|
|
495
|
+
}
|
|
496
|
+
emit.data(observeHostDisappearedV3({
|
|
497
|
+
coordRoot: root,
|
|
498
|
+
instance_id: opts.instanceId,
|
|
499
|
+
generation_id: opts.generationId,
|
|
500
|
+
observed_at: observedAt,
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
function failCommand(code, message) {
|
|
504
|
+
emit.error({ code, message });
|
|
505
|
+
process.exitCode = 1;
|
|
506
|
+
}
|
|
291
507
|
function registerIdentityCommands(parent) {
|
|
292
508
|
const identity = parent
|
|
293
509
|
.command("identity")
|
|
@@ -505,7 +721,7 @@ export function registerCouncilCommands(parent) {
|
|
|
505
721
|
"archive it first (trash-can pattern). Without --yes this prints " +
|
|
506
722
|
"the paths that would be removed and exits 0 without touching " +
|
|
507
723
|
"anything. Does NOT touch target_doc, close_handoff_path, or " +
|
|
508
|
-
"
|
|
724
|
+
"the canonical V3 event ledger, which is owned independently.")
|
|
509
725
|
.option("-y, --yes", "Required to actually delete; without this, dry-run")
|
|
510
726
|
.option("--json", "JSON envelope output")
|
|
511
727
|
.action((id, opts) => {
|
|
@@ -596,11 +812,11 @@ function runWhoami(opts) {
|
|
|
596
812
|
});
|
|
597
813
|
process.exit(1);
|
|
598
814
|
}
|
|
599
|
-
const hb =
|
|
815
|
+
const hb = readLiveCoordinationRow(root, myOwner);
|
|
600
816
|
if (!hb) {
|
|
601
817
|
emit.error({
|
|
602
|
-
code: "
|
|
603
|
-
message:
|
|
818
|
+
code: "no_live_generation",
|
|
819
|
+
message: noLiveGenerationMessage(myOwner),
|
|
604
820
|
});
|
|
605
821
|
process.exit(1);
|
|
606
822
|
}
|
|
@@ -611,20 +827,17 @@ function runWhoami(opts) {
|
|
|
611
827
|
session_id: hb.session_id,
|
|
612
828
|
kind: normalizeKind(hb.kind),
|
|
613
829
|
relation: "self",
|
|
614
|
-
started_at: hb.started_at,
|
|
830
|
+
started_at: hb.started_at ?? hb.last_heartbeat,
|
|
615
831
|
last_heartbeat: hb.last_heartbeat,
|
|
616
832
|
files_touched: hb.files_touched ?? [],
|
|
617
|
-
last_tool: hb.last_tool ?? null,
|
|
618
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
619
833
|
task: hb.task ?? null,
|
|
620
834
|
activity: activityOf(hb),
|
|
621
835
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
622
836
|
activity_source: hb.activity_source ?? null,
|
|
623
837
|
task_state: taskStateOf(hb),
|
|
838
|
+
task_state_scope: "current",
|
|
624
839
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
625
840
|
task_state_reason: hb.task_state_reason ?? null,
|
|
626
|
-
turn_summary: hb.turn_summary ?? null,
|
|
627
|
-
turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
|
|
628
841
|
platform: hb.platform ?? "claude-code",
|
|
629
842
|
};
|
|
630
843
|
emit.data({ ...row, resolution_source: resolved.source, note: SUBAGENT_NOTE });
|
|
@@ -644,37 +857,17 @@ function runList(opts) {
|
|
|
644
857
|
});
|
|
645
858
|
process.exit(1);
|
|
646
859
|
}
|
|
647
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
648
|
-
if (!existsSync(activeDir)) {
|
|
649
|
-
emit.data({ rows: [], note: SUBAGENT_NOTE });
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
860
|
// Resolve self for relation column; best-effort, missing → "unknown" on every row.
|
|
653
861
|
const myOwner = resolveOwner();
|
|
654
|
-
const myHb = myOwner ?
|
|
862
|
+
const myHb = myOwner ? readLiveCoordinationRow(root, myOwner) : null;
|
|
655
863
|
const mySession = myHb?.session_id ?? null;
|
|
656
|
-
|
|
657
|
-
const heartbeats = [];
|
|
658
|
-
for (const file of readdirSync(activeDir)) {
|
|
659
|
-
if (!file.endsWith(".json"))
|
|
660
|
-
continue;
|
|
661
|
-
try {
|
|
662
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
663
|
-
const parsed = JSON.parse(raw);
|
|
664
|
-
if (parsed && typeof parsed.instance_id === "string") {
|
|
665
|
-
heartbeats.push(parsed);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
catch {
|
|
669
|
-
// skip malformed
|
|
670
|
-
}
|
|
671
|
-
}
|
|
864
|
+
const generations = readLiveCoordinationRows(root);
|
|
672
865
|
// Apply staleness filter unless --stale.
|
|
673
866
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
674
867
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
675
868
|
const live = opts.stale
|
|
676
|
-
?
|
|
677
|
-
:
|
|
869
|
+
? generations
|
|
870
|
+
: generations.filter((h) => {
|
|
678
871
|
const ts = Date.parse(h.last_heartbeat);
|
|
679
872
|
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
680
873
|
});
|
|
@@ -706,20 +899,17 @@ function runList(opts) {
|
|
|
706
899
|
session_id: h.session_id,
|
|
707
900
|
kind: "transient",
|
|
708
901
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
709
|
-
started_at: h.started_at,
|
|
902
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
710
903
|
last_heartbeat: h.last_heartbeat,
|
|
711
904
|
files_touched: [...(h.files_touched ?? [])].sort(),
|
|
712
|
-
last_tool: h.last_tool ?? null,
|
|
713
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
714
905
|
task: h.task ?? null,
|
|
715
906
|
activity: activityOf(h),
|
|
716
907
|
activity_updated_at: h.activity_updated_at ?? null,
|
|
717
908
|
activity_source: h.activity_source ?? null,
|
|
718
909
|
task_state: taskStateOf(h),
|
|
910
|
+
task_state_scope: "current",
|
|
719
911
|
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
720
912
|
task_state_reason: h.task_state_reason ?? null,
|
|
721
|
-
turn_summary: h.turn_summary ?? null,
|
|
722
|
-
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
723
913
|
platform: h.platform ?? "claude-code",
|
|
724
914
|
});
|
|
725
915
|
continue;
|
|
@@ -737,30 +927,26 @@ function runList(opts) {
|
|
|
737
927
|
session_id: h.session_id,
|
|
738
928
|
kind: normalizeKind(h.kind),
|
|
739
929
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
740
|
-
started_at: h.started_at,
|
|
930
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
741
931
|
last_heartbeat: h.last_heartbeat,
|
|
742
932
|
files_touched: files,
|
|
743
|
-
last_tool: h.last_tool ?? null,
|
|
744
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
745
933
|
task: h.task ?? null,
|
|
746
934
|
activity: activityOf(h),
|
|
747
935
|
activity_updated_at: h.activity_updated_at ?? null,
|
|
748
936
|
activity_source: h.activity_source ?? null,
|
|
749
937
|
task_state: taskStateOf(h),
|
|
938
|
+
task_state_scope: "current",
|
|
750
939
|
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
751
940
|
task_state_reason: h.task_state_reason ?? null,
|
|
752
|
-
turn_summary: h.turn_summary ?? null,
|
|
753
|
-
turn_summary_updated_at: h.turn_summary_updated_at ?? null,
|
|
754
941
|
platform: h.platform ?? "claude-code",
|
|
755
942
|
});
|
|
756
943
|
}
|
|
757
|
-
// Guard missing started_at
|
|
758
|
-
//
|
|
759
|
-
// what made `harn agents list --all --stale` crash.
|
|
944
|
+
// Guard missing started_at so a partial observational projection cannot make
|
|
945
|
+
// `harn agents list --all --stale` throw while sorting.
|
|
760
946
|
rows.sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? ""));
|
|
761
947
|
// Cross-machine presence (ADR 0016): append sessions on OTHER machines from
|
|
762
948
|
// the locally-fetched presence refs. Advisory rows (relation=remote) — they
|
|
763
|
-
//
|
|
949
|
+
// never participate in local claim blocking.
|
|
764
950
|
for (const rm of readRemoteMachines(root)) {
|
|
765
951
|
for (const a of rm.agents) {
|
|
766
952
|
rows.push({
|
|
@@ -772,17 +958,14 @@ function runList(opts) {
|
|
|
772
958
|
started_at: a.started_at ?? "",
|
|
773
959
|
last_heartbeat: a.last_heartbeat ?? "",
|
|
774
960
|
files_touched: [...(a.files_touched ?? [])].sort(),
|
|
775
|
-
last_tool: a.last_tool ?? null,
|
|
776
|
-
last_tool_target: null,
|
|
777
961
|
task: a.task ?? null,
|
|
778
962
|
activity: a.activity ?? "unknown",
|
|
779
963
|
activity_updated_at: null,
|
|
780
964
|
activity_source: null,
|
|
781
965
|
task_state: a.task_state ?? "active",
|
|
966
|
+
task_state_scope: "current",
|
|
782
967
|
task_state_updated_at: null,
|
|
783
968
|
task_state_reason: a.task_state_reason ?? null,
|
|
784
|
-
turn_summary: a.turn_summary ?? null,
|
|
785
|
-
turn_summary_updated_at: null,
|
|
786
969
|
platform: a.platform ?? "claude-code",
|
|
787
970
|
machine: rm.machine,
|
|
788
971
|
});
|
|
@@ -809,21 +992,17 @@ function normalizeKind(kind) {
|
|
|
809
992
|
return kind;
|
|
810
993
|
}
|
|
811
994
|
/**
|
|
812
|
-
* The `
|
|
995
|
+
* The `no_live_generation` diagnostic, quoting the owner id in full.
|
|
813
996
|
*
|
|
814
|
-
* This id is actionable: the reader
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
* here reads as complete, so it gets copy-pasted into `--owner` and mints a
|
|
818
|
-
* heartbeat at a filename no reader resolves — an orphan that leaves the
|
|
819
|
-
* session looking unhealable. Abbreviate ids for display elsewhere, never in
|
|
820
|
-
* a message whose whole purpose is to hand the reader an id to pass back.
|
|
997
|
+
* This id is actionable: the reader may pass it to cache repair or explicit
|
|
998
|
+
* finalization. Abbreviate ids for display elsewhere, never in a diagnostic
|
|
999
|
+
* whose purpose is to hand the caller a canonical identity.
|
|
821
1000
|
*/
|
|
822
|
-
function
|
|
823
|
-
return `resolved owner=${owner} but no
|
|
1001
|
+
function noLiveGenerationMessage(owner) {
|
|
1002
|
+
return `resolved owner=${owner} but no authority-safe live V3 generation exists for it`;
|
|
824
1003
|
}
|
|
825
1004
|
/**
|
|
826
|
-
* The instance_id of a live
|
|
1005
|
+
* The instance_id of a live V3 generation that `prefix` strictly prefixes, or null.
|
|
827
1006
|
*
|
|
828
1007
|
* Used to recognize an abbreviated id handed back by a reader when the caller
|
|
829
1008
|
* supplied no canonical id of their own. Ambiguity is treated as "no answer":
|
|
@@ -831,14 +1010,9 @@ function noHeartbeatMessage(owner) {
|
|
|
831
1010
|
* and refusing with the wrong id in the message would be worse than the orphan.
|
|
832
1011
|
*/
|
|
833
1012
|
function liveIdWithPrefix(root, prefix) {
|
|
834
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
835
|
-
if (!existsSync(activeDir))
|
|
836
|
-
return null;
|
|
837
1013
|
const matches = new Set();
|
|
838
|
-
for (const
|
|
839
|
-
|
|
840
|
-
continue;
|
|
841
|
-
const id = file.slice(0, -".json".length);
|
|
1014
|
+
for (const row of readLiveCoordinationRows(root)) {
|
|
1015
|
+
const id = row.instance_id;
|
|
842
1016
|
if (id !== prefix && id.startsWith(prefix))
|
|
843
1017
|
matches.add(id);
|
|
844
1018
|
}
|
|
@@ -853,101 +1027,68 @@ async function runWatch(pollMs) {
|
|
|
853
1027
|
});
|
|
854
1028
|
process.exit(1);
|
|
855
1029
|
}
|
|
856
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
857
|
-
if (!existsSync(activeDir)) {
|
|
858
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
859
|
-
process.exit(1);
|
|
860
|
-
}
|
|
861
|
-
const fs = await import("node:fs");
|
|
862
1030
|
const cache = new Map();
|
|
863
1031
|
// Seed cache + print an initial roster line per live peer.
|
|
864
|
-
const initial =
|
|
865
|
-
process.stderr.write(
|
|
1032
|
+
const initial = listLiveGenerations(root);
|
|
1033
|
+
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
|
|
866
1034
|
for (const h of initial) {
|
|
867
1035
|
cache.set(h.instance_id, h);
|
|
868
|
-
emitWatchLine(`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at))} old${h.task ? `, task: "${h.task}"` : ""})`);
|
|
1036
|
+
emitWatchLine(`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`);
|
|
869
1037
|
}
|
|
870
|
-
let scheduled = null;
|
|
871
1038
|
const rescan = () => {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
for (const [id, old] of cache) {
|
|
881
|
-
if (!current.has(id)) {
|
|
882
|
-
emitWatchLine(`agent-${old.name ?? "?"} ended`);
|
|
883
|
-
cache.delete(id);
|
|
884
|
-
}
|
|
1039
|
+
const current = new Map();
|
|
1040
|
+
for (const h of listLiveGenerations(root))
|
|
1041
|
+
current.set(h.instance_id, h);
|
|
1042
|
+
// Removed agents.
|
|
1043
|
+
for (const [id, old] of cache) {
|
|
1044
|
+
if (!current.has(id)) {
|
|
1045
|
+
emitWatchLine(`agent-${old.name ?? "?"} ended`);
|
|
1046
|
+
cache.delete(id);
|
|
885
1047
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
// Diff fields we care about.
|
|
895
|
-
if ((prev.task ?? "") !== (h.task ?? "")) {
|
|
896
|
-
emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
|
|
897
|
-
}
|
|
898
|
-
if ((prev.last_tool ?? "") !== (h.last_tool ?? "") ||
|
|
899
|
-
(prev.last_tool_target ?? "") !== (h.last_tool_target ?? "")) {
|
|
900
|
-
if (h.last_tool) {
|
|
901
|
-
const target = h.last_tool_target ? ` ${truncate(h.last_tool_target, 80)}` : "";
|
|
902
|
-
emitWatchLine(`agent-${h.name ?? "?"} ${h.last_tool}${target}`);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
// File additions/removals.
|
|
906
|
-
const prevFiles = new Set(prev.files_touched ?? []);
|
|
907
|
-
const currFiles = new Set(h.files_touched ?? []);
|
|
908
|
-
for (const f of currFiles) {
|
|
909
|
-
if (!prevFiles.has(f))
|
|
910
|
-
emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
|
|
911
|
-
}
|
|
912
|
-
for (const f of prevFiles) {
|
|
913
|
-
if (!currFiles.has(f))
|
|
914
|
-
emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
|
|
915
|
-
}
|
|
1048
|
+
}
|
|
1049
|
+
// Added or changed agents.
|
|
1050
|
+
for (const [id, h] of current) {
|
|
1051
|
+
const prev = cache.get(id);
|
|
1052
|
+
if (!prev) {
|
|
1053
|
+
emitWatchLine(`agent-${h.name ?? "?"} started (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`);
|
|
916
1054
|
cache.set(id, h);
|
|
1055
|
+
continue;
|
|
917
1056
|
}
|
|
918
|
-
|
|
1057
|
+
// Diff fields we care about.
|
|
1058
|
+
if ((prev.task ?? "") !== (h.task ?? "")) {
|
|
1059
|
+
emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
|
|
1060
|
+
}
|
|
1061
|
+
// File additions/removals.
|
|
1062
|
+
const prevFiles = new Set(prev.files_touched ?? []);
|
|
1063
|
+
const currFiles = new Set(h.files_touched ?? []);
|
|
1064
|
+
for (const f of currFiles) {
|
|
1065
|
+
if (!prevFiles.has(f))
|
|
1066
|
+
emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
|
|
1067
|
+
}
|
|
1068
|
+
for (const f of prevFiles) {
|
|
1069
|
+
if (!currFiles.has(f))
|
|
1070
|
+
emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
|
|
1071
|
+
}
|
|
1072
|
+
cache.set(id, h);
|
|
1073
|
+
}
|
|
919
1074
|
};
|
|
920
|
-
const watcher = fs.watch(activeDir, { persistent: true }, () => rescan());
|
|
921
1075
|
await new Promise((resolveP) => {
|
|
1076
|
+
const timer = setInterval(rescan, Math.max(100, pollMs));
|
|
922
1077
|
const stop = () => {
|
|
923
|
-
|
|
1078
|
+
clearInterval(timer);
|
|
924
1079
|
resolveP();
|
|
925
1080
|
};
|
|
926
1081
|
process.on("SIGINT", stop);
|
|
927
1082
|
process.on("SIGTERM", stop);
|
|
928
1083
|
});
|
|
929
1084
|
}
|
|
930
|
-
function
|
|
931
|
-
const out = [];
|
|
1085
|
+
function listLiveGenerations(root) {
|
|
932
1086
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
933
1087
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
939
|
-
const parsed = JSON.parse(raw);
|
|
940
|
-
if (!parsed || typeof parsed.instance_id !== "string")
|
|
941
|
-
continue;
|
|
942
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
943
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff)
|
|
944
|
-
out.push(parsed);
|
|
945
|
-
}
|
|
946
|
-
catch {
|
|
947
|
-
// skip
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return out;
|
|
1088
|
+
return readLiveCoordinationRows(root).filter((row) => {
|
|
1089
|
+
const ts = Date.parse(row.last_heartbeat);
|
|
1090
|
+
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
1091
|
+
});
|
|
951
1092
|
}
|
|
952
1093
|
function secondsSince(iso) {
|
|
953
1094
|
const t = Date.parse(iso);
|
|
@@ -966,33 +1107,15 @@ async function runShow(name, opts) {
|
|
|
966
1107
|
});
|
|
967
1108
|
process.exit(1);
|
|
968
1109
|
}
|
|
969
|
-
|
|
970
|
-
if (!existsSync(activeDir)) {
|
|
971
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
972
|
-
process.exit(1);
|
|
973
|
-
}
|
|
974
|
-
// Read all heartbeats; match by name (case-insensitive). Apply freshness filter.
|
|
975
|
-
const matches = [];
|
|
1110
|
+
// Match authority-safe V3 generations by resolved display name.
|
|
976
1111
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
977
1112
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
978
|
-
|
|
979
|
-
if (
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if (!parsed || typeof parsed.instance_id !== "string")
|
|
985
|
-
continue;
|
|
986
|
-
if ((parsed.name ?? "").toLowerCase() !== name.toLowerCase())
|
|
987
|
-
continue;
|
|
988
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
989
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff)
|
|
990
|
-
matches.push(parsed);
|
|
991
|
-
}
|
|
992
|
-
catch {
|
|
993
|
-
// skip malformed
|
|
994
|
-
}
|
|
995
|
-
}
|
|
1113
|
+
const matches = readLiveCoordinationRows(root).filter((row) => {
|
|
1114
|
+
if ((row.name ?? "").toLowerCase() !== name.toLowerCase())
|
|
1115
|
+
return false;
|
|
1116
|
+
const ts = Date.parse(row.last_heartbeat);
|
|
1117
|
+
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
1118
|
+
});
|
|
996
1119
|
if (matches.length === 0) {
|
|
997
1120
|
emit.error({
|
|
998
1121
|
code: "no_match",
|
|
@@ -1010,7 +1133,7 @@ async function runShow(name, opts) {
|
|
|
1010
1133
|
const hb = matches[0];
|
|
1011
1134
|
const report = null;
|
|
1012
1135
|
const bqError = null;
|
|
1013
|
-
const startedAtMs = Date.parse(hb.started_at);
|
|
1136
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1014
1137
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1015
1138
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1016
1139
|
: 0;
|
|
@@ -1030,14 +1153,11 @@ async function runShow(name, opts) {
|
|
|
1030
1153
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1031
1154
|
activity_source: hb.activity_source ?? null,
|
|
1032
1155
|
task_state: taskStateOf(hb),
|
|
1156
|
+
task_state_scope: "current",
|
|
1033
1157
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1034
1158
|
task_state_reason: hb.task_state_reason ?? null,
|
|
1035
|
-
turn_summary: hb.turn_summary ?? null,
|
|
1036
|
-
turn_summary_updated_at: hb.turn_summary_updated_at ?? null,
|
|
1037
1159
|
title: report?.title ?? null,
|
|
1038
1160
|
files_held: hb.files_touched ?? [],
|
|
1039
|
-
last_tool: hb.last_tool ?? null,
|
|
1040
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
1041
1161
|
recent_prompts: report?.recent_prompts ?? [],
|
|
1042
1162
|
recent_tools: report?.recent_tools ?? [],
|
|
1043
1163
|
tool_counts: report?.tool_counts ?? [],
|
|
@@ -1061,10 +1181,6 @@ async function runShow(name, opts) {
|
|
|
1061
1181
|
lines.push(` last heartbeat: ${formatAge(heartbeatAgeSecs)} ago`);
|
|
1062
1182
|
lines.push(` activity: ${data.activity}`);
|
|
1063
1183
|
lines.push(` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`);
|
|
1064
|
-
if (data.last_tool) {
|
|
1065
|
-
const target = data.last_tool_target ? ` ${truncate(data.last_tool_target, 80)}` : "";
|
|
1066
|
-
lines.push(` last activity: ${data.last_tool}${target}`);
|
|
1067
|
-
}
|
|
1068
1184
|
if (data.files_held.length > 0) {
|
|
1069
1185
|
lines.push(` holds ${data.files_held.length} file(s):`);
|
|
1070
1186
|
for (const f of data.files_held.slice(0, 10))
|
|
@@ -1217,13 +1333,19 @@ function runSetTask(task, opts) {
|
|
|
1217
1333
|
// Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
|
|
1218
1334
|
// mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
|
|
1219
1335
|
// don't descend from a pid-map-registered anchor, so the walk can miss there.
|
|
1336
|
+
// Final fallback: the adapter/connector-stamped session id from the
|
|
1337
|
+
// environment. set-task is the session's REGISTRATION point — a fresh
|
|
1338
|
+
// (bridge) session has no heartbeat yet, so the heartbeat-validated resolver
|
|
1339
|
+
// returns null by design, and erroring here orphans the session's first
|
|
1340
|
+
// ritual command. The env id carries the same trust as an explicit
|
|
1341
|
+
// --session-id, and set-task mints the heartbeat exactly as that path does.
|
|
1220
1342
|
if (!opts?.sessionId)
|
|
1221
1343
|
ensureCursorSession(root);
|
|
1222
|
-
const myOwner = opts?.sessionId ?? resolveOwner();
|
|
1344
|
+
const myOwner = opts?.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1223
1345
|
if (!myOwner) {
|
|
1224
1346
|
emit.error({
|
|
1225
1347
|
code: "no_pidmap_entry",
|
|
1226
|
-
message: "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1348
|
+
message: "not in an agent session; no session-id environment identity and the ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1227
1349
|
});
|
|
1228
1350
|
process.exit(1);
|
|
1229
1351
|
}
|
|
@@ -1233,7 +1355,7 @@ function runSetTask(task, opts) {
|
|
|
1233
1355
|
// naming window, and subagent/workflow kinds are never named — see
|
|
1234
1356
|
// heartbeat-writer.setTask. This call is the naming call exactly when the
|
|
1235
1357
|
// stamp appears across the mutation.
|
|
1236
|
-
const priorHb =
|
|
1358
|
+
const priorHb = readCurrentCoordinationRow(myOwner);
|
|
1237
1359
|
const normalizedTask = task.length > 0 ? task : undefined;
|
|
1238
1360
|
const lifecycleWarning = priorHb && (priorHb.task_state ?? "active") !== "active" && priorHb.task !== normalizedTask
|
|
1239
1361
|
? `Task text changed while lifecycle is ${priorHb.task_state}; run \`${resolveBinName(root)} agents lifecycle active\` to reopen it explicitly.`
|
|
@@ -1251,7 +1373,7 @@ function runSetTask(task, opts) {
|
|
|
1251
1373
|
});
|
|
1252
1374
|
process.exit(1);
|
|
1253
1375
|
}
|
|
1254
|
-
const hb =
|
|
1376
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1255
1377
|
// The naming call: the mutation just stamped `suggested_session_name` for
|
|
1256
1378
|
// the first time. `first_of_session: true` means exactly "this call produced
|
|
1257
1379
|
// the session name" — it is never true with a null name (an empty first
|
|
@@ -1260,18 +1382,6 @@ function runSetTask(task, opts) {
|
|
|
1260
1382
|
? hb.suggested_session_name
|
|
1261
1383
|
: null;
|
|
1262
1384
|
const firstOfSession = suggestedName !== null;
|
|
1263
|
-
emitCanonical({
|
|
1264
|
-
type: "state.task_set",
|
|
1265
|
-
owner: myOwner,
|
|
1266
|
-
session: hb?.session_id ?? myOwner,
|
|
1267
|
-
adapter: normalizeAdapter(hb?.platform),
|
|
1268
|
-
data: {
|
|
1269
|
-
task,
|
|
1270
|
-
cleared: !task || task.length === 0,
|
|
1271
|
-
first_of_session: firstOfSession,
|
|
1272
|
-
suggested_session_name: suggestedName,
|
|
1273
|
-
},
|
|
1274
|
-
});
|
|
1275
1385
|
emit.data({
|
|
1276
1386
|
instance_id: myOwner,
|
|
1277
1387
|
name: hb?.name ?? null,
|
|
@@ -1284,7 +1394,7 @@ function runSetTask(task, opts) {
|
|
|
1284
1394
|
// exists; this result is the moment the model holds the string.
|
|
1285
1395
|
...(suggestedName
|
|
1286
1396
|
? {
|
|
1287
|
-
note:
|
|
1397
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1288
1398
|
}
|
|
1289
1399
|
: {}),
|
|
1290
1400
|
});
|
|
@@ -1310,7 +1420,7 @@ function runLifecycle(rawState, opts) {
|
|
|
1310
1420
|
}
|
|
1311
1421
|
if (!opts.sessionId)
|
|
1312
1422
|
ensureCursorSession(root);
|
|
1313
|
-
const myOwner = opts.sessionId ?? resolveOwner();
|
|
1423
|
+
const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1314
1424
|
if (!myOwner) {
|
|
1315
1425
|
emit.error({
|
|
1316
1426
|
code: "no_pidmap_entry",
|
|
@@ -1319,11 +1429,41 @@ function runLifecycle(rawState, opts) {
|
|
|
1319
1429
|
process.exitCode = 1;
|
|
1320
1430
|
return;
|
|
1321
1431
|
}
|
|
1322
|
-
|
|
1432
|
+
let hb = readCurrentCoordinationRow(myOwner);
|
|
1433
|
+
let reopenedGeneration;
|
|
1323
1434
|
if (!hb) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1435
|
+
if (state !== "active") {
|
|
1436
|
+
emit.error({
|
|
1437
|
+
code: "no_live_generation",
|
|
1438
|
+
message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
|
|
1439
|
+
});
|
|
1440
|
+
process.exitCode = 1;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
reopenedGeneration = reopenLiveCoordinationGenerationV3({
|
|
1445
|
+
coordRoot: root,
|
|
1446
|
+
owner: myOwner,
|
|
1447
|
+
nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
catch (error) {
|
|
1451
|
+
emit.error({
|
|
1452
|
+
code: "lifecycle_reopen_failed",
|
|
1453
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1454
|
+
});
|
|
1455
|
+
process.exitCode = 1;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
hb = readCurrentCoordinationRow(myOwner);
|
|
1459
|
+
if (!hb) {
|
|
1460
|
+
emit.error({
|
|
1461
|
+
code: "lifecycle_reopen_failed",
|
|
1462
|
+
message: "the fresh generation was recorded but did not project as live",
|
|
1463
|
+
});
|
|
1464
|
+
process.exitCode = 1;
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1327
1467
|
}
|
|
1328
1468
|
if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
|
|
1329
1469
|
emit.error({
|
|
@@ -1352,6 +1492,22 @@ function runLifecycle(rawState, opts) {
|
|
|
1352
1492
|
}
|
|
1353
1493
|
const priorState = hb.task_state ?? "active";
|
|
1354
1494
|
const priorReason = hb.task_state_reason || undefined;
|
|
1495
|
+
if (reopenedGeneration) {
|
|
1496
|
+
emit.data({
|
|
1497
|
+
instance_id: myOwner,
|
|
1498
|
+
task_state: "active",
|
|
1499
|
+
prior_state: null,
|
|
1500
|
+
reason: null,
|
|
1501
|
+
changed: true,
|
|
1502
|
+
generation_reopened: true,
|
|
1503
|
+
prior_generation_id: reopenedGeneration.prior_generation_id,
|
|
1504
|
+
generation_id: reopenedGeneration.generation_id,
|
|
1505
|
+
name_reminted: false,
|
|
1506
|
+
suggested_session_name: null,
|
|
1507
|
+
git_finalization_checked: false,
|
|
1508
|
+
});
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1355
1511
|
if (priorState === state && priorReason === reason) {
|
|
1356
1512
|
emit.data({
|
|
1357
1513
|
instance_id: myOwner,
|
|
@@ -1365,7 +1521,7 @@ function runLifecycle(rawState, opts) {
|
|
|
1365
1521
|
}
|
|
1366
1522
|
let finalization = null;
|
|
1367
1523
|
if (state === "done") {
|
|
1368
|
-
const history = readSessionWriteClaims(root, hb.instance_id, hb
|
|
1524
|
+
const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
|
|
1369
1525
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1370
1526
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1371
1527
|
claimHistoryComplete: history.complete,
|
|
@@ -1379,19 +1535,16 @@ function runLifecycle(rawState, opts) {
|
|
|
1379
1535
|
return;
|
|
1380
1536
|
}
|
|
1381
1537
|
}
|
|
1382
|
-
const suggestedName = buildLifecycleSuggestedName(hb.
|
|
1538
|
+
const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
|
|
1383
1539
|
const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
|
|
1384
|
-
const emitted =
|
|
1385
|
-
type: "state.task_state",
|
|
1540
|
+
const emitted = emitEventV3({
|
|
1386
1541
|
owner: myOwner,
|
|
1387
|
-
session: hb
|
|
1542
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1388
1543
|
adapter: normalizeAdapter(hb.platform),
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
name_reminted: nameReminted,
|
|
1394
|
-
git_finalization_checked: finalization !== null,
|
|
1544
|
+
observation: {
|
|
1545
|
+
event_type: "coord.lifecycle_changed",
|
|
1546
|
+
new_state: state,
|
|
1547
|
+
...(reason ? { reason } : {}),
|
|
1395
1548
|
...(nameReminted ? { suggested_session_name: suggestedName } : {}),
|
|
1396
1549
|
},
|
|
1397
1550
|
});
|
|
@@ -1414,7 +1567,7 @@ function runLifecycle(rawState, opts) {
|
|
|
1414
1567
|
git_finalization_checked: finalization !== null,
|
|
1415
1568
|
...(nameReminted
|
|
1416
1569
|
? {
|
|
1417
|
-
note:
|
|
1570
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1418
1571
|
}
|
|
1419
1572
|
: {}),
|
|
1420
1573
|
});
|
|
@@ -1440,7 +1593,7 @@ function runSuggestName(descriptionParts, opts) {
|
|
|
1440
1593
|
});
|
|
1441
1594
|
process.exit(1);
|
|
1442
1595
|
}
|
|
1443
|
-
const hb =
|
|
1596
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1444
1597
|
const agentName = hb?.name || "unknown";
|
|
1445
1598
|
// Description resolution: an explicit arg wins; with no arg, fall back to the
|
|
1446
1599
|
// agent's current declared task, so a bare `suggest-name` reprints the running
|
|
@@ -1483,6 +1636,14 @@ function runStatus(opts) {
|
|
|
1483
1636
|
});
|
|
1484
1637
|
process.exit(1);
|
|
1485
1638
|
}
|
|
1639
|
+
if (opts.endSession && !opts.endTurn) {
|
|
1640
|
+
emit.error({
|
|
1641
|
+
code: "end_session_requires_end_turn",
|
|
1642
|
+
message: "--end-session requires --end-turn so Git finalization is verified first",
|
|
1643
|
+
});
|
|
1644
|
+
process.exitCode = 1;
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1486
1647
|
// Identity resolution: prefer explicit --session-id (hook-friendly), fall
|
|
1487
1648
|
// back to ppid walk for interactive shell usage.
|
|
1488
1649
|
if (!opts.sessionId)
|
|
@@ -1495,17 +1656,21 @@ function runStatus(opts) {
|
|
|
1495
1656
|
});
|
|
1496
1657
|
process.exit(1);
|
|
1497
1658
|
}
|
|
1498
|
-
const hb =
|
|
1659
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1499
1660
|
if (!hb) {
|
|
1500
1661
|
emit.error({
|
|
1501
|
-
code: "
|
|
1502
|
-
message:
|
|
1662
|
+
code: "no_live_generation",
|
|
1663
|
+
message: noLiveGenerationMessage(myOwner),
|
|
1503
1664
|
});
|
|
1504
1665
|
process.exit(1);
|
|
1505
1666
|
}
|
|
1667
|
+
// Report-only run quality: cheap config/cursor due check first, then a
|
|
1668
|
+
// non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
|
|
1669
|
+
const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
|
|
1670
|
+
const quality = qualityForStatus(qualityEvaluation.config.requested_mode, qualityEvaluation.snapshot);
|
|
1506
1671
|
let finalization = null;
|
|
1507
1672
|
if (opts.endTurn) {
|
|
1508
|
-
const history = readSessionWriteClaims(root, hb.instance_id, hb
|
|
1673
|
+
const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
|
|
1509
1674
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1510
1675
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1511
1676
|
claimHistoryComplete: history.complete,
|
|
@@ -1519,51 +1684,55 @@ function runStatus(opts) {
|
|
|
1519
1684
|
return;
|
|
1520
1685
|
}
|
|
1521
1686
|
}
|
|
1522
|
-
|
|
1523
|
-
type: "state.status_checked",
|
|
1687
|
+
emitEventV3({
|
|
1524
1688
|
owner: myOwner,
|
|
1525
|
-
session: hb
|
|
1689
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1526
1690
|
adapter: normalizeAdapter(hb.platform),
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
agent_count: 0, // computed below, not yet available here; Phase 5 verdict reads owner-scope only
|
|
1531
|
-
included_self: true,
|
|
1691
|
+
observation: {
|
|
1692
|
+
event_type: "coord.status_observed",
|
|
1693
|
+
status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
|
|
1532
1694
|
},
|
|
1533
1695
|
});
|
|
1534
|
-
|
|
1696
|
+
let sessionEnd;
|
|
1697
|
+
if (opts.endSession) {
|
|
1698
|
+
const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
|
|
1699
|
+
const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(({ state }) => state.instance_id === canonicalInstanceId);
|
|
1700
|
+
if (records.length !== 1 || !records[0]) {
|
|
1701
|
+
emit.error({
|
|
1702
|
+
code: "session_identity_ambiguous",
|
|
1703
|
+
message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
|
|
1704
|
+
});
|
|
1705
|
+
process.exitCode = 1;
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const state = records[0].state;
|
|
1709
|
+
const requested = requestSessionEndExplicitV3({
|
|
1710
|
+
coordRoot: root,
|
|
1711
|
+
instance_id: state.instance_id,
|
|
1712
|
+
generation_id: state.generation_id,
|
|
1713
|
+
outcome: "succeeded",
|
|
1714
|
+
coordination_finalized: true,
|
|
1715
|
+
});
|
|
1716
|
+
if (requested.state === "queued" || requested.state === "already_requested") {
|
|
1717
|
+
sessionEnd = { state: requested.state, request_id: requested.request.request_id };
|
|
1718
|
+
}
|
|
1719
|
+
else if (requested.state === "recorded") {
|
|
1720
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
|
|
1721
|
+
}
|
|
1722
|
+
else if (requested.state === "already_ended") {
|
|
1723
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
|
|
1724
|
+
}
|
|
1725
|
+
else {
|
|
1726
|
+
emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
|
|
1727
|
+
process.exitCode = 1;
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1535
1732
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1536
1733
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1537
1734
|
: 0;
|
|
1538
|
-
const
|
|
1539
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1540
|
-
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1541
|
-
const livePeers = [];
|
|
1542
|
-
let peersStale = 0;
|
|
1543
|
-
if (existsSync(activeDir)) {
|
|
1544
|
-
for (const file of readdirSync(activeDir)) {
|
|
1545
|
-
if (!file.endsWith(".json"))
|
|
1546
|
-
continue;
|
|
1547
|
-
try {
|
|
1548
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
1549
|
-
const peer = JSON.parse(raw);
|
|
1550
|
-
if (!peer || typeof peer.instance_id !== "string")
|
|
1551
|
-
continue;
|
|
1552
|
-
if (peer.instance_id === myOwner)
|
|
1553
|
-
continue;
|
|
1554
|
-
const ts = Date.parse(peer.last_heartbeat);
|
|
1555
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) {
|
|
1556
|
-
livePeers.push(peer);
|
|
1557
|
-
}
|
|
1558
|
-
else {
|
|
1559
|
-
peersStale++;
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
catch {
|
|
1563
|
-
// skip malformed
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1735
|
+
const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
|
|
1567
1736
|
// Sort: file-holders first (by file count desc), then idle peers by recency.
|
|
1568
1737
|
livePeers.sort((a, b) => {
|
|
1569
1738
|
const af = a.files_touched?.length ?? 0;
|
|
@@ -1577,7 +1746,7 @@ function runStatus(opts) {
|
|
|
1577
1746
|
// Cross-machine presence (ADR 0016): sessions on other machines, advisory.
|
|
1578
1747
|
const remoteMachines = readRemoteMachines(root);
|
|
1579
1748
|
const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
|
|
1580
|
-
const ctxUsage = readContextUsage(hb.session_id, hb.platform);
|
|
1749
|
+
const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
|
|
1581
1750
|
let ctxStr;
|
|
1582
1751
|
if (!ctxUsage) {
|
|
1583
1752
|
ctxStr = "unavailable";
|
|
@@ -1612,6 +1781,7 @@ function runStatus(opts) {
|
|
|
1612
1781
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1613
1782
|
activity_source: hb.activity_source ?? null,
|
|
1614
1783
|
task_state: taskStateOf(hb),
|
|
1784
|
+
task_state_scope: "current",
|
|
1615
1785
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1616
1786
|
task_state_reason: hb.task_state_reason ?? null,
|
|
1617
1787
|
files_held: filesHeld,
|
|
@@ -1638,7 +1808,9 @@ function runStatus(opts) {
|
|
|
1638
1808
|
context_window: ctxUsage?.window ?? null,
|
|
1639
1809
|
timestamp_iso: new Date().toISOString(),
|
|
1640
1810
|
timestamp_local: timeStr,
|
|
1811
|
+
...(quality ? { quality } : {}),
|
|
1641
1812
|
...(finalization ? { finalization } : {}),
|
|
1813
|
+
...(sessionEnd ? { session_end: sessionEnd } : {}),
|
|
1642
1814
|
};
|
|
1643
1815
|
if (opts.json) {
|
|
1644
1816
|
emit.config({ format: "json" });
|
|
@@ -1654,10 +1826,7 @@ function runStatus(opts) {
|
|
|
1654
1826
|
["peers", peersStr],
|
|
1655
1827
|
["time", timeStr],
|
|
1656
1828
|
];
|
|
1657
|
-
//
|
|
1658
|
-
if (hb.turn_summary && hb.turn_summary.length > 0) {
|
|
1659
|
-
rows.splice(1, 0, ["last turn", hb.turn_summary]);
|
|
1660
|
-
}
|
|
1829
|
+
// Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
|
|
1661
1830
|
if (hb.task && hb.task.length > 0) {
|
|
1662
1831
|
rows.splice(1, 0, ["task", hb.task]);
|
|
1663
1832
|
}
|
|
@@ -1671,10 +1840,49 @@ function runStatus(opts) {
|
|
|
1671
1840
|
: `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
|
|
1672
1841
|
rows.splice(idx, 0, ["council", summary]);
|
|
1673
1842
|
}
|
|
1843
|
+
if (quality) {
|
|
1844
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
1845
|
+
rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
|
|
1846
|
+
}
|
|
1847
|
+
if (sessionEnd) {
|
|
1848
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
1849
|
+
rows.splice(idx, 0, [
|
|
1850
|
+
"session end",
|
|
1851
|
+
sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
|
|
1852
|
+
? "queued after this turn"
|
|
1853
|
+
: "recorded",
|
|
1854
|
+
]);
|
|
1855
|
+
}
|
|
1674
1856
|
// Box rendering needs predictable stdout regardless of TTY/pipe detection:
|
|
1675
1857
|
// agent runs this via Bash (no TTY) and pastes captured stdout into chat.
|
|
1676
1858
|
process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
|
|
1677
1859
|
}
|
|
1860
|
+
function qualityForStatus(mode, snapshot) {
|
|
1861
|
+
if (mode !== "report")
|
|
1862
|
+
return null;
|
|
1863
|
+
const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
|
|
1864
|
+
if (!snapshot || !fresh) {
|
|
1865
|
+
return {
|
|
1866
|
+
status: "unknown",
|
|
1867
|
+
signal_ids: [],
|
|
1868
|
+
evaluated_at: snapshot?.evaluated_at ?? null,
|
|
1869
|
+
fresh: false,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
status: snapshot.status,
|
|
1874
|
+
signal_ids: snapshot.signals
|
|
1875
|
+
.filter((signal) => signal.state === "active" && signal.severity !== "none")
|
|
1876
|
+
.map((signal) => signal.id)
|
|
1877
|
+
.sort()
|
|
1878
|
+
.slice(0, 3),
|
|
1879
|
+
evaluated_at: snapshot.evaluated_at,
|
|
1880
|
+
fresh: true,
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function formatRunQuality(status, signalIds) {
|
|
1884
|
+
return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
|
|
1885
|
+
}
|
|
1678
1886
|
function formatList(items, cap, emptyLabel) {
|
|
1679
1887
|
if (items.length === 0)
|
|
1680
1888
|
return emptyLabel;
|
|
@@ -1715,6 +1923,28 @@ function formatPeers(peers, cap, staleCount, remoteMachines = []) {
|
|
|
1715
1923
|
}
|
|
1716
1924
|
return main;
|
|
1717
1925
|
}
|
|
1926
|
+
export function collectStatusPeerHealth(root, myOwner, nowMs = Date.now()) {
|
|
1927
|
+
let rows = [];
|
|
1928
|
+
try {
|
|
1929
|
+
rows = readLiveCoordinationRows(root);
|
|
1930
|
+
}
|
|
1931
|
+
catch {
|
|
1932
|
+
// V3 authority failures must not resurrect disposable cache rows.
|
|
1933
|
+
}
|
|
1934
|
+
const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
|
|
1935
|
+
const livePeers = [];
|
|
1936
|
+
let stale = 0;
|
|
1937
|
+
for (const peer of rows) {
|
|
1938
|
+
if (peer.instance_id === myOwner)
|
|
1939
|
+
continue;
|
|
1940
|
+
const observedAtMs = Date.parse(peer.last_heartbeat);
|
|
1941
|
+
if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs)
|
|
1942
|
+
livePeers.push(peer);
|
|
1943
|
+
else
|
|
1944
|
+
stale += 1;
|
|
1945
|
+
}
|
|
1946
|
+
return { livePeers, stale };
|
|
1947
|
+
}
|
|
1718
1948
|
function fmtTokens(n) {
|
|
1719
1949
|
if (n < 1000)
|
|
1720
1950
|
return `${n}`;
|
|
@@ -1968,40 +2198,49 @@ function formatAge(secs) {
|
|
|
1968
2198
|
const h = Math.floor((secs % 86400) / 3600);
|
|
1969
2199
|
return `${d}d ${h}h`;
|
|
1970
2200
|
}
|
|
1971
|
-
/**
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
if (!line)
|
|
1990
|
-
continue;
|
|
1991
|
-
try {
|
|
1992
|
-
const ev = JSON.parse(line);
|
|
1993
|
-
if (!ev.event_type || !ev.ts)
|
|
1994
|
-
continue;
|
|
1995
|
-
const tsMs = Date.parse(ev.ts);
|
|
2201
|
+
/** Read validated canonical V3 events. */
|
|
2202
|
+
export function readAgentDiagnosticEventsInWindow(root, cutoffMs) {
|
|
2203
|
+
const control = readEventV3ControlState(root);
|
|
2204
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
2205
|
+
const ledger = readLedgerV3(root);
|
|
2206
|
+
if (!ledger.complete) {
|
|
2207
|
+
return {
|
|
2208
|
+
source: "v3",
|
|
2209
|
+
authoritative: false,
|
|
2210
|
+
reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
|
|
2211
|
+
truncated: false,
|
|
2212
|
+
bytes: ledger.bytes,
|
|
2213
|
+
events: [],
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const events = [];
|
|
2217
|
+
for (const { event } of ledger.events) {
|
|
2218
|
+
const tsMs = Date.parse(event.time.recorded_at);
|
|
1996
2219
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
|
|
1997
2220
|
continue;
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2221
|
+
events.push({
|
|
2222
|
+
event_type: event.event_type,
|
|
2223
|
+
ts: event.time.recorded_at,
|
|
2224
|
+
instance_id: event.scope.instance_id,
|
|
2225
|
+
payload: event.payload,
|
|
2226
|
+
});
|
|
2002
2227
|
}
|
|
2228
|
+
return {
|
|
2229
|
+
source: "v3",
|
|
2230
|
+
authoritative: true,
|
|
2231
|
+
truncated: false,
|
|
2232
|
+
bytes: ledger.bytes,
|
|
2233
|
+
events,
|
|
2234
|
+
};
|
|
2003
2235
|
}
|
|
2004
|
-
return
|
|
2236
|
+
return {
|
|
2237
|
+
source: "v3",
|
|
2238
|
+
authoritative: false,
|
|
2239
|
+
reason: `V3 control state is ${control.state}`,
|
|
2240
|
+
truncated: false,
|
|
2241
|
+
bytes: 0,
|
|
2242
|
+
events: [],
|
|
2243
|
+
};
|
|
2005
2244
|
}
|
|
2006
2245
|
/** Normalize adapter event data into the heartbeat platform value. */
|
|
2007
2246
|
function adapterToPlatform(adapter) {
|
|
@@ -2011,18 +2250,21 @@ function adapterToPlatform(adapter) {
|
|
|
2011
2250
|
return "cursor";
|
|
2012
2251
|
if (adapter === "codex")
|
|
2013
2252
|
return "codex";
|
|
2014
|
-
return "
|
|
2253
|
+
return "unknown";
|
|
2015
2254
|
}
|
|
2016
2255
|
/** Project a canonical health.* event into the HealEvent shape the aggregators
|
|
2017
2256
|
* already consume. Returns null for non-heal events. instance_id → display name
|
|
2018
2257
|
* via `nameById` (full-UUID keyed). */
|
|
2019
2258
|
function canonicalToHealEvent(ev, nameById) {
|
|
2020
|
-
if (ev.event_type !== "health.
|
|
2259
|
+
if (ev.event_type !== "health.observed") {
|
|
2021
2260
|
return null;
|
|
2022
2261
|
}
|
|
2023
|
-
const
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2262
|
+
const data = ev.payload ?? {};
|
|
2263
|
+
const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
|
|
2264
|
+
if (subsystem !== "pidmap" && subsystem !== "heartbeat")
|
|
2265
|
+
return null;
|
|
2266
|
+
const kind = subsystem;
|
|
2267
|
+
const reason = data.condition === "stale" ? "stale" : "missing";
|
|
2026
2268
|
const instanceId = ev.instance_id ?? "";
|
|
2027
2269
|
const name = nameById.get(instanceId);
|
|
2028
2270
|
const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
|
|
@@ -2099,13 +2341,12 @@ function runHealEvents(opts) {
|
|
|
2099
2341
|
});
|
|
2100
2342
|
process.exit(1);
|
|
2101
2343
|
}
|
|
2102
|
-
// Heal telemetry
|
|
2103
|
-
// (health.pidmap_heal / health.heartbeat_heal), emitted by the writer on
|
|
2104
|
-
// actual self-heal writes.
|
|
2344
|
+
// Heal telemetry is read from the canonical V3 ledger.
|
|
2105
2345
|
const cutoffMs = Date.now() - sinceSecs * 1000;
|
|
2106
2346
|
const nameById = buildNameById(root);
|
|
2107
2347
|
const events = [];
|
|
2108
|
-
|
|
2348
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
2349
|
+
for (const ev of diagnosticEvents.events) {
|
|
2109
2350
|
const heal = canonicalToHealEvent(ev, nameById);
|
|
2110
2351
|
if (heal)
|
|
2111
2352
|
events.push(heal);
|
|
@@ -2142,6 +2383,11 @@ function runHealEvents(opts) {
|
|
|
2142
2383
|
const recent = events.slice(0, limit);
|
|
2143
2384
|
const data = {
|
|
2144
2385
|
since: opts.since,
|
|
2386
|
+
telemetry: {
|
|
2387
|
+
source: diagnosticEvents.source,
|
|
2388
|
+
authoritative: diagnosticEvents.authoritative,
|
|
2389
|
+
reason: diagnosticEvents.reason,
|
|
2390
|
+
},
|
|
2145
2391
|
total: events.length,
|
|
2146
2392
|
by_reason: byReason,
|
|
2147
2393
|
by_kind: byKind,
|
|
@@ -2152,7 +2398,15 @@ function runHealEvents(opts) {
|
|
|
2152
2398
|
};
|
|
2153
2399
|
if (opts.csv) {
|
|
2154
2400
|
emit.config({ format: "csv" });
|
|
2155
|
-
emit.data(
|
|
2401
|
+
emit.data(data.telemetry.authoritative
|
|
2402
|
+
? recent
|
|
2403
|
+
: [
|
|
2404
|
+
{
|
|
2405
|
+
status: "unavailable",
|
|
2406
|
+
source: data.telemetry.source,
|
|
2407
|
+
reason: data.telemetry.reason,
|
|
2408
|
+
},
|
|
2409
|
+
]);
|
|
2156
2410
|
return;
|
|
2157
2411
|
}
|
|
2158
2412
|
if (opts.json) {
|
|
@@ -2164,6 +2418,11 @@ function runHealEvents(opts) {
|
|
|
2164
2418
|
if (process.stdout.isTTY) {
|
|
2165
2419
|
const lines = [];
|
|
2166
2420
|
lines.push(`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`);
|
|
2421
|
+
if (!data.telemetry.authoritative) {
|
|
2422
|
+
lines.push(` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`);
|
|
2423
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2167
2426
|
lines.push("");
|
|
2168
2427
|
if (events.length === 0) {
|
|
2169
2428
|
lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
|
|
@@ -2211,23 +2470,206 @@ function runHealEvents(opts) {
|
|
|
2211
2470
|
emit.text(`${lines.join("\n")}\n`);
|
|
2212
2471
|
}
|
|
2213
2472
|
}
|
|
2473
|
+
/** Open-span soft watermark per producer state. The producer-state reader
|
|
2474
|
+
* hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
|
|
2475
|
+
* pressure at half that gives room to act before reads start failing. */
|
|
2476
|
+
const SPAN_PRESSURE_SOFT_WATERMARK = 128;
|
|
2477
|
+
function errorText(error) {
|
|
2478
|
+
return error instanceof Error ? error.message : String(error);
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Read-only health counters for the V3 event ledger's producer surfaces. Never
|
|
2482
|
+
* mutates ledger state (no control repair, no spool drain) and never throws:
|
|
2483
|
+
* a non-live route returns `{ state: "unavailable" }`, and each sub-surface
|
|
2484
|
+
* that fails to read lands in `collection_errors` instead of aborting the rest.
|
|
2485
|
+
*/
|
|
2486
|
+
export function collectEventLedgerHealthV3(root, nowMs = Date.now()) {
|
|
2487
|
+
let control;
|
|
2488
|
+
try {
|
|
2489
|
+
control = readEventV3ControlState(root);
|
|
2490
|
+
}
|
|
2491
|
+
catch (error) {
|
|
2492
|
+
return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
|
|
2493
|
+
}
|
|
2494
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
2495
|
+
return {
|
|
2496
|
+
state: "unavailable",
|
|
2497
|
+
reason: `${control.state}: ${control.reason}`,
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
const collectionErrors = [];
|
|
2501
|
+
// 1) Open tool spans per live (non-terminal) generation + span-count pressure.
|
|
2502
|
+
const generations = [];
|
|
2503
|
+
const spanPressure = [];
|
|
2504
|
+
try {
|
|
2505
|
+
for (const { state } of listHookProducerStateRecordsV3(root)) {
|
|
2506
|
+
if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
|
|
2507
|
+
spanPressure.push({
|
|
2508
|
+
instance_id: state.instance_id,
|
|
2509
|
+
generation_id: state.generation_id,
|
|
2510
|
+
span_count: state.spans.length,
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
if (state.spans.length === 0)
|
|
2514
|
+
continue;
|
|
2515
|
+
generations.push({
|
|
2516
|
+
instance_id: state.instance_id,
|
|
2517
|
+
generation_id: state.generation_id,
|
|
2518
|
+
adapter: state.adapter,
|
|
2519
|
+
span_count: state.spans.length,
|
|
2520
|
+
turn_open: Boolean(state.current_turn_id),
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
catch (error) {
|
|
2525
|
+
collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
|
|
2526
|
+
}
|
|
2527
|
+
// 2) Pending finalization requests with age + trigger.
|
|
2528
|
+
const pending = [];
|
|
2529
|
+
try {
|
|
2530
|
+
for (const request of listSessionFinalizationRequestsV3(root)) {
|
|
2531
|
+
if (request.status !== "pending")
|
|
2532
|
+
continue;
|
|
2533
|
+
const observedMs = Date.parse(request.observed_at);
|
|
2534
|
+
pending.push({
|
|
2535
|
+
request_id: request.request_id,
|
|
2536
|
+
trigger: request.trigger,
|
|
2537
|
+
generation_id: request.generation_id,
|
|
2538
|
+
age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
|
|
2539
|
+
allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
catch (error) {
|
|
2544
|
+
collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
|
|
2545
|
+
}
|
|
2546
|
+
// 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
|
|
2547
|
+
let intakeTotal = 0;
|
|
2548
|
+
const intakeGroups = [];
|
|
2549
|
+
try {
|
|
2550
|
+
for (const group of listHookIntakeGroupsV3(root)) {
|
|
2551
|
+
const count = listHookIntakeRecordsV3(group.directory).length;
|
|
2552
|
+
if (count === 0)
|
|
2553
|
+
continue;
|
|
2554
|
+
intakeTotal += count;
|
|
2555
|
+
intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
catch (error) {
|
|
2559
|
+
collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
|
|
2560
|
+
}
|
|
2561
|
+
// 4) Diagnostics spool counts by category. Filenames only (never the
|
|
2562
|
+
// contents): `<category>-<orderkey>.json` where the orderkey leads with a
|
|
2563
|
+
// zero-padded epoch-ms, which also gives the last-24h split for free.
|
|
2564
|
+
const byCategory = {};
|
|
2565
|
+
const recentByCategory = {};
|
|
2566
|
+
let diagnosticsTotal = 0;
|
|
2567
|
+
let diagnostics24h = 0;
|
|
2568
|
+
let diagnostics1h = 0;
|
|
2569
|
+
let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
|
|
2570
|
+
try {
|
|
2571
|
+
const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
|
|
2572
|
+
if (existsSync(diagnosticsDir)) {
|
|
2573
|
+
const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
|
|
2574
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2575
|
+
for (const name of readdirSync(diagnosticsDir)) {
|
|
2576
|
+
const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
|
|
2577
|
+
const category = match?.[1];
|
|
2578
|
+
const epochMs = Number(match?.[2]);
|
|
2579
|
+
if (!category)
|
|
2580
|
+
continue;
|
|
2581
|
+
const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
|
|
2582
|
+
const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
|
|
2583
|
+
byCategory[category] = entry;
|
|
2584
|
+
recentByCategory[category] = recent;
|
|
2585
|
+
entry.total += 1;
|
|
2586
|
+
diagnosticsTotal += 1;
|
|
2587
|
+
if (Number.isFinite(epochMs)) {
|
|
2588
|
+
const recordedAt = new Date(epochMs).toISOString();
|
|
2589
|
+
if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
|
|
2590
|
+
recent.latest_at = recordedAt;
|
|
2591
|
+
}
|
|
2592
|
+
latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
|
|
2593
|
+
if (epochMs >= dayAgoMs) {
|
|
2594
|
+
entry.last_24h += 1;
|
|
2595
|
+
diagnostics24h += 1;
|
|
2596
|
+
}
|
|
2597
|
+
if (epochMs >= hourAgoMs) {
|
|
2598
|
+
recent.last_1h += 1;
|
|
2599
|
+
diagnostics1h += 1;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
catch (error) {
|
|
2606
|
+
collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
state: "live",
|
|
2610
|
+
mode: control.state,
|
|
2611
|
+
open_spans: {
|
|
2612
|
+
total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
|
|
2613
|
+
generations,
|
|
2614
|
+
},
|
|
2615
|
+
pending_finalizations: pending,
|
|
2616
|
+
intake_spool: { total: intakeTotal, groups: intakeGroups },
|
|
2617
|
+
diagnostics_spool: {
|
|
2618
|
+
total: diagnosticsTotal,
|
|
2619
|
+
last_24h: diagnostics24h,
|
|
2620
|
+
last_1h: diagnostics1h,
|
|
2621
|
+
latest_at: Number.isFinite(latestDiagnosticMs)
|
|
2622
|
+
? new Date(latestDiagnosticMs).toISOString()
|
|
2623
|
+
: null,
|
|
2624
|
+
by_category: byCategory,
|
|
2625
|
+
recent_by_category: recentByCategory,
|
|
2626
|
+
},
|
|
2627
|
+
span_pressure: spanPressure,
|
|
2628
|
+
collection_errors: collectionErrors,
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2214
2631
|
/** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
|
|
2215
|
-
* window
|
|
2216
|
-
*
|
|
2217
|
-
function readHookErrors(root, cutoffMs) {
|
|
2632
|
+
* window by exact error and phase, while separating current-hour failures from
|
|
2633
|
+
* historical rows. Each line is {ts, error, phase, ...}. */
|
|
2634
|
+
export function readHookErrors(root, cutoffMs, nowMs = Date.now()) {
|
|
2218
2635
|
const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
|
|
2219
2636
|
const byPhase = {};
|
|
2637
|
+
const byError = {};
|
|
2220
2638
|
const sampleByPhase = {};
|
|
2639
|
+
const errorPhaseCounts = new Map();
|
|
2640
|
+
const recentErrorPhaseCounts = new Map();
|
|
2221
2641
|
let total = 0;
|
|
2222
|
-
|
|
2223
|
-
|
|
2642
|
+
let last1h = 0;
|
|
2643
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
2644
|
+
if (!existsSync(p)) {
|
|
2645
|
+
return {
|
|
2646
|
+
total: 0,
|
|
2647
|
+
last1h: 0,
|
|
2648
|
+
latestAt: null,
|
|
2649
|
+
byPhase,
|
|
2650
|
+
byError,
|
|
2651
|
+
top: [],
|
|
2652
|
+
topErrors: [],
|
|
2653
|
+
recentTopErrors: [],
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2224
2656
|
let raw;
|
|
2225
2657
|
try {
|
|
2226
2658
|
raw = readFileSync(p, "utf8");
|
|
2227
2659
|
}
|
|
2228
2660
|
catch {
|
|
2229
|
-
return {
|
|
2661
|
+
return {
|
|
2662
|
+
total: 0,
|
|
2663
|
+
last1h: 0,
|
|
2664
|
+
latestAt: null,
|
|
2665
|
+
byPhase,
|
|
2666
|
+
byError,
|
|
2667
|
+
top: [],
|
|
2668
|
+
topErrors: [],
|
|
2669
|
+
recentTopErrors: [],
|
|
2670
|
+
};
|
|
2230
2671
|
}
|
|
2672
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2231
2673
|
for (const line of raw.split("\n")) {
|
|
2232
2674
|
if (!line.trim())
|
|
2233
2675
|
continue;
|
|
@@ -2237,9 +2679,22 @@ function readHookErrors(root, cutoffMs) {
|
|
|
2237
2679
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
|
|
2238
2680
|
continue;
|
|
2239
2681
|
const phase = e.phase ?? "(unknown)";
|
|
2682
|
+
const error = e.error ?? "(unknown)";
|
|
2240
2683
|
byPhase[phase] = (byPhase[phase] ?? 0) + 1;
|
|
2684
|
+
byError[error] = (byError[error] ?? 0) + 1;
|
|
2241
2685
|
if (!sampleByPhase[phase] && e.error)
|
|
2242
2686
|
sampleByPhase[phase] = e.error;
|
|
2687
|
+
const errorPhaseKey = `${error}\0${phase}`;
|
|
2688
|
+
const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
2689
|
+
errorPhase.count += 1;
|
|
2690
|
+
errorPhaseCounts.set(errorPhaseKey, errorPhase);
|
|
2691
|
+
latestMs = Math.max(latestMs, tsMs);
|
|
2692
|
+
if (tsMs >= hourAgoMs) {
|
|
2693
|
+
last1h += 1;
|
|
2694
|
+
const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
2695
|
+
recent.count += 1;
|
|
2696
|
+
recentErrorPhaseCounts.set(errorPhaseKey, recent);
|
|
2697
|
+
}
|
|
2243
2698
|
total++;
|
|
2244
2699
|
}
|
|
2245
2700
|
catch {
|
|
@@ -2250,125 +2705,163 @@ function readHookErrors(root, cutoffMs) {
|
|
|
2250
2705
|
.sort((a, b) => b[1] - a[1])
|
|
2251
2706
|
.slice(0, 5)
|
|
2252
2707
|
.map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
|
|
2253
|
-
|
|
2708
|
+
const topErrors = [...errorPhaseCounts.values()]
|
|
2709
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
2710
|
+
.slice(0, 5);
|
|
2711
|
+
const recentTopErrors = [...recentErrorPhaseCounts.values()]
|
|
2712
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
2713
|
+
.slice(0, 5);
|
|
2714
|
+
return {
|
|
2715
|
+
total,
|
|
2716
|
+
last1h,
|
|
2717
|
+
latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
|
|
2718
|
+
byPhase,
|
|
2719
|
+
byError,
|
|
2720
|
+
top,
|
|
2721
|
+
topErrors,
|
|
2722
|
+
recentTopErrors,
|
|
2723
|
+
};
|
|
2254
2724
|
}
|
|
2255
2725
|
/** Canonical event stream size + drain lag (events appended after the cursor). */
|
|
2256
2726
|
function readStreamStats(root) {
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
bytes
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
try {
|
|
2271
|
-
cursor = readFileSync(cursorPath, "utf8").trim() || null;
|
|
2272
|
-
}
|
|
2273
|
-
catch {
|
|
2274
|
-
/* ignore */
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
let lines = 0;
|
|
2278
|
-
let backlog = 0;
|
|
2279
|
-
let seenCursor = cursor === null; // no cursor → everything is "backlog"
|
|
2280
|
-
try {
|
|
2281
|
-
// Bounded tail read: the ledger grows without bound and a whole-file
|
|
2282
|
-
// readFileSync throws past V8's ~512MB string limit. lines/backlog are then
|
|
2283
|
-
// scoped to the scanned window; `bytes` (full size, via statSync above) still
|
|
2284
|
-
// reflects the true stream size for the health rollup.
|
|
2285
|
-
const { text } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
|
|
2286
|
-
for (const line of text.split("\n")) {
|
|
2287
|
-
if (!line.trim())
|
|
2288
|
-
continue;
|
|
2289
|
-
lines++;
|
|
2290
|
-
if (seenCursor) {
|
|
2291
|
-
backlog++;
|
|
2292
|
-
}
|
|
2293
|
-
else if (cursor && line.includes(`"event_id":"${cursor}"`)) {
|
|
2294
|
-
seenCursor = true;
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
catch {
|
|
2299
|
-
/* ignore */
|
|
2300
|
-
}
|
|
2301
|
-
return { bytes, lines, cursor_backlog: backlog };
|
|
2727
|
+
const read = readAgentDiagnosticEventsInWindow(root, 0);
|
|
2728
|
+
return {
|
|
2729
|
+
source: "v3",
|
|
2730
|
+
authoritative: read.authoritative,
|
|
2731
|
+
...(read.reason ? { reason: read.reason } : {}),
|
|
2732
|
+
bytes: read.bytes,
|
|
2733
|
+
lines: read.events.length,
|
|
2734
|
+
cursor_backlog: 0,
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
export function traceInstanceIdsForEventSource(nativeInstanceIds, source) {
|
|
2738
|
+
void source;
|
|
2739
|
+
return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
|
|
2302
2740
|
}
|
|
2303
2741
|
/** Map a canonical event to a concise trace line, or null to drop it. */
|
|
2304
|
-
function traceLine(ev, allTools) {
|
|
2305
|
-
const d = (ev.
|
|
2742
|
+
export function traceLine(ev, allTools) {
|
|
2743
|
+
const d = (ev.payload ?? {});
|
|
2306
2744
|
const s = (k) => (typeof d[k] === "string" ? d[k] : "");
|
|
2745
|
+
const object = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
2746
|
+
? value
|
|
2747
|
+
: {};
|
|
2307
2748
|
const clip = (v, n = 70) => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
|
|
2749
|
+
const recovery = typeof d.recovery === "object" && d.recovery !== null
|
|
2750
|
+
? d.recovery
|
|
2751
|
+
: null;
|
|
2752
|
+
const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
|
|
2753
|
+
const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
|
|
2308
2754
|
let detail = "";
|
|
2309
2755
|
switch (ev.event_type) {
|
|
2310
|
-
case "session.
|
|
2311
|
-
detail =
|
|
2756
|
+
case "session.started":
|
|
2757
|
+
detail = "generation started";
|
|
2758
|
+
break;
|
|
2759
|
+
case "session.resumed":
|
|
2760
|
+
detail = "generation resumed";
|
|
2312
2761
|
break;
|
|
2313
|
-
case "session.
|
|
2314
|
-
detail = `
|
|
2762
|
+
case "session.ended":
|
|
2763
|
+
detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2315
2764
|
break;
|
|
2316
|
-
case "
|
|
2317
|
-
detail =
|
|
2765
|
+
case "session.termination_observed":
|
|
2766
|
+
detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2318
2767
|
break;
|
|
2319
|
-
case "
|
|
2320
|
-
detail = `
|
|
2768
|
+
case "agent.delegated":
|
|
2769
|
+
detail = `delegated child=${s("child_generation_id") || "unknown"}`;
|
|
2321
2770
|
break;
|
|
2322
|
-
case "
|
|
2323
|
-
detail =
|
|
2771
|
+
case "agent.started":
|
|
2772
|
+
detail = `child started=${s("child_generation_id") || "unknown"}`;
|
|
2324
2773
|
break;
|
|
2325
|
-
case "
|
|
2326
|
-
detail = `
|
|
2774
|
+
case "agent.completed":
|
|
2775
|
+
detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
|
|
2327
2776
|
break;
|
|
2328
|
-
case "
|
|
2329
|
-
detail =
|
|
2777
|
+
case "turn.started":
|
|
2778
|
+
detail = `intent=${s("intent_kind") || "unknown"}`;
|
|
2330
2779
|
break;
|
|
2331
|
-
case "
|
|
2332
|
-
detail =
|
|
2780
|
+
case "turn.completed":
|
|
2781
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2333
2782
|
break;
|
|
2334
|
-
case "
|
|
2335
|
-
|
|
2783
|
+
case "tool.requested": {
|
|
2784
|
+
const tool = object(d.tool);
|
|
2785
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
|
|
2336
2786
|
break;
|
|
2337
|
-
|
|
2338
|
-
|
|
2787
|
+
}
|
|
2788
|
+
case "tool.completed": {
|
|
2789
|
+
const tool = object(d.tool);
|
|
2790
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
2791
|
+
break;
|
|
2792
|
+
}
|
|
2793
|
+
case "command.started":
|
|
2794
|
+
if (!allTools)
|
|
2795
|
+
return null;
|
|
2796
|
+
detail = "command started (content omitted)";
|
|
2797
|
+
break;
|
|
2798
|
+
case "command.completed":
|
|
2799
|
+
if (!allTools && !recoveryReason)
|
|
2800
|
+
return null;
|
|
2801
|
+
detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
2802
|
+
break;
|
|
2803
|
+
case "coord.task_changed":
|
|
2804
|
+
case "coord.lifecycle_changed":
|
|
2805
|
+
case "coord.presence_changed":
|
|
2806
|
+
detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
|
|
2807
|
+
break;
|
|
2808
|
+
case "coord.status_observed":
|
|
2809
|
+
detail = s("status") || "status observed";
|
|
2810
|
+
break;
|
|
2811
|
+
case "coord.claim_changed": {
|
|
2812
|
+
const target = object(d.target);
|
|
2813
|
+
detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2816
|
+
case "coord.message_observed":
|
|
2817
|
+
detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
|
|
2339
2818
|
break;
|
|
2340
|
-
case "
|
|
2341
|
-
detail = "
|
|
2819
|
+
case "coord.identity_attested":
|
|
2820
|
+
detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
|
|
2342
2821
|
break;
|
|
2343
|
-
case "
|
|
2344
|
-
detail =
|
|
2822
|
+
case "wait.started":
|
|
2823
|
+
detail = s("kind") || "wait started";
|
|
2345
2824
|
break;
|
|
2346
|
-
case "
|
|
2347
|
-
|
|
2348
|
-
case "claim.conflict":
|
|
2349
|
-
detail = clip(s("path"));
|
|
2825
|
+
case "wait.ended":
|
|
2826
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2350
2827
|
break;
|
|
2351
|
-
case "
|
|
2352
|
-
|
|
2353
|
-
detail = `
|
|
2828
|
+
case "artifact.observed": {
|
|
2829
|
+
const artifact = object(d.artifact);
|
|
2830
|
+
detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
|
|
2354
2831
|
break;
|
|
2355
|
-
|
|
2356
|
-
|
|
2832
|
+
}
|
|
2833
|
+
case "progress.observed":
|
|
2834
|
+
detail = s("kind") || "progress";
|
|
2835
|
+
break;
|
|
2836
|
+
case "health.observed":
|
|
2837
|
+
detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
|
|
2838
|
+
break;
|
|
2839
|
+
case "council.state_changed":
|
|
2840
|
+
detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2841
|
+
break;
|
|
2842
|
+
case "decision.state_changed":
|
|
2843
|
+
detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2844
|
+
break;
|
|
2845
|
+
case "lifecycle.recovered":
|
|
2846
|
+
detail = `recovered=${s("recovery_kind") || "unknown"}`;
|
|
2847
|
+
break;
|
|
2848
|
+
case "lifecycle.sweep_observed":
|
|
2849
|
+
detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
|
|
2357
2850
|
break;
|
|
2358
2851
|
default:
|
|
2359
|
-
// Noise unless --all-tools: per-line command.* + tool.post_use.
|
|
2360
|
-
if (!allTools)
|
|
2361
|
-
return null;
|
|
2362
|
-
if (ev.event_type === "tool.post_use" ||
|
|
2363
|
-
ev.event_type === "tool.post_use_failure" ||
|
|
2364
|
-
ev.event_type.startsWith("command.")) {
|
|
2365
|
-
detail = s("tool_name") || "";
|
|
2366
|
-
break;
|
|
2367
|
-
}
|
|
2368
2852
|
return null;
|
|
2369
2853
|
}
|
|
2370
2854
|
return { ts: ev.ts, event_type: ev.event_type, detail };
|
|
2371
2855
|
}
|
|
2856
|
+
export function pendingFinalizationTraceEntries(requests, instanceId) {
|
|
2857
|
+
return requests
|
|
2858
|
+
.filter((request) => request.status === "pending" && request.instance_id === instanceId)
|
|
2859
|
+
.map((request) => ({
|
|
2860
|
+
ts: request.observed_at,
|
|
2861
|
+
event_type: "session.finalization_pending",
|
|
2862
|
+
detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
|
|
2863
|
+
}));
|
|
2864
|
+
}
|
|
2372
2865
|
function runTrace(name, opts) {
|
|
2373
2866
|
if (opts.json)
|
|
2374
2867
|
emit.config({ format: "json" });
|
|
@@ -2396,8 +2889,8 @@ function runTrace(name, opts) {
|
|
|
2396
2889
|
targetId = candidates[0][0];
|
|
2397
2890
|
else if (candidates.length > 1)
|
|
2398
2891
|
targetId = candidates.map(([id]) => id).join("\x00"); // sentinel; resolved below
|
|
2399
|
-
else if (/^[0-9a-
|
|
2400
|
-
targetId = wanted;
|
|
2892
|
+
else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted))
|
|
2893
|
+
targetId = wanted;
|
|
2401
2894
|
}
|
|
2402
2895
|
if (!targetId) {
|
|
2403
2896
|
emit.error({
|
|
@@ -2408,33 +2901,16 @@ function runTrace(name, opts) {
|
|
|
2408
2901
|
}
|
|
2409
2902
|
const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
|
|
2410
2903
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 200);
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
// throws past V8's ~512MB string limit. A trace of an agent whose events all
|
|
2415
|
-
// predate the window is reported as truncated rather than crashing.
|
|
2416
|
-
const streamPath = resolve(root, ".harnery", "events.ndjson");
|
|
2904
|
+
const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
|
|
2905
|
+
const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
|
|
2906
|
+
const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
|
|
2417
2907
|
const byId = new Map();
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2420
|
-
if (!line)
|
|
2908
|
+
for (const ev of diagnosticRead.events) {
|
|
2909
|
+
if (!ev.instance_id || !candidateIds.includes(ev.instance_id))
|
|
2421
2910
|
continue;
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
continue;
|
|
2426
|
-
if (sinceMs && Date.parse(ev.ts) < sinceMs)
|
|
2427
|
-
continue;
|
|
2428
|
-
const arr = byId.get(ev.instance_id) ?? [];
|
|
2429
|
-
arr.push(ev);
|
|
2430
|
-
byId.set(ev.instance_id, arr);
|
|
2431
|
-
}
|
|
2432
|
-
catch {
|
|
2433
|
-
/* skip */
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
if (streamTruncated) {
|
|
2437
|
-
process.stderr.write(`note: event ledger exceeds ${Math.round(STREAM_SCAN_CAP_BYTES / 1024 / 1024)}MB; traced only the most recent window (older events omitted)\n`);
|
|
2911
|
+
const arr = byId.get(ev.instance_id) ?? [];
|
|
2912
|
+
arr.push(ev);
|
|
2913
|
+
byId.set(ev.instance_id, arr);
|
|
2438
2914
|
}
|
|
2439
2915
|
// If the name mapped to multiple instances, trace the one with the latest event.
|
|
2440
2916
|
let resolvedId = candidateIds[0];
|
|
@@ -2450,24 +2926,78 @@ function runTrace(name, opts) {
|
|
|
2450
2926
|
}
|
|
2451
2927
|
}
|
|
2452
2928
|
const events = byId.get(resolvedId) ?? [];
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2929
|
+
let state = foldSessionState(events, { instance_id: resolvedId });
|
|
2930
|
+
let sessionState = events.some((event) => event.event_type === "session.ended")
|
|
2931
|
+
? "ended"
|
|
2932
|
+
: "live";
|
|
2933
|
+
if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
|
|
2934
|
+
try {
|
|
2935
|
+
const view = projectCoordinationViewV3(readLedgerV3(root));
|
|
2936
|
+
const liveGeneration = view.instances[resolvedId];
|
|
2937
|
+
const terminalGeneration = Object.values(view.terminal_generations).find((candidate) => candidate.instance_id === resolvedId);
|
|
2938
|
+
const generation = liveGeneration ?? terminalGeneration;
|
|
2939
|
+
if (generation) {
|
|
2940
|
+
sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
|
|
2941
|
+
const lifecycleState = generation.lifecycle_state;
|
|
2942
|
+
state = {
|
|
2943
|
+
activity: generation.activity === "terminal" ? "idle" : generation.activity,
|
|
2944
|
+
activity_updated_at: generation.last_observed_at,
|
|
2945
|
+
activity_source: "event-v3-coordination-view",
|
|
2946
|
+
task_state: lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
|
|
2947
|
+
? lifecycleState
|
|
2948
|
+
: "active",
|
|
2949
|
+
task_state_updated_at: generation.lifecycle_state_updated_at,
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
catch {
|
|
2954
|
+
diagnosticRead.authoritative = false;
|
|
2955
|
+
diagnosticRead.reason = "V3 coordination projection is unavailable";
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
let pendingFinalizations = [];
|
|
2959
|
+
if (diagnosticRead.source === "v3") {
|
|
2960
|
+
try {
|
|
2961
|
+
pendingFinalizations = listSessionFinalizationRequestsV3(root).filter((request) => request.status === "pending" && request.instance_id === resolvedId);
|
|
2962
|
+
}
|
|
2963
|
+
catch {
|
|
2964
|
+
diagnosticRead.authoritative = false;
|
|
2965
|
+
diagnosticRead.reason = "V3 finalization requests are unreadable";
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
const lines = [
|
|
2969
|
+
...events
|
|
2970
|
+
.map((ev) => traceLine(ev, !!opts.allTools))
|
|
2971
|
+
.filter((l) => l !== null),
|
|
2972
|
+
...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
|
|
2973
|
+
]
|
|
2457
2974
|
// Sort by timestamp, not file order: codex replays events (original ts,
|
|
2458
2975
|
// appended later), so append-order ≠ chronological order.
|
|
2459
2976
|
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
2460
2977
|
const shown = lines.slice(-limit);
|
|
2461
|
-
const
|
|
2978
|
+
const nativeResolvedId = diagnosticRead.source === "v3"
|
|
2979
|
+
? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
|
|
2980
|
+
: resolvedId;
|
|
2981
|
+
const displayName = nameById.get(resolvedId) ??
|
|
2982
|
+
(nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
|
|
2983
|
+
resolvedId.slice(0, 8);
|
|
2462
2984
|
const result = {
|
|
2985
|
+
event_source: {
|
|
2986
|
+
contract: diagnosticRead.source,
|
|
2987
|
+
authoritative: diagnosticRead.authoritative,
|
|
2988
|
+
reason: diagnosticRead.reason ?? null,
|
|
2989
|
+
},
|
|
2463
2990
|
name: displayName,
|
|
2464
2991
|
instance_id: resolvedId,
|
|
2465
2992
|
other_instances: candidateIds.filter((id) => id !== resolvedId),
|
|
2993
|
+
pending_finalizations: pendingFinalizations,
|
|
2466
2994
|
total_events: events.length,
|
|
2995
|
+
session_state: sessionState,
|
|
2467
2996
|
activity: state.activity,
|
|
2468
2997
|
activity_updated_at: state.activity_updated_at ?? null,
|
|
2469
2998
|
activity_source: state.activity_source ?? null,
|
|
2470
2999
|
task_state: state.task_state,
|
|
3000
|
+
task_state_scope: sessionState === "ended" ? "historical" : "current",
|
|
2471
3001
|
task_state_updated_at: state.task_state_updated_at ?? null,
|
|
2472
3002
|
task_state_reason: state.task_state_reason ?? null,
|
|
2473
3003
|
shown: shown.length,
|
|
@@ -2480,7 +3010,10 @@ function runTrace(name, opts) {
|
|
|
2480
3010
|
emit.data(result);
|
|
2481
3011
|
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` : ""}`;
|
|
2482
3012
|
process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
|
|
2483
|
-
process.stdout.write(` activity=${state.activity} · lifecycle=${state.task_state}${state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`); // lint-ok-emission: human trace view
|
|
3013
|
+
process.stdout.write(` 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`); // lint-ok-emission: human trace view
|
|
3014
|
+
if (!diagnosticRead.authoritative) {
|
|
3015
|
+
process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
|
|
3016
|
+
}
|
|
2484
3017
|
if (shown.length === 0) {
|
|
2485
3018
|
process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
|
|
2486
3019
|
return;
|
|
@@ -2490,6 +3023,53 @@ function runTrace(name, opts) {
|
|
|
2490
3023
|
process.stdout.write(` ${t} ${l.event_type.padEnd(22)} ${l.detail}\n`); // lint-ok-emission: human trace view
|
|
2491
3024
|
}
|
|
2492
3025
|
}
|
|
3026
|
+
/**
|
|
3027
|
+
* V3 generations, not disposable heartbeat caches, are the active-agent authority.
|
|
3028
|
+
*/
|
|
3029
|
+
export function collectActiveAgentHealth(root, nowMs = Date.now()) {
|
|
3030
|
+
const source = "event-ledger-v3";
|
|
3031
|
+
let heartbeats = [];
|
|
3032
|
+
try {
|
|
3033
|
+
const control = readEventV3ControlState(root);
|
|
3034
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
3035
|
+
heartbeats = readLiveCoordinationRows(root);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
catch {
|
|
3039
|
+
// The event-ledger section reports the authority read failure. Do not
|
|
3040
|
+
// substitute stale cache rows for a live V3 route.
|
|
3041
|
+
}
|
|
3042
|
+
const byPlatform = {};
|
|
3043
|
+
const byKind = {};
|
|
3044
|
+
const bySchema = {};
|
|
3045
|
+
let stale = 0;
|
|
3046
|
+
for (const heartbeat of heartbeats) {
|
|
3047
|
+
const platform = formatPlatformLabel(heartbeat.platform);
|
|
3048
|
+
byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
|
|
3049
|
+
const kind = heartbeat.kind ?? "unknown";
|
|
3050
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
3051
|
+
// These rows exist only after an authority-safe V3 projection. The
|
|
3052
|
+
// disposable heartbeat file has its own cache-format version, which must
|
|
3053
|
+
// not be reported as the ledger schema version.
|
|
3054
|
+
bySchema.v3 = (bySchema.v3 ?? 0) + 1;
|
|
3055
|
+
const lastObservedMs = heartbeat.last_heartbeat
|
|
3056
|
+
? Date.parse(heartbeat.last_heartbeat)
|
|
3057
|
+
: Number.NaN;
|
|
3058
|
+
const ageMs = Number.isFinite(lastObservedMs)
|
|
3059
|
+
? nowMs - lastObservedMs
|
|
3060
|
+
: Number.POSITIVE_INFINITY;
|
|
3061
|
+
if (ageMs > freshnessCutoffSecs() * 1000)
|
|
3062
|
+
stale += 1;
|
|
3063
|
+
}
|
|
3064
|
+
return {
|
|
3065
|
+
source,
|
|
3066
|
+
total: heartbeats.length,
|
|
3067
|
+
by_platform: byPlatform,
|
|
3068
|
+
by_kind: byKind,
|
|
3069
|
+
by_schema_version: bySchema,
|
|
3070
|
+
stale,
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
2493
3073
|
function runHealth(opts) {
|
|
2494
3074
|
if (opts.json)
|
|
2495
3075
|
emit.config({ format: "json" });
|
|
@@ -2509,11 +3089,12 @@ function runHealth(opts) {
|
|
|
2509
3089
|
});
|
|
2510
3090
|
process.exit(1);
|
|
2511
3091
|
}
|
|
2512
|
-
const
|
|
3092
|
+
const nowMs = Date.now();
|
|
3093
|
+
const cutoffMs = nowMs - sinceSecs * 1000;
|
|
2513
3094
|
const activeDir = resolve(root, ".harnery/active");
|
|
2514
3095
|
const councilsDir = resolve(root, ".harnery/councils");
|
|
2515
|
-
// Coordination telemetry reads from the canonical
|
|
2516
|
-
// Heals come from health
|
|
3096
|
+
// Coordination telemetry reads from the canonical V3 ledger.
|
|
3097
|
+
// Heals come from health.observed; council activity from council.state_changed.
|
|
2517
3098
|
const heal = [];
|
|
2518
3099
|
let councilAdvanced = 0;
|
|
2519
3100
|
let councilClosed = 0;
|
|
@@ -2521,32 +3102,35 @@ function runHealth(opts) {
|
|
|
2521
3102
|
let sweptTotal = 0;
|
|
2522
3103
|
const sweptByReason = {};
|
|
2523
3104
|
const nameById = buildNameById(root);
|
|
2524
|
-
|
|
3105
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
3106
|
+
for (const ev of diagnosticEvents.events) {
|
|
2525
3107
|
const healEv = canonicalToHealEvent(ev, nameById);
|
|
2526
3108
|
if (healEv) {
|
|
2527
3109
|
heal.push(healEv);
|
|
2528
3110
|
continue;
|
|
2529
3111
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
3112
|
+
if (ev.event_type === "council.state_changed") {
|
|
3113
|
+
const state = String(ev.payload?.new_state ?? "");
|
|
3114
|
+
if (state === "round_open")
|
|
2532
3115
|
councilAdvanced++;
|
|
2533
|
-
|
|
2534
|
-
case "council.close":
|
|
3116
|
+
if (state === "closed")
|
|
2535
3117
|
councilClosed++;
|
|
2536
|
-
|
|
2537
|
-
case "council.archive":
|
|
3118
|
+
if (state === "archived")
|
|
2538
3119
|
councilArchived++;
|
|
2539
|
-
|
|
2540
|
-
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
switch (ev.event_type) {
|
|
3123
|
+
case "lifecycle.sweep_observed": {
|
|
2541
3124
|
sweptTotal++;
|
|
2542
|
-
const reason = String(ev.
|
|
3125
|
+
const reason = String(ev.payload?.observation ?? "unknown");
|
|
2543
3126
|
sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
|
|
2544
3127
|
break;
|
|
2545
3128
|
}
|
|
2546
3129
|
}
|
|
2547
3130
|
}
|
|
2548
|
-
const hookErrors = readHookErrors(root, cutoffMs);
|
|
3131
|
+
const hookErrors = readHookErrors(root, cutoffMs, nowMs);
|
|
2549
3132
|
const stream = readStreamStats(root);
|
|
3133
|
+
const eventLedger = collectEventLedgerHealthV3(root);
|
|
2550
3134
|
// Canonical health.* events carry the full instance_id, already resolved to
|
|
2551
3135
|
// `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
|
|
2552
3136
|
// buildNameById; no hex8→name dedup pass needed anymore.
|
|
@@ -2564,19 +3148,13 @@ function runHealth(opts) {
|
|
|
2564
3148
|
.sort((a, b) => b[1] - a[1])
|
|
2565
3149
|
.slice(0, 5)
|
|
2566
3150
|
.map(([agent, count]) => ({ agent, count }));
|
|
2567
|
-
|
|
2568
|
-
const activeByPlatform = {};
|
|
2569
|
-
const activeByKind = {};
|
|
2570
|
-
const activeBySchema = {};
|
|
2571
|
-
let activeTotal = 0;
|
|
2572
|
-
let staleHeartbeats = 0;
|
|
3151
|
+
const activeAgents = collectActiveAgentHealth(root, nowMs);
|
|
2573
3152
|
// Zombies: files in active/ that are broken: unparseable, nameless, or an
|
|
2574
3153
|
// absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
|
|
2575
3154
|
// mean dead files the sweep isn't reaping.
|
|
2576
3155
|
let zombieCount = 0;
|
|
2577
3156
|
const zombieSamples = [];
|
|
2578
3157
|
const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
|
|
2579
|
-
const nowMs = Date.now();
|
|
2580
3158
|
if (existsSync(activeDir)) {
|
|
2581
3159
|
for (const file of readdirSync(activeDir)) {
|
|
2582
3160
|
if (!file.endsWith(".json"))
|
|
@@ -2595,18 +3173,8 @@ function runHealth(opts) {
|
|
|
2595
3173
|
zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
|
|
2596
3174
|
continue;
|
|
2597
3175
|
}
|
|
2598
|
-
activeTotal++;
|
|
2599
|
-
const platform = formatPlatformLabel(hb.platform);
|
|
2600
|
-
activeByPlatform[platform] = (activeByPlatform[platform] ?? 0) + 1;
|
|
2601
|
-
const kind = hb.kind ?? "unknown";
|
|
2602
|
-
activeByKind[kind] = (activeByKind[kind] ?? 0) + 1;
|
|
2603
|
-
const sv = hb.schema_version;
|
|
2604
|
-
const schemaKey = sv === undefined ? "v0" : `v${sv}`;
|
|
2605
|
-
activeBySchema[schemaKey] = (activeBySchema[schemaKey] ?? 0) + 1;
|
|
2606
3176
|
const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
|
|
2607
3177
|
const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
|
|
2608
|
-
if (ageMs > freshnessCutoffSecs() * 1000)
|
|
2609
|
-
staleHeartbeats++;
|
|
2610
3178
|
// Zombie heuristics on a parseable heartbeat: no name, or an age so large
|
|
2611
3179
|
// it can only be a broken/epoch timestamp (a real agent would have healed).
|
|
2612
3180
|
if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
|
|
@@ -2636,46 +3204,65 @@ function runHealth(opts) {
|
|
|
2636
3204
|
anomalies.push(`${agent} self-healed ${count}x in ${opts.since}; possible idle-prune loop or PID instability`);
|
|
2637
3205
|
}
|
|
2638
3206
|
}
|
|
2639
|
-
if (
|
|
2640
|
-
|
|
3207
|
+
if (activeAgents.stale > 0) {
|
|
3208
|
+
const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
|
|
3209
|
+
const action = activeAgents.source === "event-ledger-v3"
|
|
3210
|
+
? "lifecycle reconciliation may be delayed"
|
|
3211
|
+
: "heal mechanism may not be firing";
|
|
3212
|
+
anomalies.push(`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`);
|
|
2641
3213
|
}
|
|
2642
|
-
const
|
|
3214
|
+
const expectedSchema = "v3";
|
|
3215
|
+
const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter((schema) => schema !== expectedSchema);
|
|
2643
3216
|
if (unexpectedSchemas.length > 0) {
|
|
2644
|
-
anomalies.push(`Unexpected
|
|
2645
|
-
}
|
|
2646
|
-
//
|
|
2647
|
-
//
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
? `: top phase '${top.phase}' x${top.count}${top.sample ? ` (${top.sample.slice(0, 80)})` : ""}`
|
|
2653
|
-
: "";
|
|
2654
|
-
anomalies.push(`agent-hook errored ${hookErrors.total}x in ${opts.since}${detail}`);
|
|
3217
|
+
anomalies.push(`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`);
|
|
3218
|
+
}
|
|
3219
|
+
// Only recent hook failures are active anomalies. Historical rows remain in
|
|
3220
|
+
// the report with exact error signatures and their latest timestamp.
|
|
3221
|
+
if (hookErrors.last1h > 0) {
|
|
3222
|
+
const top = hookErrors.recentTopErrors[0];
|
|
3223
|
+
const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
|
|
3224
|
+
anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
|
|
2655
3225
|
}
|
|
2656
3226
|
if (stream.cursor_backlog > 500) {
|
|
2657
3227
|
anomalies.push(`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`);
|
|
2658
3228
|
}
|
|
2659
|
-
//
|
|
2660
|
-
// append-only ledger (names + forensics for the life of the log), and
|
|
2661
|
-
// consumeSince tail-reads it, so size no longer drives latency. The
|
|
2662
|
-
// bytes/lines still surface in the summary line for visibility;
|
|
2663
|
-
// cursor_backlog above is the real drain-lag signal.
|
|
3229
|
+
// Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
|
|
2664
3230
|
if ((sweptByReason.unparseable ?? 0) > 0) {
|
|
2665
3231
|
anomalies.push(`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`);
|
|
2666
3232
|
}
|
|
2667
3233
|
if (zombieCount > 0) {
|
|
2668
3234
|
anomalies.push(`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`);
|
|
2669
3235
|
}
|
|
3236
|
+
if (eventLedger.state === "live") {
|
|
3237
|
+
// Open spans whose generation has no open turn: the turn ended without its
|
|
3238
|
+
// tool spans closing, so an explicit end can only queue, never finalize.
|
|
3239
|
+
const orphanGenerations = eventLedger.open_spans.generations.filter((generation) => !generation.turn_open);
|
|
3240
|
+
if (orphanGenerations.length > 0) {
|
|
3241
|
+
const orphanSpans = orphanGenerations.reduce((sum, generation) => sum + generation.span_count, 0);
|
|
3242
|
+
anomalies.push(`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`);
|
|
3243
|
+
}
|
|
3244
|
+
for (const pressure of eventLedger.span_pressure) {
|
|
3245
|
+
anomalies.push(`${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)`);
|
|
3246
|
+
}
|
|
3247
|
+
for (const collectionError of eventLedger.collection_errors) {
|
|
3248
|
+
anomalies.push(`event-ledger health: ${collectionError}`);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
2670
3251
|
const report = {
|
|
2671
3252
|
since: opts.since,
|
|
2672
3253
|
generated_at: new Date().toISOString(),
|
|
3254
|
+
event_telemetry: {
|
|
3255
|
+
source: diagnosticEvents.source,
|
|
3256
|
+
authoritative: diagnosticEvents.authoritative,
|
|
3257
|
+
...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
|
|
3258
|
+
},
|
|
2673
3259
|
active_agents: {
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
3260
|
+
source: activeAgents.source,
|
|
3261
|
+
total: activeAgents.total,
|
|
3262
|
+
by_platform: activeAgents.by_platform,
|
|
3263
|
+
by_kind: activeAgents.by_kind,
|
|
3264
|
+
by_schema_version: activeAgents.by_schema_version,
|
|
3265
|
+
stale: activeAgents.stale,
|
|
2679
3266
|
},
|
|
2680
3267
|
heal_events: {
|
|
2681
3268
|
total: heal.length,
|
|
@@ -2691,9 +3278,19 @@ function runHealth(opts) {
|
|
|
2691
3278
|
closed_in_window: councilClosed,
|
|
2692
3279
|
},
|
|
2693
3280
|
swept_events: { total: sweptTotal, by_reason: sweptByReason },
|
|
2694
|
-
hook_errors: {
|
|
3281
|
+
hook_errors: {
|
|
3282
|
+
total: hookErrors.total,
|
|
3283
|
+
last_1h: hookErrors.last1h,
|
|
3284
|
+
latest_at: hookErrors.latestAt,
|
|
3285
|
+
by_phase: hookErrors.byPhase,
|
|
3286
|
+
by_error: hookErrors.byError,
|
|
3287
|
+
top: hookErrors.top,
|
|
3288
|
+
top_errors: hookErrors.topErrors,
|
|
3289
|
+
recent_top_errors: hookErrors.recentTopErrors,
|
|
3290
|
+
},
|
|
2695
3291
|
stream,
|
|
2696
3292
|
zombies: { count: zombieCount, samples: zombieSamples },
|
|
3293
|
+
event_ledger: eventLedger,
|
|
2697
3294
|
anomalies,
|
|
2698
3295
|
};
|
|
2699
3296
|
if (opts.json) {
|
|
@@ -2729,16 +3326,38 @@ function renderHealthBox(report) {
|
|
|
2729
3326
|
councilParts.push(`${report.councils.closed_in_window} closed`);
|
|
2730
3327
|
if (report.councils.archived_in_window > 0)
|
|
2731
3328
|
councilParts.push(`${report.councils.archived_in_window} archived`);
|
|
2732
|
-
const
|
|
3329
|
+
const activeSource = report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
|
|
3330
|
+
const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
|
|
2733
3331
|
const sweptReasonStr = Object.entries(report.swept_events.by_reason)
|
|
2734
3332
|
.map(([r, n]) => `${r} ${n}`)
|
|
2735
3333
|
.join(", ");
|
|
2736
3334
|
const hookErrStr = report.hook_errors.total === 0
|
|
2737
3335
|
? "0"
|
|
2738
|
-
: `${report.hook_errors.total}
|
|
3336
|
+
: `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
|
|
2739
3337
|
const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
|
|
3338
|
+
const ledger = report.event_ledger;
|
|
3339
|
+
let ledgerStr;
|
|
3340
|
+
if (ledger.state !== "live") {
|
|
3341
|
+
ledgerStr = `unavailable (${ledger.reason})`;
|
|
3342
|
+
}
|
|
3343
|
+
else {
|
|
3344
|
+
const orphanGenerations = ledger.open_spans.generations.filter((generation) => !generation.turn_open).length;
|
|
3345
|
+
const pendingAges = ledger.pending_finalizations
|
|
3346
|
+
.map((request) => request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)))
|
|
3347
|
+
.join(", ");
|
|
3348
|
+
ledgerStr = [
|
|
3349
|
+
`open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
|
|
3350
|
+
`pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
|
|
3351
|
+
`intake ${ledger.intake_spool.total}`,
|
|
3352
|
+
`diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
|
|
3353
|
+
].join(" · ");
|
|
3354
|
+
}
|
|
2740
3355
|
const rows = [
|
|
2741
3356
|
["window", `last ${report.since}`],
|
|
3357
|
+
[
|
|
3358
|
+
"event source",
|
|
3359
|
+
`${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
|
|
3360
|
+
],
|
|
2742
3361
|
["active", activeStr],
|
|
2743
3362
|
[
|
|
2744
3363
|
"heals",
|
|
@@ -2748,6 +3367,7 @@ function renderHealthBox(report) {
|
|
|
2748
3367
|
["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
|
|
2749
3368
|
["hook errors", hookErrStr],
|
|
2750
3369
|
["stream", streamStr],
|
|
3370
|
+
["event ledger", ledgerStr],
|
|
2751
3371
|
[
|
|
2752
3372
|
"zombies",
|
|
2753
3373
|
report.zombies.count === 0
|
|
@@ -2851,6 +3471,8 @@ function runAdapterProbe(id, opts) {
|
|
|
2851
3471
|
replayExitCode = result.exitCode;
|
|
2852
3472
|
data.samples = samples;
|
|
2853
3473
|
data.samples_summary = result.summary;
|
|
3474
|
+
if (result.note)
|
|
3475
|
+
data.samples_note = result.note;
|
|
2854
3476
|
}
|
|
2855
3477
|
emit.data(data);
|
|
2856
3478
|
if (process.stdout.isTTY && !opts.json) {
|
|
@@ -2867,7 +3489,7 @@ function runAdapterProbe(id, opts) {
|
|
|
2867
3489
|
const summary = data.samples_summary;
|
|
2868
3490
|
lines.push("");
|
|
2869
3491
|
if (samples.length === 0) {
|
|
2870
|
-
lines.push(` Sample replay: no .json fixtures found under ${sampleDir}`);
|
|
3492
|
+
lines.push(` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`);
|
|
2871
3493
|
}
|
|
2872
3494
|
else {
|
|
2873
3495
|
lines.push(` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`);
|
|
@@ -2918,6 +3540,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
|
|
|
2918
3540
|
samples: [],
|
|
2919
3541
|
exitCode: 0,
|
|
2920
3542
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
3543
|
+
note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
|
|
2921
3544
|
};
|
|
2922
3545
|
}
|
|
2923
3546
|
const fixtures = readdirSync(sampleDir)
|
|
@@ -2929,6 +3552,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
|
|
|
2929
3552
|
samples: [],
|
|
2930
3553
|
exitCode: 0,
|
|
2931
3554
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
3555
|
+
note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
|
|
2932
3556
|
};
|
|
2933
3557
|
}
|
|
2934
3558
|
// agent-hook is the single entry point. Replay sample payloads against it,
|
|
@@ -3143,10 +3767,24 @@ function runPing(name, message, opts) {
|
|
|
3143
3767
|
});
|
|
3144
3768
|
process.exit(1);
|
|
3145
3769
|
}
|
|
3146
|
-
const myHb =
|
|
3770
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3147
3771
|
const fromName = myHb?.name ?? "anonymous";
|
|
3148
3772
|
const body = `from agent-${fromName}: ${message.trim()}`;
|
|
3149
3773
|
const doc = appendEntry(peerOwner, "handoff", body);
|
|
3774
|
+
// Canonical delivery record: sender is the envelope owner, recipient rides
|
|
3775
|
+
// in data, so read-only observers can render the communication without
|
|
3776
|
+
// joining journal files.
|
|
3777
|
+
emitEventV3({
|
|
3778
|
+
owner: myOwner,
|
|
3779
|
+
session: nativeSessionIdentity(myHb, myOwner),
|
|
3780
|
+
adapter: normalizeAdapter(myHb?.platform),
|
|
3781
|
+
observation: {
|
|
3782
|
+
event_type: "coord.message_observed",
|
|
3783
|
+
direction: "sent",
|
|
3784
|
+
subject: peerOwner,
|
|
3785
|
+
body: message.trim(),
|
|
3786
|
+
},
|
|
3787
|
+
});
|
|
3150
3788
|
const data = {
|
|
3151
3789
|
peer: name,
|
|
3152
3790
|
peer_instance_id: peerOwner,
|
|
@@ -3195,7 +3833,7 @@ async function runWait(name, opts) {
|
|
|
3195
3833
|
}
|
|
3196
3834
|
let lastProgressMs = 0;
|
|
3197
3835
|
while (true) {
|
|
3198
|
-
const hb =
|
|
3836
|
+
const hb = readCurrentCoordinationRow(peerOwner);
|
|
3199
3837
|
const now = Date.now();
|
|
3200
3838
|
const elapsedMs = now - startMs;
|
|
3201
3839
|
if (!hb) {
|
|
@@ -3230,9 +3868,8 @@ async function runWait(name, opts) {
|
|
|
3230
3868
|
const progressGapMs = Math.max(pollMs, 30_000);
|
|
3231
3869
|
if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
|
|
3232
3870
|
lastProgressMs = now;
|
|
3233
|
-
const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
|
|
3234
3871
|
const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
|
|
3235
|
-
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking
|
|
3872
|
+
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
|
|
3236
3873
|
process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
|
|
3237
3874
|
}
|
|
3238
3875
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
@@ -3267,10 +3904,20 @@ function runHeal(opts) {
|
|
|
3267
3904
|
emit.error({ code: "missing_owner", message: "--owner is required" });
|
|
3268
3905
|
process.exit(1);
|
|
3269
3906
|
}
|
|
3270
|
-
if (kind !== "pidmap" && kind !== "
|
|
3907
|
+
if (kind !== "pidmap" && kind !== "cache") {
|
|
3271
3908
|
emit.error({
|
|
3272
3909
|
code: "bad_kind",
|
|
3273
|
-
message: "--kind must be one of: pidmap,
|
|
3910
|
+
message: "--kind must be one of: pidmap, cache",
|
|
3911
|
+
});
|
|
3912
|
+
process.exit(1);
|
|
3913
|
+
}
|
|
3914
|
+
if (opts.adapter &&
|
|
3915
|
+
opts.adapter !== "claude-code" &&
|
|
3916
|
+
opts.adapter !== "cursor" &&
|
|
3917
|
+
opts.adapter !== "codex") {
|
|
3918
|
+
emit.error({
|
|
3919
|
+
code: "bad_adapter",
|
|
3920
|
+
message: "--adapter must be one of: claude-code, cursor, codex",
|
|
3274
3921
|
});
|
|
3275
3922
|
process.exit(1);
|
|
3276
3923
|
}
|
|
@@ -3282,8 +3929,8 @@ function runHeal(opts) {
|
|
|
3282
3929
|
});
|
|
3283
3930
|
process.exit(1);
|
|
3284
3931
|
}
|
|
3285
|
-
const action = kind === "pidmap" ? "heal-pidmap" :
|
|
3286
|
-
// Refuse to
|
|
3932
|
+
const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
|
|
3933
|
+
// Refuse to materialize a V3 cache at a truncated owner id.
|
|
3287
3934
|
//
|
|
3288
3935
|
// Heartbeats are keyed by the whole instance_id, so healing at an
|
|
3289
3936
|
// abbreviated id writes `.harnery/active/<prefix>.json` while every reader
|
|
@@ -3303,32 +3950,31 @@ function runHeal(opts) {
|
|
|
3303
3950
|
// whose instance_id this id is a prefix of settles it just as well, and is
|
|
3304
3951
|
// present in precisely the case that matters (the session the reader was
|
|
3305
3952
|
// trying to heal is registered, just not under the abbreviated name).
|
|
3306
|
-
if (kind === "
|
|
3953
|
+
if (kind === "cache" && !readHeartbeatCache(root, owner)) {
|
|
3307
3954
|
const canonical = opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
|
|
3308
3955
|
? opts.sessionId.trim()
|
|
3309
3956
|
: liveIdWithPrefix(root, owner);
|
|
3310
3957
|
if (canonical) {
|
|
3311
|
-
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live
|
|
3958
|
+
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
|
|
3312
3959
|
emit.error({
|
|
3313
3960
|
code: "truncated_owner",
|
|
3314
|
-
message: `--owner ${owner} is a prefix of ${canonical} (${source})
|
|
3315
|
-
`
|
|
3316
|
-
`resolves. Re-run with --owner ${canonical}.`,
|
|
3961
|
+
message: `--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
|
|
3962
|
+
`the canonical instance id; re-run with --owner ${canonical}.`,
|
|
3317
3963
|
});
|
|
3318
3964
|
process.exit(1);
|
|
3319
3965
|
}
|
|
3320
3966
|
}
|
|
3321
3967
|
// Build positional args. agent-coord's arg layout:
|
|
3322
3968
|
// heal-pidmap <instance_id> [<pid>]
|
|
3323
|
-
//
|
|
3324
|
-
// kill-heartbeat <instance_id>
|
|
3969
|
+
// repair-coordination-cache <instance_id> [<session_id>]
|
|
3325
3970
|
const helperArgs = [action, owner];
|
|
3326
3971
|
if (kind === "pidmap" && opts.pid)
|
|
3327
3972
|
helperArgs.push(opts.pid);
|
|
3328
|
-
if (kind === "
|
|
3973
|
+
if (kind === "cache" && opts.sessionId)
|
|
3329
3974
|
helperArgs.push(opts.sessionId);
|
|
3330
|
-
|
|
3331
|
-
|
|
3975
|
+
if (kind === "cache" && opts.adapter)
|
|
3976
|
+
helperArgs.push(`--adapter=${opts.adapter}`);
|
|
3977
|
+
// Both recovery actions are handled by the bundled agent-coord binary.
|
|
3332
3978
|
const helper = agentCoordOrExit(root);
|
|
3333
3979
|
const proc = spawnSync(helper, helperArgs, {
|
|
3334
3980
|
encoding: "utf8",
|
|
@@ -3341,26 +3987,21 @@ function runHeal(opts) {
|
|
|
3341
3987
|
});
|
|
3342
3988
|
process.exit(1);
|
|
3343
3989
|
}
|
|
3344
|
-
//
|
|
3990
|
+
// Read the derived cache to surface post-action state.
|
|
3345
3991
|
let after = null;
|
|
3346
3992
|
try {
|
|
3347
|
-
after =
|
|
3993
|
+
after = readHeartbeatCache(root, owner);
|
|
3348
3994
|
}
|
|
3349
3995
|
catch {
|
|
3350
3996
|
after = null;
|
|
3351
3997
|
}
|
|
3352
|
-
// Outcome semantics differ per kind. kill-heartbeat targets the heartbeat
|
|
3353
|
-
// file directly; heal-heartbeat upserts it; heal-pidmap touches a pid-map
|
|
3354
|
-
// row whose existence is independent of the heartbeat. Reporting on
|
|
3355
|
-
// "heartbeat present after" for the pidmap path was misleading: it's
|
|
3356
|
-
// unrelated to whether the heal succeeded.
|
|
3357
3998
|
const outcome = kind === "pidmap"
|
|
3358
3999
|
? proc.status === 0
|
|
3359
4000
|
? "ok"
|
|
3360
4001
|
: "failed"
|
|
3361
4002
|
: after
|
|
3362
|
-
? "
|
|
3363
|
-
: "
|
|
4003
|
+
? "cache_present"
|
|
4004
|
+
: "cache_absent";
|
|
3364
4005
|
emit.data({
|
|
3365
4006
|
rows: [
|
|
3366
4007
|
{
|
|
@@ -3383,31 +4024,35 @@ function runHeal(opts) {
|
|
|
3383
4024
|
emit.text(`agent-coord ${action} ok\n`);
|
|
3384
4025
|
}
|
|
3385
4026
|
else {
|
|
3386
|
-
emit.text(`agent-coord ${action} ok:
|
|
4027
|
+
emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
|
|
3387
4028
|
}
|
|
3388
4029
|
}
|
|
3389
|
-
// Canonical health.* emission is owned by the writer
|
|
3390
|
-
//
|
|
4030
|
+
// Canonical health.* emission is owned by the cache writer, so it fires
|
|
4031
|
+
// inside the agent-coord subprocess
|
|
3391
4032
|
// above on actual writes only: write-only telemetry, no double-emit, no
|
|
3392
4033
|
// event when an already-correct heal no-ops. (Previously emitted here
|
|
3393
4034
|
// unconditionally on every `harn agents heal`, which over-counted no-op heals.)
|
|
3394
4035
|
}
|
|
3395
4036
|
/**
|
|
3396
|
-
*
|
|
3397
|
-
*
|
|
3398
|
-
* through silently if no session (CI / direct invocation).
|
|
4037
|
+
* Emit one V3 council audit transition keyed to the exact durable manifest.
|
|
4038
|
+
* Falls through silently when no live session can attest the observation.
|
|
3399
4039
|
*/
|
|
3400
|
-
function emitCouncilStateEvent(
|
|
4040
|
+
function emitCouncilStateEvent(manifest, newState, priorState) {
|
|
3401
4041
|
const myOwner = resolveOwner();
|
|
3402
4042
|
if (!myOwner)
|
|
3403
4043
|
return;
|
|
3404
|
-
const hb =
|
|
3405
|
-
|
|
3406
|
-
type,
|
|
4044
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
4045
|
+
emitEventV3({
|
|
3407
4046
|
owner: myOwner,
|
|
3408
|
-
session: hb
|
|
4047
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
3409
4048
|
adapter: normalizeAdapter(hb?.platform),
|
|
3410
|
-
|
|
4049
|
+
observation: {
|
|
4050
|
+
event_type: "council.state_changed",
|
|
4051
|
+
council_id: manifest.council_id,
|
|
4052
|
+
...(priorState ? { prior_state: priorState } : {}),
|
|
4053
|
+
new_state: newState,
|
|
4054
|
+
record: manifest,
|
|
4055
|
+
},
|
|
3411
4056
|
});
|
|
3412
4057
|
}
|
|
3413
4058
|
// ──────── council subcommand impls ────────
|
|
@@ -3450,7 +4095,7 @@ function runCouncilCreate(objective, opts) {
|
|
|
3450
4095
|
createdBy = normalizeAgentName(opts.createdBy);
|
|
3451
4096
|
}
|
|
3452
4097
|
else if (myOwner) {
|
|
3453
|
-
const myHb =
|
|
4098
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3454
4099
|
if (myHb?.name) {
|
|
3455
4100
|
createdBy = normalizeAgentName(myHb.name);
|
|
3456
4101
|
}
|
|
@@ -3506,17 +4151,16 @@ function runCouncilCreate(objective, opts) {
|
|
|
3506
4151
|
writeFileSync(resolve(body, "invite.md"), buildInviteMarkdown(manifest), "utf8");
|
|
3507
4152
|
writeManifest(manifest);
|
|
3508
4153
|
if (myOwner) {
|
|
3509
|
-
const myHbForEmit =
|
|
3510
|
-
|
|
3511
|
-
type: "council.open",
|
|
4154
|
+
const myHbForEmit = readCurrentCoordinationRow(myOwner);
|
|
4155
|
+
emitEventV3({
|
|
3512
4156
|
owner: myOwner,
|
|
3513
|
-
session: myHbForEmit
|
|
4157
|
+
session: nativeSessionIdentity(myHbForEmit, myOwner),
|
|
3514
4158
|
adapter: normalizeAdapter(myHbForEmit?.platform),
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
4159
|
+
observation: {
|
|
4160
|
+
event_type: "council.state_changed",
|
|
4161
|
+
council_id: manifest.council_id,
|
|
4162
|
+
new_state: "active",
|
|
4163
|
+
record: manifest,
|
|
3520
4164
|
},
|
|
3521
4165
|
});
|
|
3522
4166
|
}
|
|
@@ -3528,7 +4172,7 @@ function runCouncilCreate(objective, opts) {
|
|
|
3528
4172
|
const skippedMembers = [];
|
|
3529
4173
|
for (const memberName of members) {
|
|
3530
4174
|
const bareName = memberName.replace(/^agent-/, "");
|
|
3531
|
-
if (myOwner &&
|
|
4175
|
+
if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
|
|
3532
4176
|
// Convener is themselves a member; skip the self-ping
|
|
3533
4177
|
continue;
|
|
3534
4178
|
}
|
|
@@ -3585,7 +4229,7 @@ function runCouncilList(opts) {
|
|
|
3585
4229
|
if (opts.mine) {
|
|
3586
4230
|
const myOwner = resolveOwner();
|
|
3587
4231
|
if (myOwner) {
|
|
3588
|
-
const myHb =
|
|
4232
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3589
4233
|
if (myHb?.name)
|
|
3590
4234
|
myName = normalizeAgentName(myHb.name);
|
|
3591
4235
|
}
|
|
@@ -3749,7 +4393,7 @@ function runCouncilClose(id, opts) {
|
|
|
3749
4393
|
closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
3750
4394
|
};
|
|
3751
4395
|
writeManifest(next);
|
|
3752
|
-
emitCouncilStateEvent("
|
|
4396
|
+
emitCouncilStateEvent(next, "closed", manifest.status);
|
|
3753
4397
|
// Build the transcript: every round's contributions in order.
|
|
3754
4398
|
const transcript = buildTranscript(next);
|
|
3755
4399
|
emit.data({
|
|
@@ -3788,7 +4432,7 @@ function runCouncilArchive(id, opts) {
|
|
|
3788
4432
|
// updated status. moveToArchive then physically relocates the artifacts.
|
|
3789
4433
|
writeManifest(next);
|
|
3790
4434
|
moveToArchive(next.council_id);
|
|
3791
|
-
emitCouncilStateEvent("
|
|
4435
|
+
emitCouncilStateEvent(next, "archived", manifest.status);
|
|
3792
4436
|
emit.data({
|
|
3793
4437
|
rows: [
|
|
3794
4438
|
{
|
|
@@ -3842,7 +4486,7 @@ function runCouncilUnarchive(id, opts) {
|
|
|
3842
4486
|
// (allows re-running for testing).
|
|
3843
4487
|
moveFromArchive(next.council_id);
|
|
3844
4488
|
writeManifest(next);
|
|
3845
|
-
emitCouncilStateEvent(
|
|
4489
|
+
emitCouncilStateEvent(next, restoredStatus, "archived");
|
|
3846
4490
|
emit.data({
|
|
3847
4491
|
rows: [
|
|
3848
4492
|
{
|
|
@@ -3895,7 +4539,7 @@ function runCouncilDelete(id, opts) {
|
|
|
3895
4539
|
}
|
|
3896
4540
|
const removed = deleteArchivedCouncil(manifest.council_id);
|
|
3897
4541
|
if (removed) {
|
|
3898
|
-
emitCouncilStateEvent("
|
|
4542
|
+
emitCouncilStateEvent(manifest, "deleted", "archived");
|
|
3899
4543
|
}
|
|
3900
4544
|
emit.data({
|
|
3901
4545
|
rows: [
|
|
@@ -4067,7 +4711,7 @@ function runCouncilContribute(id, opts) {
|
|
|
4067
4711
|
try {
|
|
4068
4712
|
const myOwner = resolveOwner();
|
|
4069
4713
|
if (myOwner) {
|
|
4070
|
-
const myHb =
|
|
4714
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4071
4715
|
if (myHb?.name)
|
|
4072
4716
|
actualName = normalizeAgentName(myHb.name);
|
|
4073
4717
|
}
|
|
@@ -4085,7 +4729,7 @@ function runCouncilContribute(id, opts) {
|
|
|
4085
4729
|
});
|
|
4086
4730
|
process.exit(1);
|
|
4087
4731
|
}
|
|
4088
|
-
const myHb =
|
|
4732
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4089
4733
|
if (!myHb?.name) {
|
|
4090
4734
|
emit.error({
|
|
4091
4735
|
code: "no_self_name",
|
|
@@ -4136,11 +4780,7 @@ function runCouncilContribute(id, opts) {
|
|
|
4136
4780
|
process.exit(1);
|
|
4137
4781
|
}
|
|
4138
4782
|
const path = writeContribution(manifest.council_id, manifest.current_round, myName, body);
|
|
4139
|
-
emitCouncilStateEvent("
|
|
4140
|
-
round_no: manifest.current_round,
|
|
4141
|
-
member: myName,
|
|
4142
|
-
body_summary: body.length > 1000 ? `${body.slice(0, 997)}...` : body,
|
|
4143
|
-
});
|
|
4783
|
+
emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
|
|
4144
4784
|
// Update manifest: if all members have now contributed, flip round_status.
|
|
4145
4785
|
const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
|
|
4146
4786
|
const allIn = manifest.members.every((m) => contributorsNow.includes(m));
|
|
@@ -4149,14 +4789,10 @@ function runCouncilContribute(id, opts) {
|
|
|
4149
4789
|
if (allIn) {
|
|
4150
4790
|
nextManifest = { ...manifest, round_status: "collected" };
|
|
4151
4791
|
writeManifest(nextManifest);
|
|
4152
|
-
emitCouncilStateEvent("
|
|
4153
|
-
round_no: nextManifest.current_round,
|
|
4154
|
-
});
|
|
4792
|
+
emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
|
|
4155
4793
|
if (manifest.auto_advance) {
|
|
4156
4794
|
nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
|
|
4157
|
-
emitCouncilStateEvent("
|
|
4158
|
-
round_no: nextManifest.current_round,
|
|
4159
|
-
});
|
|
4795
|
+
emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
|
|
4160
4796
|
autoAdvanced = true;
|
|
4161
4797
|
}
|
|
4162
4798
|
}
|
|
@@ -4241,7 +4877,7 @@ function runCouncilPrompt(id, member, opts) {
|
|
|
4241
4877
|
try {
|
|
4242
4878
|
const myOwner = resolveOwner();
|
|
4243
4879
|
if (myOwner) {
|
|
4244
|
-
const myHb =
|
|
4880
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4245
4881
|
if (myHb?.name)
|
|
4246
4882
|
actualName = normalizeAgentName(myHb.name);
|
|
4247
4883
|
}
|
|
@@ -4259,7 +4895,7 @@ function runCouncilPrompt(id, member, opts) {
|
|
|
4259
4895
|
});
|
|
4260
4896
|
process.exit(1);
|
|
4261
4897
|
}
|
|
4262
|
-
const myHb =
|
|
4898
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4263
4899
|
if (!myHb?.name) {
|
|
4264
4900
|
emit.error({
|
|
4265
4901
|
code: "no_self_name",
|
|
@@ -4410,12 +5046,8 @@ function runCouncilAdvance(id, opts) {
|
|
|
4410
5046
|
process.exit(1);
|
|
4411
5047
|
}
|
|
4412
5048
|
const next = advanceCouncil(manifest, !!opts.force);
|
|
4413
|
-
emitCouncilStateEvent("
|
|
4414
|
-
|
|
4415
|
-
});
|
|
4416
|
-
emitCouncilStateEvent("council.round_open", next, {
|
|
4417
|
-
round_no: next.current_round,
|
|
4418
|
-
});
|
|
5049
|
+
emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
|
|
5050
|
+
emitCouncilStateEvent(next, "round_open", "round_closed");
|
|
4419
5051
|
emit.data({
|
|
4420
5052
|
rows: [
|
|
4421
5053
|
{
|
|
@@ -4457,7 +5089,7 @@ function advanceCouncil(manifest, force) {
|
|
|
4457
5089
|
// (Convener already knows; we skip pinging them if they convened it from
|
|
4458
5090
|
// their own session.)
|
|
4459
5091
|
const myOwner = resolveOwner();
|
|
4460
|
-
const myName = myOwner ? normalizeAgentName(
|
|
5092
|
+
const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
|
|
4461
5093
|
for (const memberName of next.members) {
|
|
4462
5094
|
if (memberName === myName)
|
|
4463
5095
|
continue;
|
|
@@ -4478,7 +5110,7 @@ function advanceCouncil(manifest, force) {
|
|
|
4478
5110
|
// Hard cap on box width. Values longer than the per-row budget word-wrap to
|
|
4479
5111
|
// continuation lines (blank key column, value resumes indented). Picked to
|
|
4480
5112
|
// stay readable in narrow terminals + chat clients while giving long
|
|
4481
|
-
//
|
|
5113
|
+
// Task values need room to breathe.
|
|
4482
5114
|
const MAX_BOX_CONTENT_WIDTH = 100;
|
|
4483
5115
|
function formatBox(title, rows) {
|
|
4484
5116
|
const titleStr = ` ${title} `;
|