harnery 0.33.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/adapter.js +2 -2
- package/dist/commands/agents.d.ts +144 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +1237 -590
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/backup.d.ts +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +29 -16
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +234 -12
- package/dist/commands/checkpoint.d.ts.map +1 -1
- package/dist/commands/checkpoint.js +3 -2
- package/dist/commands/claude-desktop.d.ts +1 -1
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +3 -74
- package/dist/commands/context.js +1 -6
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +22 -38
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +91 -28
- package/dist/commands/doctor.js +10 -1
- package/dist/commands/events.d.ts +5 -0
- package/dist/commands/events.d.ts.map +1 -0
- package/dist/commands/events.js +189 -0
- package/dist/commands/governor.d.ts.map +1 -1
- package/dist/commands/governor.js +13 -4
- package/dist/commands/init.d.ts +9 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +223 -51
- package/dist/commands/journal.d.ts.map +1 -1
- package/dist/commands/journal.js +4 -13
- package/dist/commands/ledger-v3.d.ts +5 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -0
- package/dist/commands/ledger-v3.js +91 -0
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +11 -5
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/web.d.ts +6 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +67 -7
- package/dist/commands/work.d.ts.map +1 -1
- package/dist/commands/work.js +22 -5
- package/dist/core/adapter.d.ts +5 -0
- package/dist/core/adapter.d.ts.map +1 -0
- package/dist/core/adapter.js +1 -0
- package/dist/core/adapters/attest.d.ts +5 -2
- package/dist/core/adapters/attest.d.ts.map +1 -1
- package/dist/core/adapters/attest.js +8 -3
- package/dist/core/adapters/profiles.d.ts +6 -6
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +4 -4
- package/dist/core/agents/canonical-emit.d.ts +17 -15
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +22 -22
- package/dist/core/agents/cli.js +207 -234
- package/dist/core/agents/codex-archive-v3.d.ts +14 -0
- package/dist/core/agents/codex-archive-v3.d.ts.map +1 -0
- package/dist/core/agents/codex-archive-v3.js +110 -0
- package/dist/core/agents/coord-client.d.ts +19 -81
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +65 -155
- package/dist/core/agents/finalization.d.ts +4 -1
- package/dist/core/agents/finalization.d.ts.map +1 -1
- package/dist/core/agents/finalization.js +89 -32
- package/dist/core/agents/index.d.ts +4 -5
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +4 -5
- package/dist/core/agents/live-authority-v3.d.ts +59 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -0
- package/dist/core/agents/live-authority-v3.js +233 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +20 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -0
- package/dist/core/agents/live-lifecycle-v3.js +91 -0
- package/dist/core/agents/live-observation-v3.d.ts +25 -0
- package/dist/core/agents/live-observation-v3.d.ts.map +1 -0
- package/dist/core/agents/live-observation-v3.js +157 -0
- package/dist/core/agents/render/prompt-context.d.ts +4 -4
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +23 -58
- package/dist/core/agents/render/session-context.d.ts +1 -1
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +8 -32
- package/dist/core/agents/rules/claim-conflict.d.ts +3 -3
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +79 -127
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +3 -18
- package/dist/core/agents/rules/stop-hook.d.ts +9 -50
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +193 -236
- package/dist/core/agents/session-events.d.ts +5 -7
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +127 -95
- package/dist/core/agents/session-finalization-state-v3.d.ts +30 -0
- package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -0
- package/dist/core/agents/session-finalization-state-v3.js +40 -0
- package/dist/core/agents/session-finalizer-v3.d.ts +107 -0
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -0
- package/dist/core/agents/session-finalizer-v3.js +734 -0
- package/dist/core/agents/session-name-display.d.ts +40 -0
- package/dist/core/agents/session-name-display.d.ts.map +1 -0
- package/dist/core/agents/session-name-display.js +119 -0
- package/dist/core/agents/state/activity-log.d.ts +3 -3
- package/dist/core/agents/state/activity-log.d.ts.map +1 -1
- package/dist/core/agents/state/activity-log.js +7 -16
- package/dist/core/agents/state/heartbeat-writer.d.ts +23 -55
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +56 -228
- package/dist/core/agents/state/live-coordination-view.d.ts +31 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -0
- package/dist/core/agents/state/live-coordination-view.js +209 -0
- package/dist/core/agents/state/session-state.d.ts +4 -3
- package/dist/core/agents/state/session-state.d.ts.map +1 -1
- package/dist/core/agents/state/session-state.js +18 -9
- package/dist/core/agents/state/stale-sweep.d.ts +1 -1
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +26 -15
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +5 -7
- package/dist/core/config.d.ts +33 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +98 -2
- package/dist/core/context/index.d.ts +1 -4
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +9 -9
- package/dist/core/events/span-parent.d.ts +15 -0
- package/dist/core/events/span-parent.d.ts.map +1 -0
- package/dist/core/events/span-parent.js +34 -0
- package/dist/core/events/v3/advance.d.ts +14 -0
- package/dist/core/events/v3/advance.d.ts.map +1 -0
- package/dist/core/events/v3/advance.js +134 -0
- package/dist/core/events/v3/authority-outbox.d.ts +78 -0
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -0
- package/dist/core/events/v3/authority-outbox.js +399 -0
- package/dist/core/events/v3/base-builder.d.ts +20 -0
- package/dist/core/events/v3/base-builder.d.ts.map +1 -0
- package/dist/core/events/v3/base-builder.js +29 -0
- package/dist/core/events/v3/base-contract.d.ts +7576 -0
- package/dist/core/events/v3/base-contract.d.ts.map +1 -0
- package/dist/core/events/v3/base-contract.js +647 -0
- package/dist/core/events/v3/bootstrap.d.ts +34 -0
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -0
- package/dist/core/events/v3/bootstrap.js +175 -0
- package/dist/core/events/v3/builder.d.ts +20 -0
- package/dist/core/events/v3/builder.d.ts.map +1 -0
- package/dist/core/events/v3/builder.js +28 -0
- package/dist/core/events/v3/canonical.d.ts +31 -0
- package/dist/core/events/v3/canonical.d.ts.map +1 -0
- package/dist/core/events/v3/canonical.js +100 -0
- package/dist/core/events/v3/capabilities.d.ts +31 -0
- package/dist/core/events/v3/capabilities.d.ts.map +1 -0
- package/dist/core/events/v3/capabilities.js +107 -0
- package/dist/core/events/v3/capability-drift.d.ts +22 -0
- package/dist/core/events/v3/capability-drift.d.ts.map +1 -0
- package/dist/core/events/v3/capability-drift.js +134 -0
- package/dist/core/events/v3/catalog.d.ts +38 -0
- package/dist/core/events/v3/catalog.d.ts.map +1 -0
- package/dist/core/events/v3/catalog.js +135 -0
- package/dist/core/events/v3/clock-order.d.ts +34 -0
- package/dist/core/events/v3/clock-order.d.ts.map +1 -0
- package/dist/core/events/v3/clock-order.js +35 -0
- package/dist/core/events/v3/contract.d.ts +171 -0
- package/dist/core/events/v3/contract.d.ts.map +1 -0
- package/dist/core/events/v3/contract.js +180 -0
- package/dist/core/events/v3/control.d.ts +111 -0
- package/dist/core/events/v3/control.d.ts.map +1 -0
- package/dist/core/events/v3/control.js +419 -0
- package/dist/core/events/v3/coordination-view.d.ts +69 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -0
- package/dist/core/events/v3/coordination-view.js +119 -0
- package/dist/core/events/v3/economics.d.ts +77 -0
- package/dist/core/events/v3/economics.d.ts.map +1 -0
- package/dist/core/events/v3/economics.js +274 -0
- package/dist/core/events/v3/finalization-view.d.ts +26 -0
- package/dist/core/events/v3/finalization-view.d.ts.map +1 -0
- package/dist/core/events/v3/finalization-view.js +37 -0
- package/dist/core/events/v3/fingerprint-keys.d.ts +25 -0
- package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -0
- package/dist/core/events/v3/fingerprint-keys.js +217 -0
- package/dist/core/events/v3/generated.d.ts +4 -0
- package/dist/core/events/v3/generated.d.ts.map +1 -0
- package/dist/core/events/v3/generated.js +47 -0
- package/dist/core/events/v3/ids.d.ts +9 -0
- package/dist/core/events/v3/ids.d.ts.map +1 -0
- package/dist/core/events/v3/ids.js +25 -0
- package/dist/core/events/v3/index.d.ts +35 -0
- package/dist/core/events/v3/index.d.ts.map +1 -0
- package/dist/core/events/v3/index.js +33 -0
- package/dist/core/events/v3/latency.d.ts +63 -0
- package/dist/core/events/v3/latency.d.ts.map +1 -0
- package/dist/core/events/v3/latency.js +412 -0
- package/dist/core/events/v3/live-feed.d.ts +45 -0
- package/dist/core/events/v3/live-feed.d.ts.map +1 -0
- package/dist/core/events/v3/live-feed.js +246 -0
- package/dist/core/events/v3/live-observation.d.ts +35 -0
- package/dist/core/events/v3/live-observation.d.ts.map +1 -0
- package/dist/core/events/v3/live-observation.js +1 -0
- package/dist/core/events/v3/live-routing.d.ts +57 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -0
- package/dist/core/events/v3/live-routing.js +107 -0
- package/dist/core/events/v3/producers/command-recorder.d.ts +67 -0
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -0
- package/dist/core/events/v3/producers/command-recorder.js +477 -0
- package/dist/core/events/v3/producers/command.d.ts +50 -0
- package/dist/core/events/v3/producers/command.d.ts.map +1 -0
- package/dist/core/events/v3/producers/command.js +135 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +53 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.js +376 -0
- package/dist/core/events/v3/producers/coordination.d.ts +90 -0
- package/dist/core/events/v3/producers/coordination.d.ts.map +1 -0
- package/dist/core/events/v3/producers/coordination.js +229 -0
- package/dist/core/events/v3/producers/hook-base.d.ts +54 -0
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -0
- package/dist/core/events/v3/producers/hook-base.js +424 -0
- package/dist/core/events/v3/producers/hook.d.ts +29 -0
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -0
- package/dist/core/events/v3/producers/hook.js +71 -0
- package/dist/core/events/v3/producers/intake.d.ts +65 -0
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -0
- package/dist/core/events/v3/producers/intake.js +244 -0
- package/dist/core/events/v3/producers/recorder.d.ts +240 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -0
- package/dist/core/events/v3/producers/recorder.js +2176 -0
- package/dist/core/events/v3/projection.d.ts +101 -0
- package/dist/core/events/v3/projection.d.ts.map +1 -0
- package/dist/core/events/v3/projection.js +479 -0
- package/dist/core/events/v3/reader.d.ts +75 -0
- package/dist/core/events/v3/reader.d.ts.map +1 -0
- package/dist/core/events/v3/reader.js +778 -0
- package/dist/core/events/v3/recovery-record.d.ts +47 -0
- package/dist/core/events/v3/recovery-record.d.ts.map +1 -0
- package/dist/core/events/v3/recovery-record.js +167 -0
- package/dist/core/events/v3/recovery.d.ts +22 -0
- package/dist/core/events/v3/recovery.d.ts.map +1 -0
- package/dist/core/events/v3/recovery.js +227 -0
- package/dist/core/events/v3/span-state.d.ts +52 -0
- package/dist/core/events/v3/span-state.d.ts.map +1 -0
- package/dist/core/events/v3/span-state.js +142 -0
- package/dist/core/events/v3/targets.d.ts +23 -0
- package/dist/core/events/v3/targets.d.ts.map +1 -0
- package/dist/core/events/v3/targets.js +134 -0
- package/dist/core/events/v3/turn-telemetry.d.ts +41 -0
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -0
- package/dist/core/events/v3/turn-telemetry.js +168 -0
- package/dist/core/events/v3/validate.d.ts +9 -0
- package/dist/core/events/v3/validate.d.ts.map +1 -0
- package/dist/core/events/v3/validate.js +281 -0
- package/dist/core/events/v3/writer.d.ts +56 -0
- package/dist/core/events/v3/writer.d.ts.map +1 -0
- package/dist/core/events/v3/writer.js +335 -0
- package/dist/core/governor/index.d.ts +1 -1
- package/dist/core/governor/index.d.ts.map +1 -1
- package/dist/core/governor/index.js +1 -1
- package/dist/core/governor/plan-api.d.ts +2 -1
- package/dist/core/governor/plan-api.d.ts.map +1 -1
- package/dist/core/governor/plan-api.js +6 -3
- package/dist/core/governor/read.d.ts +1 -1
- package/dist/core/governor/read.d.ts.map +1 -1
- package/dist/core/governor/read.js +1 -1
- package/dist/core/governor/state.d.ts +9 -0
- package/dist/core/governor/state.d.ts.map +1 -1
- package/dist/core/governor/state.js +40 -3
- package/dist/core/guard/config.d.ts +5 -0
- package/dist/core/guard/config.d.ts.map +1 -0
- package/dist/core/guard/config.js +157 -0
- package/dist/core/guard/coordinator.d.ts +14 -0
- package/dist/core/guard/coordinator.d.ts.map +1 -0
- package/dist/core/guard/coordinator.js +173 -0
- package/dist/core/guard/evaluator.d.ts +4 -0
- package/dist/core/guard/evaluator.d.ts.map +1 -0
- package/dist/core/guard/evaluator.js +242 -0
- package/dist/core/guard/evidence-v3.d.ts +15 -0
- package/dist/core/guard/evidence-v3.d.ts.map +1 -0
- package/dist/core/guard/evidence-v3.js +171 -0
- package/dist/core/guard/index.d.ts +10 -0
- package/dist/core/guard/index.d.ts.map +1 -0
- package/dist/core/guard/index.js +6 -0
- package/dist/core/guard/live-source-v3.d.ts +40 -0
- package/dist/core/guard/live-source-v3.d.ts.map +1 -0
- package/dist/core/guard/live-source-v3.js +128 -0
- package/dist/core/guard/storage.d.ts +27 -0
- package/dist/core/guard/storage.d.ts.map +1 -0
- package/dist/core/guard/storage.js +140 -0
- package/dist/core/guard/types.d.ts +119 -0
- package/dist/core/guard/types.d.ts.map +1 -0
- package/dist/core/guard/types.js +1 -0
- package/dist/core/hooks/adapter/detect.d.ts +18 -1
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +29 -0
- package/dist/core/hooks/adapter/events.d.ts +7 -4
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +12 -4
- package/dist/core/hooks/adapter/output.d.ts +2 -2
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.d.ts +7 -4
- package/dist/core/hooks/adapter/parse.d.ts.map +1 -1
- package/dist/core/hooks/adapter/parse.js +63 -25
- package/dist/core/hooks/adapter/wiring.d.ts +30 -4
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +89 -11
- package/dist/core/hooks/cli.d.ts +1 -1
- package/dist/core/hooks/cli.js +306 -444
- package/dist/core/hooks/effects/image-capture.d.ts +20 -34
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +212 -221
- package/dist/core/hooks/effects/index.d.ts +1 -9
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +1 -24
- package/dist/core/hooks/events/input-hash.d.ts +7 -0
- package/dist/core/hooks/events/input-hash.d.ts.map +1 -0
- package/dist/core/hooks/events/input-hash.js +43 -0
- package/dist/core/hooks/index.d.ts +4 -3
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +2 -3
- package/dist/core/hooks/resolve/coord-root.d.ts +1 -1
- package/dist/core/hooks/resolve/coord-root.js +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +16 -15
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +51 -42
- package/dist/core/hooks/resolve/transcript.d.ts +33 -3
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +161 -14
- package/dist/core/hooks/session-name-presence.d.ts +9 -10
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +15 -15
- package/dist/core/hooks/unsafe-cross-shell.d.ts +1 -1
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -1
- package/dist/core/journal/index.d.ts +2 -2
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +9 -22
- package/dist/core/presence/blob.d.ts +4 -3
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +45 -44
- package/dist/core/presence/codec-digest.d.ts +29 -0
- package/dist/core/presence/codec-digest.d.ts.map +1 -0
- package/dist/core/presence/codec-digest.js +170 -0
- package/dist/core/presence/relay-client.d.ts +1 -1
- package/dist/core/presence/relay-client.d.ts.map +1 -1
- package/dist/core/presence/relay-client.js +5 -5
- package/dist/core/work/index.d.ts +1 -1
- package/dist/core/work/index.d.ts.map +1 -1
- package/dist/core/work/index.js +1 -1
- package/dist/core/work/read.d.ts +1 -1
- package/dist/core/work/read.d.ts.map +1 -1
- package/dist/core/work/read.js +1 -1
- package/dist/core/work/runner.d.ts.map +1 -1
- package/dist/core/work/runner.js +6 -0
- package/dist/core/work/state.d.ts +9 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +17 -3
- package/dist/core/workflow/engine.js +24 -25
- package/dist/core/workflow/live-session-v3.d.ts +19 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -0
- package/dist/core/workflow/live-session-v3.js +86 -0
- package/dist/core/workflow/scope-id.d.ts +3 -0
- package/dist/core/workflow/scope-id.d.ts.map +1 -0
- package/dist/core/workflow/scope-id.js +5 -0
- package/dist/core/workflow/workspaces/leases.d.ts +4 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +53 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/browser/client.d.ts +41 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +203 -0
- package/dist/lib/browser/critique.d.ts +28 -2
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +74 -18
- package/dist/lib/browser/geometry.d.ts +1 -1
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +130 -6
- package/dist/lib/browser/index.d.ts +3 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +3 -0
- package/dist/lib/browser/qa-plan.d.ts +174 -0
- package/dist/lib/browser/qa-plan.d.ts.map +1 -0
- package/dist/lib/browser/qa-plan.js +321 -0
- package/dist/lib/browser/qa-reuse.d.ts +99 -0
- package/dist/lib/browser/qa-reuse.d.ts.map +1 -0
- package/dist/lib/browser/qa-reuse.js +222 -0
- package/dist/lib/browser/qa-snapshot.d.ts +64 -0
- package/dist/lib/browser/qa-snapshot.d.ts.map +1 -0
- package/dist/lib/browser/qa-snapshot.js +171 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +22 -1
- package/dist/lib/browser/tiling.d.ts +71 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -0
- package/dist/lib/browser/tiling.js +118 -0
- package/dist/lib/claude-desktop.d.ts +0 -47
- package/dist/lib/claude-desktop.d.ts.map +1 -1
- package/dist/lib/claude-desktop.js +1 -169
- package/dist/lib/context/index.d.ts +0 -3
- package/dist/lib/context/index.d.ts.map +1 -1
- package/dist/lib/context/index.js +20 -42
- package/dist/lib/council/index.d.ts +2 -2
- package/dist/lib/council/index.d.ts.map +1 -1
- package/dist/lib/council/index.js +9 -20
- package/dist/lib/docs-frontmatter.d.ts +1 -6
- package/dist/lib/docs-frontmatter.d.ts.map +1 -1
- package/dist/lib/docs-frontmatter.js +12 -78
- package/dist/lib/docs-index.d.ts +9 -0
- package/dist/lib/docs-index.d.ts.map +1 -1
- package/dist/lib/docs-index.js +32 -3
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +14 -30
- package/dist/lib/docs-metadata-audit.d.ts +19 -0
- package/dist/lib/docs-metadata-audit.d.ts.map +1 -0
- package/dist/lib/docs-metadata-audit.js +132 -0
- package/dist/lib/docs-metadata-managed.d.ts +23 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -0
- package/dist/lib/docs-metadata-managed.js +78 -0
- package/dist/lib/docs-metadata-sync.d.ts +20 -0
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -0
- package/dist/lib/docs-metadata-sync.js +222 -0
- package/dist/lib/docs-metadata-v2-schema.d.ts +273 -0
- package/dist/lib/docs-metadata-v2-schema.d.ts.map +1 -0
- package/dist/lib/docs-metadata-v2-schema.js +134 -0
- package/dist/lib/docs-metadata-v2.d.ts +21 -0
- package/dist/lib/docs-metadata-v2.d.ts.map +1 -0
- package/dist/lib/docs-metadata-v2.js +424 -0
- package/dist/lib/docs-new.d.ts +11 -0
- package/dist/lib/docs-new.d.ts.map +1 -0
- package/dist/lib/docs-new.js +52 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -1
- package/dist/lib/docs-sweep.js +45 -29
- package/dist/lib/headless/index.d.ts +98 -0
- package/dist/lib/headless/index.d.ts.map +1 -0
- package/dist/lib/headless/index.js +211 -0
- package/dist/lib/identities/assume.d.ts +4 -4
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +52 -68
- package/dist/lib/instructions/apply.d.ts +5 -4
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +32 -20
- package/dist/lib/instructions/bundle.d.ts +1 -1
- package/dist/lib/instructions/bundle.d.ts.map +1 -1
- package/dist/lib/instructions/bundle.js +1 -1
- package/dist/lib/instructions/splice.d.ts +0 -1
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +22 -6
- package/dist/lib/instructions/templates.d.ts +7 -5
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +79 -9
- package/package.json +26 -5
- package/schemas/config.schema.json +120 -2
- package/schemas/docs-metadata-v2.schema.json +441 -0
- package/schemas/event-v3.schema.json +24472 -0
- package/src/commander.ts +4 -0
- package/src/commands/adapter.ts +2 -2
- package/src/commands/agents.ts +1530 -593
- package/src/commands/artifacts.ts +7 -6
- package/src/commands/backup.ts +29 -21
- package/src/commands/browse.ts +349 -8
- package/src/commands/checkpoint.ts +4 -3
- package/src/commands/claude-desktop.ts +2 -101
- package/src/commands/context.ts +1 -6
- package/src/commands/decision.ts +27 -44
- package/src/commands/docs.ts +100 -33
- package/src/commands/doctor.ts +14 -1
- package/src/commands/events.ts +256 -0
- package/src/commands/governor.ts +18 -4
- package/src/commands/init.ts +253 -57
- package/src/commands/instructions.ts +1 -1
- package/src/commands/journal.ts +4 -13
- package/src/commands/ledger-v3.ts +111 -0
- package/src/commands/presence.ts +11 -10
- package/src/commands/sync.ts +1 -1
- package/src/commands/web.ts +67 -8
- package/src/commands/work.ts +28 -5
- package/src/core/adapter.ts +5 -0
- package/src/core/adapters/attest.ts +8 -3
- package/src/core/adapters/profiles.ts +4 -6
- package/src/core/agents/canonical-emit.ts +34 -25
- package/src/core/agents/cli.ts +236 -262
- package/src/core/agents/codex-archive-v3.ts +138 -0
- package/src/core/agents/coord-client.ts +75 -188
- package/src/core/agents/finalization.ts +120 -38
- package/src/core/agents/index.ts +4 -5
- package/src/core/agents/live-authority-v3.ts +375 -0
- package/src/core/agents/live-lifecycle-v3.ts +140 -0
- package/src/core/agents/live-observation-v3.ts +216 -0
- package/src/core/agents/render/prompt-context.ts +29 -60
- package/src/core/agents/render/session-context.ts +10 -31
- package/src/core/agents/rules/claim-conflict.ts +85 -126
- package/src/core/agents/rules/commit-conflict.ts +3 -14
- package/src/core/agents/rules/stop-hook.ts +239 -293
- package/src/core/agents/session-events.ts +137 -104
- package/src/core/agents/session-finalization-state-v3.ts +90 -0
- package/src/core/agents/session-finalizer-v3.ts +953 -0
- package/src/core/agents/session-name-display.ts +149 -0
- package/src/core/agents/state/activity-log.ts +6 -14
- package/src/core/agents/state/heartbeat-writer.ts +82 -277
- package/src/core/agents/state/live-coordination-view.ts +290 -0
- package/src/core/agents/state/session-state.ts +24 -12
- package/src/core/agents/state/stale-sweep.ts +40 -17
- package/src/core/artifacts/index.ts +4 -5
- package/src/core/config.ts +148 -3
- package/src/core/context/index.ts +9 -17
- package/src/core/events/span-parent.ts +46 -0
- package/src/core/events/v3/advance.ts +168 -0
- package/src/core/events/v3/authority-outbox.ts +548 -0
- package/src/core/events/v3/base-builder.ts +60 -0
- package/src/core/events/v3/base-contract.ts +882 -0
- package/src/core/events/v3/bootstrap.ts +226 -0
- package/src/core/events/v3/builder.ts +59 -0
- package/src/core/events/v3/canonical.ts +153 -0
- package/src/core/events/v3/capabilities.ts +171 -0
- package/src/core/events/v3/capability-drift.ts +177 -0
- package/src/core/events/v3/catalog.ts +202 -0
- package/src/core/events/v3/clock-order.ts +41 -0
- package/src/core/events/v3/contract.ts +365 -0
- package/src/core/events/v3/control.ts +576 -0
- package/src/core/events/v3/coordination-view.ts +193 -0
- package/src/core/events/v3/economics.ts +384 -0
- package/src/core/events/v3/finalization-view.ts +67 -0
- package/src/core/events/v3/fingerprint-keys.ts +260 -0
- package/src/core/events/v3/generated.ts +48 -0
- package/src/core/events/v3/ids.ts +33 -0
- package/src/core/events/v3/index.ts +322 -0
- package/src/core/events/v3/latency.ts +551 -0
- package/src/core/events/v3/live-feed.ts +319 -0
- package/src/core/events/v3/live-observation.ts +39 -0
- package/src/core/events/v3/live-routing.ts +135 -0
- package/src/core/events/v3/producers/command-recorder.ts +631 -0
- package/src/core/events/v3/producers/command.ts +201 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +538 -0
- package/src/core/events/v3/producers/coordination.ts +355 -0
- package/src/core/events/v3/producers/hook-base.ts +578 -0
- package/src/core/events/v3/producers/hook.ts +119 -0
- package/src/core/events/v3/producers/intake.ts +319 -0
- package/src/core/events/v3/producers/recorder.ts +2805 -0
- package/src/core/events/v3/projection.ts +653 -0
- package/src/core/events/v3/reader.ts +1127 -0
- package/src/core/events/v3/recovery-record.ts +244 -0
- package/src/core/events/v3/recovery.ts +271 -0
- package/src/core/events/v3/span-state.ts +187 -0
- package/src/core/events/v3/targets.ts +182 -0
- package/src/core/events/v3/turn-telemetry.ts +259 -0
- package/src/core/events/v3/validate.ts +361 -0
- package/src/core/events/v3/writer.ts +419 -0
- package/src/core/governor/index.ts +3 -0
- package/src/core/governor/plan-api.ts +10 -2
- package/src/core/governor/read.ts +3 -0
- package/src/core/governor/state.ts +52 -5
- package/src/core/guard/config.ts +206 -0
- package/src/core/guard/coordinator.ts +227 -0
- package/src/core/guard/evaluator.ts +343 -0
- package/src/core/guard/evidence-v3.ts +227 -0
- package/src/core/guard/index.ts +35 -0
- package/src/core/guard/live-source-v3.ts +204 -0
- package/src/core/guard/storage.ts +188 -0
- package/src/core/guard/types.ts +152 -0
- package/src/core/hooks/adapter/detect.ts +28 -1
- package/src/core/hooks/adapter/events.ts +14 -6
- package/src/core/hooks/adapter/output.ts +7 -2
- package/src/core/hooks/adapter/parse.ts +80 -38
- package/src/core/hooks/adapter/wiring.ts +119 -13
- package/src/core/hooks/cli.ts +352 -466
- package/src/core/hooks/effects/image-capture.ts +228 -238
- package/src/core/hooks/effects/index.ts +1 -36
- package/src/core/hooks/events/input-hash.ts +47 -0
- package/src/core/hooks/index.ts +4 -11
- package/src/core/hooks/resolve/coord-root.ts +1 -1
- package/src/core/hooks/resolve/owner.ts +52 -45
- package/src/core/hooks/resolve/transcript.ts +171 -14
- package/src/core/hooks/session-name-presence.ts +14 -15
- package/src/core/hooks/unsafe-cross-shell.ts +1 -1
- package/src/core/journal/index.ts +15 -22
- package/src/core/presence/blob.ts +48 -41
- package/src/core/presence/codec-digest.ts +215 -0
- package/src/core/presence/relay-client.ts +5 -5
- package/src/core/work/index.ts +3 -0
- package/src/core/work/read.ts +3 -0
- package/src/core/work/runner.ts +6 -0
- package/src/core/work/state.ts +27 -3
- package/src/core/workflow/engine.ts +24 -24
- package/src/core/workflow/live-session-v3.ts +126 -0
- package/src/core/workflow/scope-id.ts +6 -0
- package/src/core/workflow/workspaces/leases.ts +56 -19
- package/src/index.ts +1 -1
- package/src/lib/browser/client.ts +230 -0
- package/src/lib/browser/critique.ts +104 -27
- package/src/lib/browser/geometry.ts +143 -14
- package/src/lib/browser/index.ts +34 -0
- package/src/lib/browser/qa-plan.ts +501 -0
- package/src/lib/browser/qa-reuse.ts +306 -0
- package/src/lib/browser/qa-snapshot.ts +223 -0
- package/src/lib/browser/runts.ts +27 -2
- package/src/lib/browser/tiling.ts +164 -0
- package/src/lib/claude-desktop.ts +1 -232
- package/src/lib/context/index.ts +22 -46
- package/src/lib/council/index.ts +9 -21
- package/src/lib/docs-frontmatter.ts +8 -79
- package/src/lib/docs-index.ts +33 -3
- package/src/lib/docs-lint.ts +15 -29
- package/src/lib/docs-metadata-audit.ts +166 -0
- package/src/lib/docs-metadata-managed.ts +83 -0
- package/src/lib/docs-metadata-sync.ts +262 -0
- package/src/lib/docs-metadata-v2-schema.ts +138 -0
- package/src/lib/docs-metadata-v2.ts +646 -0
- package/src/lib/docs-new.ts +63 -0
- package/src/lib/docs-sweep.ts +42 -28
- package/src/lib/headless/index.ts +287 -0
- package/src/lib/identities/assume.ts +67 -86
- package/src/lib/instructions/apply.ts +33 -20
- package/src/lib/instructions/bundle.ts +2 -2
- package/src/lib/instructions/splice.ts +21 -7
- package/src/lib/instructions/templates.ts +85 -13
- package/dist/core/agents/cli-emit.d.ts +0 -27
- package/dist/core/agents/cli-emit.d.ts.map +0 -1
- package/dist/core/agents/cli-emit.js +0 -57
- package/dist/core/agents/codex-replay.d.ts +0 -29
- package/dist/core/agents/codex-replay.d.ts.map +0 -1
- package/dist/core/agents/codex-replay.js +0 -138
- package/dist/core/agents/events/consume.d.ts +0 -82
- package/dist/core/agents/events/consume.d.ts.map +0 -1
- package/dist/core/agents/events/consume.js +0 -195
- package/dist/core/agents/events/emit.d.ts +0 -42
- package/dist/core/agents/events/emit.d.ts.map +0 -1
- package/dist/core/agents/events/emit.js +0 -75
- package/dist/core/agents/events/ulid.d.ts +0 -11
- package/dist/core/agents/events/ulid.d.ts.map +0 -1
- package/dist/core/agents/events/ulid.js +0 -47
- package/dist/core/agents/state/heartbeat-projector.d.ts +0 -76
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +0 -1
- package/dist/core/agents/state/heartbeat-projector.js +0 -581
- package/dist/core/hooks/events/emit.d.ts +0 -31
- package/dist/core/hooks/events/emit.d.ts.map +0 -1
- package/dist/core/hooks/events/emit.js +0 -93
- package/dist/core/hooks/events/rotate.d.ts +0 -43
- package/dist/core/hooks/events/rotate.d.ts.map +0 -1
- package/dist/core/hooks/events/rotate.js +0 -142
- package/dist/core/hooks/events/schema.d.ts +0 -341
- package/dist/core/hooks/events/schema.d.ts.map +0 -1
- package/dist/core/hooks/events/schema.js +0 -12
- package/dist/core/hooks/events/ulid.d.ts +0 -10
- package/dist/core/hooks/events/ulid.d.ts.map +0 -1
- package/dist/core/hooks/events/ulid.js +0 -47
- package/dist/lib/docs-frontmatter-migrate.d.ts +0 -33
- package/dist/lib/docs-frontmatter-migrate.d.ts.map +0 -1
- package/dist/lib/docs-frontmatter-migrate.js +0 -364
- package/src/core/agents/cli-emit.ts +0 -64
- package/src/core/agents/codex-replay.ts +0 -163
- package/src/core/agents/events/consume.ts +0 -254
- package/src/core/agents/events/emit.ts +0 -115
- package/src/core/agents/events/ulid.ts +0 -51
- package/src/core/agents/state/heartbeat-projector.ts +0 -645
- package/src/core/hooks/events/emit.ts +0 -125
- package/src/core/hooks/events/rotate.ts +0 -151
- package/src/core/hooks/events/schema.ts +0 -582
- package/src/core/hooks/events/ulid.ts +0 -51
- package/src/lib/docs-frontmatter-migrate.ts +0 -427
package/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,40 +1373,43 @@ 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
|
|
1258
1380
|
// declaration or a `--session-id` relay both keep the window open instead).
|
|
1259
|
-
const
|
|
1381
|
+
const firstSuggestedName = !priorHb?.suggested_session_name && hb?.suggested_session_name
|
|
1260
1382
|
? hb.suggested_session_name
|
|
1261
1383
|
: null;
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1384
|
+
// A readable but misclassified first display would otherwise leave the
|
|
1385
|
+
// session permanently latched: set-task is allowed through the gate, but a
|
|
1386
|
+
// normal repeat used to return no name and therefore could not establish a
|
|
1387
|
+
// fresh ordered transcript boundary. Re-emit the unchanged pending name as
|
|
1388
|
+
// an explicit retry. This does not rename the session or claim that the
|
|
1389
|
+
// display was seen; it only gives the next exact block new mint evidence.
|
|
1390
|
+
const retriedSuggestedName = !firstSuggestedName &&
|
|
1391
|
+
priorHb?.suggested_session_name &&
|
|
1392
|
+
priorHb.session_name_seen_for !== priorHb.suggested_session_name &&
|
|
1393
|
+
hb?.suggested_session_name === priorHb.suggested_session_name
|
|
1394
|
+
? hb.suggested_session_name
|
|
1395
|
+
: null;
|
|
1396
|
+
const suggestedName = firstSuggestedName ?? retriedSuggestedName;
|
|
1397
|
+
const firstOfSession = firstSuggestedName !== null;
|
|
1398
|
+
const sessionNameRetry = retriedSuggestedName !== null;
|
|
1275
1399
|
emit.data({
|
|
1276
1400
|
instance_id: myOwner,
|
|
1277
1401
|
name: hb?.name ?? null,
|
|
1278
1402
|
task: hb?.task ?? null,
|
|
1279
1403
|
cleared: !task || task.length === 0,
|
|
1280
1404
|
first_of_session: firstOfSession,
|
|
1405
|
+
session_name_retry: sessionNameRetry,
|
|
1281
1406
|
suggested_session_name: suggestedName,
|
|
1282
1407
|
...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
|
|
1283
1408
|
// Right-time instruction: the UserPromptSubmit nudge fires before the name
|
|
1284
1409
|
// exists; this result is the moment the model holds the string.
|
|
1285
1410
|
...(suggestedName
|
|
1286
1411
|
? {
|
|
1287
|
-
note:
|
|
1412
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1288
1413
|
}
|
|
1289
1414
|
: {}),
|
|
1290
1415
|
});
|
|
@@ -1310,7 +1435,7 @@ function runLifecycle(rawState, opts) {
|
|
|
1310
1435
|
}
|
|
1311
1436
|
if (!opts.sessionId)
|
|
1312
1437
|
ensureCursorSession(root);
|
|
1313
|
-
const myOwner = opts.sessionId ?? resolveOwner();
|
|
1438
|
+
const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1314
1439
|
if (!myOwner) {
|
|
1315
1440
|
emit.error({
|
|
1316
1441
|
code: "no_pidmap_entry",
|
|
@@ -1319,11 +1444,41 @@ function runLifecycle(rawState, opts) {
|
|
|
1319
1444
|
process.exitCode = 1;
|
|
1320
1445
|
return;
|
|
1321
1446
|
}
|
|
1322
|
-
|
|
1447
|
+
let hb = readCurrentCoordinationRow(myOwner);
|
|
1448
|
+
let reopenedGeneration;
|
|
1323
1449
|
if (!hb) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1450
|
+
if (state !== "active") {
|
|
1451
|
+
emit.error({
|
|
1452
|
+
code: "no_live_generation",
|
|
1453
|
+
message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
|
|
1454
|
+
});
|
|
1455
|
+
process.exitCode = 1;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
try {
|
|
1459
|
+
reopenedGeneration = reopenLiveCoordinationGenerationV3({
|
|
1460
|
+
coordRoot: root,
|
|
1461
|
+
owner: myOwner,
|
|
1462
|
+
nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
emit.error({
|
|
1467
|
+
code: "lifecycle_reopen_failed",
|
|
1468
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1469
|
+
});
|
|
1470
|
+
process.exitCode = 1;
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
hb = readCurrentCoordinationRow(myOwner);
|
|
1474
|
+
if (!hb) {
|
|
1475
|
+
emit.error({
|
|
1476
|
+
code: "lifecycle_reopen_failed",
|
|
1477
|
+
message: "the fresh generation was recorded but did not project as live",
|
|
1478
|
+
});
|
|
1479
|
+
process.exitCode = 1;
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1327
1482
|
}
|
|
1328
1483
|
if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
|
|
1329
1484
|
emit.error({
|
|
@@ -1352,6 +1507,22 @@ function runLifecycle(rawState, opts) {
|
|
|
1352
1507
|
}
|
|
1353
1508
|
const priorState = hb.task_state ?? "active";
|
|
1354
1509
|
const priorReason = hb.task_state_reason || undefined;
|
|
1510
|
+
if (reopenedGeneration) {
|
|
1511
|
+
emit.data({
|
|
1512
|
+
instance_id: myOwner,
|
|
1513
|
+
task_state: "active",
|
|
1514
|
+
prior_state: null,
|
|
1515
|
+
reason: null,
|
|
1516
|
+
changed: true,
|
|
1517
|
+
generation_reopened: true,
|
|
1518
|
+
prior_generation_id: reopenedGeneration.prior_generation_id,
|
|
1519
|
+
generation_id: reopenedGeneration.generation_id,
|
|
1520
|
+
name_reminted: false,
|
|
1521
|
+
suggested_session_name: null,
|
|
1522
|
+
git_finalization_checked: false,
|
|
1523
|
+
});
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1355
1526
|
if (priorState === state && priorReason === reason) {
|
|
1356
1527
|
emit.data({
|
|
1357
1528
|
instance_id: myOwner,
|
|
@@ -1365,7 +1536,7 @@ function runLifecycle(rawState, opts) {
|
|
|
1365
1536
|
}
|
|
1366
1537
|
let finalization = null;
|
|
1367
1538
|
if (state === "done") {
|
|
1368
|
-
const history = readSessionWriteClaims(root, hb.instance_id, hb
|
|
1539
|
+
const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
|
|
1369
1540
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1370
1541
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1371
1542
|
claimHistoryComplete: history.complete,
|
|
@@ -1379,19 +1550,16 @@ function runLifecycle(rawState, opts) {
|
|
|
1379
1550
|
return;
|
|
1380
1551
|
}
|
|
1381
1552
|
}
|
|
1382
|
-
const suggestedName = buildLifecycleSuggestedName(hb.
|
|
1553
|
+
const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
|
|
1383
1554
|
const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
|
|
1384
|
-
const emitted =
|
|
1385
|
-
type: "state.task_state",
|
|
1555
|
+
const emitted = emitEventV3({
|
|
1386
1556
|
owner: myOwner,
|
|
1387
|
-
session: hb
|
|
1557
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1388
1558
|
adapter: normalizeAdapter(hb.platform),
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
name_reminted: nameReminted,
|
|
1394
|
-
git_finalization_checked: finalization !== null,
|
|
1559
|
+
observation: {
|
|
1560
|
+
event_type: "coord.lifecycle_changed",
|
|
1561
|
+
new_state: state,
|
|
1562
|
+
...(reason ? { reason } : {}),
|
|
1395
1563
|
...(nameReminted ? { suggested_session_name: suggestedName } : {}),
|
|
1396
1564
|
},
|
|
1397
1565
|
});
|
|
@@ -1414,7 +1582,7 @@ function runLifecycle(rawState, opts) {
|
|
|
1414
1582
|
git_finalization_checked: finalization !== null,
|
|
1415
1583
|
...(nameReminted
|
|
1416
1584
|
? {
|
|
1417
|
-
note:
|
|
1585
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1418
1586
|
}
|
|
1419
1587
|
: {}),
|
|
1420
1588
|
});
|
|
@@ -1440,7 +1608,7 @@ function runSuggestName(descriptionParts, opts) {
|
|
|
1440
1608
|
});
|
|
1441
1609
|
process.exit(1);
|
|
1442
1610
|
}
|
|
1443
|
-
const hb =
|
|
1611
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1444
1612
|
const agentName = hb?.name || "unknown";
|
|
1445
1613
|
// Description resolution: an explicit arg wins; with no arg, fall back to the
|
|
1446
1614
|
// agent's current declared task, so a bare `suggest-name` reprints the running
|
|
@@ -1483,6 +1651,14 @@ function runStatus(opts) {
|
|
|
1483
1651
|
});
|
|
1484
1652
|
process.exit(1);
|
|
1485
1653
|
}
|
|
1654
|
+
if (opts.endSession && !opts.endTurn) {
|
|
1655
|
+
emit.error({
|
|
1656
|
+
code: "end_session_requires_end_turn",
|
|
1657
|
+
message: "--end-session requires --end-turn so Git finalization is verified first",
|
|
1658
|
+
});
|
|
1659
|
+
process.exitCode = 1;
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1486
1662
|
// Identity resolution: prefer explicit --session-id (hook-friendly), fall
|
|
1487
1663
|
// back to ppid walk for interactive shell usage.
|
|
1488
1664
|
if (!opts.sessionId)
|
|
@@ -1495,17 +1671,21 @@ function runStatus(opts) {
|
|
|
1495
1671
|
});
|
|
1496
1672
|
process.exit(1);
|
|
1497
1673
|
}
|
|
1498
|
-
const hb =
|
|
1674
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1499
1675
|
if (!hb) {
|
|
1500
1676
|
emit.error({
|
|
1501
|
-
code: "
|
|
1502
|
-
message:
|
|
1677
|
+
code: "no_live_generation",
|
|
1678
|
+
message: noLiveGenerationMessage(myOwner),
|
|
1503
1679
|
});
|
|
1504
1680
|
process.exit(1);
|
|
1505
1681
|
}
|
|
1682
|
+
// Report-only run quality: cheap config/cursor due check first, then a
|
|
1683
|
+
// non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
|
|
1684
|
+
const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
|
|
1685
|
+
const quality = qualityForStatus(qualityEvaluation.config.requested_mode, qualityEvaluation.snapshot);
|
|
1506
1686
|
let finalization = null;
|
|
1507
1687
|
if (opts.endTurn) {
|
|
1508
|
-
const history = readSessionWriteClaims(root, hb.instance_id, hb
|
|
1688
|
+
const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
|
|
1509
1689
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1510
1690
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1511
1691
|
claimHistoryComplete: history.complete,
|
|
@@ -1519,51 +1699,55 @@ function runStatus(opts) {
|
|
|
1519
1699
|
return;
|
|
1520
1700
|
}
|
|
1521
1701
|
}
|
|
1522
|
-
|
|
1523
|
-
type: "state.status_checked",
|
|
1702
|
+
emitEventV3({
|
|
1524
1703
|
owner: myOwner,
|
|
1525
|
-
session: hb
|
|
1704
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1526
1705
|
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,
|
|
1706
|
+
observation: {
|
|
1707
|
+
event_type: "coord.status_observed",
|
|
1708
|
+
status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
|
|
1532
1709
|
},
|
|
1533
1710
|
});
|
|
1534
|
-
|
|
1711
|
+
let sessionEnd;
|
|
1712
|
+
if (opts.endSession) {
|
|
1713
|
+
const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
|
|
1714
|
+
const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(({ state }) => state.instance_id === canonicalInstanceId);
|
|
1715
|
+
if (records.length !== 1 || !records[0]) {
|
|
1716
|
+
emit.error({
|
|
1717
|
+
code: "session_identity_ambiguous",
|
|
1718
|
+
message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
|
|
1719
|
+
});
|
|
1720
|
+
process.exitCode = 1;
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const state = records[0].state;
|
|
1724
|
+
const requested = requestSessionEndExplicitV3({
|
|
1725
|
+
coordRoot: root,
|
|
1726
|
+
instance_id: state.instance_id,
|
|
1727
|
+
generation_id: state.generation_id,
|
|
1728
|
+
outcome: "succeeded",
|
|
1729
|
+
coordination_finalized: true,
|
|
1730
|
+
});
|
|
1731
|
+
if (requested.state === "queued" || requested.state === "already_requested") {
|
|
1732
|
+
sessionEnd = { state: requested.state, request_id: requested.request.request_id };
|
|
1733
|
+
}
|
|
1734
|
+
else if (requested.state === "recorded") {
|
|
1735
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
|
|
1736
|
+
}
|
|
1737
|
+
else if (requested.state === "already_ended") {
|
|
1738
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
|
|
1742
|
+
process.exitCode = 1;
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1535
1747
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1536
1748
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1537
1749
|
: 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
|
-
}
|
|
1750
|
+
const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
|
|
1567
1751
|
// Sort: file-holders first (by file count desc), then idle peers by recency.
|
|
1568
1752
|
livePeers.sort((a, b) => {
|
|
1569
1753
|
const af = a.files_touched?.length ?? 0;
|
|
@@ -1577,7 +1761,7 @@ function runStatus(opts) {
|
|
|
1577
1761
|
// Cross-machine presence (ADR 0016): sessions on other machines, advisory.
|
|
1578
1762
|
const remoteMachines = readRemoteMachines(root);
|
|
1579
1763
|
const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
|
|
1580
|
-
const ctxUsage = readContextUsage(hb.session_id, hb.platform);
|
|
1764
|
+
const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
|
|
1581
1765
|
let ctxStr;
|
|
1582
1766
|
if (!ctxUsage) {
|
|
1583
1767
|
ctxStr = "unavailable";
|
|
@@ -1612,6 +1796,7 @@ function runStatus(opts) {
|
|
|
1612
1796
|
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1613
1797
|
activity_source: hb.activity_source ?? null,
|
|
1614
1798
|
task_state: taskStateOf(hb),
|
|
1799
|
+
task_state_scope: "current",
|
|
1615
1800
|
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1616
1801
|
task_state_reason: hb.task_state_reason ?? null,
|
|
1617
1802
|
files_held: filesHeld,
|
|
@@ -1638,7 +1823,9 @@ function runStatus(opts) {
|
|
|
1638
1823
|
context_window: ctxUsage?.window ?? null,
|
|
1639
1824
|
timestamp_iso: new Date().toISOString(),
|
|
1640
1825
|
timestamp_local: timeStr,
|
|
1826
|
+
...(quality ? { quality } : {}),
|
|
1641
1827
|
...(finalization ? { finalization } : {}),
|
|
1828
|
+
...(sessionEnd ? { session_end: sessionEnd } : {}),
|
|
1642
1829
|
};
|
|
1643
1830
|
if (opts.json) {
|
|
1644
1831
|
emit.config({ format: "json" });
|
|
@@ -1654,10 +1841,7 @@ function runStatus(opts) {
|
|
|
1654
1841
|
["peers", peersStr],
|
|
1655
1842
|
["time", timeStr],
|
|
1656
1843
|
];
|
|
1657
|
-
//
|
|
1658
|
-
if (hb.turn_summary && hb.turn_summary.length > 0) {
|
|
1659
|
-
rows.splice(1, 0, ["last turn", hb.turn_summary]);
|
|
1660
|
-
}
|
|
1844
|
+
// Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
|
|
1661
1845
|
if (hb.task && hb.task.length > 0) {
|
|
1662
1846
|
rows.splice(1, 0, ["task", hb.task]);
|
|
1663
1847
|
}
|
|
@@ -1671,10 +1855,49 @@ function runStatus(opts) {
|
|
|
1671
1855
|
: `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
|
|
1672
1856
|
rows.splice(idx, 0, ["council", summary]);
|
|
1673
1857
|
}
|
|
1858
|
+
if (quality) {
|
|
1859
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
1860
|
+
rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
|
|
1861
|
+
}
|
|
1862
|
+
if (sessionEnd) {
|
|
1863
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
1864
|
+
rows.splice(idx, 0, [
|
|
1865
|
+
"session end",
|
|
1866
|
+
sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
|
|
1867
|
+
? "queued after this turn"
|
|
1868
|
+
: "recorded",
|
|
1869
|
+
]);
|
|
1870
|
+
}
|
|
1674
1871
|
// Box rendering needs predictable stdout regardless of TTY/pipe detection:
|
|
1675
1872
|
// agent runs this via Bash (no TTY) and pastes captured stdout into chat.
|
|
1676
1873
|
process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
|
|
1677
1874
|
}
|
|
1875
|
+
function qualityForStatus(mode, snapshot) {
|
|
1876
|
+
if (mode !== "report")
|
|
1877
|
+
return null;
|
|
1878
|
+
const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
|
|
1879
|
+
if (!snapshot || !fresh) {
|
|
1880
|
+
return {
|
|
1881
|
+
status: "unknown",
|
|
1882
|
+
signal_ids: [],
|
|
1883
|
+
evaluated_at: snapshot?.evaluated_at ?? null,
|
|
1884
|
+
fresh: false,
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
status: snapshot.status,
|
|
1889
|
+
signal_ids: snapshot.signals
|
|
1890
|
+
.filter((signal) => signal.state === "active" && signal.severity !== "none")
|
|
1891
|
+
.map((signal) => signal.id)
|
|
1892
|
+
.sort()
|
|
1893
|
+
.slice(0, 3),
|
|
1894
|
+
evaluated_at: snapshot.evaluated_at,
|
|
1895
|
+
fresh: true,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
function formatRunQuality(status, signalIds) {
|
|
1899
|
+
return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
|
|
1900
|
+
}
|
|
1678
1901
|
function formatList(items, cap, emptyLabel) {
|
|
1679
1902
|
if (items.length === 0)
|
|
1680
1903
|
return emptyLabel;
|
|
@@ -1715,6 +1938,28 @@ function formatPeers(peers, cap, staleCount, remoteMachines = []) {
|
|
|
1715
1938
|
}
|
|
1716
1939
|
return main;
|
|
1717
1940
|
}
|
|
1941
|
+
export function collectStatusPeerHealth(root, myOwner, nowMs = Date.now()) {
|
|
1942
|
+
let rows = [];
|
|
1943
|
+
try {
|
|
1944
|
+
rows = readLiveCoordinationRows(root);
|
|
1945
|
+
}
|
|
1946
|
+
catch {
|
|
1947
|
+
// V3 authority failures must not resurrect disposable cache rows.
|
|
1948
|
+
}
|
|
1949
|
+
const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
|
|
1950
|
+
const livePeers = [];
|
|
1951
|
+
let stale = 0;
|
|
1952
|
+
for (const peer of rows) {
|
|
1953
|
+
if (peer.instance_id === myOwner)
|
|
1954
|
+
continue;
|
|
1955
|
+
const observedAtMs = Date.parse(peer.last_heartbeat);
|
|
1956
|
+
if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs)
|
|
1957
|
+
livePeers.push(peer);
|
|
1958
|
+
else
|
|
1959
|
+
stale += 1;
|
|
1960
|
+
}
|
|
1961
|
+
return { livePeers, stale };
|
|
1962
|
+
}
|
|
1718
1963
|
function fmtTokens(n) {
|
|
1719
1964
|
if (n < 1000)
|
|
1720
1965
|
return `${n}`;
|
|
@@ -1968,40 +2213,49 @@ function formatAge(secs) {
|
|
|
1968
2213
|
const h = Math.floor((secs % 86400) / 3600);
|
|
1969
2214
|
return `${d}d ${h}h`;
|
|
1970
2215
|
}
|
|
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);
|
|
2216
|
+
/** Read validated canonical V3 events. */
|
|
2217
|
+
export function readAgentDiagnosticEventsInWindow(root, cutoffMs) {
|
|
2218
|
+
const control = readEventV3ControlState(root);
|
|
2219
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
2220
|
+
const ledger = readLedgerV3(root);
|
|
2221
|
+
if (!ledger.complete) {
|
|
2222
|
+
return {
|
|
2223
|
+
source: "v3",
|
|
2224
|
+
authoritative: false,
|
|
2225
|
+
reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
|
|
2226
|
+
truncated: false,
|
|
2227
|
+
bytes: ledger.bytes,
|
|
2228
|
+
events: [],
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
const events = [];
|
|
2232
|
+
for (const { event } of ledger.events) {
|
|
2233
|
+
const tsMs = Date.parse(event.time.recorded_at);
|
|
1996
2234
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
|
|
1997
2235
|
continue;
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2236
|
+
events.push({
|
|
2237
|
+
event_type: event.event_type,
|
|
2238
|
+
ts: event.time.recorded_at,
|
|
2239
|
+
instance_id: event.scope.instance_id,
|
|
2240
|
+
payload: event.payload,
|
|
2241
|
+
});
|
|
2002
2242
|
}
|
|
2243
|
+
return {
|
|
2244
|
+
source: "v3",
|
|
2245
|
+
authoritative: true,
|
|
2246
|
+
truncated: false,
|
|
2247
|
+
bytes: ledger.bytes,
|
|
2248
|
+
events,
|
|
2249
|
+
};
|
|
2003
2250
|
}
|
|
2004
|
-
return
|
|
2251
|
+
return {
|
|
2252
|
+
source: "v3",
|
|
2253
|
+
authoritative: false,
|
|
2254
|
+
reason: `V3 control state is ${control.state}`,
|
|
2255
|
+
truncated: false,
|
|
2256
|
+
bytes: 0,
|
|
2257
|
+
events: [],
|
|
2258
|
+
};
|
|
2005
2259
|
}
|
|
2006
2260
|
/** Normalize adapter event data into the heartbeat platform value. */
|
|
2007
2261
|
function adapterToPlatform(adapter) {
|
|
@@ -2011,18 +2265,21 @@ function adapterToPlatform(adapter) {
|
|
|
2011
2265
|
return "cursor";
|
|
2012
2266
|
if (adapter === "codex")
|
|
2013
2267
|
return "codex";
|
|
2014
|
-
return "
|
|
2268
|
+
return "unknown";
|
|
2015
2269
|
}
|
|
2016
2270
|
/** Project a canonical health.* event into the HealEvent shape the aggregators
|
|
2017
2271
|
* already consume. Returns null for non-heal events. instance_id → display name
|
|
2018
2272
|
* via `nameById` (full-UUID keyed). */
|
|
2019
2273
|
function canonicalToHealEvent(ev, nameById) {
|
|
2020
|
-
if (ev.event_type !== "health.
|
|
2274
|
+
if (ev.event_type !== "health.observed") {
|
|
2021
2275
|
return null;
|
|
2022
2276
|
}
|
|
2023
|
-
const
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2277
|
+
const data = ev.payload ?? {};
|
|
2278
|
+
const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
|
|
2279
|
+
if (subsystem !== "pidmap" && subsystem !== "heartbeat")
|
|
2280
|
+
return null;
|
|
2281
|
+
const kind = subsystem;
|
|
2282
|
+
const reason = data.condition === "stale" ? "stale" : "missing";
|
|
2026
2283
|
const instanceId = ev.instance_id ?? "";
|
|
2027
2284
|
const name = nameById.get(instanceId);
|
|
2028
2285
|
const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
|
|
@@ -2099,13 +2356,12 @@ function runHealEvents(opts) {
|
|
|
2099
2356
|
});
|
|
2100
2357
|
process.exit(1);
|
|
2101
2358
|
}
|
|
2102
|
-
// Heal telemetry
|
|
2103
|
-
// (health.pidmap_heal / health.heartbeat_heal), emitted by the writer on
|
|
2104
|
-
// actual self-heal writes.
|
|
2359
|
+
// Heal telemetry is read from the canonical V3 ledger.
|
|
2105
2360
|
const cutoffMs = Date.now() - sinceSecs * 1000;
|
|
2106
2361
|
const nameById = buildNameById(root);
|
|
2107
2362
|
const events = [];
|
|
2108
|
-
|
|
2363
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
2364
|
+
for (const ev of diagnosticEvents.events) {
|
|
2109
2365
|
const heal = canonicalToHealEvent(ev, nameById);
|
|
2110
2366
|
if (heal)
|
|
2111
2367
|
events.push(heal);
|
|
@@ -2142,6 +2398,11 @@ function runHealEvents(opts) {
|
|
|
2142
2398
|
const recent = events.slice(0, limit);
|
|
2143
2399
|
const data = {
|
|
2144
2400
|
since: opts.since,
|
|
2401
|
+
telemetry: {
|
|
2402
|
+
source: diagnosticEvents.source,
|
|
2403
|
+
authoritative: diagnosticEvents.authoritative,
|
|
2404
|
+
reason: diagnosticEvents.reason,
|
|
2405
|
+
},
|
|
2145
2406
|
total: events.length,
|
|
2146
2407
|
by_reason: byReason,
|
|
2147
2408
|
by_kind: byKind,
|
|
@@ -2152,7 +2413,15 @@ function runHealEvents(opts) {
|
|
|
2152
2413
|
};
|
|
2153
2414
|
if (opts.csv) {
|
|
2154
2415
|
emit.config({ format: "csv" });
|
|
2155
|
-
emit.data(
|
|
2416
|
+
emit.data(data.telemetry.authoritative
|
|
2417
|
+
? recent
|
|
2418
|
+
: [
|
|
2419
|
+
{
|
|
2420
|
+
status: "unavailable",
|
|
2421
|
+
source: data.telemetry.source,
|
|
2422
|
+
reason: data.telemetry.reason,
|
|
2423
|
+
},
|
|
2424
|
+
]);
|
|
2156
2425
|
return;
|
|
2157
2426
|
}
|
|
2158
2427
|
if (opts.json) {
|
|
@@ -2164,6 +2433,11 @@ function runHealEvents(opts) {
|
|
|
2164
2433
|
if (process.stdout.isTTY) {
|
|
2165
2434
|
const lines = [];
|
|
2166
2435
|
lines.push(`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`);
|
|
2436
|
+
if (!data.telemetry.authoritative) {
|
|
2437
|
+
lines.push(` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`);
|
|
2438
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2167
2441
|
lines.push("");
|
|
2168
2442
|
if (events.length === 0) {
|
|
2169
2443
|
lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
|
|
@@ -2211,23 +2485,206 @@ function runHealEvents(opts) {
|
|
|
2211
2485
|
emit.text(`${lines.join("\n")}\n`);
|
|
2212
2486
|
}
|
|
2213
2487
|
}
|
|
2488
|
+
/** Open-span soft watermark per producer state. The producer-state reader
|
|
2489
|
+
* hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
|
|
2490
|
+
* pressure at half that gives room to act before reads start failing. */
|
|
2491
|
+
const SPAN_PRESSURE_SOFT_WATERMARK = 128;
|
|
2492
|
+
function errorText(error) {
|
|
2493
|
+
return error instanceof Error ? error.message : String(error);
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Read-only health counters for the V3 event ledger's producer surfaces. Never
|
|
2497
|
+
* mutates ledger state (no control repair, no spool drain) and never throws:
|
|
2498
|
+
* a non-live route returns `{ state: "unavailable" }`, and each sub-surface
|
|
2499
|
+
* that fails to read lands in `collection_errors` instead of aborting the rest.
|
|
2500
|
+
*/
|
|
2501
|
+
export function collectEventLedgerHealthV3(root, nowMs = Date.now()) {
|
|
2502
|
+
let control;
|
|
2503
|
+
try {
|
|
2504
|
+
control = readEventV3ControlState(root);
|
|
2505
|
+
}
|
|
2506
|
+
catch (error) {
|
|
2507
|
+
return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
|
|
2508
|
+
}
|
|
2509
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
2510
|
+
return {
|
|
2511
|
+
state: "unavailable",
|
|
2512
|
+
reason: `${control.state}: ${control.reason}`,
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
const collectionErrors = [];
|
|
2516
|
+
// 1) Open tool spans per live (non-terminal) generation + span-count pressure.
|
|
2517
|
+
const generations = [];
|
|
2518
|
+
const spanPressure = [];
|
|
2519
|
+
try {
|
|
2520
|
+
for (const { state } of listHookProducerStateRecordsV3(root)) {
|
|
2521
|
+
if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
|
|
2522
|
+
spanPressure.push({
|
|
2523
|
+
instance_id: state.instance_id,
|
|
2524
|
+
generation_id: state.generation_id,
|
|
2525
|
+
span_count: state.spans.length,
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
if (state.spans.length === 0)
|
|
2529
|
+
continue;
|
|
2530
|
+
generations.push({
|
|
2531
|
+
instance_id: state.instance_id,
|
|
2532
|
+
generation_id: state.generation_id,
|
|
2533
|
+
adapter: state.adapter,
|
|
2534
|
+
span_count: state.spans.length,
|
|
2535
|
+
turn_open: Boolean(state.current_turn_id),
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
catch (error) {
|
|
2540
|
+
collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
|
|
2541
|
+
}
|
|
2542
|
+
// 2) Pending finalization requests with age + trigger.
|
|
2543
|
+
const pending = [];
|
|
2544
|
+
try {
|
|
2545
|
+
for (const request of listSessionFinalizationRequestsV3(root)) {
|
|
2546
|
+
if (request.status !== "pending")
|
|
2547
|
+
continue;
|
|
2548
|
+
const observedMs = Date.parse(request.observed_at);
|
|
2549
|
+
pending.push({
|
|
2550
|
+
request_id: request.request_id,
|
|
2551
|
+
trigger: request.trigger,
|
|
2552
|
+
generation_id: request.generation_id,
|
|
2553
|
+
age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
|
|
2554
|
+
allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
catch (error) {
|
|
2559
|
+
collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
|
|
2560
|
+
}
|
|
2561
|
+
// 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
|
|
2562
|
+
let intakeTotal = 0;
|
|
2563
|
+
const intakeGroups = [];
|
|
2564
|
+
try {
|
|
2565
|
+
for (const group of listHookIntakeGroupsV3(root)) {
|
|
2566
|
+
const count = listHookIntakeRecordsV3(group.directory).length;
|
|
2567
|
+
if (count === 0)
|
|
2568
|
+
continue;
|
|
2569
|
+
intakeTotal += count;
|
|
2570
|
+
intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
catch (error) {
|
|
2574
|
+
collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
|
|
2575
|
+
}
|
|
2576
|
+
// 4) Diagnostics spool counts by category. Filenames only (never the
|
|
2577
|
+
// contents): `<category>-<orderkey>.json` where the orderkey leads with a
|
|
2578
|
+
// zero-padded epoch-ms, which also gives the last-24h split for free.
|
|
2579
|
+
const byCategory = {};
|
|
2580
|
+
const recentByCategory = {};
|
|
2581
|
+
let diagnosticsTotal = 0;
|
|
2582
|
+
let diagnostics24h = 0;
|
|
2583
|
+
let diagnostics1h = 0;
|
|
2584
|
+
let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
|
|
2585
|
+
try {
|
|
2586
|
+
const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
|
|
2587
|
+
if (existsSync(diagnosticsDir)) {
|
|
2588
|
+
const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
|
|
2589
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2590
|
+
for (const name of readdirSync(diagnosticsDir)) {
|
|
2591
|
+
const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
|
|
2592
|
+
const category = match?.[1];
|
|
2593
|
+
const epochMs = Number(match?.[2]);
|
|
2594
|
+
if (!category)
|
|
2595
|
+
continue;
|
|
2596
|
+
const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
|
|
2597
|
+
const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
|
|
2598
|
+
byCategory[category] = entry;
|
|
2599
|
+
recentByCategory[category] = recent;
|
|
2600
|
+
entry.total += 1;
|
|
2601
|
+
diagnosticsTotal += 1;
|
|
2602
|
+
if (Number.isFinite(epochMs)) {
|
|
2603
|
+
const recordedAt = new Date(epochMs).toISOString();
|
|
2604
|
+
if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
|
|
2605
|
+
recent.latest_at = recordedAt;
|
|
2606
|
+
}
|
|
2607
|
+
latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
|
|
2608
|
+
if (epochMs >= dayAgoMs) {
|
|
2609
|
+
entry.last_24h += 1;
|
|
2610
|
+
diagnostics24h += 1;
|
|
2611
|
+
}
|
|
2612
|
+
if (epochMs >= hourAgoMs) {
|
|
2613
|
+
recent.last_1h += 1;
|
|
2614
|
+
diagnostics1h += 1;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
catch (error) {
|
|
2621
|
+
collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
|
|
2622
|
+
}
|
|
2623
|
+
return {
|
|
2624
|
+
state: "live",
|
|
2625
|
+
mode: control.state,
|
|
2626
|
+
open_spans: {
|
|
2627
|
+
total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
|
|
2628
|
+
generations,
|
|
2629
|
+
},
|
|
2630
|
+
pending_finalizations: pending,
|
|
2631
|
+
intake_spool: { total: intakeTotal, groups: intakeGroups },
|
|
2632
|
+
diagnostics_spool: {
|
|
2633
|
+
total: diagnosticsTotal,
|
|
2634
|
+
last_24h: diagnostics24h,
|
|
2635
|
+
last_1h: diagnostics1h,
|
|
2636
|
+
latest_at: Number.isFinite(latestDiagnosticMs)
|
|
2637
|
+
? new Date(latestDiagnosticMs).toISOString()
|
|
2638
|
+
: null,
|
|
2639
|
+
by_category: byCategory,
|
|
2640
|
+
recent_by_category: recentByCategory,
|
|
2641
|
+
},
|
|
2642
|
+
span_pressure: spanPressure,
|
|
2643
|
+
collection_errors: collectionErrors,
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2214
2646
|
/** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
|
|
2215
|
-
* window
|
|
2216
|
-
*
|
|
2217
|
-
function readHookErrors(root, cutoffMs) {
|
|
2647
|
+
* window by exact error and phase, while separating current-hour failures from
|
|
2648
|
+
* historical rows. Each line is {ts, error, phase, ...}. */
|
|
2649
|
+
export function readHookErrors(root, cutoffMs, nowMs = Date.now()) {
|
|
2218
2650
|
const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
|
|
2219
2651
|
const byPhase = {};
|
|
2652
|
+
const byError = {};
|
|
2220
2653
|
const sampleByPhase = {};
|
|
2654
|
+
const errorPhaseCounts = new Map();
|
|
2655
|
+
const recentErrorPhaseCounts = new Map();
|
|
2221
2656
|
let total = 0;
|
|
2222
|
-
|
|
2223
|
-
|
|
2657
|
+
let last1h = 0;
|
|
2658
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
2659
|
+
if (!existsSync(p)) {
|
|
2660
|
+
return {
|
|
2661
|
+
total: 0,
|
|
2662
|
+
last1h: 0,
|
|
2663
|
+
latestAt: null,
|
|
2664
|
+
byPhase,
|
|
2665
|
+
byError,
|
|
2666
|
+
top: [],
|
|
2667
|
+
topErrors: [],
|
|
2668
|
+
recentTopErrors: [],
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2224
2671
|
let raw;
|
|
2225
2672
|
try {
|
|
2226
2673
|
raw = readFileSync(p, "utf8");
|
|
2227
2674
|
}
|
|
2228
2675
|
catch {
|
|
2229
|
-
return {
|
|
2676
|
+
return {
|
|
2677
|
+
total: 0,
|
|
2678
|
+
last1h: 0,
|
|
2679
|
+
latestAt: null,
|
|
2680
|
+
byPhase,
|
|
2681
|
+
byError,
|
|
2682
|
+
top: [],
|
|
2683
|
+
topErrors: [],
|
|
2684
|
+
recentTopErrors: [],
|
|
2685
|
+
};
|
|
2230
2686
|
}
|
|
2687
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2231
2688
|
for (const line of raw.split("\n")) {
|
|
2232
2689
|
if (!line.trim())
|
|
2233
2690
|
continue;
|
|
@@ -2237,9 +2694,22 @@ function readHookErrors(root, cutoffMs) {
|
|
|
2237
2694
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
|
|
2238
2695
|
continue;
|
|
2239
2696
|
const phase = e.phase ?? "(unknown)";
|
|
2697
|
+
const error = e.error ?? "(unknown)";
|
|
2240
2698
|
byPhase[phase] = (byPhase[phase] ?? 0) + 1;
|
|
2699
|
+
byError[error] = (byError[error] ?? 0) + 1;
|
|
2241
2700
|
if (!sampleByPhase[phase] && e.error)
|
|
2242
2701
|
sampleByPhase[phase] = e.error;
|
|
2702
|
+
const errorPhaseKey = `${error}\0${phase}`;
|
|
2703
|
+
const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
2704
|
+
errorPhase.count += 1;
|
|
2705
|
+
errorPhaseCounts.set(errorPhaseKey, errorPhase);
|
|
2706
|
+
latestMs = Math.max(latestMs, tsMs);
|
|
2707
|
+
if (tsMs >= hourAgoMs) {
|
|
2708
|
+
last1h += 1;
|
|
2709
|
+
const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
2710
|
+
recent.count += 1;
|
|
2711
|
+
recentErrorPhaseCounts.set(errorPhaseKey, recent);
|
|
2712
|
+
}
|
|
2243
2713
|
total++;
|
|
2244
2714
|
}
|
|
2245
2715
|
catch {
|
|
@@ -2250,125 +2720,163 @@ function readHookErrors(root, cutoffMs) {
|
|
|
2250
2720
|
.sort((a, b) => b[1] - a[1])
|
|
2251
2721
|
.slice(0, 5)
|
|
2252
2722
|
.map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
|
|
2253
|
-
|
|
2723
|
+
const topErrors = [...errorPhaseCounts.values()]
|
|
2724
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
2725
|
+
.slice(0, 5);
|
|
2726
|
+
const recentTopErrors = [...recentErrorPhaseCounts.values()]
|
|
2727
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
2728
|
+
.slice(0, 5);
|
|
2729
|
+
return {
|
|
2730
|
+
total,
|
|
2731
|
+
last1h,
|
|
2732
|
+
latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
|
|
2733
|
+
byPhase,
|
|
2734
|
+
byError,
|
|
2735
|
+
top,
|
|
2736
|
+
topErrors,
|
|
2737
|
+
recentTopErrors,
|
|
2738
|
+
};
|
|
2254
2739
|
}
|
|
2255
2740
|
/** Canonical event stream size + drain lag (events appended after the cursor). */
|
|
2256
2741
|
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 };
|
|
2742
|
+
const read = readAgentDiagnosticEventsInWindow(root, 0);
|
|
2743
|
+
return {
|
|
2744
|
+
source: "v3",
|
|
2745
|
+
authoritative: read.authoritative,
|
|
2746
|
+
...(read.reason ? { reason: read.reason } : {}),
|
|
2747
|
+
bytes: read.bytes,
|
|
2748
|
+
lines: read.events.length,
|
|
2749
|
+
cursor_backlog: 0,
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
export function traceInstanceIdsForEventSource(nativeInstanceIds, source) {
|
|
2753
|
+
void source;
|
|
2754
|
+
return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
|
|
2302
2755
|
}
|
|
2303
2756
|
/** Map a canonical event to a concise trace line, or null to drop it. */
|
|
2304
|
-
function traceLine(ev, allTools) {
|
|
2305
|
-
const d = (ev.
|
|
2757
|
+
export function traceLine(ev, allTools) {
|
|
2758
|
+
const d = (ev.payload ?? {});
|
|
2306
2759
|
const s = (k) => (typeof d[k] === "string" ? d[k] : "");
|
|
2760
|
+
const object = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
2761
|
+
? value
|
|
2762
|
+
: {};
|
|
2307
2763
|
const clip = (v, n = 70) => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
|
|
2764
|
+
const recovery = typeof d.recovery === "object" && d.recovery !== null
|
|
2765
|
+
? d.recovery
|
|
2766
|
+
: null;
|
|
2767
|
+
const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
|
|
2768
|
+
const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
|
|
2308
2769
|
let detail = "";
|
|
2309
2770
|
switch (ev.event_type) {
|
|
2310
|
-
case "session.
|
|
2311
|
-
detail =
|
|
2771
|
+
case "session.started":
|
|
2772
|
+
detail = "generation started";
|
|
2312
2773
|
break;
|
|
2313
|
-
case "session.
|
|
2314
|
-
detail =
|
|
2774
|
+
case "session.resumed":
|
|
2775
|
+
detail = "generation resumed";
|
|
2315
2776
|
break;
|
|
2316
|
-
case "
|
|
2317
|
-
detail =
|
|
2777
|
+
case "session.ended":
|
|
2778
|
+
detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2318
2779
|
break;
|
|
2319
|
-
case "
|
|
2320
|
-
detail = `
|
|
2780
|
+
case "session.termination_observed":
|
|
2781
|
+
detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2321
2782
|
break;
|
|
2322
|
-
case "
|
|
2323
|
-
detail =
|
|
2783
|
+
case "agent.delegated":
|
|
2784
|
+
detail = `delegated child=${s("child_generation_id") || "unknown"}`;
|
|
2324
2785
|
break;
|
|
2325
|
-
case "
|
|
2326
|
-
detail = `
|
|
2786
|
+
case "agent.started":
|
|
2787
|
+
detail = `child started=${s("child_generation_id") || "unknown"}`;
|
|
2327
2788
|
break;
|
|
2328
|
-
case "
|
|
2329
|
-
detail =
|
|
2789
|
+
case "agent.completed":
|
|
2790
|
+
detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
|
|
2330
2791
|
break;
|
|
2331
|
-
case "
|
|
2332
|
-
detail =
|
|
2792
|
+
case "turn.started":
|
|
2793
|
+
detail = `intent=${s("intent_kind") || "unknown"}`;
|
|
2333
2794
|
break;
|
|
2334
|
-
case "
|
|
2335
|
-
detail =
|
|
2795
|
+
case "turn.completed":
|
|
2796
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2336
2797
|
break;
|
|
2337
|
-
case "
|
|
2338
|
-
|
|
2798
|
+
case "tool.requested": {
|
|
2799
|
+
const tool = object(d.tool);
|
|
2800
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
|
|
2339
2801
|
break;
|
|
2340
|
-
|
|
2341
|
-
|
|
2802
|
+
}
|
|
2803
|
+
case "tool.completed": {
|
|
2804
|
+
const tool = object(d.tool);
|
|
2805
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
2342
2806
|
break;
|
|
2343
|
-
|
|
2344
|
-
|
|
2807
|
+
}
|
|
2808
|
+
case "command.started":
|
|
2809
|
+
if (!allTools)
|
|
2810
|
+
return null;
|
|
2811
|
+
detail = "command started (content omitted)";
|
|
2812
|
+
break;
|
|
2813
|
+
case "command.completed":
|
|
2814
|
+
if (!allTools && !recoveryReason)
|
|
2815
|
+
return null;
|
|
2816
|
+
detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
2817
|
+
break;
|
|
2818
|
+
case "coord.task_changed":
|
|
2819
|
+
case "coord.lifecycle_changed":
|
|
2820
|
+
case "coord.presence_changed":
|
|
2821
|
+
detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
|
|
2822
|
+
break;
|
|
2823
|
+
case "coord.status_observed":
|
|
2824
|
+
detail = s("status") || "status observed";
|
|
2825
|
+
break;
|
|
2826
|
+
case "coord.claim_changed": {
|
|
2827
|
+
const target = object(d.target);
|
|
2828
|
+
detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
|
|
2829
|
+
break;
|
|
2830
|
+
}
|
|
2831
|
+
case "coord.message_observed":
|
|
2832
|
+
detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
|
|
2833
|
+
break;
|
|
2834
|
+
case "coord.identity_attested":
|
|
2835
|
+
detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
|
|
2836
|
+
break;
|
|
2837
|
+
case "wait.started":
|
|
2838
|
+
detail = s("kind") || "wait started";
|
|
2345
2839
|
break;
|
|
2346
|
-
case "
|
|
2347
|
-
|
|
2348
|
-
case "claim.conflict":
|
|
2349
|
-
detail = clip(s("path"));
|
|
2840
|
+
case "wait.ended":
|
|
2841
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2350
2842
|
break;
|
|
2351
|
-
case "
|
|
2352
|
-
|
|
2353
|
-
detail = `
|
|
2843
|
+
case "artifact.observed": {
|
|
2844
|
+
const artifact = object(d.artifact);
|
|
2845
|
+
detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
|
|
2354
2846
|
break;
|
|
2355
|
-
|
|
2356
|
-
|
|
2847
|
+
}
|
|
2848
|
+
case "progress.observed":
|
|
2849
|
+
detail = s("kind") || "progress";
|
|
2850
|
+
break;
|
|
2851
|
+
case "health.observed":
|
|
2852
|
+
detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
|
|
2853
|
+
break;
|
|
2854
|
+
case "council.state_changed":
|
|
2855
|
+
detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2856
|
+
break;
|
|
2857
|
+
case "decision.state_changed":
|
|
2858
|
+
detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2859
|
+
break;
|
|
2860
|
+
case "lifecycle.recovered":
|
|
2861
|
+
detail = `recovered=${s("recovery_kind") || "unknown"}`;
|
|
2862
|
+
break;
|
|
2863
|
+
case "lifecycle.sweep_observed":
|
|
2864
|
+
detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
|
|
2357
2865
|
break;
|
|
2358
2866
|
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
2867
|
return null;
|
|
2369
2868
|
}
|
|
2370
2869
|
return { ts: ev.ts, event_type: ev.event_type, detail };
|
|
2371
2870
|
}
|
|
2871
|
+
export function pendingFinalizationTraceEntries(requests, instanceId) {
|
|
2872
|
+
return requests
|
|
2873
|
+
.filter((request) => request.status === "pending" && request.instance_id === instanceId)
|
|
2874
|
+
.map((request) => ({
|
|
2875
|
+
ts: request.observed_at,
|
|
2876
|
+
event_type: "session.finalization_pending",
|
|
2877
|
+
detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
|
|
2878
|
+
}));
|
|
2879
|
+
}
|
|
2372
2880
|
function runTrace(name, opts) {
|
|
2373
2881
|
if (opts.json)
|
|
2374
2882
|
emit.config({ format: "json" });
|
|
@@ -2396,8 +2904,8 @@ function runTrace(name, opts) {
|
|
|
2396
2904
|
targetId = candidates[0][0];
|
|
2397
2905
|
else if (candidates.length > 1)
|
|
2398
2906
|
targetId = candidates.map(([id]) => id).join("\x00"); // sentinel; resolved below
|
|
2399
|
-
else if (/^[0-9a-
|
|
2400
|
-
targetId = wanted;
|
|
2907
|
+
else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted))
|
|
2908
|
+
targetId = wanted;
|
|
2401
2909
|
}
|
|
2402
2910
|
if (!targetId) {
|
|
2403
2911
|
emit.error({
|
|
@@ -2408,33 +2916,16 @@ function runTrace(name, opts) {
|
|
|
2408
2916
|
}
|
|
2409
2917
|
const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
|
|
2410
2918
|
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");
|
|
2919
|
+
const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
|
|
2920
|
+
const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
|
|
2921
|
+
const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
|
|
2417
2922
|
const byId = new Map();
|
|
2418
|
-
const
|
|
2419
|
-
|
|
2420
|
-
if (!line)
|
|
2923
|
+
for (const ev of diagnosticRead.events) {
|
|
2924
|
+
if (!ev.instance_id || !candidateIds.includes(ev.instance_id))
|
|
2421
2925
|
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`);
|
|
2926
|
+
const arr = byId.get(ev.instance_id) ?? [];
|
|
2927
|
+
arr.push(ev);
|
|
2928
|
+
byId.set(ev.instance_id, arr);
|
|
2438
2929
|
}
|
|
2439
2930
|
// If the name mapped to multiple instances, trace the one with the latest event.
|
|
2440
2931
|
let resolvedId = candidateIds[0];
|
|
@@ -2450,24 +2941,78 @@ function runTrace(name, opts) {
|
|
|
2450
2941
|
}
|
|
2451
2942
|
}
|
|
2452
2943
|
const events = byId.get(resolvedId) ?? [];
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2944
|
+
let state = foldSessionState(events, { instance_id: resolvedId });
|
|
2945
|
+
let sessionState = events.some((event) => event.event_type === "session.ended")
|
|
2946
|
+
? "ended"
|
|
2947
|
+
: "live";
|
|
2948
|
+
if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
|
|
2949
|
+
try {
|
|
2950
|
+
const view = projectCoordinationViewV3(readLedgerV3(root));
|
|
2951
|
+
const liveGeneration = view.instances[resolvedId];
|
|
2952
|
+
const terminalGeneration = Object.values(view.terminal_generations).find((candidate) => candidate.instance_id === resolvedId);
|
|
2953
|
+
const generation = liveGeneration ?? terminalGeneration;
|
|
2954
|
+
if (generation) {
|
|
2955
|
+
sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
|
|
2956
|
+
const lifecycleState = generation.lifecycle_state;
|
|
2957
|
+
state = {
|
|
2958
|
+
activity: generation.activity === "terminal" ? "idle" : generation.activity,
|
|
2959
|
+
activity_updated_at: generation.last_observed_at,
|
|
2960
|
+
activity_source: "event-v3-coordination-view",
|
|
2961
|
+
task_state: lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
|
|
2962
|
+
? lifecycleState
|
|
2963
|
+
: "active",
|
|
2964
|
+
task_state_updated_at: generation.lifecycle_state_updated_at,
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
catch {
|
|
2969
|
+
diagnosticRead.authoritative = false;
|
|
2970
|
+
diagnosticRead.reason = "V3 coordination projection is unavailable";
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
let pendingFinalizations = [];
|
|
2974
|
+
if (diagnosticRead.source === "v3") {
|
|
2975
|
+
try {
|
|
2976
|
+
pendingFinalizations = listSessionFinalizationRequestsV3(root).filter((request) => request.status === "pending" && request.instance_id === resolvedId);
|
|
2977
|
+
}
|
|
2978
|
+
catch {
|
|
2979
|
+
diagnosticRead.authoritative = false;
|
|
2980
|
+
diagnosticRead.reason = "V3 finalization requests are unreadable";
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
const lines = [
|
|
2984
|
+
...events
|
|
2985
|
+
.map((ev) => traceLine(ev, !!opts.allTools))
|
|
2986
|
+
.filter((l) => l !== null),
|
|
2987
|
+
...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
|
|
2988
|
+
]
|
|
2457
2989
|
// Sort by timestamp, not file order: codex replays events (original ts,
|
|
2458
2990
|
// appended later), so append-order ≠ chronological order.
|
|
2459
2991
|
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
2460
2992
|
const shown = lines.slice(-limit);
|
|
2461
|
-
const
|
|
2993
|
+
const nativeResolvedId = diagnosticRead.source === "v3"
|
|
2994
|
+
? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
|
|
2995
|
+
: resolvedId;
|
|
2996
|
+
const displayName = nameById.get(resolvedId) ??
|
|
2997
|
+
(nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
|
|
2998
|
+
resolvedId.slice(0, 8);
|
|
2462
2999
|
const result = {
|
|
3000
|
+
event_source: {
|
|
3001
|
+
contract: diagnosticRead.source,
|
|
3002
|
+
authoritative: diagnosticRead.authoritative,
|
|
3003
|
+
reason: diagnosticRead.reason ?? null,
|
|
3004
|
+
},
|
|
2463
3005
|
name: displayName,
|
|
2464
3006
|
instance_id: resolvedId,
|
|
2465
3007
|
other_instances: candidateIds.filter((id) => id !== resolvedId),
|
|
3008
|
+
pending_finalizations: pendingFinalizations,
|
|
2466
3009
|
total_events: events.length,
|
|
3010
|
+
session_state: sessionState,
|
|
2467
3011
|
activity: state.activity,
|
|
2468
3012
|
activity_updated_at: state.activity_updated_at ?? null,
|
|
2469
3013
|
activity_source: state.activity_source ?? null,
|
|
2470
3014
|
task_state: state.task_state,
|
|
3015
|
+
task_state_scope: sessionState === "ended" ? "historical" : "current",
|
|
2471
3016
|
task_state_updated_at: state.task_state_updated_at ?? null,
|
|
2472
3017
|
task_state_reason: state.task_state_reason ?? null,
|
|
2473
3018
|
shown: shown.length,
|
|
@@ -2480,7 +3025,10 @@ function runTrace(name, opts) {
|
|
|
2480
3025
|
emit.data(result);
|
|
2481
3026
|
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
3027
|
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
|
|
3028
|
+
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
|
|
3029
|
+
if (!diagnosticRead.authoritative) {
|
|
3030
|
+
process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
|
|
3031
|
+
}
|
|
2484
3032
|
if (shown.length === 0) {
|
|
2485
3033
|
process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
|
|
2486
3034
|
return;
|
|
@@ -2490,6 +3038,53 @@ function runTrace(name, opts) {
|
|
|
2490
3038
|
process.stdout.write(` ${t} ${l.event_type.padEnd(22)} ${l.detail}\n`); // lint-ok-emission: human trace view
|
|
2491
3039
|
}
|
|
2492
3040
|
}
|
|
3041
|
+
/**
|
|
3042
|
+
* V3 generations, not disposable heartbeat caches, are the active-agent authority.
|
|
3043
|
+
*/
|
|
3044
|
+
export function collectActiveAgentHealth(root, nowMs = Date.now()) {
|
|
3045
|
+
const source = "event-ledger-v3";
|
|
3046
|
+
let heartbeats = [];
|
|
3047
|
+
try {
|
|
3048
|
+
const control = readEventV3ControlState(root);
|
|
3049
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
3050
|
+
heartbeats = readLiveCoordinationRows(root);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
catch {
|
|
3054
|
+
// The event-ledger section reports the authority read failure. Do not
|
|
3055
|
+
// substitute stale cache rows for a live V3 route.
|
|
3056
|
+
}
|
|
3057
|
+
const byPlatform = {};
|
|
3058
|
+
const byKind = {};
|
|
3059
|
+
const bySchema = {};
|
|
3060
|
+
let stale = 0;
|
|
3061
|
+
for (const heartbeat of heartbeats) {
|
|
3062
|
+
const platform = formatPlatformLabel(heartbeat.platform);
|
|
3063
|
+
byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
|
|
3064
|
+
const kind = heartbeat.kind ?? "unknown";
|
|
3065
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
3066
|
+
// These rows exist only after an authority-safe V3 projection. The
|
|
3067
|
+
// disposable heartbeat file has its own cache-format version, which must
|
|
3068
|
+
// not be reported as the ledger schema version.
|
|
3069
|
+
bySchema.v3 = (bySchema.v3 ?? 0) + 1;
|
|
3070
|
+
const lastObservedMs = heartbeat.last_heartbeat
|
|
3071
|
+
? Date.parse(heartbeat.last_heartbeat)
|
|
3072
|
+
: Number.NaN;
|
|
3073
|
+
const ageMs = Number.isFinite(lastObservedMs)
|
|
3074
|
+
? nowMs - lastObservedMs
|
|
3075
|
+
: Number.POSITIVE_INFINITY;
|
|
3076
|
+
if (ageMs > freshnessCutoffSecs() * 1000)
|
|
3077
|
+
stale += 1;
|
|
3078
|
+
}
|
|
3079
|
+
return {
|
|
3080
|
+
source,
|
|
3081
|
+
total: heartbeats.length,
|
|
3082
|
+
by_platform: byPlatform,
|
|
3083
|
+
by_kind: byKind,
|
|
3084
|
+
by_schema_version: bySchema,
|
|
3085
|
+
stale,
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
2493
3088
|
function runHealth(opts) {
|
|
2494
3089
|
if (opts.json)
|
|
2495
3090
|
emit.config({ format: "json" });
|
|
@@ -2509,11 +3104,12 @@ function runHealth(opts) {
|
|
|
2509
3104
|
});
|
|
2510
3105
|
process.exit(1);
|
|
2511
3106
|
}
|
|
2512
|
-
const
|
|
3107
|
+
const nowMs = Date.now();
|
|
3108
|
+
const cutoffMs = nowMs - sinceSecs * 1000;
|
|
2513
3109
|
const activeDir = resolve(root, ".harnery/active");
|
|
2514
3110
|
const councilsDir = resolve(root, ".harnery/councils");
|
|
2515
|
-
// Coordination telemetry reads from the canonical
|
|
2516
|
-
// Heals come from health
|
|
3111
|
+
// Coordination telemetry reads from the canonical V3 ledger.
|
|
3112
|
+
// Heals come from health.observed; council activity from council.state_changed.
|
|
2517
3113
|
const heal = [];
|
|
2518
3114
|
let councilAdvanced = 0;
|
|
2519
3115
|
let councilClosed = 0;
|
|
@@ -2521,32 +3117,35 @@ function runHealth(opts) {
|
|
|
2521
3117
|
let sweptTotal = 0;
|
|
2522
3118
|
const sweptByReason = {};
|
|
2523
3119
|
const nameById = buildNameById(root);
|
|
2524
|
-
|
|
3120
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
3121
|
+
for (const ev of diagnosticEvents.events) {
|
|
2525
3122
|
const healEv = canonicalToHealEvent(ev, nameById);
|
|
2526
3123
|
if (healEv) {
|
|
2527
3124
|
heal.push(healEv);
|
|
2528
3125
|
continue;
|
|
2529
3126
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
3127
|
+
if (ev.event_type === "council.state_changed") {
|
|
3128
|
+
const state = String(ev.payload?.new_state ?? "");
|
|
3129
|
+
if (state === "round_open")
|
|
2532
3130
|
councilAdvanced++;
|
|
2533
|
-
|
|
2534
|
-
case "council.close":
|
|
3131
|
+
if (state === "closed")
|
|
2535
3132
|
councilClosed++;
|
|
2536
|
-
|
|
2537
|
-
case "council.archive":
|
|
3133
|
+
if (state === "archived")
|
|
2538
3134
|
councilArchived++;
|
|
2539
|
-
|
|
2540
|
-
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
switch (ev.event_type) {
|
|
3138
|
+
case "lifecycle.sweep_observed": {
|
|
2541
3139
|
sweptTotal++;
|
|
2542
|
-
const reason = String(ev.
|
|
3140
|
+
const reason = String(ev.payload?.observation ?? "unknown");
|
|
2543
3141
|
sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
|
|
2544
3142
|
break;
|
|
2545
3143
|
}
|
|
2546
3144
|
}
|
|
2547
3145
|
}
|
|
2548
|
-
const hookErrors = readHookErrors(root, cutoffMs);
|
|
3146
|
+
const hookErrors = readHookErrors(root, cutoffMs, nowMs);
|
|
2549
3147
|
const stream = readStreamStats(root);
|
|
3148
|
+
const eventLedger = collectEventLedgerHealthV3(root);
|
|
2550
3149
|
// Canonical health.* events carry the full instance_id, already resolved to
|
|
2551
3150
|
// `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
|
|
2552
3151
|
// buildNameById; no hex8→name dedup pass needed anymore.
|
|
@@ -2564,19 +3163,13 @@ function runHealth(opts) {
|
|
|
2564
3163
|
.sort((a, b) => b[1] - a[1])
|
|
2565
3164
|
.slice(0, 5)
|
|
2566
3165
|
.map(([agent, count]) => ({ agent, count }));
|
|
2567
|
-
|
|
2568
|
-
const activeByPlatform = {};
|
|
2569
|
-
const activeByKind = {};
|
|
2570
|
-
const activeBySchema = {};
|
|
2571
|
-
let activeTotal = 0;
|
|
2572
|
-
let staleHeartbeats = 0;
|
|
3166
|
+
const activeAgents = collectActiveAgentHealth(root, nowMs);
|
|
2573
3167
|
// Zombies: files in active/ that are broken: unparseable, nameless, or an
|
|
2574
3168
|
// absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
|
|
2575
3169
|
// mean dead files the sweep isn't reaping.
|
|
2576
3170
|
let zombieCount = 0;
|
|
2577
3171
|
const zombieSamples = [];
|
|
2578
3172
|
const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
|
|
2579
|
-
const nowMs = Date.now();
|
|
2580
3173
|
if (existsSync(activeDir)) {
|
|
2581
3174
|
for (const file of readdirSync(activeDir)) {
|
|
2582
3175
|
if (!file.endsWith(".json"))
|
|
@@ -2595,18 +3188,8 @@ function runHealth(opts) {
|
|
|
2595
3188
|
zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
|
|
2596
3189
|
continue;
|
|
2597
3190
|
}
|
|
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
3191
|
const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
|
|
2607
3192
|
const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
|
|
2608
|
-
if (ageMs > freshnessCutoffSecs() * 1000)
|
|
2609
|
-
staleHeartbeats++;
|
|
2610
3193
|
// Zombie heuristics on a parseable heartbeat: no name, or an age so large
|
|
2611
3194
|
// it can only be a broken/epoch timestamp (a real agent would have healed).
|
|
2612
3195
|
if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
|
|
@@ -2636,46 +3219,65 @@ function runHealth(opts) {
|
|
|
2636
3219
|
anomalies.push(`${agent} self-healed ${count}x in ${opts.since}; possible idle-prune loop or PID instability`);
|
|
2637
3220
|
}
|
|
2638
3221
|
}
|
|
2639
|
-
if (
|
|
2640
|
-
|
|
3222
|
+
if (activeAgents.stale > 0) {
|
|
3223
|
+
const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
|
|
3224
|
+
const action = activeAgents.source === "event-ledger-v3"
|
|
3225
|
+
? "lifecycle reconciliation may be delayed"
|
|
3226
|
+
: "heal mechanism may not be firing";
|
|
3227
|
+
anomalies.push(`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`);
|
|
2641
3228
|
}
|
|
2642
|
-
const
|
|
3229
|
+
const expectedSchema = "v3";
|
|
3230
|
+
const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter((schema) => schema !== expectedSchema);
|
|
2643
3231
|
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}`);
|
|
3232
|
+
anomalies.push(`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`);
|
|
3233
|
+
}
|
|
3234
|
+
// Only recent hook failures are active anomalies. Historical rows remain in
|
|
3235
|
+
// the report with exact error signatures and their latest timestamp.
|
|
3236
|
+
if (hookErrors.last1h > 0) {
|
|
3237
|
+
const top = hookErrors.recentTopErrors[0];
|
|
3238
|
+
const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
|
|
3239
|
+
anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
|
|
2655
3240
|
}
|
|
2656
3241
|
if (stream.cursor_backlog > 500) {
|
|
2657
3242
|
anomalies.push(`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`);
|
|
2658
3243
|
}
|
|
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.
|
|
3244
|
+
// Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
|
|
2664
3245
|
if ((sweptByReason.unparseable ?? 0) > 0) {
|
|
2665
3246
|
anomalies.push(`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`);
|
|
2666
3247
|
}
|
|
2667
3248
|
if (zombieCount > 0) {
|
|
2668
3249
|
anomalies.push(`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`);
|
|
2669
3250
|
}
|
|
3251
|
+
if (eventLedger.state === "live") {
|
|
3252
|
+
// Open spans whose generation has no open turn: the turn ended without its
|
|
3253
|
+
// tool spans closing, so an explicit end can only queue, never finalize.
|
|
3254
|
+
const orphanGenerations = eventLedger.open_spans.generations.filter((generation) => !generation.turn_open);
|
|
3255
|
+
if (orphanGenerations.length > 0) {
|
|
3256
|
+
const orphanSpans = orphanGenerations.reduce((sum, generation) => sum + generation.span_count, 0);
|
|
3257
|
+
anomalies.push(`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`);
|
|
3258
|
+
}
|
|
3259
|
+
for (const pressure of eventLedger.span_pressure) {
|
|
3260
|
+
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)`);
|
|
3261
|
+
}
|
|
3262
|
+
for (const collectionError of eventLedger.collection_errors) {
|
|
3263
|
+
anomalies.push(`event-ledger health: ${collectionError}`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
2670
3266
|
const report = {
|
|
2671
3267
|
since: opts.since,
|
|
2672
3268
|
generated_at: new Date().toISOString(),
|
|
3269
|
+
event_telemetry: {
|
|
3270
|
+
source: diagnosticEvents.source,
|
|
3271
|
+
authoritative: diagnosticEvents.authoritative,
|
|
3272
|
+
...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
|
|
3273
|
+
},
|
|
2673
3274
|
active_agents: {
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
3275
|
+
source: activeAgents.source,
|
|
3276
|
+
total: activeAgents.total,
|
|
3277
|
+
by_platform: activeAgents.by_platform,
|
|
3278
|
+
by_kind: activeAgents.by_kind,
|
|
3279
|
+
by_schema_version: activeAgents.by_schema_version,
|
|
3280
|
+
stale: activeAgents.stale,
|
|
2679
3281
|
},
|
|
2680
3282
|
heal_events: {
|
|
2681
3283
|
total: heal.length,
|
|
@@ -2691,9 +3293,19 @@ function runHealth(opts) {
|
|
|
2691
3293
|
closed_in_window: councilClosed,
|
|
2692
3294
|
},
|
|
2693
3295
|
swept_events: { total: sweptTotal, by_reason: sweptByReason },
|
|
2694
|
-
hook_errors: {
|
|
3296
|
+
hook_errors: {
|
|
3297
|
+
total: hookErrors.total,
|
|
3298
|
+
last_1h: hookErrors.last1h,
|
|
3299
|
+
latest_at: hookErrors.latestAt,
|
|
3300
|
+
by_phase: hookErrors.byPhase,
|
|
3301
|
+
by_error: hookErrors.byError,
|
|
3302
|
+
top: hookErrors.top,
|
|
3303
|
+
top_errors: hookErrors.topErrors,
|
|
3304
|
+
recent_top_errors: hookErrors.recentTopErrors,
|
|
3305
|
+
},
|
|
2695
3306
|
stream,
|
|
2696
3307
|
zombies: { count: zombieCount, samples: zombieSamples },
|
|
3308
|
+
event_ledger: eventLedger,
|
|
2697
3309
|
anomalies,
|
|
2698
3310
|
};
|
|
2699
3311
|
if (opts.json) {
|
|
@@ -2729,16 +3341,38 @@ function renderHealthBox(report) {
|
|
|
2729
3341
|
councilParts.push(`${report.councils.closed_in_window} closed`);
|
|
2730
3342
|
if (report.councils.archived_in_window > 0)
|
|
2731
3343
|
councilParts.push(`${report.councils.archived_in_window} archived`);
|
|
2732
|
-
const
|
|
3344
|
+
const activeSource = report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
|
|
3345
|
+
const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
|
|
2733
3346
|
const sweptReasonStr = Object.entries(report.swept_events.by_reason)
|
|
2734
3347
|
.map(([r, n]) => `${r} ${n}`)
|
|
2735
3348
|
.join(", ");
|
|
2736
3349
|
const hookErrStr = report.hook_errors.total === 0
|
|
2737
3350
|
? "0"
|
|
2738
|
-
: `${report.hook_errors.total}
|
|
3351
|
+
: `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
|
|
2739
3352
|
const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
|
|
3353
|
+
const ledger = report.event_ledger;
|
|
3354
|
+
let ledgerStr;
|
|
3355
|
+
if (ledger.state !== "live") {
|
|
3356
|
+
ledgerStr = `unavailable (${ledger.reason})`;
|
|
3357
|
+
}
|
|
3358
|
+
else {
|
|
3359
|
+
const orphanGenerations = ledger.open_spans.generations.filter((generation) => !generation.turn_open).length;
|
|
3360
|
+
const pendingAges = ledger.pending_finalizations
|
|
3361
|
+
.map((request) => request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)))
|
|
3362
|
+
.join(", ");
|
|
3363
|
+
ledgerStr = [
|
|
3364
|
+
`open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
|
|
3365
|
+
`pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
|
|
3366
|
+
`intake ${ledger.intake_spool.total}`,
|
|
3367
|
+
`diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
|
|
3368
|
+
].join(" · ");
|
|
3369
|
+
}
|
|
2740
3370
|
const rows = [
|
|
2741
3371
|
["window", `last ${report.since}`],
|
|
3372
|
+
[
|
|
3373
|
+
"event source",
|
|
3374
|
+
`${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
|
|
3375
|
+
],
|
|
2742
3376
|
["active", activeStr],
|
|
2743
3377
|
[
|
|
2744
3378
|
"heals",
|
|
@@ -2748,6 +3382,7 @@ function renderHealthBox(report) {
|
|
|
2748
3382
|
["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
|
|
2749
3383
|
["hook errors", hookErrStr],
|
|
2750
3384
|
["stream", streamStr],
|
|
3385
|
+
["event ledger", ledgerStr],
|
|
2751
3386
|
[
|
|
2752
3387
|
"zombies",
|
|
2753
3388
|
report.zombies.count === 0
|
|
@@ -2851,6 +3486,8 @@ function runAdapterProbe(id, opts) {
|
|
|
2851
3486
|
replayExitCode = result.exitCode;
|
|
2852
3487
|
data.samples = samples;
|
|
2853
3488
|
data.samples_summary = result.summary;
|
|
3489
|
+
if (result.note)
|
|
3490
|
+
data.samples_note = result.note;
|
|
2854
3491
|
}
|
|
2855
3492
|
emit.data(data);
|
|
2856
3493
|
if (process.stdout.isTTY && !opts.json) {
|
|
@@ -2867,7 +3504,7 @@ function runAdapterProbe(id, opts) {
|
|
|
2867
3504
|
const summary = data.samples_summary;
|
|
2868
3505
|
lines.push("");
|
|
2869
3506
|
if (samples.length === 0) {
|
|
2870
|
-
lines.push(` Sample replay: no .json fixtures found under ${sampleDir}`);
|
|
3507
|
+
lines.push(` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`);
|
|
2871
3508
|
}
|
|
2872
3509
|
else {
|
|
2873
3510
|
lines.push(` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`);
|
|
@@ -2918,6 +3555,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
|
|
|
2918
3555
|
samples: [],
|
|
2919
3556
|
exitCode: 0,
|
|
2920
3557
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
3558
|
+
note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
|
|
2921
3559
|
};
|
|
2922
3560
|
}
|
|
2923
3561
|
const fixtures = readdirSync(sampleDir)
|
|
@@ -2929,6 +3567,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
|
|
|
2929
3567
|
samples: [],
|
|
2930
3568
|
exitCode: 0,
|
|
2931
3569
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
3570
|
+
note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
|
|
2932
3571
|
};
|
|
2933
3572
|
}
|
|
2934
3573
|
// agent-hook is the single entry point. Replay sample payloads against it,
|
|
@@ -3143,10 +3782,24 @@ function runPing(name, message, opts) {
|
|
|
3143
3782
|
});
|
|
3144
3783
|
process.exit(1);
|
|
3145
3784
|
}
|
|
3146
|
-
const myHb =
|
|
3785
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3147
3786
|
const fromName = myHb?.name ?? "anonymous";
|
|
3148
3787
|
const body = `from agent-${fromName}: ${message.trim()}`;
|
|
3149
3788
|
const doc = appendEntry(peerOwner, "handoff", body);
|
|
3789
|
+
// Canonical delivery record: sender is the envelope owner, recipient rides
|
|
3790
|
+
// in data, so read-only observers can render the communication without
|
|
3791
|
+
// joining journal files.
|
|
3792
|
+
emitEventV3({
|
|
3793
|
+
owner: myOwner,
|
|
3794
|
+
session: nativeSessionIdentity(myHb, myOwner),
|
|
3795
|
+
adapter: normalizeAdapter(myHb?.platform),
|
|
3796
|
+
observation: {
|
|
3797
|
+
event_type: "coord.message_observed",
|
|
3798
|
+
direction: "sent",
|
|
3799
|
+
subject: peerOwner,
|
|
3800
|
+
body: message.trim(),
|
|
3801
|
+
},
|
|
3802
|
+
});
|
|
3150
3803
|
const data = {
|
|
3151
3804
|
peer: name,
|
|
3152
3805
|
peer_instance_id: peerOwner,
|
|
@@ -3195,7 +3848,7 @@ async function runWait(name, opts) {
|
|
|
3195
3848
|
}
|
|
3196
3849
|
let lastProgressMs = 0;
|
|
3197
3850
|
while (true) {
|
|
3198
|
-
const hb =
|
|
3851
|
+
const hb = readCurrentCoordinationRow(peerOwner);
|
|
3199
3852
|
const now = Date.now();
|
|
3200
3853
|
const elapsedMs = now - startMs;
|
|
3201
3854
|
if (!hb) {
|
|
@@ -3230,9 +3883,8 @@ async function runWait(name, opts) {
|
|
|
3230
3883
|
const progressGapMs = Math.max(pollMs, 30_000);
|
|
3231
3884
|
if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
|
|
3232
3885
|
lastProgressMs = now;
|
|
3233
|
-
const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
|
|
3234
3886
|
const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
|
|
3235
|
-
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking
|
|
3887
|
+
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
|
|
3236
3888
|
process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
|
|
3237
3889
|
}
|
|
3238
3890
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
@@ -3267,10 +3919,20 @@ function runHeal(opts) {
|
|
|
3267
3919
|
emit.error({ code: "missing_owner", message: "--owner is required" });
|
|
3268
3920
|
process.exit(1);
|
|
3269
3921
|
}
|
|
3270
|
-
if (kind !== "pidmap" && kind !== "
|
|
3922
|
+
if (kind !== "pidmap" && kind !== "cache") {
|
|
3271
3923
|
emit.error({
|
|
3272
3924
|
code: "bad_kind",
|
|
3273
|
-
message: "--kind must be one of: pidmap,
|
|
3925
|
+
message: "--kind must be one of: pidmap, cache",
|
|
3926
|
+
});
|
|
3927
|
+
process.exit(1);
|
|
3928
|
+
}
|
|
3929
|
+
if (opts.adapter &&
|
|
3930
|
+
opts.adapter !== "claude-code" &&
|
|
3931
|
+
opts.adapter !== "cursor" &&
|
|
3932
|
+
opts.adapter !== "codex") {
|
|
3933
|
+
emit.error({
|
|
3934
|
+
code: "bad_adapter",
|
|
3935
|
+
message: "--adapter must be one of: claude-code, cursor, codex",
|
|
3274
3936
|
});
|
|
3275
3937
|
process.exit(1);
|
|
3276
3938
|
}
|
|
@@ -3282,8 +3944,8 @@ function runHeal(opts) {
|
|
|
3282
3944
|
});
|
|
3283
3945
|
process.exit(1);
|
|
3284
3946
|
}
|
|
3285
|
-
const action = kind === "pidmap" ? "heal-pidmap" :
|
|
3286
|
-
// Refuse to
|
|
3947
|
+
const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
|
|
3948
|
+
// Refuse to materialize a V3 cache at a truncated owner id.
|
|
3287
3949
|
//
|
|
3288
3950
|
// Heartbeats are keyed by the whole instance_id, so healing at an
|
|
3289
3951
|
// abbreviated id writes `.harnery/active/<prefix>.json` while every reader
|
|
@@ -3303,32 +3965,31 @@ function runHeal(opts) {
|
|
|
3303
3965
|
// whose instance_id this id is a prefix of settles it just as well, and is
|
|
3304
3966
|
// present in precisely the case that matters (the session the reader was
|
|
3305
3967
|
// trying to heal is registered, just not under the abbreviated name).
|
|
3306
|
-
if (kind === "
|
|
3968
|
+
if (kind === "cache" && !readHeartbeatCache(root, owner)) {
|
|
3307
3969
|
const canonical = opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
|
|
3308
3970
|
? opts.sessionId.trim()
|
|
3309
3971
|
: liveIdWithPrefix(root, owner);
|
|
3310
3972
|
if (canonical) {
|
|
3311
|
-
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live
|
|
3973
|
+
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
|
|
3312
3974
|
emit.error({
|
|
3313
3975
|
code: "truncated_owner",
|
|
3314
|
-
message: `--owner ${owner} is a prefix of ${canonical} (${source})
|
|
3315
|
-
`
|
|
3316
|
-
`resolves. Re-run with --owner ${canonical}.`,
|
|
3976
|
+
message: `--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
|
|
3977
|
+
`the canonical instance id; re-run with --owner ${canonical}.`,
|
|
3317
3978
|
});
|
|
3318
3979
|
process.exit(1);
|
|
3319
3980
|
}
|
|
3320
3981
|
}
|
|
3321
3982
|
// Build positional args. agent-coord's arg layout:
|
|
3322
3983
|
// heal-pidmap <instance_id> [<pid>]
|
|
3323
|
-
//
|
|
3324
|
-
// kill-heartbeat <instance_id>
|
|
3984
|
+
// repair-coordination-cache <instance_id> [<session_id>]
|
|
3325
3985
|
const helperArgs = [action, owner];
|
|
3326
3986
|
if (kind === "pidmap" && opts.pid)
|
|
3327
3987
|
helperArgs.push(opts.pid);
|
|
3328
|
-
if (kind === "
|
|
3988
|
+
if (kind === "cache" && opts.sessionId)
|
|
3329
3989
|
helperArgs.push(opts.sessionId);
|
|
3330
|
-
|
|
3331
|
-
|
|
3990
|
+
if (kind === "cache" && opts.adapter)
|
|
3991
|
+
helperArgs.push(`--adapter=${opts.adapter}`);
|
|
3992
|
+
// Both recovery actions are handled by the bundled agent-coord binary.
|
|
3332
3993
|
const helper = agentCoordOrExit(root);
|
|
3333
3994
|
const proc = spawnSync(helper, helperArgs, {
|
|
3334
3995
|
encoding: "utf8",
|
|
@@ -3341,26 +4002,21 @@ function runHeal(opts) {
|
|
|
3341
4002
|
});
|
|
3342
4003
|
process.exit(1);
|
|
3343
4004
|
}
|
|
3344
|
-
//
|
|
4005
|
+
// Read the derived cache to surface post-action state.
|
|
3345
4006
|
let after = null;
|
|
3346
4007
|
try {
|
|
3347
|
-
after =
|
|
4008
|
+
after = readHeartbeatCache(root, owner);
|
|
3348
4009
|
}
|
|
3349
4010
|
catch {
|
|
3350
4011
|
after = null;
|
|
3351
4012
|
}
|
|
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
4013
|
const outcome = kind === "pidmap"
|
|
3358
4014
|
? proc.status === 0
|
|
3359
4015
|
? "ok"
|
|
3360
4016
|
: "failed"
|
|
3361
4017
|
: after
|
|
3362
|
-
? "
|
|
3363
|
-
: "
|
|
4018
|
+
? "cache_present"
|
|
4019
|
+
: "cache_absent";
|
|
3364
4020
|
emit.data({
|
|
3365
4021
|
rows: [
|
|
3366
4022
|
{
|
|
@@ -3383,31 +4039,35 @@ function runHeal(opts) {
|
|
|
3383
4039
|
emit.text(`agent-coord ${action} ok\n`);
|
|
3384
4040
|
}
|
|
3385
4041
|
else {
|
|
3386
|
-
emit.text(`agent-coord ${action} ok:
|
|
4042
|
+
emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
|
|
3387
4043
|
}
|
|
3388
4044
|
}
|
|
3389
|
-
// Canonical health.* emission is owned by the writer
|
|
3390
|
-
//
|
|
4045
|
+
// Canonical health.* emission is owned by the cache writer, so it fires
|
|
4046
|
+
// inside the agent-coord subprocess
|
|
3391
4047
|
// above on actual writes only: write-only telemetry, no double-emit, no
|
|
3392
4048
|
// event when an already-correct heal no-ops. (Previously emitted here
|
|
3393
4049
|
// unconditionally on every `harn agents heal`, which over-counted no-op heals.)
|
|
3394
4050
|
}
|
|
3395
4051
|
/**
|
|
3396
|
-
*
|
|
3397
|
-
*
|
|
3398
|
-
* through silently if no session (CI / direct invocation).
|
|
4052
|
+
* Emit one V3 council audit transition keyed to the exact durable manifest.
|
|
4053
|
+
* Falls through silently when no live session can attest the observation.
|
|
3399
4054
|
*/
|
|
3400
|
-
function emitCouncilStateEvent(
|
|
4055
|
+
function emitCouncilStateEvent(manifest, newState, priorState) {
|
|
3401
4056
|
const myOwner = resolveOwner();
|
|
3402
4057
|
if (!myOwner)
|
|
3403
4058
|
return;
|
|
3404
|
-
const hb =
|
|
3405
|
-
|
|
3406
|
-
type,
|
|
4059
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
4060
|
+
emitEventV3({
|
|
3407
4061
|
owner: myOwner,
|
|
3408
|
-
session: hb
|
|
4062
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
3409
4063
|
adapter: normalizeAdapter(hb?.platform),
|
|
3410
|
-
|
|
4064
|
+
observation: {
|
|
4065
|
+
event_type: "council.state_changed",
|
|
4066
|
+
council_id: manifest.council_id,
|
|
4067
|
+
...(priorState ? { prior_state: priorState } : {}),
|
|
4068
|
+
new_state: newState,
|
|
4069
|
+
record: manifest,
|
|
4070
|
+
},
|
|
3411
4071
|
});
|
|
3412
4072
|
}
|
|
3413
4073
|
// ──────── council subcommand impls ────────
|
|
@@ -3450,7 +4110,7 @@ function runCouncilCreate(objective, opts) {
|
|
|
3450
4110
|
createdBy = normalizeAgentName(opts.createdBy);
|
|
3451
4111
|
}
|
|
3452
4112
|
else if (myOwner) {
|
|
3453
|
-
const myHb =
|
|
4113
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3454
4114
|
if (myHb?.name) {
|
|
3455
4115
|
createdBy = normalizeAgentName(myHb.name);
|
|
3456
4116
|
}
|
|
@@ -3506,17 +4166,16 @@ function runCouncilCreate(objective, opts) {
|
|
|
3506
4166
|
writeFileSync(resolve(body, "invite.md"), buildInviteMarkdown(manifest), "utf8");
|
|
3507
4167
|
writeManifest(manifest);
|
|
3508
4168
|
if (myOwner) {
|
|
3509
|
-
const myHbForEmit =
|
|
3510
|
-
|
|
3511
|
-
type: "council.open",
|
|
4169
|
+
const myHbForEmit = readCurrentCoordinationRow(myOwner);
|
|
4170
|
+
emitEventV3({
|
|
3512
4171
|
owner: myOwner,
|
|
3513
|
-
session: myHbForEmit
|
|
4172
|
+
session: nativeSessionIdentity(myHbForEmit, myOwner),
|
|
3514
4173
|
adapter: normalizeAdapter(myHbForEmit?.platform),
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
4174
|
+
observation: {
|
|
4175
|
+
event_type: "council.state_changed",
|
|
4176
|
+
council_id: manifest.council_id,
|
|
4177
|
+
new_state: "active",
|
|
4178
|
+
record: manifest,
|
|
3520
4179
|
},
|
|
3521
4180
|
});
|
|
3522
4181
|
}
|
|
@@ -3528,7 +4187,7 @@ function runCouncilCreate(objective, opts) {
|
|
|
3528
4187
|
const skippedMembers = [];
|
|
3529
4188
|
for (const memberName of members) {
|
|
3530
4189
|
const bareName = memberName.replace(/^agent-/, "");
|
|
3531
|
-
if (myOwner &&
|
|
4190
|
+
if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
|
|
3532
4191
|
// Convener is themselves a member; skip the self-ping
|
|
3533
4192
|
continue;
|
|
3534
4193
|
}
|
|
@@ -3585,7 +4244,7 @@ function runCouncilList(opts) {
|
|
|
3585
4244
|
if (opts.mine) {
|
|
3586
4245
|
const myOwner = resolveOwner();
|
|
3587
4246
|
if (myOwner) {
|
|
3588
|
-
const myHb =
|
|
4247
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3589
4248
|
if (myHb?.name)
|
|
3590
4249
|
myName = normalizeAgentName(myHb.name);
|
|
3591
4250
|
}
|
|
@@ -3749,7 +4408,7 @@ function runCouncilClose(id, opts) {
|
|
|
3749
4408
|
closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
3750
4409
|
};
|
|
3751
4410
|
writeManifest(next);
|
|
3752
|
-
emitCouncilStateEvent("
|
|
4411
|
+
emitCouncilStateEvent(next, "closed", manifest.status);
|
|
3753
4412
|
// Build the transcript: every round's contributions in order.
|
|
3754
4413
|
const transcript = buildTranscript(next);
|
|
3755
4414
|
emit.data({
|
|
@@ -3788,7 +4447,7 @@ function runCouncilArchive(id, opts) {
|
|
|
3788
4447
|
// updated status. moveToArchive then physically relocates the artifacts.
|
|
3789
4448
|
writeManifest(next);
|
|
3790
4449
|
moveToArchive(next.council_id);
|
|
3791
|
-
emitCouncilStateEvent("
|
|
4450
|
+
emitCouncilStateEvent(next, "archived", manifest.status);
|
|
3792
4451
|
emit.data({
|
|
3793
4452
|
rows: [
|
|
3794
4453
|
{
|
|
@@ -3842,7 +4501,7 @@ function runCouncilUnarchive(id, opts) {
|
|
|
3842
4501
|
// (allows re-running for testing).
|
|
3843
4502
|
moveFromArchive(next.council_id);
|
|
3844
4503
|
writeManifest(next);
|
|
3845
|
-
emitCouncilStateEvent(
|
|
4504
|
+
emitCouncilStateEvent(next, restoredStatus, "archived");
|
|
3846
4505
|
emit.data({
|
|
3847
4506
|
rows: [
|
|
3848
4507
|
{
|
|
@@ -3895,7 +4554,7 @@ function runCouncilDelete(id, opts) {
|
|
|
3895
4554
|
}
|
|
3896
4555
|
const removed = deleteArchivedCouncil(manifest.council_id);
|
|
3897
4556
|
if (removed) {
|
|
3898
|
-
emitCouncilStateEvent("
|
|
4557
|
+
emitCouncilStateEvent(manifest, "deleted", "archived");
|
|
3899
4558
|
}
|
|
3900
4559
|
emit.data({
|
|
3901
4560
|
rows: [
|
|
@@ -4067,7 +4726,7 @@ function runCouncilContribute(id, opts) {
|
|
|
4067
4726
|
try {
|
|
4068
4727
|
const myOwner = resolveOwner();
|
|
4069
4728
|
if (myOwner) {
|
|
4070
|
-
const myHb =
|
|
4729
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4071
4730
|
if (myHb?.name)
|
|
4072
4731
|
actualName = normalizeAgentName(myHb.name);
|
|
4073
4732
|
}
|
|
@@ -4085,7 +4744,7 @@ function runCouncilContribute(id, opts) {
|
|
|
4085
4744
|
});
|
|
4086
4745
|
process.exit(1);
|
|
4087
4746
|
}
|
|
4088
|
-
const myHb =
|
|
4747
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4089
4748
|
if (!myHb?.name) {
|
|
4090
4749
|
emit.error({
|
|
4091
4750
|
code: "no_self_name",
|
|
@@ -4136,11 +4795,7 @@ function runCouncilContribute(id, opts) {
|
|
|
4136
4795
|
process.exit(1);
|
|
4137
4796
|
}
|
|
4138
4797
|
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
|
-
});
|
|
4798
|
+
emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
|
|
4144
4799
|
// Update manifest: if all members have now contributed, flip round_status.
|
|
4145
4800
|
const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
|
|
4146
4801
|
const allIn = manifest.members.every((m) => contributorsNow.includes(m));
|
|
@@ -4149,14 +4804,10 @@ function runCouncilContribute(id, opts) {
|
|
|
4149
4804
|
if (allIn) {
|
|
4150
4805
|
nextManifest = { ...manifest, round_status: "collected" };
|
|
4151
4806
|
writeManifest(nextManifest);
|
|
4152
|
-
emitCouncilStateEvent("
|
|
4153
|
-
round_no: nextManifest.current_round,
|
|
4154
|
-
});
|
|
4807
|
+
emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
|
|
4155
4808
|
if (manifest.auto_advance) {
|
|
4156
4809
|
nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
|
|
4157
|
-
emitCouncilStateEvent("
|
|
4158
|
-
round_no: nextManifest.current_round,
|
|
4159
|
-
});
|
|
4810
|
+
emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
|
|
4160
4811
|
autoAdvanced = true;
|
|
4161
4812
|
}
|
|
4162
4813
|
}
|
|
@@ -4241,7 +4892,7 @@ function runCouncilPrompt(id, member, opts) {
|
|
|
4241
4892
|
try {
|
|
4242
4893
|
const myOwner = resolveOwner();
|
|
4243
4894
|
if (myOwner) {
|
|
4244
|
-
const myHb =
|
|
4895
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4245
4896
|
if (myHb?.name)
|
|
4246
4897
|
actualName = normalizeAgentName(myHb.name);
|
|
4247
4898
|
}
|
|
@@ -4259,7 +4910,7 @@ function runCouncilPrompt(id, member, opts) {
|
|
|
4259
4910
|
});
|
|
4260
4911
|
process.exit(1);
|
|
4261
4912
|
}
|
|
4262
|
-
const myHb =
|
|
4913
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4263
4914
|
if (!myHb?.name) {
|
|
4264
4915
|
emit.error({
|
|
4265
4916
|
code: "no_self_name",
|
|
@@ -4410,12 +5061,8 @@ function runCouncilAdvance(id, opts) {
|
|
|
4410
5061
|
process.exit(1);
|
|
4411
5062
|
}
|
|
4412
5063
|
const next = advanceCouncil(manifest, !!opts.force);
|
|
4413
|
-
emitCouncilStateEvent("
|
|
4414
|
-
|
|
4415
|
-
});
|
|
4416
|
-
emitCouncilStateEvent("council.round_open", next, {
|
|
4417
|
-
round_no: next.current_round,
|
|
4418
|
-
});
|
|
5064
|
+
emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
|
|
5065
|
+
emitCouncilStateEvent(next, "round_open", "round_closed");
|
|
4419
5066
|
emit.data({
|
|
4420
5067
|
rows: [
|
|
4421
5068
|
{
|
|
@@ -4457,7 +5104,7 @@ function advanceCouncil(manifest, force) {
|
|
|
4457
5104
|
// (Convener already knows; we skip pinging them if they convened it from
|
|
4458
5105
|
// their own session.)
|
|
4459
5106
|
const myOwner = resolveOwner();
|
|
4460
|
-
const myName = myOwner ? normalizeAgentName(
|
|
5107
|
+
const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
|
|
4461
5108
|
for (const memberName of next.members) {
|
|
4462
5109
|
if (memberName === myName)
|
|
4463
5110
|
continue;
|
|
@@ -4478,7 +5125,7 @@ function advanceCouncil(manifest, force) {
|
|
|
4478
5125
|
// Hard cap on box width. Values longer than the per-row budget word-wrap to
|
|
4479
5126
|
// continuation lines (blank key column, value resumes indented). Picked to
|
|
4480
5127
|
// stay readable in narrow terminals + chat clients while giving long
|
|
4481
|
-
//
|
|
5128
|
+
// Task values need room to breathe.
|
|
4482
5129
|
const MAX_BOX_CONTENT_WIDTH = 100;
|
|
4483
5130
|
function formatBox(title, rows) {
|
|
4484
5131
|
const titleStr = ` ${title} `;
|