harnery 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -1
- 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 +1408 -565
- 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 +2 -2
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +2 -2
- 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.d.ts +4 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +55 -7
- 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 +10 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +233 -51
- package/dist/commands/instructions.d.ts +4 -0
- package/dist/commands/instructions.d.ts.map +1 -0
- package/dist/commands/instructions.js +100 -0
- 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 +15 -6
- 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 +27 -25
- package/dist/core/agents/cli.js +212 -235
- 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 -70
- 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 +5 -5
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +5 -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 +10 -4
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +61 -55
- 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 +15 -33
- 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 +29 -46
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +59 -199
- 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 +45 -0
- package/dist/core/agents/state/session-state.d.ts.map +1 -0
- package/dist/core/agents/state/session-state.js +90 -0
- 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 +13 -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 +64 -24
- 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 +345 -400
- package/dist/core/hooks/codex-authorization.d.ts +30 -0
- package/dist/core/hooks/codex-authorization.d.ts.map +1 -0
- package/dist/core/hooks/codex-authorization.js +199 -0
- package/dist/core/hooks/effects/image-capture.d.ts +20 -34
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +212 -221
- package/dist/core/hooks/effects/index.d.ts +1 -9
- package/dist/core/hooks/effects/index.d.ts.map +1 -1
- package/dist/core/hooks/effects/index.js +1 -24
- package/dist/core/hooks/events/input-hash.d.ts +7 -0
- package/dist/core/hooks/events/input-hash.d.ts.map +1 -0
- package/dist/core/hooks/events/input-hash.js +43 -0
- package/dist/core/hooks/index.d.ts +4 -3
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +2 -3
- package/dist/core/hooks/resolve/coord-root.d.ts +1 -1
- package/dist/core/hooks/resolve/coord-root.js +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +16 -15
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +51 -42
- package/dist/core/hooks/resolve/transcript.d.ts +33 -3
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +155 -14
- package/dist/core/hooks/session-name-presence.d.ts +9 -10
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +15 -15
- package/dist/core/hooks/unsafe-cross-shell.d.ts +1 -1
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -1
- package/dist/core/journal/index.d.ts +2 -2
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +9 -22
- package/dist/core/presence/blob.d.ts +8 -3
- package/dist/core/presence/blob.d.ts.map +1 -1
- package/dist/core/presence/blob.js +54 -41
- 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/index.js +15 -1
- 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/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 +33 -0
- package/dist/lib/instructions/bundle.d.ts.map +1 -0
- package/dist/lib/instructions/bundle.js +149 -0
- 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 +27 -6
- 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 +7 -1
- package/src/commands/adapter.ts +2 -2
- package/src/commands/agents.ts +1737 -570
- 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 +7 -5
- 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 +73 -8
- package/src/commands/events.ts +256 -0
- package/src/commands/governor.ts +18 -4
- package/src/commands/init.ts +264 -57
- package/src/commands/instructions.ts +129 -0
- package/src/commands/journal.ts +4 -13
- package/src/commands/ledger-v3.ts +111 -0
- package/src/commands/presence.ts +18 -11
- 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 +39 -28
- package/src/core/agents/cli.ts +242 -263
- package/src/core/agents/codex-archive-v3.ts +138 -0
- package/src/core/agents/coord-client.ts +75 -177
- package/src/core/agents/finalization.ts +120 -38
- package/src/core/agents/index.ts +5 -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 +89 -55
- package/src/core/agents/render/session-context.ts +23 -32
- 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 +98 -242
- package/src/core/agents/state/live-coordination-view.ts +290 -0
- package/src/core/agents/state/session-state.ts +143 -0
- 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 +15 -6
- package/src/core/hooks/adapter/output.ts +7 -2
- package/src/core/hooks/adapter/parse.ts +81 -36
- package/src/core/hooks/adapter/wiring.ts +119 -13
- package/src/core/hooks/cli.ts +398 -422
- package/src/core/hooks/codex-authorization.ts +253 -0
- package/src/core/hooks/effects/image-capture.ts +228 -238
- package/src/core/hooks/effects/index.ts +1 -36
- package/src/core/hooks/events/input-hash.ts +47 -0
- package/src/core/hooks/index.ts +4 -11
- package/src/core/hooks/resolve/coord-root.ts +1 -1
- package/src/core/hooks/resolve/owner.ts +52 -45
- package/src/core/hooks/resolve/transcript.ts +165 -14
- package/src/core/hooks/session-name-presence.ts +14 -15
- package/src/core/hooks/unsafe-cross-shell.ts +1 -1
- package/src/core/journal/index.ts +15 -22
- package/src/core/presence/blob.ts +63 -38
- package/src/core/presence/codec-digest.ts +215 -0
- package/src/core/presence/index.ts +18 -1
- 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/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 +199 -0
- 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 -65
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +0 -1
- package/dist/core/agents/state/heartbeat-projector.js +0 -527
- 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 -312
- 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 -582
- 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 -538
- 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,17 +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 {
|
|
24
|
-
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";
|
|
27
|
+
import { foldSessionState, } from "../core/agents/state/session-state.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";
|
|
25
37
|
import { readRemoteMachines } from "../core/presence/index.js";
|
|
26
38
|
import { registerContextCommand } from "./context.js";
|
|
39
|
+
function readCurrentCoordinationRow(instanceId) {
|
|
40
|
+
const root = monorepoRoot();
|
|
41
|
+
return root ? readLiveCoordinationRow(root, instanceId) : null;
|
|
42
|
+
}
|
|
27
43
|
/** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
|
|
28
44
|
* under V8's ~512MB max string length so a `readFileSync` of the whole file can
|
|
29
45
|
* never throw; covers ample recent history for a diagnostic scan. */
|
|
30
|
-
const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
|
|
31
46
|
import { parsePsChainLine } from "../core/hooks/resolve/anchor.js";
|
|
32
47
|
import { appendEntry, resolveOwnerByName } from "../core/journal/index.js";
|
|
33
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";
|
|
@@ -42,7 +57,7 @@ import { displayName as displayAgentName, ensureIdentity, listIdentities, lookup
|
|
|
42
57
|
function freshnessCutoffSecs() {
|
|
43
58
|
return coordFreshnessSeconds();
|
|
44
59
|
}
|
|
45
|
-
const SUBAGENT_NOTE = "Bash identity is process-level
|
|
60
|
+
const SUBAGENT_NOTE = "Bash identity is process-level; if you're running inside a subagent, " +
|
|
46
61
|
"this resolves to the parent group's name, not the subagent's. A subagent-aware " +
|
|
47
62
|
"bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
|
|
48
63
|
/**
|
|
@@ -101,6 +116,28 @@ function formatPlatformLabel(platform) {
|
|
|
101
116
|
return "Codex";
|
|
102
117
|
return "CC";
|
|
103
118
|
}
|
|
119
|
+
function activityOf(hb) {
|
|
120
|
+
return hb.activity ?? "unknown";
|
|
121
|
+
}
|
|
122
|
+
function taskStateOf(hb) {
|
|
123
|
+
return hb.task_state ?? "active";
|
|
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
|
+
}
|
|
137
|
+
function lifecycleLabel(hb) {
|
|
138
|
+
const state = taskStateOf(hb);
|
|
139
|
+
return state === "blocked" && hb.task_state_reason ? `${state}: ${hb.task_state_reason}` : state;
|
|
140
|
+
}
|
|
104
141
|
let emit;
|
|
105
142
|
export function registerAgentsCommand(program, emitParam, programContext) {
|
|
106
143
|
emit = emitParam;
|
|
@@ -119,7 +156,7 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
119
156
|
.command("list")
|
|
120
157
|
.description("List all active agents (folds kind=transient by default)")
|
|
121
158
|
.option("--all", "Include raw kind=transient rows (no fold)")
|
|
122
|
-
.option("--stale", "Include
|
|
159
|
+
.option("--stale", "Include generations older than the freshness window")
|
|
123
160
|
.option("--json", "JSON output (alias for --format json)")
|
|
124
161
|
.action((opts) => {
|
|
125
162
|
runList(opts);
|
|
@@ -129,7 +166,8 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
129
166
|
.description("End-of-turn status box (name + session age + files held + peer count)")
|
|
130
167
|
.option("--json", "JSON output instead of the box")
|
|
131
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")
|
|
132
|
-
.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. " +
|
|
133
171
|
"Use this when calling from a hook (the hook's process tree may not lead back to Claude Code's session pid). " +
|
|
134
172
|
"The Stop hook payload includes session_id; pass it through.")
|
|
135
173
|
.action((opts) => {
|
|
@@ -142,13 +180,13 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
142
180
|
"is set-task (it suggests a name on the first focus declaration); reach for this to reprint " +
|
|
143
181
|
"or re-suggest after a topic pivot. Read-only.")
|
|
144
182
|
.option("--json", "JSON output instead of the bare name")
|
|
145
|
-
.option("--session-id <id>", "Lookup
|
|
183
|
+
.option("--session-id <id>", "Lookup the V3 generation by session_id directly, bypassing the ppid walk.")
|
|
146
184
|
.action((description, opts) => {
|
|
147
185
|
runSuggestName(description, opts);
|
|
148
186
|
});
|
|
149
187
|
cmd
|
|
150
188
|
.command("watch")
|
|
151
|
-
.description("Stream peer state changes
|
|
189
|
+
.description("Stream peer state changes from the authoritative V3 coordination projection. " +
|
|
152
190
|
"Prints one line per delta: started / ended / activity / file claim / task change.")
|
|
153
191
|
.option("--poll-ms <n>", "Debounce window after a change event", "200")
|
|
154
192
|
.action(async (opts) => {
|
|
@@ -164,14 +202,14 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
164
202
|
});
|
|
165
203
|
cmd
|
|
166
204
|
.command("trace <name>")
|
|
167
|
-
.description("Reconstruct one agent's coordination lifecycle from
|
|
168
|
-
"session.
|
|
205
|
+
.description("Reconstruct one agent's coordination lifecycle from the active canonical event ledger: " +
|
|
206
|
+
"session.started → prompts → turns → tools → observations → session.ended, " +
|
|
169
207
|
"in chronological order. The answer to 'what happened to this agent / why did " +
|
|
170
208
|
"it vanish?' without hand-grepping the stream. Accepts a name (agent-Foo or Foo) " +
|
|
171
209
|
"or an instance_id.")
|
|
172
210
|
.option("--since <window>", "Only events newer than Nh|Nd (default: all)")
|
|
173
211
|
.option("--limit <n>", "Show at most N most-recent events. Default: 200.", "200")
|
|
174
|
-
.option("--all-tools", "Include tool.
|
|
212
|
+
.option("--all-tools", "Include tool.completed + command.* (default: hidden as noise)")
|
|
175
213
|
.option("--json", "JSON envelope output")
|
|
176
214
|
.action((name, opts) => {
|
|
177
215
|
runTrace(name, opts);
|
|
@@ -184,6 +222,49 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
184
222
|
.action((text, opts) => {
|
|
185
223
|
runSetTask(text.join(" "), opts);
|
|
186
224
|
});
|
|
225
|
+
cmd
|
|
226
|
+
.command("lifecycle <state>")
|
|
227
|
+
.description("Declare this session's task lifecycle: active, blocked, or done")
|
|
228
|
+
.option("--reason <text>", "Explain the lifecycle declaration")
|
|
229
|
+
.option("--session-id <id>", "Target the heartbeat with this session_id directly, bypassing the ppid walk.")
|
|
230
|
+
.action((state, opts) => {
|
|
231
|
+
runLifecycle(state, opts);
|
|
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
|
+
});
|
|
187
268
|
cmd
|
|
188
269
|
.command("release-claim <path>")
|
|
189
270
|
.description("Drop a file claim from your heartbeat. Operator escape hatch when a " +
|
|
@@ -250,15 +331,13 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
250
331
|
cmd
|
|
251
332
|
.command("heal")
|
|
252
333
|
.description("Force a coord-layer recovery action on a specific agent. " +
|
|
253
|
-
"Kinds: pidmap (
|
|
254
|
-
"
|
|
255
|
-
"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).")
|
|
256
336
|
.requiredOption("--owner <id>", "Target agent's instance_id")
|
|
257
|
-
.requiredOption("--kind <kind>", "pidmap |
|
|
258
|
-
.option("--session-id <id>", "(--kind
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"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.")
|
|
262
341
|
.option("--pid <pid>", "(--kind pidmap) PID to register in pid-map. Default: walk " +
|
|
263
342
|
"this shell's ppid chain for a claude process. Pass explicitly " +
|
|
264
343
|
"when calling from outside Claude Code's Bash tool tree (e.g. " +
|
|
@@ -269,6 +348,162 @@ export function registerAgentsCommand(program, emitParam, programContext) {
|
|
|
269
348
|
});
|
|
270
349
|
registerIdentityCommands(cmd);
|
|
271
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
|
+
}
|
|
272
507
|
function registerIdentityCommands(parent) {
|
|
273
508
|
const identity = parent
|
|
274
509
|
.command("identity")
|
|
@@ -486,7 +721,7 @@ export function registerCouncilCommands(parent) {
|
|
|
486
721
|
"archive it first (trash-can pattern). Without --yes this prints " +
|
|
487
722
|
"the paths that would be removed and exits 0 without touching " +
|
|
488
723
|
"anything. Does NOT touch target_doc, close_handoff_path, or " +
|
|
489
|
-
"
|
|
724
|
+
"the canonical V3 event ledger, which is owned independently.")
|
|
490
725
|
.option("-y, --yes", "Required to actually delete; without this, dry-run")
|
|
491
726
|
.option("--json", "JSON envelope output")
|
|
492
727
|
.action((id, opts) => {
|
|
@@ -577,11 +812,11 @@ function runWhoami(opts) {
|
|
|
577
812
|
});
|
|
578
813
|
process.exit(1);
|
|
579
814
|
}
|
|
580
|
-
const hb =
|
|
815
|
+
const hb = readLiveCoordinationRow(root, myOwner);
|
|
581
816
|
if (!hb) {
|
|
582
817
|
emit.error({
|
|
583
|
-
code: "
|
|
584
|
-
message:
|
|
818
|
+
code: "no_live_generation",
|
|
819
|
+
message: noLiveGenerationMessage(myOwner),
|
|
585
820
|
});
|
|
586
821
|
process.exit(1);
|
|
587
822
|
}
|
|
@@ -592,14 +827,17 @@ function runWhoami(opts) {
|
|
|
592
827
|
session_id: hb.session_id,
|
|
593
828
|
kind: normalizeKind(hb.kind),
|
|
594
829
|
relation: "self",
|
|
595
|
-
started_at: hb.started_at,
|
|
830
|
+
started_at: hb.started_at ?? hb.last_heartbeat,
|
|
596
831
|
last_heartbeat: hb.last_heartbeat,
|
|
597
832
|
files_touched: hb.files_touched ?? [],
|
|
598
|
-
last_tool: hb.last_tool ?? null,
|
|
599
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
600
833
|
task: hb.task ?? null,
|
|
601
|
-
|
|
602
|
-
|
|
834
|
+
activity: activityOf(hb),
|
|
835
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
836
|
+
activity_source: hb.activity_source ?? null,
|
|
837
|
+
task_state: taskStateOf(hb),
|
|
838
|
+
task_state_scope: "current",
|
|
839
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
840
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
603
841
|
platform: hb.platform ?? "claude-code",
|
|
604
842
|
};
|
|
605
843
|
emit.data({ ...row, resolution_source: resolved.source, note: SUBAGENT_NOTE });
|
|
@@ -619,37 +857,17 @@ function runList(opts) {
|
|
|
619
857
|
});
|
|
620
858
|
process.exit(1);
|
|
621
859
|
}
|
|
622
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
623
|
-
if (!existsSync(activeDir)) {
|
|
624
|
-
emit.data({ rows: [], note: SUBAGENT_NOTE });
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
860
|
// Resolve self for relation column; best-effort, missing → "unknown" on every row.
|
|
628
861
|
const myOwner = resolveOwner();
|
|
629
|
-
const myHb = myOwner ?
|
|
862
|
+
const myHb = myOwner ? readLiveCoordinationRow(root, myOwner) : null;
|
|
630
863
|
const mySession = myHb?.session_id ?? null;
|
|
631
|
-
|
|
632
|
-
const heartbeats = [];
|
|
633
|
-
for (const file of readdirSync(activeDir)) {
|
|
634
|
-
if (!file.endsWith(".json"))
|
|
635
|
-
continue;
|
|
636
|
-
try {
|
|
637
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
638
|
-
const parsed = JSON.parse(raw);
|
|
639
|
-
if (parsed && typeof parsed.instance_id === "string") {
|
|
640
|
-
heartbeats.push(parsed);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
catch {
|
|
644
|
-
// skip malformed
|
|
645
|
-
}
|
|
646
|
-
}
|
|
864
|
+
const generations = readLiveCoordinationRows(root);
|
|
647
865
|
// Apply staleness filter unless --stale.
|
|
648
866
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
649
867
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
650
868
|
const live = opts.stale
|
|
651
|
-
?
|
|
652
|
-
:
|
|
869
|
+
? generations
|
|
870
|
+
: generations.filter((h) => {
|
|
653
871
|
const ts = Date.parse(h.last_heartbeat);
|
|
654
872
|
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
655
873
|
});
|
|
@@ -681,14 +899,17 @@ function runList(opts) {
|
|
|
681
899
|
session_id: h.session_id,
|
|
682
900
|
kind: "transient",
|
|
683
901
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
684
|
-
started_at: h.started_at,
|
|
902
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
685
903
|
last_heartbeat: h.last_heartbeat,
|
|
686
904
|
files_touched: [...(h.files_touched ?? [])].sort(),
|
|
687
|
-
last_tool: h.last_tool ?? null,
|
|
688
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
689
905
|
task: h.task ?? null,
|
|
690
|
-
|
|
691
|
-
|
|
906
|
+
activity: activityOf(h),
|
|
907
|
+
activity_updated_at: h.activity_updated_at ?? null,
|
|
908
|
+
activity_source: h.activity_source ?? null,
|
|
909
|
+
task_state: taskStateOf(h),
|
|
910
|
+
task_state_scope: "current",
|
|
911
|
+
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
912
|
+
task_state_reason: h.task_state_reason ?? null,
|
|
692
913
|
platform: h.platform ?? "claude-code",
|
|
693
914
|
});
|
|
694
915
|
continue;
|
|
@@ -706,24 +927,26 @@ function runList(opts) {
|
|
|
706
927
|
session_id: h.session_id,
|
|
707
928
|
kind: normalizeKind(h.kind),
|
|
708
929
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
709
|
-
started_at: h.started_at,
|
|
930
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
710
931
|
last_heartbeat: h.last_heartbeat,
|
|
711
932
|
files_touched: files,
|
|
712
|
-
last_tool: h.last_tool ?? null,
|
|
713
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
714
933
|
task: h.task ?? null,
|
|
715
|
-
|
|
716
|
-
|
|
934
|
+
activity: activityOf(h),
|
|
935
|
+
activity_updated_at: h.activity_updated_at ?? null,
|
|
936
|
+
activity_source: h.activity_source ?? null,
|
|
937
|
+
task_state: taskStateOf(h),
|
|
938
|
+
task_state_scope: "current",
|
|
939
|
+
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
940
|
+
task_state_reason: h.task_state_reason ?? null,
|
|
717
941
|
platform: h.platform ?? "claude-code",
|
|
718
942
|
});
|
|
719
943
|
}
|
|
720
|
-
// Guard missing started_at
|
|
721
|
-
//
|
|
722
|
-
// 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.
|
|
723
946
|
rows.sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? ""));
|
|
724
947
|
// Cross-machine presence (ADR 0016): append sessions on OTHER machines from
|
|
725
948
|
// the locally-fetched presence refs. Advisory rows (relation=remote) — they
|
|
726
|
-
//
|
|
949
|
+
// never participate in local claim blocking.
|
|
727
950
|
for (const rm of readRemoteMachines(root)) {
|
|
728
951
|
for (const a of rm.agents) {
|
|
729
952
|
rows.push({
|
|
@@ -735,11 +958,14 @@ function runList(opts) {
|
|
|
735
958
|
started_at: a.started_at ?? "",
|
|
736
959
|
last_heartbeat: a.last_heartbeat ?? "",
|
|
737
960
|
files_touched: [...(a.files_touched ?? [])].sort(),
|
|
738
|
-
last_tool: a.last_tool ?? null,
|
|
739
|
-
last_tool_target: null,
|
|
740
961
|
task: a.task ?? null,
|
|
741
|
-
|
|
742
|
-
|
|
962
|
+
activity: a.activity ?? "unknown",
|
|
963
|
+
activity_updated_at: null,
|
|
964
|
+
activity_source: null,
|
|
965
|
+
task_state: a.task_state ?? "active",
|
|
966
|
+
task_state_scope: "current",
|
|
967
|
+
task_state_updated_at: null,
|
|
968
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
743
969
|
platform: a.platform ?? "claude-code",
|
|
744
970
|
machine: rm.machine,
|
|
745
971
|
});
|
|
@@ -766,21 +992,17 @@ function normalizeKind(kind) {
|
|
|
766
992
|
return kind;
|
|
767
993
|
}
|
|
768
994
|
/**
|
|
769
|
-
* The `
|
|
995
|
+
* The `no_live_generation` diagnostic, quoting the owner id in full.
|
|
770
996
|
*
|
|
771
|
-
* This id is actionable: the reader
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
* here reads as complete, so it gets copy-pasted into `--owner` and mints a
|
|
775
|
-
* heartbeat at a filename no reader resolves — an orphan that leaves the
|
|
776
|
-
* session looking unhealable. Abbreviate ids for display elsewhere, never in
|
|
777
|
-
* 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.
|
|
778
1000
|
*/
|
|
779
|
-
function
|
|
780
|
-
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`;
|
|
781
1003
|
}
|
|
782
1004
|
/**
|
|
783
|
-
* The instance_id of a live
|
|
1005
|
+
* The instance_id of a live V3 generation that `prefix` strictly prefixes, or null.
|
|
784
1006
|
*
|
|
785
1007
|
* Used to recognize an abbreviated id handed back by a reader when the caller
|
|
786
1008
|
* supplied no canonical id of their own. Ambiguity is treated as "no answer":
|
|
@@ -788,14 +1010,9 @@ function noHeartbeatMessage(owner) {
|
|
|
788
1010
|
* and refusing with the wrong id in the message would be worse than the orphan.
|
|
789
1011
|
*/
|
|
790
1012
|
function liveIdWithPrefix(root, prefix) {
|
|
791
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
792
|
-
if (!existsSync(activeDir))
|
|
793
|
-
return null;
|
|
794
1013
|
const matches = new Set();
|
|
795
|
-
for (const
|
|
796
|
-
|
|
797
|
-
continue;
|
|
798
|
-
const id = file.slice(0, -".json".length);
|
|
1014
|
+
for (const row of readLiveCoordinationRows(root)) {
|
|
1015
|
+
const id = row.instance_id;
|
|
799
1016
|
if (id !== prefix && id.startsWith(prefix))
|
|
800
1017
|
matches.add(id);
|
|
801
1018
|
}
|
|
@@ -810,101 +1027,68 @@ async function runWatch(pollMs) {
|
|
|
810
1027
|
});
|
|
811
1028
|
process.exit(1);
|
|
812
1029
|
}
|
|
813
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
814
|
-
if (!existsSync(activeDir)) {
|
|
815
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
816
|
-
process.exit(1);
|
|
817
|
-
}
|
|
818
|
-
const fs = await import("node:fs");
|
|
819
1030
|
const cache = new Map();
|
|
820
1031
|
// Seed cache + print an initial roster line per live peer.
|
|
821
|
-
const initial =
|
|
822
|
-
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
|
|
823
1034
|
for (const h of initial) {
|
|
824
1035
|
cache.set(h.instance_id, h);
|
|
825
|
-
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}"` : ""})`);
|
|
826
1037
|
}
|
|
827
|
-
let scheduled = null;
|
|
828
1038
|
const rescan = () => {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
for (const [id, old] of cache) {
|
|
838
|
-
if (!current.has(id)) {
|
|
839
|
-
emitWatchLine(`agent-${old.name ?? "?"} ended`);
|
|
840
|
-
cache.delete(id);
|
|
841
|
-
}
|
|
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);
|
|
842
1047
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
// Diff fields we care about.
|
|
852
|
-
if ((prev.task ?? "") !== (h.task ?? "")) {
|
|
853
|
-
emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
|
|
854
|
-
}
|
|
855
|
-
if ((prev.last_tool ?? "") !== (h.last_tool ?? "") ||
|
|
856
|
-
(prev.last_tool_target ?? "") !== (h.last_tool_target ?? "")) {
|
|
857
|
-
if (h.last_tool) {
|
|
858
|
-
const target = h.last_tool_target ? ` ${truncate(h.last_tool_target, 80)}` : "";
|
|
859
|
-
emitWatchLine(`agent-${h.name ?? "?"} ${h.last_tool}${target}`);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
// File additions/removals.
|
|
863
|
-
const prevFiles = new Set(prev.files_touched ?? []);
|
|
864
|
-
const currFiles = new Set(h.files_touched ?? []);
|
|
865
|
-
for (const f of currFiles) {
|
|
866
|
-
if (!prevFiles.has(f))
|
|
867
|
-
emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
|
|
868
|
-
}
|
|
869
|
-
for (const f of prevFiles) {
|
|
870
|
-
if (!currFiles.has(f))
|
|
871
|
-
emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
|
|
872
|
-
}
|
|
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}"` : ""})`);
|
|
873
1054
|
cache.set(id, h);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
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}`);
|
|
874
1071
|
}
|
|
875
|
-
|
|
1072
|
+
cache.set(id, h);
|
|
1073
|
+
}
|
|
876
1074
|
};
|
|
877
|
-
const watcher = fs.watch(activeDir, { persistent: true }, () => rescan());
|
|
878
1075
|
await new Promise((resolveP) => {
|
|
1076
|
+
const timer = setInterval(rescan, Math.max(100, pollMs));
|
|
879
1077
|
const stop = () => {
|
|
880
|
-
|
|
1078
|
+
clearInterval(timer);
|
|
881
1079
|
resolveP();
|
|
882
1080
|
};
|
|
883
1081
|
process.on("SIGINT", stop);
|
|
884
1082
|
process.on("SIGTERM", stop);
|
|
885
1083
|
});
|
|
886
1084
|
}
|
|
887
|
-
function
|
|
888
|
-
const out = [];
|
|
1085
|
+
function listLiveGenerations(root) {
|
|
889
1086
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
890
1087
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
896
|
-
const parsed = JSON.parse(raw);
|
|
897
|
-
if (!parsed || typeof parsed.instance_id !== "string")
|
|
898
|
-
continue;
|
|
899
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
900
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff)
|
|
901
|
-
out.push(parsed);
|
|
902
|
-
}
|
|
903
|
-
catch {
|
|
904
|
-
// skip
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
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
|
+
});
|
|
908
1092
|
}
|
|
909
1093
|
function secondsSince(iso) {
|
|
910
1094
|
const t = Date.parse(iso);
|
|
@@ -923,33 +1107,15 @@ async function runShow(name, opts) {
|
|
|
923
1107
|
});
|
|
924
1108
|
process.exit(1);
|
|
925
1109
|
}
|
|
926
|
-
|
|
927
|
-
if (!existsSync(activeDir)) {
|
|
928
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
929
|
-
process.exit(1);
|
|
930
|
-
}
|
|
931
|
-
// Read all heartbeats; match by name (case-insensitive). Apply freshness filter.
|
|
932
|
-
const matches = [];
|
|
1110
|
+
// Match authority-safe V3 generations by resolved display name.
|
|
933
1111
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
934
1112
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
935
|
-
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (!parsed || typeof parsed.instance_id !== "string")
|
|
942
|
-
continue;
|
|
943
|
-
if ((parsed.name ?? "").toLowerCase() !== name.toLowerCase())
|
|
944
|
-
continue;
|
|
945
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
946
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff)
|
|
947
|
-
matches.push(parsed);
|
|
948
|
-
}
|
|
949
|
-
catch {
|
|
950
|
-
// skip malformed
|
|
951
|
-
}
|
|
952
|
-
}
|
|
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
|
+
});
|
|
953
1119
|
if (matches.length === 0) {
|
|
954
1120
|
emit.error({
|
|
955
1121
|
code: "no_match",
|
|
@@ -967,7 +1133,7 @@ async function runShow(name, opts) {
|
|
|
967
1133
|
const hb = matches[0];
|
|
968
1134
|
const report = null;
|
|
969
1135
|
const bqError = null;
|
|
970
|
-
const startedAtMs = Date.parse(hb.started_at);
|
|
1136
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
971
1137
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
972
1138
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
973
1139
|
: 0;
|
|
@@ -983,12 +1149,15 @@ async function runShow(name, opts) {
|
|
|
983
1149
|
age_secs: ageSecs,
|
|
984
1150
|
last_heartbeat_secs_ago: heartbeatAgeSecs,
|
|
985
1151
|
task: hb.task ?? null,
|
|
986
|
-
|
|
987
|
-
|
|
1152
|
+
activity: activityOf(hb),
|
|
1153
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1154
|
+
activity_source: hb.activity_source ?? null,
|
|
1155
|
+
task_state: taskStateOf(hb),
|
|
1156
|
+
task_state_scope: "current",
|
|
1157
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1158
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
988
1159
|
title: report?.title ?? null,
|
|
989
1160
|
files_held: hb.files_touched ?? [],
|
|
990
|
-
last_tool: hb.last_tool ?? null,
|
|
991
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
992
1161
|
recent_prompts: report?.recent_prompts ?? [],
|
|
993
1162
|
recent_tools: report?.recent_tools ?? [],
|
|
994
1163
|
tool_counts: report?.tool_counts ?? [],
|
|
@@ -1010,10 +1179,8 @@ async function runShow(name, opts) {
|
|
|
1010
1179
|
lines.push(`agent-${data.name} ${subtitle}`);
|
|
1011
1180
|
lines.push(` session ${formatAge(ageSecs)} old · kind=${data.kind} · session_id=${data.session_id.slice(0, 8)}…`);
|
|
1012
1181
|
lines.push(` last heartbeat: ${formatAge(heartbeatAgeSecs)} ago`);
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
lines.push(` last activity: ${data.last_tool}${target}`);
|
|
1016
|
-
}
|
|
1182
|
+
lines.push(` activity: ${data.activity}`);
|
|
1183
|
+
lines.push(` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`);
|
|
1017
1184
|
if (data.files_held.length > 0) {
|
|
1018
1185
|
lines.push(` holds ${data.files_held.length} file(s):`);
|
|
1019
1186
|
for (const f of data.files_held.slice(0, 10))
|
|
@@ -1166,13 +1333,19 @@ function runSetTask(task, opts) {
|
|
|
1166
1333
|
// Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
|
|
1167
1334
|
// mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
|
|
1168
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.
|
|
1169
1342
|
if (!opts?.sessionId)
|
|
1170
1343
|
ensureCursorSession(root);
|
|
1171
|
-
const myOwner = opts?.sessionId ?? resolveOwner();
|
|
1344
|
+
const myOwner = opts?.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1172
1345
|
if (!myOwner) {
|
|
1173
1346
|
emit.error({
|
|
1174
1347
|
code: "no_pidmap_entry",
|
|
1175
|
-
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)",
|
|
1176
1349
|
});
|
|
1177
1350
|
process.exit(1);
|
|
1178
1351
|
}
|
|
@@ -1182,7 +1355,11 @@ function runSetTask(task, opts) {
|
|
|
1182
1355
|
// naming window, and subagent/workflow kinds are never named — see
|
|
1183
1356
|
// heartbeat-writer.setTask. This call is the naming call exactly when the
|
|
1184
1357
|
// stamp appears across the mutation.
|
|
1185
|
-
const priorHb =
|
|
1358
|
+
const priorHb = readCurrentCoordinationRow(myOwner);
|
|
1359
|
+
const normalizedTask = task.length > 0 ? task : undefined;
|
|
1360
|
+
const lifecycleWarning = priorHb && (priorHb.task_state ?? "active") !== "active" && priorHb.task !== normalizedTask
|
|
1361
|
+
? `Task text changed while lifecycle is ${priorHb.task_state}; run \`${resolveBinName(root)} agents lifecycle active\` to reopen it explicitly.`
|
|
1362
|
+
: null;
|
|
1186
1363
|
// Heartbeat mutation goes through agent-coord (atomic temp+rename).
|
|
1187
1364
|
const helper = agentCoordOrExit(root);
|
|
1188
1365
|
const result = spawnSync(helper, ["set-task", myOwner, task], {
|
|
@@ -1196,7 +1373,7 @@ function runSetTask(task, opts) {
|
|
|
1196
1373
|
});
|
|
1197
1374
|
process.exit(1);
|
|
1198
1375
|
}
|
|
1199
|
-
const hb =
|
|
1376
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1200
1377
|
// The naming call: the mutation just stamped `suggested_session_name` for
|
|
1201
1378
|
// the first time. `first_of_session: true` means exactly "this call produced
|
|
1202
1379
|
// the session name" — it is never true with a null name (an empty first
|
|
@@ -1205,18 +1382,6 @@ function runSetTask(task, opts) {
|
|
|
1205
1382
|
? hb.suggested_session_name
|
|
1206
1383
|
: null;
|
|
1207
1384
|
const firstOfSession = suggestedName !== null;
|
|
1208
|
-
emitCanonical({
|
|
1209
|
-
type: "state.task_set",
|
|
1210
|
-
owner: myOwner,
|
|
1211
|
-
session: hb?.session_id ?? myOwner,
|
|
1212
|
-
adapter: normalizeAdapter(hb?.platform),
|
|
1213
|
-
data: {
|
|
1214
|
-
task,
|
|
1215
|
-
cleared: !task || task.length === 0,
|
|
1216
|
-
first_of_session: firstOfSession,
|
|
1217
|
-
suggested_session_name: suggestedName,
|
|
1218
|
-
},
|
|
1219
|
-
});
|
|
1220
1385
|
emit.data({
|
|
1221
1386
|
instance_id: myOwner,
|
|
1222
1387
|
name: hb?.name ?? null,
|
|
@@ -1224,11 +1389,185 @@ function runSetTask(task, opts) {
|
|
|
1224
1389
|
cleared: !task || task.length === 0,
|
|
1225
1390
|
first_of_session: firstOfSession,
|
|
1226
1391
|
suggested_session_name: suggestedName,
|
|
1392
|
+
...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
|
|
1227
1393
|
// Right-time instruction: the UserPromptSubmit nudge fires before the name
|
|
1228
1394
|
// exists; this result is the moment the model holds the string.
|
|
1229
1395
|
...(suggestedName
|
|
1230
1396
|
? {
|
|
1231
|
-
note:
|
|
1397
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1398
|
+
}
|
|
1399
|
+
: {}),
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
function runLifecycle(rawState, opts) {
|
|
1403
|
+
const state = rawState.trim().toLowerCase();
|
|
1404
|
+
if (state !== "active" && state !== "blocked" && state !== "done") {
|
|
1405
|
+
emit.error({
|
|
1406
|
+
code: "invalid_lifecycle_state",
|
|
1407
|
+
message: "lifecycle state must be one of: active, blocked, done",
|
|
1408
|
+
});
|
|
1409
|
+
process.exitCode = 1;
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const root = monorepoRoot();
|
|
1413
|
+
if (!root) {
|
|
1414
|
+
emit.error({
|
|
1415
|
+
code: "not_in_repo",
|
|
1416
|
+
message: "not in an agent session; coord_root() returned null",
|
|
1417
|
+
});
|
|
1418
|
+
process.exitCode = 1;
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (!opts.sessionId)
|
|
1422
|
+
ensureCursorSession(root);
|
|
1423
|
+
const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1424
|
+
if (!myOwner) {
|
|
1425
|
+
emit.error({
|
|
1426
|
+
code: "no_pidmap_entry",
|
|
1427
|
+
message: "not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1428
|
+
});
|
|
1429
|
+
process.exitCode = 1;
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
let hb = readCurrentCoordinationRow(myOwner);
|
|
1433
|
+
let reopenedGeneration;
|
|
1434
|
+
if (!hb) {
|
|
1435
|
+
if (state !== "active") {
|
|
1436
|
+
emit.error({
|
|
1437
|
+
code: "no_live_generation",
|
|
1438
|
+
message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
|
|
1439
|
+
});
|
|
1440
|
+
process.exitCode = 1;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
reopenedGeneration = reopenLiveCoordinationGenerationV3({
|
|
1445
|
+
coordRoot: root,
|
|
1446
|
+
owner: myOwner,
|
|
1447
|
+
nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
catch (error) {
|
|
1451
|
+
emit.error({
|
|
1452
|
+
code: "lifecycle_reopen_failed",
|
|
1453
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1454
|
+
});
|
|
1455
|
+
process.exitCode = 1;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
hb = readCurrentCoordinationRow(myOwner);
|
|
1459
|
+
if (!hb) {
|
|
1460
|
+
emit.error({
|
|
1461
|
+
code: "lifecycle_reopen_failed",
|
|
1462
|
+
message: "the fresh generation was recorded but did not project as live",
|
|
1463
|
+
});
|
|
1464
|
+
process.exitCode = 1;
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
|
|
1469
|
+
emit.error({
|
|
1470
|
+
code: "lifecycle_not_human_facing",
|
|
1471
|
+
message: "task lifecycle declarations are limited to human-facing sessions",
|
|
1472
|
+
});
|
|
1473
|
+
process.exitCode = 1;
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const reason = opts.reason?.trim() || undefined;
|
|
1477
|
+
if (state === "blocked" && !reason) {
|
|
1478
|
+
emit.error({
|
|
1479
|
+
code: "blocked_reason_required",
|
|
1480
|
+
message: "lifecycle blocked requires --reason <text>",
|
|
1481
|
+
});
|
|
1482
|
+
process.exitCode = 1;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (state === "done" && !hb.task) {
|
|
1486
|
+
emit.error({
|
|
1487
|
+
code: "task_required_for_done",
|
|
1488
|
+
message: "declare a current task with agents set-task before marking it done",
|
|
1489
|
+
});
|
|
1490
|
+
process.exitCode = 1;
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const priorState = hb.task_state ?? "active";
|
|
1494
|
+
const priorReason = hb.task_state_reason || undefined;
|
|
1495
|
+
if (reopenedGeneration) {
|
|
1496
|
+
emit.data({
|
|
1497
|
+
instance_id: myOwner,
|
|
1498
|
+
task_state: "active",
|
|
1499
|
+
prior_state: null,
|
|
1500
|
+
reason: null,
|
|
1501
|
+
changed: true,
|
|
1502
|
+
generation_reopened: true,
|
|
1503
|
+
prior_generation_id: reopenedGeneration.prior_generation_id,
|
|
1504
|
+
generation_id: reopenedGeneration.generation_id,
|
|
1505
|
+
name_reminted: false,
|
|
1506
|
+
suggested_session_name: null,
|
|
1507
|
+
git_finalization_checked: false,
|
|
1508
|
+
});
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (priorState === state && priorReason === reason) {
|
|
1512
|
+
emit.data({
|
|
1513
|
+
instance_id: myOwner,
|
|
1514
|
+
task_state: state,
|
|
1515
|
+
reason: reason ?? null,
|
|
1516
|
+
changed: false,
|
|
1517
|
+
name_reminted: false,
|
|
1518
|
+
git_finalization_checked: false,
|
|
1519
|
+
});
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
let finalization = null;
|
|
1523
|
+
if (state === "done") {
|
|
1524
|
+
const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
|
|
1525
|
+
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1526
|
+
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1527
|
+
claimHistoryComplete: history.complete,
|
|
1528
|
+
});
|
|
1529
|
+
if (!finalization.ok) {
|
|
1530
|
+
emit.error({
|
|
1531
|
+
code: "git_not_finalized",
|
|
1532
|
+
message: formatGitFinalizationFailure(finalization, resolveBinName(root)).replace("the status box was not issued", "lifecycle was not changed"),
|
|
1533
|
+
});
|
|
1534
|
+
process.exitCode = 1;
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
|
|
1539
|
+
const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
|
|
1540
|
+
const emitted = emitEventV3({
|
|
1541
|
+
owner: myOwner,
|
|
1542
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1543
|
+
adapter: normalizeAdapter(hb.platform),
|
|
1544
|
+
observation: {
|
|
1545
|
+
event_type: "coord.lifecycle_changed",
|
|
1546
|
+
new_state: state,
|
|
1547
|
+
...(reason ? { reason } : {}),
|
|
1548
|
+
...(nameReminted ? { suggested_session_name: suggestedName } : {}),
|
|
1549
|
+
},
|
|
1550
|
+
});
|
|
1551
|
+
if (!emitted) {
|
|
1552
|
+
emit.error({
|
|
1553
|
+
code: "lifecycle_event_failed",
|
|
1554
|
+
message: "the lifecycle event could not be recorded; heartbeat state was left unchanged",
|
|
1555
|
+
});
|
|
1556
|
+
process.exitCode = 1;
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
emit.data({
|
|
1560
|
+
instance_id: myOwner,
|
|
1561
|
+
task_state: state,
|
|
1562
|
+
prior_state: priorState,
|
|
1563
|
+
reason: reason ?? null,
|
|
1564
|
+
changed: true,
|
|
1565
|
+
name_reminted: nameReminted,
|
|
1566
|
+
suggested_session_name: nameReminted ? suggestedName : null,
|
|
1567
|
+
git_finalization_checked: finalization !== null,
|
|
1568
|
+
...(nameReminted
|
|
1569
|
+
? {
|
|
1570
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1232
1571
|
}
|
|
1233
1572
|
: {}),
|
|
1234
1573
|
});
|
|
@@ -1254,7 +1593,7 @@ function runSuggestName(descriptionParts, opts) {
|
|
|
1254
1593
|
});
|
|
1255
1594
|
process.exit(1);
|
|
1256
1595
|
}
|
|
1257
|
-
const hb =
|
|
1596
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1258
1597
|
const agentName = hb?.name || "unknown";
|
|
1259
1598
|
// Description resolution: an explicit arg wins; with no arg, fall back to the
|
|
1260
1599
|
// agent's current declared task, so a bare `suggest-name` reprints the running
|
|
@@ -1297,6 +1636,14 @@ function runStatus(opts) {
|
|
|
1297
1636
|
});
|
|
1298
1637
|
process.exit(1);
|
|
1299
1638
|
}
|
|
1639
|
+
if (opts.endSession && !opts.endTurn) {
|
|
1640
|
+
emit.error({
|
|
1641
|
+
code: "end_session_requires_end_turn",
|
|
1642
|
+
message: "--end-session requires --end-turn so Git finalization is verified first",
|
|
1643
|
+
});
|
|
1644
|
+
process.exitCode = 1;
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1300
1647
|
// Identity resolution: prefer explicit --session-id (hook-friendly), fall
|
|
1301
1648
|
// back to ppid walk for interactive shell usage.
|
|
1302
1649
|
if (!opts.sessionId)
|
|
@@ -1309,17 +1656,21 @@ function runStatus(opts) {
|
|
|
1309
1656
|
});
|
|
1310
1657
|
process.exit(1);
|
|
1311
1658
|
}
|
|
1312
|
-
const hb =
|
|
1659
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1313
1660
|
if (!hb) {
|
|
1314
1661
|
emit.error({
|
|
1315
|
-
code: "
|
|
1316
|
-
message:
|
|
1662
|
+
code: "no_live_generation",
|
|
1663
|
+
message: noLiveGenerationMessage(myOwner),
|
|
1317
1664
|
});
|
|
1318
1665
|
process.exit(1);
|
|
1319
1666
|
}
|
|
1667
|
+
// Report-only run quality: cheap config/cursor due check first, then a
|
|
1668
|
+
// non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
|
|
1669
|
+
const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
|
|
1670
|
+
const quality = qualityForStatus(qualityEvaluation.config.requested_mode, qualityEvaluation.snapshot);
|
|
1320
1671
|
let finalization = null;
|
|
1321
1672
|
if (opts.endTurn) {
|
|
1322
|
-
const history = readSessionWriteClaims(root, hb.instance_id, hb
|
|
1673
|
+
const history = readSessionWriteClaims(root, hb.instance_id, nativeSessionIdentity(hb, myOwner));
|
|
1323
1674
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1324
1675
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1325
1676
|
claimHistoryComplete: history.complete,
|
|
@@ -1333,51 +1684,55 @@ function runStatus(opts) {
|
|
|
1333
1684
|
return;
|
|
1334
1685
|
}
|
|
1335
1686
|
}
|
|
1336
|
-
|
|
1337
|
-
type: "state.status_checked",
|
|
1687
|
+
emitEventV3({
|
|
1338
1688
|
owner: myOwner,
|
|
1339
|
-
session: hb
|
|
1689
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1340
1690
|
adapter: normalizeAdapter(hb.platform),
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
agent_count: 0, // computed below, not yet available here; Phase 5 verdict reads owner-scope only
|
|
1345
|
-
included_self: true,
|
|
1691
|
+
observation: {
|
|
1692
|
+
event_type: "coord.status_observed",
|
|
1693
|
+
status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
|
|
1346
1694
|
},
|
|
1347
1695
|
});
|
|
1348
|
-
|
|
1696
|
+
let sessionEnd;
|
|
1697
|
+
if (opts.endSession) {
|
|
1698
|
+
const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
|
|
1699
|
+
const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(({ state }) => state.instance_id === canonicalInstanceId);
|
|
1700
|
+
if (records.length !== 1 || !records[0]) {
|
|
1701
|
+
emit.error({
|
|
1702
|
+
code: "session_identity_ambiguous",
|
|
1703
|
+
message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
|
|
1704
|
+
});
|
|
1705
|
+
process.exitCode = 1;
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const state = records[0].state;
|
|
1709
|
+
const requested = requestSessionEndExplicitV3({
|
|
1710
|
+
coordRoot: root,
|
|
1711
|
+
instance_id: state.instance_id,
|
|
1712
|
+
generation_id: state.generation_id,
|
|
1713
|
+
outcome: "succeeded",
|
|
1714
|
+
coordination_finalized: true,
|
|
1715
|
+
});
|
|
1716
|
+
if (requested.state === "queued" || requested.state === "already_requested") {
|
|
1717
|
+
sessionEnd = { state: requested.state, request_id: requested.request.request_id };
|
|
1718
|
+
}
|
|
1719
|
+
else if (requested.state === "recorded") {
|
|
1720
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
|
|
1721
|
+
}
|
|
1722
|
+
else if (requested.state === "already_ended") {
|
|
1723
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
|
|
1724
|
+
}
|
|
1725
|
+
else {
|
|
1726
|
+
emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
|
|
1727
|
+
process.exitCode = 1;
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1349
1732
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1350
1733
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1351
1734
|
: 0;
|
|
1352
|
-
const
|
|
1353
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1354
|
-
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1355
|
-
const livePeers = [];
|
|
1356
|
-
let peersStale = 0;
|
|
1357
|
-
if (existsSync(activeDir)) {
|
|
1358
|
-
for (const file of readdirSync(activeDir)) {
|
|
1359
|
-
if (!file.endsWith(".json"))
|
|
1360
|
-
continue;
|
|
1361
|
-
try {
|
|
1362
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
1363
|
-
const peer = JSON.parse(raw);
|
|
1364
|
-
if (!peer || typeof peer.instance_id !== "string")
|
|
1365
|
-
continue;
|
|
1366
|
-
if (peer.instance_id === myOwner)
|
|
1367
|
-
continue;
|
|
1368
|
-
const ts = Date.parse(peer.last_heartbeat);
|
|
1369
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) {
|
|
1370
|
-
livePeers.push(peer);
|
|
1371
|
-
}
|
|
1372
|
-
else {
|
|
1373
|
-
peersStale++;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
catch {
|
|
1377
|
-
// skip malformed
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1735
|
+
const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
|
|
1381
1736
|
// Sort: file-holders first (by file count desc), then idle peers by recency.
|
|
1382
1737
|
livePeers.sort((a, b) => {
|
|
1383
1738
|
const af = a.files_touched?.length ?? 0;
|
|
@@ -1391,7 +1746,7 @@ function runStatus(opts) {
|
|
|
1391
1746
|
// Cross-machine presence (ADR 0016): sessions on other machines, advisory.
|
|
1392
1747
|
const remoteMachines = readRemoteMachines(root);
|
|
1393
1748
|
const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
|
|
1394
|
-
const ctxUsage = readContextUsage(hb.session_id, hb.platform);
|
|
1749
|
+
const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
|
|
1395
1750
|
let ctxStr;
|
|
1396
1751
|
if (!ctxUsage) {
|
|
1397
1752
|
ctxStr = "unavailable";
|
|
@@ -1422,6 +1777,13 @@ function runStatus(opts) {
|
|
|
1422
1777
|
instance_id: hb.instance_id,
|
|
1423
1778
|
kind: normalizeKind(hb.kind),
|
|
1424
1779
|
session_age_secs: ageSecs,
|
|
1780
|
+
activity: activityOf(hb),
|
|
1781
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1782
|
+
activity_source: hb.activity_source ?? null,
|
|
1783
|
+
task_state: taskStateOf(hb),
|
|
1784
|
+
task_state_scope: "current",
|
|
1785
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1786
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
1425
1787
|
files_held: filesHeld,
|
|
1426
1788
|
peers_live: livePeers.length,
|
|
1427
1789
|
peers_stale: peersStale,
|
|
@@ -1435,6 +1797,9 @@ function runStatus(opts) {
|
|
|
1435
1797
|
agents: m.agents.map((a) => ({
|
|
1436
1798
|
name: a.name || "unnamed",
|
|
1437
1799
|
task: a.task ?? null,
|
|
1800
|
+
activity: a.activity ?? "unknown",
|
|
1801
|
+
task_state: a.task_state ?? "active",
|
|
1802
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
1438
1803
|
files: a.files_touched?.length ?? 0,
|
|
1439
1804
|
})),
|
|
1440
1805
|
})),
|
|
@@ -1443,7 +1808,9 @@ function runStatus(opts) {
|
|
|
1443
1808
|
context_window: ctxUsage?.window ?? null,
|
|
1444
1809
|
timestamp_iso: new Date().toISOString(),
|
|
1445
1810
|
timestamp_local: timeStr,
|
|
1811
|
+
...(quality ? { quality } : {}),
|
|
1446
1812
|
...(finalization ? { finalization } : {}),
|
|
1813
|
+
...(sessionEnd ? { session_end: sessionEnd } : {}),
|
|
1447
1814
|
};
|
|
1448
1815
|
if (opts.json) {
|
|
1449
1816
|
emit.config({ format: "json" });
|
|
@@ -1452,15 +1819,14 @@ function runStatus(opts) {
|
|
|
1452
1819
|
}
|
|
1453
1820
|
const rows = [
|
|
1454
1821
|
["session", formatAge(ageSecs)],
|
|
1822
|
+
["activity", activityOf(hb)],
|
|
1823
|
+
["lifecycle", lifecycleLabel(hb)],
|
|
1455
1824
|
["context", ctxStr],
|
|
1456
1825
|
["files", filesStr],
|
|
1457
1826
|
["peers", peersStr],
|
|
1458
1827
|
["time", timeStr],
|
|
1459
1828
|
];
|
|
1460
|
-
//
|
|
1461
|
-
if (hb.turn_summary && hb.turn_summary.length > 0) {
|
|
1462
|
-
rows.splice(1, 0, ["last turn", hb.turn_summary]);
|
|
1463
|
-
}
|
|
1829
|
+
// Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
|
|
1464
1830
|
if (hb.task && hb.task.length > 0) {
|
|
1465
1831
|
rows.splice(1, 0, ["task", hb.task]);
|
|
1466
1832
|
}
|
|
@@ -1474,10 +1840,49 @@ function runStatus(opts) {
|
|
|
1474
1840
|
: `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
|
|
1475
1841
|
rows.splice(idx, 0, ["council", summary]);
|
|
1476
1842
|
}
|
|
1843
|
+
if (quality) {
|
|
1844
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
1845
|
+
rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
|
|
1846
|
+
}
|
|
1847
|
+
if (sessionEnd) {
|
|
1848
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
1849
|
+
rows.splice(idx, 0, [
|
|
1850
|
+
"session end",
|
|
1851
|
+
sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
|
|
1852
|
+
? "queued after this turn"
|
|
1853
|
+
: "recorded",
|
|
1854
|
+
]);
|
|
1855
|
+
}
|
|
1477
1856
|
// Box rendering needs predictable stdout regardless of TTY/pipe detection:
|
|
1478
1857
|
// agent runs this via Bash (no TTY) and pastes captured stdout into chat.
|
|
1479
1858
|
process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
|
|
1480
1859
|
}
|
|
1860
|
+
function qualityForStatus(mode, snapshot) {
|
|
1861
|
+
if (mode !== "report")
|
|
1862
|
+
return null;
|
|
1863
|
+
const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
|
|
1864
|
+
if (!snapshot || !fresh) {
|
|
1865
|
+
return {
|
|
1866
|
+
status: "unknown",
|
|
1867
|
+
signal_ids: [],
|
|
1868
|
+
evaluated_at: snapshot?.evaluated_at ?? null,
|
|
1869
|
+
fresh: false,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
status: snapshot.status,
|
|
1874
|
+
signal_ids: snapshot.signals
|
|
1875
|
+
.filter((signal) => signal.state === "active" && signal.severity !== "none")
|
|
1876
|
+
.map((signal) => signal.id)
|
|
1877
|
+
.sort()
|
|
1878
|
+
.slice(0, 3),
|
|
1879
|
+
evaluated_at: snapshot.evaluated_at,
|
|
1880
|
+
fresh: true,
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function formatRunQuality(status, signalIds) {
|
|
1884
|
+
return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
|
|
1885
|
+
}
|
|
1481
1886
|
function formatList(items, cap, emptyLabel) {
|
|
1482
1887
|
if (items.length === 0)
|
|
1483
1888
|
return emptyLabel;
|
|
@@ -1518,6 +1923,28 @@ function formatPeers(peers, cap, staleCount, remoteMachines = []) {
|
|
|
1518
1923
|
}
|
|
1519
1924
|
return main;
|
|
1520
1925
|
}
|
|
1926
|
+
export function collectStatusPeerHealth(root, myOwner, nowMs = Date.now()) {
|
|
1927
|
+
let rows = [];
|
|
1928
|
+
try {
|
|
1929
|
+
rows = readLiveCoordinationRows(root);
|
|
1930
|
+
}
|
|
1931
|
+
catch {
|
|
1932
|
+
// V3 authority failures must not resurrect disposable cache rows.
|
|
1933
|
+
}
|
|
1934
|
+
const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
|
|
1935
|
+
const livePeers = [];
|
|
1936
|
+
let stale = 0;
|
|
1937
|
+
for (const peer of rows) {
|
|
1938
|
+
if (peer.instance_id === myOwner)
|
|
1939
|
+
continue;
|
|
1940
|
+
const observedAtMs = Date.parse(peer.last_heartbeat);
|
|
1941
|
+
if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs)
|
|
1942
|
+
livePeers.push(peer);
|
|
1943
|
+
else
|
|
1944
|
+
stale += 1;
|
|
1945
|
+
}
|
|
1946
|
+
return { livePeers, stale };
|
|
1947
|
+
}
|
|
1521
1948
|
function fmtTokens(n) {
|
|
1522
1949
|
if (n < 1000)
|
|
1523
1950
|
return `${n}`;
|
|
@@ -1771,40 +2198,49 @@ function formatAge(secs) {
|
|
|
1771
2198
|
const h = Math.floor((secs % 86400) / 3600);
|
|
1772
2199
|
return `${d}d ${h}h`;
|
|
1773
2200
|
}
|
|
1774
|
-
/**
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
if (!line)
|
|
1793
|
-
continue;
|
|
1794
|
-
try {
|
|
1795
|
-
const ev = JSON.parse(line);
|
|
1796
|
-
if (!ev.event_type || !ev.ts)
|
|
1797
|
-
continue;
|
|
1798
|
-
const tsMs = Date.parse(ev.ts);
|
|
2201
|
+
/** Read validated canonical V3 events. */
|
|
2202
|
+
export function readAgentDiagnosticEventsInWindow(root, cutoffMs) {
|
|
2203
|
+
const control = readEventV3ControlState(root);
|
|
2204
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
2205
|
+
const ledger = readLedgerV3(root);
|
|
2206
|
+
if (!ledger.complete) {
|
|
2207
|
+
return {
|
|
2208
|
+
source: "v3",
|
|
2209
|
+
authoritative: false,
|
|
2210
|
+
reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
|
|
2211
|
+
truncated: false,
|
|
2212
|
+
bytes: ledger.bytes,
|
|
2213
|
+
events: [],
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const events = [];
|
|
2217
|
+
for (const { event } of ledger.events) {
|
|
2218
|
+
const tsMs = Date.parse(event.time.recorded_at);
|
|
1799
2219
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
|
|
1800
2220
|
continue;
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2221
|
+
events.push({
|
|
2222
|
+
event_type: event.event_type,
|
|
2223
|
+
ts: event.time.recorded_at,
|
|
2224
|
+
instance_id: event.scope.instance_id,
|
|
2225
|
+
payload: event.payload,
|
|
2226
|
+
});
|
|
1805
2227
|
}
|
|
2228
|
+
return {
|
|
2229
|
+
source: "v3",
|
|
2230
|
+
authoritative: true,
|
|
2231
|
+
truncated: false,
|
|
2232
|
+
bytes: ledger.bytes,
|
|
2233
|
+
events,
|
|
2234
|
+
};
|
|
1806
2235
|
}
|
|
1807
|
-
return
|
|
2236
|
+
return {
|
|
2237
|
+
source: "v3",
|
|
2238
|
+
authoritative: false,
|
|
2239
|
+
reason: `V3 control state is ${control.state}`,
|
|
2240
|
+
truncated: false,
|
|
2241
|
+
bytes: 0,
|
|
2242
|
+
events: [],
|
|
2243
|
+
};
|
|
1808
2244
|
}
|
|
1809
2245
|
/** Normalize adapter event data into the heartbeat platform value. */
|
|
1810
2246
|
function adapterToPlatform(adapter) {
|
|
@@ -1814,18 +2250,21 @@ function adapterToPlatform(adapter) {
|
|
|
1814
2250
|
return "cursor";
|
|
1815
2251
|
if (adapter === "codex")
|
|
1816
2252
|
return "codex";
|
|
1817
|
-
return "
|
|
2253
|
+
return "unknown";
|
|
1818
2254
|
}
|
|
1819
2255
|
/** Project a canonical health.* event into the HealEvent shape the aggregators
|
|
1820
2256
|
* already consume. Returns null for non-heal events. instance_id → display name
|
|
1821
2257
|
* via `nameById` (full-UUID keyed). */
|
|
1822
2258
|
function canonicalToHealEvent(ev, nameById) {
|
|
1823
|
-
if (ev.event_type !== "health.
|
|
2259
|
+
if (ev.event_type !== "health.observed") {
|
|
1824
2260
|
return null;
|
|
1825
2261
|
}
|
|
1826
|
-
const
|
|
1827
|
-
const
|
|
1828
|
-
|
|
2262
|
+
const data = ev.payload ?? {};
|
|
2263
|
+
const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
|
|
2264
|
+
if (subsystem !== "pidmap" && subsystem !== "heartbeat")
|
|
2265
|
+
return null;
|
|
2266
|
+
const kind = subsystem;
|
|
2267
|
+
const reason = data.condition === "stale" ? "stale" : "missing";
|
|
1829
2268
|
const instanceId = ev.instance_id ?? "";
|
|
1830
2269
|
const name = nameById.get(instanceId);
|
|
1831
2270
|
const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
|
|
@@ -1902,13 +2341,12 @@ function runHealEvents(opts) {
|
|
|
1902
2341
|
});
|
|
1903
2342
|
process.exit(1);
|
|
1904
2343
|
}
|
|
1905
|
-
// Heal telemetry
|
|
1906
|
-
// (health.pidmap_heal / health.heartbeat_heal), emitted by the writer on
|
|
1907
|
-
// actual self-heal writes.
|
|
2344
|
+
// Heal telemetry is read from the canonical V3 ledger.
|
|
1908
2345
|
const cutoffMs = Date.now() - sinceSecs * 1000;
|
|
1909
2346
|
const nameById = buildNameById(root);
|
|
1910
2347
|
const events = [];
|
|
1911
|
-
|
|
2348
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
2349
|
+
for (const ev of diagnosticEvents.events) {
|
|
1912
2350
|
const heal = canonicalToHealEvent(ev, nameById);
|
|
1913
2351
|
if (heal)
|
|
1914
2352
|
events.push(heal);
|
|
@@ -1945,6 +2383,11 @@ function runHealEvents(opts) {
|
|
|
1945
2383
|
const recent = events.slice(0, limit);
|
|
1946
2384
|
const data = {
|
|
1947
2385
|
since: opts.since,
|
|
2386
|
+
telemetry: {
|
|
2387
|
+
source: diagnosticEvents.source,
|
|
2388
|
+
authoritative: diagnosticEvents.authoritative,
|
|
2389
|
+
reason: diagnosticEvents.reason,
|
|
2390
|
+
},
|
|
1948
2391
|
total: events.length,
|
|
1949
2392
|
by_reason: byReason,
|
|
1950
2393
|
by_kind: byKind,
|
|
@@ -1955,7 +2398,15 @@ function runHealEvents(opts) {
|
|
|
1955
2398
|
};
|
|
1956
2399
|
if (opts.csv) {
|
|
1957
2400
|
emit.config({ format: "csv" });
|
|
1958
|
-
emit.data(
|
|
2401
|
+
emit.data(data.telemetry.authoritative
|
|
2402
|
+
? recent
|
|
2403
|
+
: [
|
|
2404
|
+
{
|
|
2405
|
+
status: "unavailable",
|
|
2406
|
+
source: data.telemetry.source,
|
|
2407
|
+
reason: data.telemetry.reason,
|
|
2408
|
+
},
|
|
2409
|
+
]);
|
|
1959
2410
|
return;
|
|
1960
2411
|
}
|
|
1961
2412
|
if (opts.json) {
|
|
@@ -1967,6 +2418,11 @@ function runHealEvents(opts) {
|
|
|
1967
2418
|
if (process.stdout.isTTY) {
|
|
1968
2419
|
const lines = [];
|
|
1969
2420
|
lines.push(`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`);
|
|
2421
|
+
if (!data.telemetry.authoritative) {
|
|
2422
|
+
lines.push(` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`);
|
|
2423
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
1970
2426
|
lines.push("");
|
|
1971
2427
|
if (events.length === 0) {
|
|
1972
2428
|
lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
|
|
@@ -2014,23 +2470,206 @@ function runHealEvents(opts) {
|
|
|
2014
2470
|
emit.text(`${lines.join("\n")}\n`);
|
|
2015
2471
|
}
|
|
2016
2472
|
}
|
|
2473
|
+
/** Open-span soft watermark per producer state. The producer-state reader
|
|
2474
|
+
* hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
|
|
2475
|
+
* pressure at half that gives room to act before reads start failing. */
|
|
2476
|
+
const SPAN_PRESSURE_SOFT_WATERMARK = 128;
|
|
2477
|
+
function errorText(error) {
|
|
2478
|
+
return error instanceof Error ? error.message : String(error);
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Read-only health counters for the V3 event ledger's producer surfaces. Never
|
|
2482
|
+
* mutates ledger state (no control repair, no spool drain) and never throws:
|
|
2483
|
+
* a non-live route returns `{ state: "unavailable" }`, and each sub-surface
|
|
2484
|
+
* that fails to read lands in `collection_errors` instead of aborting the rest.
|
|
2485
|
+
*/
|
|
2486
|
+
export function collectEventLedgerHealthV3(root, nowMs = Date.now()) {
|
|
2487
|
+
let control;
|
|
2488
|
+
try {
|
|
2489
|
+
control = readEventV3ControlState(root);
|
|
2490
|
+
}
|
|
2491
|
+
catch (error) {
|
|
2492
|
+
return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
|
|
2493
|
+
}
|
|
2494
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
2495
|
+
return {
|
|
2496
|
+
state: "unavailable",
|
|
2497
|
+
reason: `${control.state}: ${control.reason}`,
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
const collectionErrors = [];
|
|
2501
|
+
// 1) Open tool spans per live (non-terminal) generation + span-count pressure.
|
|
2502
|
+
const generations = [];
|
|
2503
|
+
const spanPressure = [];
|
|
2504
|
+
try {
|
|
2505
|
+
for (const { state } of listHookProducerStateRecordsV3(root)) {
|
|
2506
|
+
if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
|
|
2507
|
+
spanPressure.push({
|
|
2508
|
+
instance_id: state.instance_id,
|
|
2509
|
+
generation_id: state.generation_id,
|
|
2510
|
+
span_count: state.spans.length,
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
if (state.spans.length === 0)
|
|
2514
|
+
continue;
|
|
2515
|
+
generations.push({
|
|
2516
|
+
instance_id: state.instance_id,
|
|
2517
|
+
generation_id: state.generation_id,
|
|
2518
|
+
adapter: state.adapter,
|
|
2519
|
+
span_count: state.spans.length,
|
|
2520
|
+
turn_open: Boolean(state.current_turn_id),
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
catch (error) {
|
|
2525
|
+
collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
|
|
2526
|
+
}
|
|
2527
|
+
// 2) Pending finalization requests with age + trigger.
|
|
2528
|
+
const pending = [];
|
|
2529
|
+
try {
|
|
2530
|
+
for (const request of listSessionFinalizationRequestsV3(root)) {
|
|
2531
|
+
if (request.status !== "pending")
|
|
2532
|
+
continue;
|
|
2533
|
+
const observedMs = Date.parse(request.observed_at);
|
|
2534
|
+
pending.push({
|
|
2535
|
+
request_id: request.request_id,
|
|
2536
|
+
trigger: request.trigger,
|
|
2537
|
+
generation_id: request.generation_id,
|
|
2538
|
+
age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
|
|
2539
|
+
allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
catch (error) {
|
|
2544
|
+
collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
|
|
2545
|
+
}
|
|
2546
|
+
// 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
|
|
2547
|
+
let intakeTotal = 0;
|
|
2548
|
+
const intakeGroups = [];
|
|
2549
|
+
try {
|
|
2550
|
+
for (const group of listHookIntakeGroupsV3(root)) {
|
|
2551
|
+
const count = listHookIntakeRecordsV3(group.directory).length;
|
|
2552
|
+
if (count === 0)
|
|
2553
|
+
continue;
|
|
2554
|
+
intakeTotal += count;
|
|
2555
|
+
intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
catch (error) {
|
|
2559
|
+
collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
|
|
2560
|
+
}
|
|
2561
|
+
// 4) Diagnostics spool counts by category. Filenames only (never the
|
|
2562
|
+
// contents): `<category>-<orderkey>.json` where the orderkey leads with a
|
|
2563
|
+
// zero-padded epoch-ms, which also gives the last-24h split for free.
|
|
2564
|
+
const byCategory = {};
|
|
2565
|
+
const recentByCategory = {};
|
|
2566
|
+
let diagnosticsTotal = 0;
|
|
2567
|
+
let diagnostics24h = 0;
|
|
2568
|
+
let diagnostics1h = 0;
|
|
2569
|
+
let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
|
|
2570
|
+
try {
|
|
2571
|
+
const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
|
|
2572
|
+
if (existsSync(diagnosticsDir)) {
|
|
2573
|
+
const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
|
|
2574
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2575
|
+
for (const name of readdirSync(diagnosticsDir)) {
|
|
2576
|
+
const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
|
|
2577
|
+
const category = match?.[1];
|
|
2578
|
+
const epochMs = Number(match?.[2]);
|
|
2579
|
+
if (!category)
|
|
2580
|
+
continue;
|
|
2581
|
+
const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
|
|
2582
|
+
const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
|
|
2583
|
+
byCategory[category] = entry;
|
|
2584
|
+
recentByCategory[category] = recent;
|
|
2585
|
+
entry.total += 1;
|
|
2586
|
+
diagnosticsTotal += 1;
|
|
2587
|
+
if (Number.isFinite(epochMs)) {
|
|
2588
|
+
const recordedAt = new Date(epochMs).toISOString();
|
|
2589
|
+
if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
|
|
2590
|
+
recent.latest_at = recordedAt;
|
|
2591
|
+
}
|
|
2592
|
+
latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
|
|
2593
|
+
if (epochMs >= dayAgoMs) {
|
|
2594
|
+
entry.last_24h += 1;
|
|
2595
|
+
diagnostics24h += 1;
|
|
2596
|
+
}
|
|
2597
|
+
if (epochMs >= hourAgoMs) {
|
|
2598
|
+
recent.last_1h += 1;
|
|
2599
|
+
diagnostics1h += 1;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
catch (error) {
|
|
2606
|
+
collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
state: "live",
|
|
2610
|
+
mode: control.state,
|
|
2611
|
+
open_spans: {
|
|
2612
|
+
total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
|
|
2613
|
+
generations,
|
|
2614
|
+
},
|
|
2615
|
+
pending_finalizations: pending,
|
|
2616
|
+
intake_spool: { total: intakeTotal, groups: intakeGroups },
|
|
2617
|
+
diagnostics_spool: {
|
|
2618
|
+
total: diagnosticsTotal,
|
|
2619
|
+
last_24h: diagnostics24h,
|
|
2620
|
+
last_1h: diagnostics1h,
|
|
2621
|
+
latest_at: Number.isFinite(latestDiagnosticMs)
|
|
2622
|
+
? new Date(latestDiagnosticMs).toISOString()
|
|
2623
|
+
: null,
|
|
2624
|
+
by_category: byCategory,
|
|
2625
|
+
recent_by_category: recentByCategory,
|
|
2626
|
+
},
|
|
2627
|
+
span_pressure: spanPressure,
|
|
2628
|
+
collection_errors: collectionErrors,
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2017
2631
|
/** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
|
|
2018
|
-
* window
|
|
2019
|
-
*
|
|
2020
|
-
function readHookErrors(root, cutoffMs) {
|
|
2632
|
+
* window by exact error and phase, while separating current-hour failures from
|
|
2633
|
+
* historical rows. Each line is {ts, error, phase, ...}. */
|
|
2634
|
+
export function readHookErrors(root, cutoffMs, nowMs = Date.now()) {
|
|
2021
2635
|
const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
|
|
2022
2636
|
const byPhase = {};
|
|
2637
|
+
const byError = {};
|
|
2023
2638
|
const sampleByPhase = {};
|
|
2639
|
+
const errorPhaseCounts = new Map();
|
|
2640
|
+
const recentErrorPhaseCounts = new Map();
|
|
2024
2641
|
let total = 0;
|
|
2025
|
-
|
|
2026
|
-
|
|
2642
|
+
let last1h = 0;
|
|
2643
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
2644
|
+
if (!existsSync(p)) {
|
|
2645
|
+
return {
|
|
2646
|
+
total: 0,
|
|
2647
|
+
last1h: 0,
|
|
2648
|
+
latestAt: null,
|
|
2649
|
+
byPhase,
|
|
2650
|
+
byError,
|
|
2651
|
+
top: [],
|
|
2652
|
+
topErrors: [],
|
|
2653
|
+
recentTopErrors: [],
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2027
2656
|
let raw;
|
|
2028
2657
|
try {
|
|
2029
2658
|
raw = readFileSync(p, "utf8");
|
|
2030
2659
|
}
|
|
2031
2660
|
catch {
|
|
2032
|
-
return {
|
|
2661
|
+
return {
|
|
2662
|
+
total: 0,
|
|
2663
|
+
last1h: 0,
|
|
2664
|
+
latestAt: null,
|
|
2665
|
+
byPhase,
|
|
2666
|
+
byError,
|
|
2667
|
+
top: [],
|
|
2668
|
+
topErrors: [],
|
|
2669
|
+
recentTopErrors: [],
|
|
2670
|
+
};
|
|
2033
2671
|
}
|
|
2672
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2034
2673
|
for (const line of raw.split("\n")) {
|
|
2035
2674
|
if (!line.trim())
|
|
2036
2675
|
continue;
|
|
@@ -2040,9 +2679,22 @@ function readHookErrors(root, cutoffMs) {
|
|
|
2040
2679
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs)
|
|
2041
2680
|
continue;
|
|
2042
2681
|
const phase = e.phase ?? "(unknown)";
|
|
2682
|
+
const error = e.error ?? "(unknown)";
|
|
2043
2683
|
byPhase[phase] = (byPhase[phase] ?? 0) + 1;
|
|
2684
|
+
byError[error] = (byError[error] ?? 0) + 1;
|
|
2044
2685
|
if (!sampleByPhase[phase] && e.error)
|
|
2045
2686
|
sampleByPhase[phase] = e.error;
|
|
2687
|
+
const errorPhaseKey = `${error}\0${phase}`;
|
|
2688
|
+
const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
2689
|
+
errorPhase.count += 1;
|
|
2690
|
+
errorPhaseCounts.set(errorPhaseKey, errorPhase);
|
|
2691
|
+
latestMs = Math.max(latestMs, tsMs);
|
|
2692
|
+
if (tsMs >= hourAgoMs) {
|
|
2693
|
+
last1h += 1;
|
|
2694
|
+
const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
2695
|
+
recent.count += 1;
|
|
2696
|
+
recentErrorPhaseCounts.set(errorPhaseKey, recent);
|
|
2697
|
+
}
|
|
2046
2698
|
total++;
|
|
2047
2699
|
}
|
|
2048
2700
|
catch {
|
|
@@ -2053,119 +2705,163 @@ function readHookErrors(root, cutoffMs) {
|
|
|
2053
2705
|
.sort((a, b) => b[1] - a[1])
|
|
2054
2706
|
.slice(0, 5)
|
|
2055
2707
|
.map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
|
|
2056
|
-
|
|
2708
|
+
const topErrors = [...errorPhaseCounts.values()]
|
|
2709
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
2710
|
+
.slice(0, 5);
|
|
2711
|
+
const recentTopErrors = [...recentErrorPhaseCounts.values()]
|
|
2712
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
2713
|
+
.slice(0, 5);
|
|
2714
|
+
return {
|
|
2715
|
+
total,
|
|
2716
|
+
last1h,
|
|
2717
|
+
latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
|
|
2718
|
+
byPhase,
|
|
2719
|
+
byError,
|
|
2720
|
+
top,
|
|
2721
|
+
topErrors,
|
|
2722
|
+
recentTopErrors,
|
|
2723
|
+
};
|
|
2057
2724
|
}
|
|
2058
2725
|
/** Canonical event stream size + drain lag (events appended after the cursor). */
|
|
2059
2726
|
function readStreamStats(root) {
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
bytes
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
try {
|
|
2074
|
-
cursor = readFileSync(cursorPath, "utf8").trim() || null;
|
|
2075
|
-
}
|
|
2076
|
-
catch {
|
|
2077
|
-
/* ignore */
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
let lines = 0;
|
|
2081
|
-
let backlog = 0;
|
|
2082
|
-
let seenCursor = cursor === null; // no cursor → everything is "backlog"
|
|
2083
|
-
try {
|
|
2084
|
-
// Bounded tail read: the ledger grows without bound and a whole-file
|
|
2085
|
-
// readFileSync throws past V8's ~512MB string limit. lines/backlog are then
|
|
2086
|
-
// scoped to the scanned window; `bytes` (full size, via statSync above) still
|
|
2087
|
-
// reflects the true stream size for the health rollup.
|
|
2088
|
-
const { text } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
|
|
2089
|
-
for (const line of text.split("\n")) {
|
|
2090
|
-
if (!line.trim())
|
|
2091
|
-
continue;
|
|
2092
|
-
lines++;
|
|
2093
|
-
if (seenCursor) {
|
|
2094
|
-
backlog++;
|
|
2095
|
-
}
|
|
2096
|
-
else if (cursor && line.includes(`"event_id":"${cursor}"`)) {
|
|
2097
|
-
seenCursor = true;
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
catch {
|
|
2102
|
-
/* ignore */
|
|
2103
|
-
}
|
|
2104
|
-
return { bytes, lines, cursor_backlog: backlog };
|
|
2727
|
+
const read = readAgentDiagnosticEventsInWindow(root, 0);
|
|
2728
|
+
return {
|
|
2729
|
+
source: "v3",
|
|
2730
|
+
authoritative: read.authoritative,
|
|
2731
|
+
...(read.reason ? { reason: read.reason } : {}),
|
|
2732
|
+
bytes: read.bytes,
|
|
2733
|
+
lines: read.events.length,
|
|
2734
|
+
cursor_backlog: 0,
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
export function traceInstanceIdsForEventSource(nativeInstanceIds, source) {
|
|
2738
|
+
void source;
|
|
2739
|
+
return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
|
|
2105
2740
|
}
|
|
2106
2741
|
/** Map a canonical event to a concise trace line, or null to drop it. */
|
|
2107
|
-
function traceLine(ev, allTools) {
|
|
2108
|
-
const d = (ev.
|
|
2742
|
+
export function traceLine(ev, allTools) {
|
|
2743
|
+
const d = (ev.payload ?? {});
|
|
2109
2744
|
const s = (k) => (typeof d[k] === "string" ? d[k] : "");
|
|
2745
|
+
const object = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
2746
|
+
? value
|
|
2747
|
+
: {};
|
|
2110
2748
|
const clip = (v, n = 70) => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
|
|
2749
|
+
const recovery = typeof d.recovery === "object" && d.recovery !== null
|
|
2750
|
+
? d.recovery
|
|
2751
|
+
: null;
|
|
2752
|
+
const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
|
|
2753
|
+
const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
|
|
2111
2754
|
let detail = "";
|
|
2112
2755
|
switch (ev.event_type) {
|
|
2113
|
-
case "session.
|
|
2114
|
-
detail =
|
|
2756
|
+
case "session.started":
|
|
2757
|
+
detail = "generation started";
|
|
2115
2758
|
break;
|
|
2116
|
-
case "session.
|
|
2117
|
-
detail =
|
|
2759
|
+
case "session.resumed":
|
|
2760
|
+
detail = "generation resumed";
|
|
2118
2761
|
break;
|
|
2119
|
-
case "
|
|
2120
|
-
detail =
|
|
2762
|
+
case "session.ended":
|
|
2763
|
+
detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2121
2764
|
break;
|
|
2122
|
-
case "
|
|
2123
|
-
detail = `
|
|
2765
|
+
case "session.termination_observed":
|
|
2766
|
+
detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
2124
2767
|
break;
|
|
2125
|
-
case "
|
|
2126
|
-
detail =
|
|
2768
|
+
case "agent.delegated":
|
|
2769
|
+
detail = `delegated child=${s("child_generation_id") || "unknown"}`;
|
|
2127
2770
|
break;
|
|
2128
|
-
case "
|
|
2129
|
-
detail = `
|
|
2771
|
+
case "agent.started":
|
|
2772
|
+
detail = `child started=${s("child_generation_id") || "unknown"}`;
|
|
2130
2773
|
break;
|
|
2131
|
-
case "
|
|
2132
|
-
detail =
|
|
2774
|
+
case "agent.completed":
|
|
2775
|
+
detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
|
|
2133
2776
|
break;
|
|
2134
|
-
case "
|
|
2135
|
-
detail =
|
|
2777
|
+
case "turn.started":
|
|
2778
|
+
detail = `intent=${s("intent_kind") || "unknown"}`;
|
|
2136
2779
|
break;
|
|
2137
|
-
case "
|
|
2138
|
-
detail = "
|
|
2780
|
+
case "turn.completed":
|
|
2781
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2139
2782
|
break;
|
|
2140
|
-
case "
|
|
2141
|
-
|
|
2783
|
+
case "tool.requested": {
|
|
2784
|
+
const tool = object(d.tool);
|
|
2785
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
|
|
2142
2786
|
break;
|
|
2143
|
-
|
|
2144
|
-
case "
|
|
2145
|
-
|
|
2146
|
-
detail =
|
|
2787
|
+
}
|
|
2788
|
+
case "tool.completed": {
|
|
2789
|
+
const tool = object(d.tool);
|
|
2790
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
2147
2791
|
break;
|
|
2148
|
-
|
|
2149
|
-
case "
|
|
2150
|
-
|
|
2792
|
+
}
|
|
2793
|
+
case "command.started":
|
|
2794
|
+
if (!allTools)
|
|
2795
|
+
return null;
|
|
2796
|
+
detail = "command started (content omitted)";
|
|
2797
|
+
break;
|
|
2798
|
+
case "command.completed":
|
|
2799
|
+
if (!allTools && !recoveryReason)
|
|
2800
|
+
return null;
|
|
2801
|
+
detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
2802
|
+
break;
|
|
2803
|
+
case "coord.task_changed":
|
|
2804
|
+
case "coord.lifecycle_changed":
|
|
2805
|
+
case "coord.presence_changed":
|
|
2806
|
+
detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
|
|
2807
|
+
break;
|
|
2808
|
+
case "coord.status_observed":
|
|
2809
|
+
detail = s("status") || "status observed";
|
|
2151
2810
|
break;
|
|
2152
|
-
case "
|
|
2153
|
-
|
|
2811
|
+
case "coord.claim_changed": {
|
|
2812
|
+
const target = object(d.target);
|
|
2813
|
+
detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2816
|
+
case "coord.message_observed":
|
|
2817
|
+
detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
|
|
2818
|
+
break;
|
|
2819
|
+
case "coord.identity_attested":
|
|
2820
|
+
detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
|
|
2821
|
+
break;
|
|
2822
|
+
case "wait.started":
|
|
2823
|
+
detail = s("kind") || "wait started";
|
|
2824
|
+
break;
|
|
2825
|
+
case "wait.ended":
|
|
2826
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2827
|
+
break;
|
|
2828
|
+
case "artifact.observed": {
|
|
2829
|
+
const artifact = object(d.artifact);
|
|
2830
|
+
detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
|
|
2831
|
+
break;
|
|
2832
|
+
}
|
|
2833
|
+
case "progress.observed":
|
|
2834
|
+
detail = s("kind") || "progress";
|
|
2835
|
+
break;
|
|
2836
|
+
case "health.observed":
|
|
2837
|
+
detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
|
|
2838
|
+
break;
|
|
2839
|
+
case "council.state_changed":
|
|
2840
|
+
detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2841
|
+
break;
|
|
2842
|
+
case "decision.state_changed":
|
|
2843
|
+
detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2844
|
+
break;
|
|
2845
|
+
case "lifecycle.recovered":
|
|
2846
|
+
detail = `recovered=${s("recovery_kind") || "unknown"}`;
|
|
2847
|
+
break;
|
|
2848
|
+
case "lifecycle.sweep_observed":
|
|
2849
|
+
detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
|
|
2154
2850
|
break;
|
|
2155
2851
|
default:
|
|
2156
|
-
// Noise unless --all-tools: per-line command.* + tool.post_use.
|
|
2157
|
-
if (!allTools)
|
|
2158
|
-
return null;
|
|
2159
|
-
if (ev.event_type === "tool.post_use" ||
|
|
2160
|
-
ev.event_type === "tool.post_use_failure" ||
|
|
2161
|
-
ev.event_type.startsWith("command.")) {
|
|
2162
|
-
detail = s("tool_name") || "";
|
|
2163
|
-
break;
|
|
2164
|
-
}
|
|
2165
2852
|
return null;
|
|
2166
2853
|
}
|
|
2167
2854
|
return { ts: ev.ts, event_type: ev.event_type, detail };
|
|
2168
2855
|
}
|
|
2856
|
+
export function pendingFinalizationTraceEntries(requests, instanceId) {
|
|
2857
|
+
return requests
|
|
2858
|
+
.filter((request) => request.status === "pending" && request.instance_id === instanceId)
|
|
2859
|
+
.map((request) => ({
|
|
2860
|
+
ts: request.observed_at,
|
|
2861
|
+
event_type: "session.finalization_pending",
|
|
2862
|
+
detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
|
|
2863
|
+
}));
|
|
2864
|
+
}
|
|
2169
2865
|
function runTrace(name, opts) {
|
|
2170
2866
|
if (opts.json)
|
|
2171
2867
|
emit.config({ format: "json" });
|
|
@@ -2193,8 +2889,8 @@ function runTrace(name, opts) {
|
|
|
2193
2889
|
targetId = candidates[0][0];
|
|
2194
2890
|
else if (candidates.length > 1)
|
|
2195
2891
|
targetId = candidates.map(([id]) => id).join("\x00"); // sentinel; resolved below
|
|
2196
|
-
else if (/^[0-9a-
|
|
2197
|
-
targetId = wanted;
|
|
2892
|
+
else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted))
|
|
2893
|
+
targetId = wanted;
|
|
2198
2894
|
}
|
|
2199
2895
|
if (!targetId) {
|
|
2200
2896
|
emit.error({
|
|
@@ -2205,33 +2901,16 @@ function runTrace(name, opts) {
|
|
|
2205
2901
|
}
|
|
2206
2902
|
const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
|
|
2207
2903
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 200);
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
// throws past V8's ~512MB string limit. A trace of an agent whose events all
|
|
2212
|
-
// predate the window is reported as truncated rather than crashing.
|
|
2213
|
-
const streamPath = resolve(root, ".harnery", "events.ndjson");
|
|
2904
|
+
const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
|
|
2905
|
+
const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
|
|
2906
|
+
const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
|
|
2214
2907
|
const byId = new Map();
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
if (!line)
|
|
2908
|
+
for (const ev of diagnosticRead.events) {
|
|
2909
|
+
if (!ev.instance_id || !candidateIds.includes(ev.instance_id))
|
|
2218
2910
|
continue;
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
continue;
|
|
2223
|
-
if (sinceMs && Date.parse(ev.ts) < sinceMs)
|
|
2224
|
-
continue;
|
|
2225
|
-
const arr = byId.get(ev.instance_id) ?? [];
|
|
2226
|
-
arr.push(ev);
|
|
2227
|
-
byId.set(ev.instance_id, arr);
|
|
2228
|
-
}
|
|
2229
|
-
catch {
|
|
2230
|
-
/* skip */
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
if (streamTruncated) {
|
|
2234
|
-
process.stderr.write(`note: event ledger exceeds ${Math.round(STREAM_SCAN_CAP_BYTES / 1024 / 1024)}MB; traced only the most recent window (older events omitted)\n`);
|
|
2911
|
+
const arr = byId.get(ev.instance_id) ?? [];
|
|
2912
|
+
arr.push(ev);
|
|
2913
|
+
byId.set(ev.instance_id, arr);
|
|
2235
2914
|
}
|
|
2236
2915
|
// If the name mapped to multiple instances, trace the one with the latest event.
|
|
2237
2916
|
let resolvedId = candidateIds[0];
|
|
@@ -2247,19 +2926,80 @@ function runTrace(name, opts) {
|
|
|
2247
2926
|
}
|
|
2248
2927
|
}
|
|
2249
2928
|
const events = byId.get(resolvedId) ?? [];
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2929
|
+
let state = foldSessionState(events, { instance_id: resolvedId });
|
|
2930
|
+
let sessionState = events.some((event) => event.event_type === "session.ended")
|
|
2931
|
+
? "ended"
|
|
2932
|
+
: "live";
|
|
2933
|
+
if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
|
|
2934
|
+
try {
|
|
2935
|
+
const view = projectCoordinationViewV3(readLedgerV3(root));
|
|
2936
|
+
const liveGeneration = view.instances[resolvedId];
|
|
2937
|
+
const terminalGeneration = Object.values(view.terminal_generations).find((candidate) => candidate.instance_id === resolvedId);
|
|
2938
|
+
const generation = liveGeneration ?? terminalGeneration;
|
|
2939
|
+
if (generation) {
|
|
2940
|
+
sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
|
|
2941
|
+
const lifecycleState = generation.lifecycle_state;
|
|
2942
|
+
state = {
|
|
2943
|
+
activity: generation.activity === "terminal" ? "idle" : generation.activity,
|
|
2944
|
+
activity_updated_at: generation.last_observed_at,
|
|
2945
|
+
activity_source: "event-v3-coordination-view",
|
|
2946
|
+
task_state: lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
|
|
2947
|
+
? lifecycleState
|
|
2948
|
+
: "active",
|
|
2949
|
+
task_state_updated_at: generation.lifecycle_state_updated_at,
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
catch {
|
|
2954
|
+
diagnosticRead.authoritative = false;
|
|
2955
|
+
diagnosticRead.reason = "V3 coordination projection is unavailable";
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
let pendingFinalizations = [];
|
|
2959
|
+
if (diagnosticRead.source === "v3") {
|
|
2960
|
+
try {
|
|
2961
|
+
pendingFinalizations = listSessionFinalizationRequestsV3(root).filter((request) => request.status === "pending" && request.instance_id === resolvedId);
|
|
2962
|
+
}
|
|
2963
|
+
catch {
|
|
2964
|
+
diagnosticRead.authoritative = false;
|
|
2965
|
+
diagnosticRead.reason = "V3 finalization requests are unreadable";
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
const lines = [
|
|
2969
|
+
...events
|
|
2970
|
+
.map((ev) => traceLine(ev, !!opts.allTools))
|
|
2971
|
+
.filter((l) => l !== null),
|
|
2972
|
+
...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
|
|
2973
|
+
]
|
|
2253
2974
|
// Sort by timestamp, not file order: codex replays events (original ts,
|
|
2254
2975
|
// appended later), so append-order ≠ chronological order.
|
|
2255
2976
|
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
2256
2977
|
const shown = lines.slice(-limit);
|
|
2257
|
-
const
|
|
2978
|
+
const nativeResolvedId = diagnosticRead.source === "v3"
|
|
2979
|
+
? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
|
|
2980
|
+
: resolvedId;
|
|
2981
|
+
const displayName = nameById.get(resolvedId) ??
|
|
2982
|
+
(nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
|
|
2983
|
+
resolvedId.slice(0, 8);
|
|
2258
2984
|
const result = {
|
|
2985
|
+
event_source: {
|
|
2986
|
+
contract: diagnosticRead.source,
|
|
2987
|
+
authoritative: diagnosticRead.authoritative,
|
|
2988
|
+
reason: diagnosticRead.reason ?? null,
|
|
2989
|
+
},
|
|
2259
2990
|
name: displayName,
|
|
2260
2991
|
instance_id: resolvedId,
|
|
2261
2992
|
other_instances: candidateIds.filter((id) => id !== resolvedId),
|
|
2993
|
+
pending_finalizations: pendingFinalizations,
|
|
2262
2994
|
total_events: events.length,
|
|
2995
|
+
session_state: sessionState,
|
|
2996
|
+
activity: state.activity,
|
|
2997
|
+
activity_updated_at: state.activity_updated_at ?? null,
|
|
2998
|
+
activity_source: state.activity_source ?? null,
|
|
2999
|
+
task_state: state.task_state,
|
|
3000
|
+
task_state_scope: sessionState === "ended" ? "historical" : "current",
|
|
3001
|
+
task_state_updated_at: state.task_state_updated_at ?? null,
|
|
3002
|
+
task_state_reason: state.task_state_reason ?? null,
|
|
2263
3003
|
shown: shown.length,
|
|
2264
3004
|
entries: shown,
|
|
2265
3005
|
};
|
|
@@ -2270,6 +3010,10 @@ function runTrace(name, opts) {
|
|
|
2270
3010
|
emit.data(result);
|
|
2271
3011
|
const header = `Trace: agent-${displayName} (${resolvedId.slice(0, 8)}…) ${events.length} events${result.other_instances.length ? ` · ${result.other_instances.length} older instance(s) of this name` : ""}`;
|
|
2272
3012
|
process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
|
|
3013
|
+
process.stdout.write(` activity=${state.activity} · session=${sessionState} · lifecycle=${sessionState === "ended" ? `historical(${state.task_state})` : state.task_state}${sessionState !== "ended" && state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`); // lint-ok-emission: human trace view
|
|
3014
|
+
if (!diagnosticRead.authoritative) {
|
|
3015
|
+
process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
|
|
3016
|
+
}
|
|
2273
3017
|
if (shown.length === 0) {
|
|
2274
3018
|
process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
|
|
2275
3019
|
return;
|
|
@@ -2279,6 +3023,53 @@ function runTrace(name, opts) {
|
|
|
2279
3023
|
process.stdout.write(` ${t} ${l.event_type.padEnd(22)} ${l.detail}\n`); // lint-ok-emission: human trace view
|
|
2280
3024
|
}
|
|
2281
3025
|
}
|
|
3026
|
+
/**
|
|
3027
|
+
* V3 generations, not disposable heartbeat caches, are the active-agent authority.
|
|
3028
|
+
*/
|
|
3029
|
+
export function collectActiveAgentHealth(root, nowMs = Date.now()) {
|
|
3030
|
+
const source = "event-ledger-v3";
|
|
3031
|
+
let heartbeats = [];
|
|
3032
|
+
try {
|
|
3033
|
+
const control = readEventV3ControlState(root);
|
|
3034
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
3035
|
+
heartbeats = readLiveCoordinationRows(root);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
catch {
|
|
3039
|
+
// The event-ledger section reports the authority read failure. Do not
|
|
3040
|
+
// substitute stale cache rows for a live V3 route.
|
|
3041
|
+
}
|
|
3042
|
+
const byPlatform = {};
|
|
3043
|
+
const byKind = {};
|
|
3044
|
+
const bySchema = {};
|
|
3045
|
+
let stale = 0;
|
|
3046
|
+
for (const heartbeat of heartbeats) {
|
|
3047
|
+
const platform = formatPlatformLabel(heartbeat.platform);
|
|
3048
|
+
byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
|
|
3049
|
+
const kind = heartbeat.kind ?? "unknown";
|
|
3050
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
3051
|
+
// These rows exist only after an authority-safe V3 projection. The
|
|
3052
|
+
// disposable heartbeat file has its own cache-format version, which must
|
|
3053
|
+
// not be reported as the ledger schema version.
|
|
3054
|
+
bySchema.v3 = (bySchema.v3 ?? 0) + 1;
|
|
3055
|
+
const lastObservedMs = heartbeat.last_heartbeat
|
|
3056
|
+
? Date.parse(heartbeat.last_heartbeat)
|
|
3057
|
+
: Number.NaN;
|
|
3058
|
+
const ageMs = Number.isFinite(lastObservedMs)
|
|
3059
|
+
? nowMs - lastObservedMs
|
|
3060
|
+
: Number.POSITIVE_INFINITY;
|
|
3061
|
+
if (ageMs > freshnessCutoffSecs() * 1000)
|
|
3062
|
+
stale += 1;
|
|
3063
|
+
}
|
|
3064
|
+
return {
|
|
3065
|
+
source,
|
|
3066
|
+
total: heartbeats.length,
|
|
3067
|
+
by_platform: byPlatform,
|
|
3068
|
+
by_kind: byKind,
|
|
3069
|
+
by_schema_version: bySchema,
|
|
3070
|
+
stale,
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
2282
3073
|
function runHealth(opts) {
|
|
2283
3074
|
if (opts.json)
|
|
2284
3075
|
emit.config({ format: "json" });
|
|
@@ -2298,11 +3089,12 @@ function runHealth(opts) {
|
|
|
2298
3089
|
});
|
|
2299
3090
|
process.exit(1);
|
|
2300
3091
|
}
|
|
2301
|
-
const
|
|
3092
|
+
const nowMs = Date.now();
|
|
3093
|
+
const cutoffMs = nowMs - sinceSecs * 1000;
|
|
2302
3094
|
const activeDir = resolve(root, ".harnery/active");
|
|
2303
3095
|
const councilsDir = resolve(root, ".harnery/councils");
|
|
2304
|
-
// Coordination telemetry reads from the canonical
|
|
2305
|
-
// Heals come from health
|
|
3096
|
+
// Coordination telemetry reads from the canonical V3 ledger.
|
|
3097
|
+
// Heals come from health.observed; council activity from council.state_changed.
|
|
2306
3098
|
const heal = [];
|
|
2307
3099
|
let councilAdvanced = 0;
|
|
2308
3100
|
let councilClosed = 0;
|
|
@@ -2310,32 +3102,35 @@ function runHealth(opts) {
|
|
|
2310
3102
|
let sweptTotal = 0;
|
|
2311
3103
|
const sweptByReason = {};
|
|
2312
3104
|
const nameById = buildNameById(root);
|
|
2313
|
-
|
|
3105
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
3106
|
+
for (const ev of diagnosticEvents.events) {
|
|
2314
3107
|
const healEv = canonicalToHealEvent(ev, nameById);
|
|
2315
3108
|
if (healEv) {
|
|
2316
3109
|
heal.push(healEv);
|
|
2317
3110
|
continue;
|
|
2318
3111
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
3112
|
+
if (ev.event_type === "council.state_changed") {
|
|
3113
|
+
const state = String(ev.payload?.new_state ?? "");
|
|
3114
|
+
if (state === "round_open")
|
|
2321
3115
|
councilAdvanced++;
|
|
2322
|
-
|
|
2323
|
-
case "council.close":
|
|
3116
|
+
if (state === "closed")
|
|
2324
3117
|
councilClosed++;
|
|
2325
|
-
|
|
2326
|
-
case "council.archive":
|
|
3118
|
+
if (state === "archived")
|
|
2327
3119
|
councilArchived++;
|
|
2328
|
-
|
|
2329
|
-
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
switch (ev.event_type) {
|
|
3123
|
+
case "lifecycle.sweep_observed": {
|
|
2330
3124
|
sweptTotal++;
|
|
2331
|
-
const reason = String(ev.
|
|
3125
|
+
const reason = String(ev.payload?.observation ?? "unknown");
|
|
2332
3126
|
sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
|
|
2333
3127
|
break;
|
|
2334
3128
|
}
|
|
2335
3129
|
}
|
|
2336
3130
|
}
|
|
2337
|
-
const hookErrors = readHookErrors(root, cutoffMs);
|
|
3131
|
+
const hookErrors = readHookErrors(root, cutoffMs, nowMs);
|
|
2338
3132
|
const stream = readStreamStats(root);
|
|
3133
|
+
const eventLedger = collectEventLedgerHealthV3(root);
|
|
2339
3134
|
// Canonical health.* events carry the full instance_id, already resolved to
|
|
2340
3135
|
// `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
|
|
2341
3136
|
// buildNameById; no hex8→name dedup pass needed anymore.
|
|
@@ -2353,19 +3148,13 @@ function runHealth(opts) {
|
|
|
2353
3148
|
.sort((a, b) => b[1] - a[1])
|
|
2354
3149
|
.slice(0, 5)
|
|
2355
3150
|
.map(([agent, count]) => ({ agent, count }));
|
|
2356
|
-
|
|
2357
|
-
const activeByPlatform = {};
|
|
2358
|
-
const activeByKind = {};
|
|
2359
|
-
const activeBySchema = {};
|
|
2360
|
-
let activeTotal = 0;
|
|
2361
|
-
let staleHeartbeats = 0;
|
|
3151
|
+
const activeAgents = collectActiveAgentHealth(root, nowMs);
|
|
2362
3152
|
// Zombies: files in active/ that are broken: unparseable, nameless, or an
|
|
2363
3153
|
// absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
|
|
2364
3154
|
// mean dead files the sweep isn't reaping.
|
|
2365
3155
|
let zombieCount = 0;
|
|
2366
3156
|
const zombieSamples = [];
|
|
2367
3157
|
const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
|
|
2368
|
-
const nowMs = Date.now();
|
|
2369
3158
|
if (existsSync(activeDir)) {
|
|
2370
3159
|
for (const file of readdirSync(activeDir)) {
|
|
2371
3160
|
if (!file.endsWith(".json"))
|
|
@@ -2384,18 +3173,8 @@ function runHealth(opts) {
|
|
|
2384
3173
|
zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
|
|
2385
3174
|
continue;
|
|
2386
3175
|
}
|
|
2387
|
-
activeTotal++;
|
|
2388
|
-
const platform = formatPlatformLabel(hb.platform);
|
|
2389
|
-
activeByPlatform[platform] = (activeByPlatform[platform] ?? 0) + 1;
|
|
2390
|
-
const kind = hb.kind ?? "unknown";
|
|
2391
|
-
activeByKind[kind] = (activeByKind[kind] ?? 0) + 1;
|
|
2392
|
-
const sv = hb.schema_version;
|
|
2393
|
-
const schemaKey = sv === undefined ? "v0" : `v${sv}`;
|
|
2394
|
-
activeBySchema[schemaKey] = (activeBySchema[schemaKey] ?? 0) + 1;
|
|
2395
3176
|
const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
|
|
2396
3177
|
const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
|
|
2397
|
-
if (ageMs > freshnessCutoffSecs() * 1000)
|
|
2398
|
-
staleHeartbeats++;
|
|
2399
3178
|
// Zombie heuristics on a parseable heartbeat: no name, or an age so large
|
|
2400
3179
|
// it can only be a broken/epoch timestamp (a real agent would have healed).
|
|
2401
3180
|
if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
|
|
@@ -2425,46 +3204,65 @@ function runHealth(opts) {
|
|
|
2425
3204
|
anomalies.push(`${agent} self-healed ${count}x in ${opts.since}; possible idle-prune loop or PID instability`);
|
|
2426
3205
|
}
|
|
2427
3206
|
}
|
|
2428
|
-
if (
|
|
2429
|
-
|
|
3207
|
+
if (activeAgents.stale > 0) {
|
|
3208
|
+
const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
|
|
3209
|
+
const action = activeAgents.source === "event-ledger-v3"
|
|
3210
|
+
? "lifecycle reconciliation may be delayed"
|
|
3211
|
+
: "heal mechanism may not be firing";
|
|
3212
|
+
anomalies.push(`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`);
|
|
2430
3213
|
}
|
|
2431
|
-
const
|
|
3214
|
+
const expectedSchema = "v3";
|
|
3215
|
+
const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter((schema) => schema !== expectedSchema);
|
|
2432
3216
|
if (unexpectedSchemas.length > 0) {
|
|
2433
|
-
anomalies.push(`Unexpected
|
|
2434
|
-
}
|
|
2435
|
-
//
|
|
2436
|
-
//
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
const
|
|
2440
|
-
|
|
2441
|
-
? `: top phase '${top.phase}' x${top.count}${top.sample ? ` (${top.sample.slice(0, 80)})` : ""}`
|
|
2442
|
-
: "";
|
|
2443
|
-
anomalies.push(`agent-hook errored ${hookErrors.total}x in ${opts.since}${detail}`);
|
|
3217
|
+
anomalies.push(`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`);
|
|
3218
|
+
}
|
|
3219
|
+
// Only recent hook failures are active anomalies. Historical rows remain in
|
|
3220
|
+
// the report with exact error signatures and their latest timestamp.
|
|
3221
|
+
if (hookErrors.last1h > 0) {
|
|
3222
|
+
const top = hookErrors.recentTopErrors[0];
|
|
3223
|
+
const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
|
|
3224
|
+
anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
|
|
2444
3225
|
}
|
|
2445
3226
|
if (stream.cursor_backlog > 500) {
|
|
2446
3227
|
anomalies.push(`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`);
|
|
2447
3228
|
}
|
|
2448
|
-
//
|
|
2449
|
-
// append-only ledger (names + forensics for the life of the log), and
|
|
2450
|
-
// consumeSince tail-reads it, so size no longer drives latency. The
|
|
2451
|
-
// bytes/lines still surface in the summary line for visibility;
|
|
2452
|
-
// cursor_backlog above is the real drain-lag signal.
|
|
3229
|
+
// Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
|
|
2453
3230
|
if ((sweptByReason.unparseable ?? 0) > 0) {
|
|
2454
3231
|
anomalies.push(`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`);
|
|
2455
3232
|
}
|
|
2456
3233
|
if (zombieCount > 0) {
|
|
2457
3234
|
anomalies.push(`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`);
|
|
2458
3235
|
}
|
|
3236
|
+
if (eventLedger.state === "live") {
|
|
3237
|
+
// Open spans whose generation has no open turn: the turn ended without its
|
|
3238
|
+
// tool spans closing, so an explicit end can only queue, never finalize.
|
|
3239
|
+
const orphanGenerations = eventLedger.open_spans.generations.filter((generation) => !generation.turn_open);
|
|
3240
|
+
if (orphanGenerations.length > 0) {
|
|
3241
|
+
const orphanSpans = orphanGenerations.reduce((sum, generation) => sum + generation.span_count, 0);
|
|
3242
|
+
anomalies.push(`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`);
|
|
3243
|
+
}
|
|
3244
|
+
for (const pressure of eventLedger.span_pressure) {
|
|
3245
|
+
anomalies.push(`${pressure.instance_id} (${pressure.generation_id}) holds ${pressure.span_count} open spans (soft watermark ${SPAN_PRESSURE_SOFT_WATERMARK}; the reader's 256-span cap makes the state unreadable)`);
|
|
3246
|
+
}
|
|
3247
|
+
for (const collectionError of eventLedger.collection_errors) {
|
|
3248
|
+
anomalies.push(`event-ledger health: ${collectionError}`);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
2459
3251
|
const report = {
|
|
2460
3252
|
since: opts.since,
|
|
2461
3253
|
generated_at: new Date().toISOString(),
|
|
3254
|
+
event_telemetry: {
|
|
3255
|
+
source: diagnosticEvents.source,
|
|
3256
|
+
authoritative: diagnosticEvents.authoritative,
|
|
3257
|
+
...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
|
|
3258
|
+
},
|
|
2462
3259
|
active_agents: {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
3260
|
+
source: activeAgents.source,
|
|
3261
|
+
total: activeAgents.total,
|
|
3262
|
+
by_platform: activeAgents.by_platform,
|
|
3263
|
+
by_kind: activeAgents.by_kind,
|
|
3264
|
+
by_schema_version: activeAgents.by_schema_version,
|
|
3265
|
+
stale: activeAgents.stale,
|
|
2468
3266
|
},
|
|
2469
3267
|
heal_events: {
|
|
2470
3268
|
total: heal.length,
|
|
@@ -2480,9 +3278,19 @@ function runHealth(opts) {
|
|
|
2480
3278
|
closed_in_window: councilClosed,
|
|
2481
3279
|
},
|
|
2482
3280
|
swept_events: { total: sweptTotal, by_reason: sweptByReason },
|
|
2483
|
-
hook_errors: {
|
|
3281
|
+
hook_errors: {
|
|
3282
|
+
total: hookErrors.total,
|
|
3283
|
+
last_1h: hookErrors.last1h,
|
|
3284
|
+
latest_at: hookErrors.latestAt,
|
|
3285
|
+
by_phase: hookErrors.byPhase,
|
|
3286
|
+
by_error: hookErrors.byError,
|
|
3287
|
+
top: hookErrors.top,
|
|
3288
|
+
top_errors: hookErrors.topErrors,
|
|
3289
|
+
recent_top_errors: hookErrors.recentTopErrors,
|
|
3290
|
+
},
|
|
2484
3291
|
stream,
|
|
2485
3292
|
zombies: { count: zombieCount, samples: zombieSamples },
|
|
3293
|
+
event_ledger: eventLedger,
|
|
2486
3294
|
anomalies,
|
|
2487
3295
|
};
|
|
2488
3296
|
if (opts.json) {
|
|
@@ -2518,16 +3326,38 @@ function renderHealthBox(report) {
|
|
|
2518
3326
|
councilParts.push(`${report.councils.closed_in_window} closed`);
|
|
2519
3327
|
if (report.councils.archived_in_window > 0)
|
|
2520
3328
|
councilParts.push(`${report.councils.archived_in_window} archived`);
|
|
2521
|
-
const
|
|
3329
|
+
const activeSource = report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
|
|
3330
|
+
const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
|
|
2522
3331
|
const sweptReasonStr = Object.entries(report.swept_events.by_reason)
|
|
2523
3332
|
.map(([r, n]) => `${r} ${n}`)
|
|
2524
3333
|
.join(", ");
|
|
2525
3334
|
const hookErrStr = report.hook_errors.total === 0
|
|
2526
3335
|
? "0"
|
|
2527
|
-
: `${report.hook_errors.total}
|
|
3336
|
+
: `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
|
|
2528
3337
|
const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
|
|
3338
|
+
const ledger = report.event_ledger;
|
|
3339
|
+
let ledgerStr;
|
|
3340
|
+
if (ledger.state !== "live") {
|
|
3341
|
+
ledgerStr = `unavailable (${ledger.reason})`;
|
|
3342
|
+
}
|
|
3343
|
+
else {
|
|
3344
|
+
const orphanGenerations = ledger.open_spans.generations.filter((generation) => !generation.turn_open).length;
|
|
3345
|
+
const pendingAges = ledger.pending_finalizations
|
|
3346
|
+
.map((request) => request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)))
|
|
3347
|
+
.join(", ");
|
|
3348
|
+
ledgerStr = [
|
|
3349
|
+
`open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
|
|
3350
|
+
`pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
|
|
3351
|
+
`intake ${ledger.intake_spool.total}`,
|
|
3352
|
+
`diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
|
|
3353
|
+
].join(" · ");
|
|
3354
|
+
}
|
|
2529
3355
|
const rows = [
|
|
2530
3356
|
["window", `last ${report.since}`],
|
|
3357
|
+
[
|
|
3358
|
+
"event source",
|
|
3359
|
+
`${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
|
|
3360
|
+
],
|
|
2531
3361
|
["active", activeStr],
|
|
2532
3362
|
[
|
|
2533
3363
|
"heals",
|
|
@@ -2537,6 +3367,7 @@ function renderHealthBox(report) {
|
|
|
2537
3367
|
["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
|
|
2538
3368
|
["hook errors", hookErrStr],
|
|
2539
3369
|
["stream", streamStr],
|
|
3370
|
+
["event ledger", ledgerStr],
|
|
2540
3371
|
[
|
|
2541
3372
|
"zombies",
|
|
2542
3373
|
report.zombies.count === 0
|
|
@@ -2640,6 +3471,8 @@ function runAdapterProbe(id, opts) {
|
|
|
2640
3471
|
replayExitCode = result.exitCode;
|
|
2641
3472
|
data.samples = samples;
|
|
2642
3473
|
data.samples_summary = result.summary;
|
|
3474
|
+
if (result.note)
|
|
3475
|
+
data.samples_note = result.note;
|
|
2643
3476
|
}
|
|
2644
3477
|
emit.data(data);
|
|
2645
3478
|
if (process.stdout.isTTY && !opts.json) {
|
|
@@ -2656,7 +3489,7 @@ function runAdapterProbe(id, opts) {
|
|
|
2656
3489
|
const summary = data.samples_summary;
|
|
2657
3490
|
lines.push("");
|
|
2658
3491
|
if (samples.length === 0) {
|
|
2659
|
-
lines.push(` Sample replay: no .json fixtures found under ${sampleDir}`);
|
|
3492
|
+
lines.push(` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`);
|
|
2660
3493
|
}
|
|
2661
3494
|
else {
|
|
2662
3495
|
lines.push(` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`);
|
|
@@ -2707,6 +3540,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
|
|
|
2707
3540
|
samples: [],
|
|
2708
3541
|
exitCode: 0,
|
|
2709
3542
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
3543
|
+
note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
|
|
2710
3544
|
};
|
|
2711
3545
|
}
|
|
2712
3546
|
const fixtures = readdirSync(sampleDir)
|
|
@@ -2718,6 +3552,7 @@ function replayAdapterSamples(adapter, root, relativeSampleDir, filter) {
|
|
|
2718
3552
|
samples: [],
|
|
2719
3553
|
exitCode: 0,
|
|
2720
3554
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
3555
|
+
note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
|
|
2721
3556
|
};
|
|
2722
3557
|
}
|
|
2723
3558
|
// agent-hook is the single entry point. Replay sample payloads against it,
|
|
@@ -2932,10 +3767,24 @@ function runPing(name, message, opts) {
|
|
|
2932
3767
|
});
|
|
2933
3768
|
process.exit(1);
|
|
2934
3769
|
}
|
|
2935
|
-
const myHb =
|
|
3770
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
2936
3771
|
const fromName = myHb?.name ?? "anonymous";
|
|
2937
3772
|
const body = `from agent-${fromName}: ${message.trim()}`;
|
|
2938
3773
|
const doc = appendEntry(peerOwner, "handoff", body);
|
|
3774
|
+
// Canonical delivery record: sender is the envelope owner, recipient rides
|
|
3775
|
+
// in data, so read-only observers can render the communication without
|
|
3776
|
+
// joining journal files.
|
|
3777
|
+
emitEventV3({
|
|
3778
|
+
owner: myOwner,
|
|
3779
|
+
session: nativeSessionIdentity(myHb, myOwner),
|
|
3780
|
+
adapter: normalizeAdapter(myHb?.platform),
|
|
3781
|
+
observation: {
|
|
3782
|
+
event_type: "coord.message_observed",
|
|
3783
|
+
direction: "sent",
|
|
3784
|
+
subject: peerOwner,
|
|
3785
|
+
body: message.trim(),
|
|
3786
|
+
},
|
|
3787
|
+
});
|
|
2939
3788
|
const data = {
|
|
2940
3789
|
peer: name,
|
|
2941
3790
|
peer_instance_id: peerOwner,
|
|
@@ -2984,7 +3833,7 @@ async function runWait(name, opts) {
|
|
|
2984
3833
|
}
|
|
2985
3834
|
let lastProgressMs = 0;
|
|
2986
3835
|
while (true) {
|
|
2987
|
-
const hb =
|
|
3836
|
+
const hb = readCurrentCoordinationRow(peerOwner);
|
|
2988
3837
|
const now = Date.now();
|
|
2989
3838
|
const elapsedMs = now - startMs;
|
|
2990
3839
|
if (!hb) {
|
|
@@ -3019,9 +3868,8 @@ async function runWait(name, opts) {
|
|
|
3019
3868
|
const progressGapMs = Math.max(pollMs, 30_000);
|
|
3020
3869
|
if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
|
|
3021
3870
|
lastProgressMs = now;
|
|
3022
|
-
const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
|
|
3023
3871
|
const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
|
|
3024
|
-
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking
|
|
3872
|
+
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
|
|
3025
3873
|
process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
|
|
3026
3874
|
}
|
|
3027
3875
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
@@ -3056,10 +3904,20 @@ function runHeal(opts) {
|
|
|
3056
3904
|
emit.error({ code: "missing_owner", message: "--owner is required" });
|
|
3057
3905
|
process.exit(1);
|
|
3058
3906
|
}
|
|
3059
|
-
if (kind !== "pidmap" && kind !== "
|
|
3907
|
+
if (kind !== "pidmap" && kind !== "cache") {
|
|
3060
3908
|
emit.error({
|
|
3061
3909
|
code: "bad_kind",
|
|
3062
|
-
message: "--kind must be one of: pidmap,
|
|
3910
|
+
message: "--kind must be one of: pidmap, cache",
|
|
3911
|
+
});
|
|
3912
|
+
process.exit(1);
|
|
3913
|
+
}
|
|
3914
|
+
if (opts.adapter &&
|
|
3915
|
+
opts.adapter !== "claude-code" &&
|
|
3916
|
+
opts.adapter !== "cursor" &&
|
|
3917
|
+
opts.adapter !== "codex") {
|
|
3918
|
+
emit.error({
|
|
3919
|
+
code: "bad_adapter",
|
|
3920
|
+
message: "--adapter must be one of: claude-code, cursor, codex",
|
|
3063
3921
|
});
|
|
3064
3922
|
process.exit(1);
|
|
3065
3923
|
}
|
|
@@ -3071,8 +3929,8 @@ function runHeal(opts) {
|
|
|
3071
3929
|
});
|
|
3072
3930
|
process.exit(1);
|
|
3073
3931
|
}
|
|
3074
|
-
const action = kind === "pidmap" ? "heal-pidmap" :
|
|
3075
|
-
// Refuse to
|
|
3932
|
+
const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
|
|
3933
|
+
// Refuse to materialize a V3 cache at a truncated owner id.
|
|
3076
3934
|
//
|
|
3077
3935
|
// Heartbeats are keyed by the whole instance_id, so healing at an
|
|
3078
3936
|
// abbreviated id writes `.harnery/active/<prefix>.json` while every reader
|
|
@@ -3092,32 +3950,31 @@ function runHeal(opts) {
|
|
|
3092
3950
|
// whose instance_id this id is a prefix of settles it just as well, and is
|
|
3093
3951
|
// present in precisely the case that matters (the session the reader was
|
|
3094
3952
|
// trying to heal is registered, just not under the abbreviated name).
|
|
3095
|
-
if (kind === "
|
|
3953
|
+
if (kind === "cache" && !readHeartbeatCache(root, owner)) {
|
|
3096
3954
|
const canonical = opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
|
|
3097
3955
|
? opts.sessionId.trim()
|
|
3098
3956
|
: liveIdWithPrefix(root, owner);
|
|
3099
3957
|
if (canonical) {
|
|
3100
|
-
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live
|
|
3958
|
+
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
|
|
3101
3959
|
emit.error({
|
|
3102
3960
|
code: "truncated_owner",
|
|
3103
|
-
message: `--owner ${owner} is a prefix of ${canonical} (${source})
|
|
3104
|
-
`
|
|
3105
|
-
`resolves. Re-run with --owner ${canonical}.`,
|
|
3961
|
+
message: `--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
|
|
3962
|
+
`the canonical instance id; re-run with --owner ${canonical}.`,
|
|
3106
3963
|
});
|
|
3107
3964
|
process.exit(1);
|
|
3108
3965
|
}
|
|
3109
3966
|
}
|
|
3110
3967
|
// Build positional args. agent-coord's arg layout:
|
|
3111
3968
|
// heal-pidmap <instance_id> [<pid>]
|
|
3112
|
-
//
|
|
3113
|
-
// kill-heartbeat <instance_id>
|
|
3969
|
+
// repair-coordination-cache <instance_id> [<session_id>]
|
|
3114
3970
|
const helperArgs = [action, owner];
|
|
3115
3971
|
if (kind === "pidmap" && opts.pid)
|
|
3116
3972
|
helperArgs.push(opts.pid);
|
|
3117
|
-
if (kind === "
|
|
3973
|
+
if (kind === "cache" && opts.sessionId)
|
|
3118
3974
|
helperArgs.push(opts.sessionId);
|
|
3119
|
-
|
|
3120
|
-
|
|
3975
|
+
if (kind === "cache" && opts.adapter)
|
|
3976
|
+
helperArgs.push(`--adapter=${opts.adapter}`);
|
|
3977
|
+
// Both recovery actions are handled by the bundled agent-coord binary.
|
|
3121
3978
|
const helper = agentCoordOrExit(root);
|
|
3122
3979
|
const proc = spawnSync(helper, helperArgs, {
|
|
3123
3980
|
encoding: "utf8",
|
|
@@ -3130,26 +3987,21 @@ function runHeal(opts) {
|
|
|
3130
3987
|
});
|
|
3131
3988
|
process.exit(1);
|
|
3132
3989
|
}
|
|
3133
|
-
//
|
|
3990
|
+
// Read the derived cache to surface post-action state.
|
|
3134
3991
|
let after = null;
|
|
3135
3992
|
try {
|
|
3136
|
-
after =
|
|
3993
|
+
after = readHeartbeatCache(root, owner);
|
|
3137
3994
|
}
|
|
3138
3995
|
catch {
|
|
3139
3996
|
after = null;
|
|
3140
3997
|
}
|
|
3141
|
-
// Outcome semantics differ per kind. kill-heartbeat targets the heartbeat
|
|
3142
|
-
// file directly; heal-heartbeat upserts it; heal-pidmap touches a pid-map
|
|
3143
|
-
// row whose existence is independent of the heartbeat. Reporting on
|
|
3144
|
-
// "heartbeat present after" for the pidmap path was misleading: it's
|
|
3145
|
-
// unrelated to whether the heal succeeded.
|
|
3146
3998
|
const outcome = kind === "pidmap"
|
|
3147
3999
|
? proc.status === 0
|
|
3148
4000
|
? "ok"
|
|
3149
4001
|
: "failed"
|
|
3150
4002
|
: after
|
|
3151
|
-
? "
|
|
3152
|
-
: "
|
|
4003
|
+
? "cache_present"
|
|
4004
|
+
: "cache_absent";
|
|
3153
4005
|
emit.data({
|
|
3154
4006
|
rows: [
|
|
3155
4007
|
{
|
|
@@ -3172,31 +4024,35 @@ function runHeal(opts) {
|
|
|
3172
4024
|
emit.text(`agent-coord ${action} ok\n`);
|
|
3173
4025
|
}
|
|
3174
4026
|
else {
|
|
3175
|
-
emit.text(`agent-coord ${action} ok:
|
|
4027
|
+
emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
|
|
3176
4028
|
}
|
|
3177
4029
|
}
|
|
3178
|
-
// Canonical health.* emission is owned by the writer
|
|
3179
|
-
//
|
|
4030
|
+
// Canonical health.* emission is owned by the cache writer, so it fires
|
|
4031
|
+
// inside the agent-coord subprocess
|
|
3180
4032
|
// above on actual writes only: write-only telemetry, no double-emit, no
|
|
3181
4033
|
// event when an already-correct heal no-ops. (Previously emitted here
|
|
3182
4034
|
// unconditionally on every `harn agents heal`, which over-counted no-op heals.)
|
|
3183
4035
|
}
|
|
3184
4036
|
/**
|
|
3185
|
-
*
|
|
3186
|
-
*
|
|
3187
|
-
* through silently if no session (CI / direct invocation).
|
|
4037
|
+
* Emit one V3 council audit transition keyed to the exact durable manifest.
|
|
4038
|
+
* Falls through silently when no live session can attest the observation.
|
|
3188
4039
|
*/
|
|
3189
|
-
function emitCouncilStateEvent(
|
|
4040
|
+
function emitCouncilStateEvent(manifest, newState, priorState) {
|
|
3190
4041
|
const myOwner = resolveOwner();
|
|
3191
4042
|
if (!myOwner)
|
|
3192
4043
|
return;
|
|
3193
|
-
const hb =
|
|
3194
|
-
|
|
3195
|
-
type,
|
|
4044
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
4045
|
+
emitEventV3({
|
|
3196
4046
|
owner: myOwner,
|
|
3197
|
-
session: hb
|
|
4047
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
3198
4048
|
adapter: normalizeAdapter(hb?.platform),
|
|
3199
|
-
|
|
4049
|
+
observation: {
|
|
4050
|
+
event_type: "council.state_changed",
|
|
4051
|
+
council_id: manifest.council_id,
|
|
4052
|
+
...(priorState ? { prior_state: priorState } : {}),
|
|
4053
|
+
new_state: newState,
|
|
4054
|
+
record: manifest,
|
|
4055
|
+
},
|
|
3200
4056
|
});
|
|
3201
4057
|
}
|
|
3202
4058
|
// ──────── council subcommand impls ────────
|
|
@@ -3239,7 +4095,7 @@ function runCouncilCreate(objective, opts) {
|
|
|
3239
4095
|
createdBy = normalizeAgentName(opts.createdBy);
|
|
3240
4096
|
}
|
|
3241
4097
|
else if (myOwner) {
|
|
3242
|
-
const myHb =
|
|
4098
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3243
4099
|
if (myHb?.name) {
|
|
3244
4100
|
createdBy = normalizeAgentName(myHb.name);
|
|
3245
4101
|
}
|
|
@@ -3295,17 +4151,16 @@ function runCouncilCreate(objective, opts) {
|
|
|
3295
4151
|
writeFileSync(resolve(body, "invite.md"), buildInviteMarkdown(manifest), "utf8");
|
|
3296
4152
|
writeManifest(manifest);
|
|
3297
4153
|
if (myOwner) {
|
|
3298
|
-
const myHbForEmit =
|
|
3299
|
-
|
|
3300
|
-
type: "council.open",
|
|
4154
|
+
const myHbForEmit = readCurrentCoordinationRow(myOwner);
|
|
4155
|
+
emitEventV3({
|
|
3301
4156
|
owner: myOwner,
|
|
3302
|
-
session: myHbForEmit
|
|
4157
|
+
session: nativeSessionIdentity(myHbForEmit, myOwner),
|
|
3303
4158
|
adapter: normalizeAdapter(myHbForEmit?.platform),
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
4159
|
+
observation: {
|
|
4160
|
+
event_type: "council.state_changed",
|
|
4161
|
+
council_id: manifest.council_id,
|
|
4162
|
+
new_state: "active",
|
|
4163
|
+
record: manifest,
|
|
3309
4164
|
},
|
|
3310
4165
|
});
|
|
3311
4166
|
}
|
|
@@ -3317,7 +4172,7 @@ function runCouncilCreate(objective, opts) {
|
|
|
3317
4172
|
const skippedMembers = [];
|
|
3318
4173
|
for (const memberName of members) {
|
|
3319
4174
|
const bareName = memberName.replace(/^agent-/, "");
|
|
3320
|
-
if (myOwner &&
|
|
4175
|
+
if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
|
|
3321
4176
|
// Convener is themselves a member; skip the self-ping
|
|
3322
4177
|
continue;
|
|
3323
4178
|
}
|
|
@@ -3374,7 +4229,7 @@ function runCouncilList(opts) {
|
|
|
3374
4229
|
if (opts.mine) {
|
|
3375
4230
|
const myOwner = resolveOwner();
|
|
3376
4231
|
if (myOwner) {
|
|
3377
|
-
const myHb =
|
|
4232
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3378
4233
|
if (myHb?.name)
|
|
3379
4234
|
myName = normalizeAgentName(myHb.name);
|
|
3380
4235
|
}
|
|
@@ -3538,7 +4393,7 @@ function runCouncilClose(id, opts) {
|
|
|
3538
4393
|
closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
3539
4394
|
};
|
|
3540
4395
|
writeManifest(next);
|
|
3541
|
-
emitCouncilStateEvent("
|
|
4396
|
+
emitCouncilStateEvent(next, "closed", manifest.status);
|
|
3542
4397
|
// Build the transcript: every round's contributions in order.
|
|
3543
4398
|
const transcript = buildTranscript(next);
|
|
3544
4399
|
emit.data({
|
|
@@ -3577,7 +4432,7 @@ function runCouncilArchive(id, opts) {
|
|
|
3577
4432
|
// updated status. moveToArchive then physically relocates the artifacts.
|
|
3578
4433
|
writeManifest(next);
|
|
3579
4434
|
moveToArchive(next.council_id);
|
|
3580
|
-
emitCouncilStateEvent("
|
|
4435
|
+
emitCouncilStateEvent(next, "archived", manifest.status);
|
|
3581
4436
|
emit.data({
|
|
3582
4437
|
rows: [
|
|
3583
4438
|
{
|
|
@@ -3631,7 +4486,7 @@ function runCouncilUnarchive(id, opts) {
|
|
|
3631
4486
|
// (allows re-running for testing).
|
|
3632
4487
|
moveFromArchive(next.council_id);
|
|
3633
4488
|
writeManifest(next);
|
|
3634
|
-
emitCouncilStateEvent(
|
|
4489
|
+
emitCouncilStateEvent(next, restoredStatus, "archived");
|
|
3635
4490
|
emit.data({
|
|
3636
4491
|
rows: [
|
|
3637
4492
|
{
|
|
@@ -3684,7 +4539,7 @@ function runCouncilDelete(id, opts) {
|
|
|
3684
4539
|
}
|
|
3685
4540
|
const removed = deleteArchivedCouncil(manifest.council_id);
|
|
3686
4541
|
if (removed) {
|
|
3687
|
-
emitCouncilStateEvent("
|
|
4542
|
+
emitCouncilStateEvent(manifest, "deleted", "archived");
|
|
3688
4543
|
}
|
|
3689
4544
|
emit.data({
|
|
3690
4545
|
rows: [
|
|
@@ -3856,7 +4711,7 @@ function runCouncilContribute(id, opts) {
|
|
|
3856
4711
|
try {
|
|
3857
4712
|
const myOwner = resolveOwner();
|
|
3858
4713
|
if (myOwner) {
|
|
3859
|
-
const myHb =
|
|
4714
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3860
4715
|
if (myHb?.name)
|
|
3861
4716
|
actualName = normalizeAgentName(myHb.name);
|
|
3862
4717
|
}
|
|
@@ -3874,7 +4729,7 @@ function runCouncilContribute(id, opts) {
|
|
|
3874
4729
|
});
|
|
3875
4730
|
process.exit(1);
|
|
3876
4731
|
}
|
|
3877
|
-
const myHb =
|
|
4732
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3878
4733
|
if (!myHb?.name) {
|
|
3879
4734
|
emit.error({
|
|
3880
4735
|
code: "no_self_name",
|
|
@@ -3925,11 +4780,7 @@ function runCouncilContribute(id, opts) {
|
|
|
3925
4780
|
process.exit(1);
|
|
3926
4781
|
}
|
|
3927
4782
|
const path = writeContribution(manifest.council_id, manifest.current_round, myName, body);
|
|
3928
|
-
emitCouncilStateEvent("
|
|
3929
|
-
round_no: manifest.current_round,
|
|
3930
|
-
member: myName,
|
|
3931
|
-
body_summary: body.length > 1000 ? `${body.slice(0, 997)}...` : body,
|
|
3932
|
-
});
|
|
4783
|
+
emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
|
|
3933
4784
|
// Update manifest: if all members have now contributed, flip round_status.
|
|
3934
4785
|
const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
|
|
3935
4786
|
const allIn = manifest.members.every((m) => contributorsNow.includes(m));
|
|
@@ -3938,14 +4789,10 @@ function runCouncilContribute(id, opts) {
|
|
|
3938
4789
|
if (allIn) {
|
|
3939
4790
|
nextManifest = { ...manifest, round_status: "collected" };
|
|
3940
4791
|
writeManifest(nextManifest);
|
|
3941
|
-
emitCouncilStateEvent("
|
|
3942
|
-
round_no: nextManifest.current_round,
|
|
3943
|
-
});
|
|
4792
|
+
emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
|
|
3944
4793
|
if (manifest.auto_advance) {
|
|
3945
4794
|
nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
|
|
3946
|
-
emitCouncilStateEvent("
|
|
3947
|
-
round_no: nextManifest.current_round,
|
|
3948
|
-
});
|
|
4795
|
+
emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
|
|
3949
4796
|
autoAdvanced = true;
|
|
3950
4797
|
}
|
|
3951
4798
|
}
|
|
@@ -4030,7 +4877,7 @@ function runCouncilPrompt(id, member, opts) {
|
|
|
4030
4877
|
try {
|
|
4031
4878
|
const myOwner = resolveOwner();
|
|
4032
4879
|
if (myOwner) {
|
|
4033
|
-
const myHb =
|
|
4880
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4034
4881
|
if (myHb?.name)
|
|
4035
4882
|
actualName = normalizeAgentName(myHb.name);
|
|
4036
4883
|
}
|
|
@@ -4048,7 +4895,7 @@ function runCouncilPrompt(id, member, opts) {
|
|
|
4048
4895
|
});
|
|
4049
4896
|
process.exit(1);
|
|
4050
4897
|
}
|
|
4051
|
-
const myHb =
|
|
4898
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4052
4899
|
if (!myHb?.name) {
|
|
4053
4900
|
emit.error({
|
|
4054
4901
|
code: "no_self_name",
|
|
@@ -4199,12 +5046,8 @@ function runCouncilAdvance(id, opts) {
|
|
|
4199
5046
|
process.exit(1);
|
|
4200
5047
|
}
|
|
4201
5048
|
const next = advanceCouncil(manifest, !!opts.force);
|
|
4202
|
-
emitCouncilStateEvent("
|
|
4203
|
-
|
|
4204
|
-
});
|
|
4205
|
-
emitCouncilStateEvent("council.round_open", next, {
|
|
4206
|
-
round_no: next.current_round,
|
|
4207
|
-
});
|
|
5049
|
+
emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
|
|
5050
|
+
emitCouncilStateEvent(next, "round_open", "round_closed");
|
|
4208
5051
|
emit.data({
|
|
4209
5052
|
rows: [
|
|
4210
5053
|
{
|
|
@@ -4246,7 +5089,7 @@ function advanceCouncil(manifest, force) {
|
|
|
4246
5089
|
// (Convener already knows; we skip pinging them if they convened it from
|
|
4247
5090
|
// their own session.)
|
|
4248
5091
|
const myOwner = resolveOwner();
|
|
4249
|
-
const myName = myOwner ? normalizeAgentName(
|
|
5092
|
+
const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
|
|
4250
5093
|
for (const memberName of next.members) {
|
|
4251
5094
|
if (memberName === myName)
|
|
4252
5095
|
continue;
|
|
@@ -4267,7 +5110,7 @@ function advanceCouncil(manifest, force) {
|
|
|
4267
5110
|
// Hard cap on box width. Values longer than the per-row budget word-wrap to
|
|
4268
5111
|
// continuation lines (blank key column, value resumes indented). Picked to
|
|
4269
5112
|
// stay readable in narrow terminals + chat clients while giving long
|
|
4270
|
-
//
|
|
5113
|
+
// Task values need room to breathe.
|
|
4271
5114
|
const MAX_BOX_CONTENT_WIDTH = 100;
|
|
4272
5115
|
function formatBox(title, rows) {
|
|
4273
5116
|
const titleStr = ` ${title} `;
|