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/src/commands/agents.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* harn agents whoami current agent's name + instance_id + claims
|
|
5
5
|
* harn agents list all active agents (default: fold transients)
|
|
6
6
|
* harn agents list --all include raw kind=transient rows
|
|
7
|
-
* harn agents list --stale include
|
|
7
|
+
* harn agents list --stale include generations older than the freshness window
|
|
8
8
|
* harn agents list --json JSON output (alias for --format json)
|
|
9
9
|
* harn agents status end-of-turn status box (name + age + files + peers)
|
|
10
10
|
* harn agents heal-events PIDMAP_HEAL telemetry (pid-map self-heal frequency)
|
|
@@ -29,7 +29,6 @@ import { basename, join, resolve } from "node:path";
|
|
|
29
29
|
import type { Command } from "commander";
|
|
30
30
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
31
31
|
import { coordBinPath } from "../core/agents/coord-bin.ts";
|
|
32
|
-
import { readStreamTailBounded } from "../core/agents/events/consume.ts";
|
|
33
32
|
import {
|
|
34
33
|
checkGitFinalization,
|
|
35
34
|
formatGitFinalizationFailure,
|
|
@@ -37,24 +36,72 @@ import {
|
|
|
37
36
|
readSessionWriteClaims,
|
|
38
37
|
} from "../core/agents/finalization.ts";
|
|
39
38
|
import {
|
|
40
|
-
|
|
41
|
-
type Heartbeat,
|
|
39
|
+
emitEventV3,
|
|
42
40
|
monorepoRoot,
|
|
43
41
|
normalizeAdapter,
|
|
44
|
-
readHeartbeat,
|
|
45
42
|
resolveOwner,
|
|
46
43
|
resolveOwnerBySessionEnv,
|
|
47
44
|
resolveOwnerWithSource,
|
|
45
|
+
sessionIdentityFromEnv,
|
|
48
46
|
} from "../core/agents/index.ts";
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
47
|
+
import { reopenLiveCoordinationGenerationV3 } from "../core/agents/live-authority-v3.ts";
|
|
48
|
+
import {
|
|
49
|
+
listSessionFinalizationRequestsV3,
|
|
50
|
+
observeHostDisappearedV3,
|
|
51
|
+
reconcileSessionFinalizationV3,
|
|
52
|
+
requestSessionEndExplicitV3,
|
|
53
|
+
type SessionFinalizationRequestV3,
|
|
54
|
+
} from "../core/agents/session-finalizer-v3.ts";
|
|
55
|
+
import { SESSION_NAME_DISPLAY_NOTE } from "../core/agents/session-name-display.ts";
|
|
56
|
+
import {
|
|
57
|
+
buildLifecycleSuggestedName,
|
|
58
|
+
buildSuggestedName,
|
|
59
|
+
type Heartbeat,
|
|
60
|
+
readHeartbeat as readHeartbeatCache,
|
|
61
|
+
} from "../core/agents/state/heartbeat-writer.ts";
|
|
62
|
+
import {
|
|
63
|
+
readLiveCoordinationRow,
|
|
64
|
+
readLiveCoordinationRows,
|
|
65
|
+
} from "../core/agents/state/live-coordination-view.ts";
|
|
66
|
+
import {
|
|
67
|
+
type AgentActivity,
|
|
68
|
+
foldSessionState,
|
|
69
|
+
type TaskState,
|
|
70
|
+
} from "../core/agents/state/session-state.ts";
|
|
71
|
+
import {
|
|
72
|
+
coordFreshnessSeconds,
|
|
73
|
+
resolveBinName,
|
|
74
|
+
sessionFinalizationConfig,
|
|
75
|
+
} from "../core/config.ts";
|
|
76
|
+
import type { EventTypeV3 } from "../core/events/v3/contract.ts";
|
|
77
|
+
import { readEventV3ControlState } from "../core/events/v3/control.ts";
|
|
78
|
+
import { projectCoordinationViewV3 } from "../core/events/v3/coordination-view.ts";
|
|
79
|
+
import { liveInstanceIdV3 } from "../core/events/v3/live-routing.ts";
|
|
80
|
+
import {
|
|
81
|
+
listHookIntakeGroupsV3,
|
|
82
|
+
listHookIntakeRecordsV3,
|
|
83
|
+
} from "../core/events/v3/producers/intake.ts";
|
|
84
|
+
import {
|
|
85
|
+
listHookProducerStateRecordsV3,
|
|
86
|
+
readHookProducerStateV3,
|
|
87
|
+
} from "../core/events/v3/producers/recorder.ts";
|
|
88
|
+
import { readLedgerV3 } from "../core/events/v3/reader.ts";
|
|
89
|
+
import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../core/events/v3/writer.ts";
|
|
90
|
+
import type { RunQualitySnapshot, RunQualityStatus } from "../core/guard/index.ts";
|
|
91
|
+
import { evaluateRunQualityIfDue } from "../core/guard/index.ts";
|
|
51
92
|
import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
|
|
52
93
|
import { registerContextCommand } from "./context.ts";
|
|
53
94
|
|
|
95
|
+
type LiveCoordinationRow = ReturnType<typeof readLiveCoordinationRows>[number];
|
|
96
|
+
|
|
97
|
+
function readCurrentCoordinationRow(instanceId: string): LiveCoordinationRow | null {
|
|
98
|
+
const root = monorepoRoot();
|
|
99
|
+
return root ? readLiveCoordinationRow(root, instanceId) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
54
102
|
/** Cap for CLI scans of the unbounded event ledger (`trace` / `health`). Well
|
|
55
103
|
* under V8's ~512MB max string length so a `readFileSync` of the whole file can
|
|
56
104
|
* never throw; covers ample recent history for a diagnostic scan. */
|
|
57
|
-
const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
|
|
58
105
|
|
|
59
106
|
import { parsePsChainLine } from "../core/hooks/resolve/anchor.ts";
|
|
60
107
|
import { appendEntry, resolveOwnerByName } from "../core/journal/index.ts";
|
|
@@ -105,7 +152,7 @@ function freshnessCutoffSecs(): number {
|
|
|
105
152
|
}
|
|
106
153
|
|
|
107
154
|
const SUBAGENT_NOTE =
|
|
108
|
-
"Bash identity is process-level
|
|
155
|
+
"Bash identity is process-level; if you're running inside a subagent, " +
|
|
109
156
|
"this resolves to the parent group's name, not the subagent's. A subagent-aware " +
|
|
110
157
|
"bridge (per-shell marker file at .harnery/shells/<pid>) is out of scope.";
|
|
111
158
|
|
|
@@ -179,17 +226,47 @@ interface Row {
|
|
|
179
226
|
started_at: string;
|
|
180
227
|
last_heartbeat: string;
|
|
181
228
|
files_touched: string[];
|
|
182
|
-
last_tool?: string | null;
|
|
183
|
-
last_tool_target?: string | null;
|
|
184
229
|
task?: string | null;
|
|
185
|
-
|
|
186
|
-
|
|
230
|
+
activity: AgentActivity;
|
|
231
|
+
activity_updated_at?: string | null;
|
|
232
|
+
activity_source?: string | null;
|
|
233
|
+
task_state: TaskState;
|
|
234
|
+
task_state_scope: "current" | "historical";
|
|
235
|
+
task_state_updated_at?: string | null;
|
|
236
|
+
task_state_reason?: string | null;
|
|
187
237
|
platform?: string | null;
|
|
188
238
|
/** Set on relation=remote rows: the machine label the row arrived from
|
|
189
239
|
* via the cross-machine presence transport (ADR 0016). */
|
|
190
240
|
machine?: string | null;
|
|
191
241
|
}
|
|
192
242
|
|
|
243
|
+
function activityOf(hb: Pick<Heartbeat, "activity">): AgentActivity {
|
|
244
|
+
return hb.activity ?? "unknown";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function taskStateOf(hb: Pick<Heartbeat, "task_state">): TaskState {
|
|
248
|
+
return hb.task_state ?? "active";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Producer joins use the private native session ID; `session_id` is the canonical V3 fingerprint. */
|
|
252
|
+
export function nativeSessionIdentity(
|
|
253
|
+
row: Pick<Heartbeat, "native_session_id" | "session_id"> | null | undefined,
|
|
254
|
+
fallback: string,
|
|
255
|
+
): string {
|
|
256
|
+
if (row?.native_session_id) return row.native_session_id;
|
|
257
|
+
// A projection-only row carries the privacy-safe canonical session
|
|
258
|
+
// fingerprint, not the adapter's native session ID needed to join the hook
|
|
259
|
+
// producer. Until a disposable cache exists, the resolved owner is the only
|
|
260
|
+
// native identity available to command surfaces such as `agents status`.
|
|
261
|
+
if (row?.session_id && !/^sid_[a-f0-9]{64}$/.test(row.session_id)) return row.session_id;
|
|
262
|
+
return fallback;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function lifecycleLabel(hb: Pick<Heartbeat, "task_state" | "task_state_reason">): string {
|
|
266
|
+
const state = taskStateOf(hb);
|
|
267
|
+
return state === "blocked" && hb.task_state_reason ? `${state}: ${hb.task_state_reason}` : state;
|
|
268
|
+
}
|
|
269
|
+
|
|
193
270
|
let emit: EmitContext;
|
|
194
271
|
|
|
195
272
|
export function registerAgentsCommand(
|
|
@@ -218,7 +295,7 @@ export function registerAgentsCommand(
|
|
|
218
295
|
.command("list")
|
|
219
296
|
.description("List all active agents (folds kind=transient by default)")
|
|
220
297
|
.option("--all", "Include raw kind=transient rows (no fold)")
|
|
221
|
-
.option("--stale", "Include
|
|
298
|
+
.option("--stale", "Include generations older than the freshness window")
|
|
222
299
|
.option("--json", "JSON output (alias for --format json)")
|
|
223
300
|
.action((opts: { all?: boolean; stale?: boolean; json?: boolean }) => {
|
|
224
301
|
runList(opts);
|
|
@@ -232,15 +309,21 @@ export function registerAgentsCommand(
|
|
|
232
309
|
"--end-turn",
|
|
233
310
|
"Treat this as the turn's closing status: issue the box only when this session's held paths are committed and their repositories are pushed",
|
|
234
311
|
)
|
|
312
|
+
.option(
|
|
313
|
+
"--end-session",
|
|
314
|
+
"After the closing status, durably request an authoritative session end as soon as this exact turn closes (requires --end-turn)",
|
|
315
|
+
)
|
|
235
316
|
.option(
|
|
236
317
|
"--session-id <id>",
|
|
237
|
-
"Lookup
|
|
318
|
+
"Lookup the V3 generation by session_id directly, bypassing the ppid walk. " +
|
|
238
319
|
"Use this when calling from a hook (the hook's process tree may not lead back to Claude Code's session pid). " +
|
|
239
320
|
"The Stop hook payload includes session_id; pass it through.",
|
|
240
321
|
)
|
|
241
|
-
.action(
|
|
242
|
-
|
|
243
|
-
|
|
322
|
+
.action(
|
|
323
|
+
(opts: { endTurn?: boolean; endSession?: boolean; json?: boolean; sessionId?: string }) => {
|
|
324
|
+
runStatus(opts);
|
|
325
|
+
},
|
|
326
|
+
);
|
|
244
327
|
|
|
245
328
|
cmd
|
|
246
329
|
.command("suggest-name [description...]")
|
|
@@ -253,7 +336,7 @@ export function registerAgentsCommand(
|
|
|
253
336
|
.option("--json", "JSON output instead of the bare name")
|
|
254
337
|
.option(
|
|
255
338
|
"--session-id <id>",
|
|
256
|
-
"Lookup
|
|
339
|
+
"Lookup the V3 generation by session_id directly, bypassing the ppid walk.",
|
|
257
340
|
)
|
|
258
341
|
.action((description: string[], opts: { json?: boolean; sessionId?: string }) => {
|
|
259
342
|
runSuggestName(description, opts);
|
|
@@ -262,7 +345,7 @@ export function registerAgentsCommand(
|
|
|
262
345
|
cmd
|
|
263
346
|
.command("watch")
|
|
264
347
|
.description(
|
|
265
|
-
"Stream peer state changes
|
|
348
|
+
"Stream peer state changes from the authoritative V3 coordination projection. " +
|
|
266
349
|
"Prints one line per delta: started / ended / activity / file claim / task change.",
|
|
267
350
|
)
|
|
268
351
|
.option("--poll-ms <n>", "Debounce window after a change event", "200")
|
|
@@ -284,15 +367,15 @@ export function registerAgentsCommand(
|
|
|
284
367
|
cmd
|
|
285
368
|
.command("trace <name>")
|
|
286
369
|
.description(
|
|
287
|
-
"Reconstruct one agent's coordination lifecycle from
|
|
288
|
-
"session.
|
|
370
|
+
"Reconstruct one agent's coordination lifecycle from the active canonical event ledger: " +
|
|
371
|
+
"session.started → prompts → turns → tools → observations → session.ended, " +
|
|
289
372
|
"in chronological order. The answer to 'what happened to this agent / why did " +
|
|
290
373
|
"it vanish?' without hand-grepping the stream. Accepts a name (agent-Foo or Foo) " +
|
|
291
374
|
"or an instance_id.",
|
|
292
375
|
)
|
|
293
376
|
.option("--since <window>", "Only events newer than Nh|Nd (default: all)")
|
|
294
377
|
.option("--limit <n>", "Show at most N most-recent events. Default: 200.", "200")
|
|
295
|
-
.option("--all-tools", "Include tool.
|
|
378
|
+
.option("--all-tools", "Include tool.completed + command.* (default: hidden as noise)")
|
|
296
379
|
.option("--json", "JSON envelope output")
|
|
297
380
|
.action(
|
|
298
381
|
(
|
|
@@ -317,6 +400,72 @@ export function registerAgentsCommand(
|
|
|
317
400
|
runSetTask(text.join(" "), opts);
|
|
318
401
|
});
|
|
319
402
|
|
|
403
|
+
cmd
|
|
404
|
+
.command("lifecycle <state>")
|
|
405
|
+
.description("Declare this session's task lifecycle: active, blocked, or done")
|
|
406
|
+
.option("--reason <text>", "Explain the lifecycle declaration")
|
|
407
|
+
.option(
|
|
408
|
+
"--session-id <id>",
|
|
409
|
+
"Target the heartbeat with this session_id directly, bypassing the ppid walk.",
|
|
410
|
+
)
|
|
411
|
+
.action((state: string, opts: { reason?: string; sessionId?: string }) => {
|
|
412
|
+
runLifecycle(state, opts);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
cmd
|
|
416
|
+
.command("end")
|
|
417
|
+
.description(
|
|
418
|
+
"Finalize the current V3 session explicitly, or durably queue finalization until the current turn and tool spans close.",
|
|
419
|
+
)
|
|
420
|
+
.option("--session-id <id>", "Native adapter session id to finalize")
|
|
421
|
+
.option("--instance-id <id>", "Canonical V3 instance id to finalize")
|
|
422
|
+
.option(
|
|
423
|
+
"--outcome <outcome>",
|
|
424
|
+
"succeeded | failed | cancelled | timed_out | denied | interrupted | unknown",
|
|
425
|
+
"succeeded",
|
|
426
|
+
)
|
|
427
|
+
.action((opts: { sessionId?: string; instanceId?: string; outcome: string }) => {
|
|
428
|
+
runEndSession(opts);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
cmd
|
|
432
|
+
.command("reconcile")
|
|
433
|
+
.description(
|
|
434
|
+
"Reconcile archive, idle, parent/run completion, stale, superseded, and host lifecycle signals into V3 session finalization.",
|
|
435
|
+
)
|
|
436
|
+
.option("--watch", "Keep reconciling until interrupted")
|
|
437
|
+
.option("--interval-seconds <n>", "Watch interval in seconds")
|
|
438
|
+
.option("--json", "JSON output")
|
|
439
|
+
.action(async (opts: { watch?: boolean; intervalSeconds?: string; json?: boolean }) => {
|
|
440
|
+
await runSessionReconcile(opts);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
cmd
|
|
444
|
+
.command("observe-archive")
|
|
445
|
+
.description(
|
|
446
|
+
"Record an adapter archive or unarchive observation and reconcile it through the canonical V3 finalizer.",
|
|
447
|
+
)
|
|
448
|
+
.requiredOption("--adapter <id>", "claude-code | codex | cursor")
|
|
449
|
+
.requiredOption("--session-id <id>", "Native adapter session id")
|
|
450
|
+
.option("--unarchived", "Cancel a pending archive finalization")
|
|
451
|
+
.option("--observed-at <iso>", "Observation time (defaults to now)")
|
|
452
|
+
.action(
|
|
453
|
+
(opts: { adapter: string; sessionId: string; unarchived?: boolean; observedAt?: string }) =>
|
|
454
|
+
runObserveArchive(opts),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
cmd
|
|
458
|
+
.command("observe-host-disappeared")
|
|
459
|
+
.description(
|
|
460
|
+
"Record a provisional host-loss observation; finalization follows only after the configured cascade grace period.",
|
|
461
|
+
)
|
|
462
|
+
.requiredOption("--instance-id <id>", "Canonical V3 instance id")
|
|
463
|
+
.requiredOption("--generation-id <id>", "Canonical V3 generation id")
|
|
464
|
+
.option("--observed-at <iso>", "Observation time (defaults to now)")
|
|
465
|
+
.action((opts: { instanceId: string; generationId: string; observedAt?: string }) => {
|
|
466
|
+
runObserveHostDisappeared(opts);
|
|
467
|
+
});
|
|
468
|
+
|
|
320
469
|
cmd
|
|
321
470
|
.command("release-claim <path>")
|
|
322
471
|
.description(
|
|
@@ -428,18 +577,19 @@ export function registerAgentsCommand(
|
|
|
428
577
|
.command("heal")
|
|
429
578
|
.description(
|
|
430
579
|
"Force a coord-layer recovery action on a specific agent. " +
|
|
431
|
-
"Kinds: pidmap (
|
|
432
|
-
"
|
|
433
|
-
"the live hooks use, so it governs the operation safely.",
|
|
580
|
+
"Kinds: pidmap (repair process attribution) and cache (rebuild the " +
|
|
581
|
+
"disposable V3 coordination cache from the authoritative ledger).",
|
|
434
582
|
)
|
|
435
583
|
.requiredOption("--owner <id>", "Target agent's instance_id")
|
|
436
|
-
.requiredOption("--kind <kind>", "pidmap |
|
|
584
|
+
.requiredOption("--kind <kind>", "pidmap | cache")
|
|
437
585
|
.option(
|
|
438
586
|
"--session-id <id>",
|
|
439
|
-
"(--kind
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
587
|
+
"(--kind cache) native session id used to join the authoritative V3 generation.",
|
|
588
|
+
)
|
|
589
|
+
.option(
|
|
590
|
+
"--adapter <id>",
|
|
591
|
+
"(--kind cache) adapter used to validate the authoritative V3 generation: " +
|
|
592
|
+
"claude-code | cursor | codex.",
|
|
443
593
|
)
|
|
444
594
|
.option(
|
|
445
595
|
"--pid <pid>",
|
|
@@ -450,13 +600,218 @@ export function registerAgentsCommand(
|
|
|
450
600
|
)
|
|
451
601
|
.option("--json", "JSON envelope output")
|
|
452
602
|
.action(
|
|
453
|
-
(opts: {
|
|
603
|
+
(opts: {
|
|
604
|
+
owner: string;
|
|
605
|
+
kind: string;
|
|
606
|
+
sessionId?: string;
|
|
607
|
+
adapter?: string;
|
|
608
|
+
pid?: string;
|
|
609
|
+
json?: boolean;
|
|
610
|
+
}) => {
|
|
454
611
|
runHeal(opts);
|
|
455
612
|
},
|
|
456
613
|
);
|
|
457
614
|
registerIdentityCommands(cmd);
|
|
458
615
|
}
|
|
459
616
|
|
|
617
|
+
const SESSION_OUTCOMES = new Set([
|
|
618
|
+
"succeeded",
|
|
619
|
+
"failed",
|
|
620
|
+
"cancelled",
|
|
621
|
+
"timed_out",
|
|
622
|
+
"denied",
|
|
623
|
+
"interrupted",
|
|
624
|
+
"unknown",
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
function runEndSession(opts: { sessionId?: string; instanceId?: string; outcome: string }) {
|
|
628
|
+
const root = monorepoRoot();
|
|
629
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
630
|
+
if (!SESSION_OUTCOMES.has(opts.outcome)) {
|
|
631
|
+
return failCommand("invalid_outcome", "invalid session outcome");
|
|
632
|
+
}
|
|
633
|
+
const target = opts.instanceId ?? opts.sessionId ?? resolveOwner();
|
|
634
|
+
if (!target) {
|
|
635
|
+
return failCommand(
|
|
636
|
+
"session_identity_missing",
|
|
637
|
+
"could not resolve the current session; pass --session-id or --instance-id",
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
const byInstance = listHookProducerStateRecordsV3(root, { includeTerminal: false }).filter(
|
|
641
|
+
({ state }) => state.instance_id === target,
|
|
642
|
+
);
|
|
643
|
+
const byNative = (["claude-code", "codex", "cursor"] as const).flatMap((adapter) => {
|
|
644
|
+
const state = readHookProducerStateV3(root, adapter, target);
|
|
645
|
+
return state && !state.terminal ? [{ path: "", modified_at_ms: 0, state }] : [];
|
|
646
|
+
});
|
|
647
|
+
const matches = byInstance.length > 0 ? byInstance : byNative;
|
|
648
|
+
if (matches.length !== 1) {
|
|
649
|
+
return failCommand(
|
|
650
|
+
"session_identity_ambiguous",
|
|
651
|
+
`expected one live V3 generation for the target; found ${matches.length}`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
const record = matches[0];
|
|
655
|
+
if (!record) {
|
|
656
|
+
return failCommand("session_identity_missing", "live V3 generation disappeared");
|
|
657
|
+
}
|
|
658
|
+
const state = record.state;
|
|
659
|
+
if (state.delegations.length > 0) {
|
|
660
|
+
return failCommand(
|
|
661
|
+
"session_work_open",
|
|
662
|
+
`cannot finalize while ${state.delegations.length} delegated child(ren) remain open`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const history = readSessionWriteClaims(root, state.instance_id, state.session_id);
|
|
666
|
+
const finalized = checkGitFinalization(root, history.paths, {
|
|
667
|
+
claimHistoryComplete: history.complete,
|
|
668
|
+
});
|
|
669
|
+
if (!finalized.ok) {
|
|
670
|
+
return failCommand(
|
|
671
|
+
"git_not_finalized",
|
|
672
|
+
formatGitFinalizationFailure(finalized, resolveBinName(root)),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const result = requestSessionEndExplicitV3({
|
|
676
|
+
coordRoot: root,
|
|
677
|
+
instance_id: state.instance_id,
|
|
678
|
+
generation_id: state.generation_id,
|
|
679
|
+
outcome: opts.outcome as
|
|
680
|
+
| "succeeded"
|
|
681
|
+
| "failed"
|
|
682
|
+
| "cancelled"
|
|
683
|
+
| "timed_out"
|
|
684
|
+
| "denied"
|
|
685
|
+
| "interrupted"
|
|
686
|
+
| "unknown",
|
|
687
|
+
coordination_finalized: true,
|
|
688
|
+
});
|
|
689
|
+
if (
|
|
690
|
+
result.state !== "recorded" &&
|
|
691
|
+
result.state !== "already_ended" &&
|
|
692
|
+
result.state !== "queued" &&
|
|
693
|
+
result.state !== "already_requested"
|
|
694
|
+
) {
|
|
695
|
+
return failCommand("session_end_failed", JSON.stringify(result));
|
|
696
|
+
}
|
|
697
|
+
const terminalEventId =
|
|
698
|
+
result.state === "recorded"
|
|
699
|
+
? result.event.event_id
|
|
700
|
+
: result.state === "already_ended"
|
|
701
|
+
? result.event_id
|
|
702
|
+
: undefined;
|
|
703
|
+
const requestId =
|
|
704
|
+
result.state === "queued" || result.state === "already_requested"
|
|
705
|
+
? result.request.request_id
|
|
706
|
+
: undefined;
|
|
707
|
+
emit.data({
|
|
708
|
+
ok: true,
|
|
709
|
+
state: result.state,
|
|
710
|
+
instance_id: state.instance_id,
|
|
711
|
+
generation_id: state.generation_id,
|
|
712
|
+
...(terminalEventId ? { terminal_event_id: terminalEventId } : {}),
|
|
713
|
+
...(requestId ? { request_id: requestId } : {}),
|
|
714
|
+
authority: "approved",
|
|
715
|
+
reason: "approved_explicit_end",
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function runSessionReconcile(opts: {
|
|
720
|
+
watch?: boolean;
|
|
721
|
+
intervalSeconds?: string;
|
|
722
|
+
json?: boolean;
|
|
723
|
+
}): Promise<void> {
|
|
724
|
+
const root = monorepoRoot();
|
|
725
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
726
|
+
if (opts.json) emit.config({ format: "json" });
|
|
727
|
+
const interval = opts.intervalSeconds
|
|
728
|
+
? Number.parseInt(opts.intervalSeconds, 10)
|
|
729
|
+
: sessionFinalizationConfig(root).reconcileIntervalSeconds;
|
|
730
|
+
if (!Number.isSafeInteger(interval) || interval < 1) {
|
|
731
|
+
return failCommand("invalid_interval", "interval-seconds must be a positive integer");
|
|
732
|
+
}
|
|
733
|
+
let stopped = false;
|
|
734
|
+
do {
|
|
735
|
+
emit.data(reconcileSessionFinalizationV3(root));
|
|
736
|
+
if (!opts.watch || stopped) return;
|
|
737
|
+
await new Promise<void>((resolvePromise) => {
|
|
738
|
+
const timer = setTimeout(resolvePromise, interval * 1_000);
|
|
739
|
+
const stop = () => {
|
|
740
|
+
stopped = true;
|
|
741
|
+
clearTimeout(timer);
|
|
742
|
+
process.off("SIGINT", stop);
|
|
743
|
+
process.off("SIGTERM", stop);
|
|
744
|
+
resolvePromise();
|
|
745
|
+
};
|
|
746
|
+
process.once("SIGINT", stop);
|
|
747
|
+
process.once("SIGTERM", stop);
|
|
748
|
+
});
|
|
749
|
+
} while (opts.watch && !stopped);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function runObserveArchive(opts: {
|
|
753
|
+
adapter: string;
|
|
754
|
+
sessionId: string;
|
|
755
|
+
unarchived?: boolean;
|
|
756
|
+
observedAt?: string;
|
|
757
|
+
}) {
|
|
758
|
+
const root = monorepoRoot();
|
|
759
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
760
|
+
if (!(["claude-code", "codex", "cursor"] as string[]).includes(opts.adapter)) {
|
|
761
|
+
return failCommand("invalid_adapter", "adapter must be claude-code, codex, or cursor");
|
|
762
|
+
}
|
|
763
|
+
const adapter = opts.adapter as "claude-code" | "codex" | "cursor";
|
|
764
|
+
const observedAt = opts.observedAt ?? new Date().toISOString();
|
|
765
|
+
if (Number.isNaN(Date.parse(observedAt))) {
|
|
766
|
+
return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
|
|
767
|
+
}
|
|
768
|
+
emit.data(
|
|
769
|
+
reconcileSessionFinalizationV3(root, {
|
|
770
|
+
now: new Date(observedAt),
|
|
771
|
+
archive_observations: [
|
|
772
|
+
{
|
|
773
|
+
adapter,
|
|
774
|
+
native_session_id: opts.sessionId,
|
|
775
|
+
archived: !opts.unarchived,
|
|
776
|
+
observed_at: observedAt,
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
}),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function runObserveHostDisappeared(opts: {
|
|
784
|
+
instanceId: string;
|
|
785
|
+
generationId: string;
|
|
786
|
+
observedAt?: string;
|
|
787
|
+
}) {
|
|
788
|
+
const root = monorepoRoot();
|
|
789
|
+
if (!root) return failCommand("not_in_repo", "not in an agent session");
|
|
790
|
+
if (!/^inst_[A-Za-z0-9._-]+$/.test(opts.instanceId)) {
|
|
791
|
+
return failCommand("invalid_instance_id", "invalid V3 instance id");
|
|
792
|
+
}
|
|
793
|
+
if (!/^gen_[A-Za-z0-9._-]+$/.test(opts.generationId)) {
|
|
794
|
+
return failCommand("invalid_generation_id", "invalid V3 generation id");
|
|
795
|
+
}
|
|
796
|
+
const observedAt = opts.observedAt ?? new Date().toISOString();
|
|
797
|
+
if (Number.isNaN(Date.parse(observedAt))) {
|
|
798
|
+
return failCommand("invalid_observed_at", "observed-at must be an ISO timestamp");
|
|
799
|
+
}
|
|
800
|
+
emit.data(
|
|
801
|
+
observeHostDisappearedV3({
|
|
802
|
+
coordRoot: root,
|
|
803
|
+
instance_id: opts.instanceId as `inst_${string}`,
|
|
804
|
+
generation_id: opts.generationId as `gen_${string}`,
|
|
805
|
+
observed_at: observedAt,
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function failCommand(code: string, message: string): void {
|
|
811
|
+
emit.error({ code, message });
|
|
812
|
+
process.exitCode = 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
460
815
|
function registerIdentityCommands(parent: Command): void {
|
|
461
816
|
const identity = parent
|
|
462
817
|
.command("identity")
|
|
@@ -740,7 +1095,7 @@ export function registerCouncilCommands(parent: Command): void {
|
|
|
740
1095
|
"archive it first (trash-can pattern). Without --yes this prints " +
|
|
741
1096
|
"the paths that would be removed and exits 0 without touching " +
|
|
742
1097
|
"anything. Does NOT touch target_doc, close_handoff_path, or " +
|
|
743
|
-
"
|
|
1098
|
+
"the canonical V3 event ledger, which is owned independently.",
|
|
744
1099
|
)
|
|
745
1100
|
.option("-y, --yes", "Required to actually delete; without this, dry-run")
|
|
746
1101
|
.option("--json", "JSON envelope output")
|
|
@@ -879,11 +1234,11 @@ function runWhoami(opts: { json?: boolean }): void {
|
|
|
879
1234
|
process.exit(1);
|
|
880
1235
|
}
|
|
881
1236
|
|
|
882
|
-
const hb =
|
|
1237
|
+
const hb = readLiveCoordinationRow(root, myOwner);
|
|
883
1238
|
if (!hb) {
|
|
884
1239
|
emit.error({
|
|
885
|
-
code: "
|
|
886
|
-
message:
|
|
1240
|
+
code: "no_live_generation",
|
|
1241
|
+
message: noLiveGenerationMessage(myOwner),
|
|
887
1242
|
});
|
|
888
1243
|
process.exit(1);
|
|
889
1244
|
}
|
|
@@ -895,14 +1250,17 @@ function runWhoami(opts: { json?: boolean }): void {
|
|
|
895
1250
|
session_id: hb.session_id,
|
|
896
1251
|
kind: normalizeKind(hb.kind),
|
|
897
1252
|
relation: "self",
|
|
898
|
-
started_at: hb.started_at,
|
|
1253
|
+
started_at: hb.started_at ?? hb.last_heartbeat,
|
|
899
1254
|
last_heartbeat: hb.last_heartbeat,
|
|
900
1255
|
files_touched: hb.files_touched ?? [],
|
|
901
|
-
last_tool: hb.last_tool ?? null,
|
|
902
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
903
1256
|
task: hb.task ?? null,
|
|
904
|
-
|
|
905
|
-
|
|
1257
|
+
activity: activityOf(hb),
|
|
1258
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1259
|
+
activity_source: hb.activity_source ?? null,
|
|
1260
|
+
task_state: taskStateOf(hb),
|
|
1261
|
+
task_state_scope: "current",
|
|
1262
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1263
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
906
1264
|
platform: hb.platform ?? "claude-code",
|
|
907
1265
|
};
|
|
908
1266
|
|
|
@@ -926,38 +1284,19 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
926
1284
|
process.exit(1);
|
|
927
1285
|
}
|
|
928
1286
|
|
|
929
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
930
|
-
if (!existsSync(activeDir)) {
|
|
931
|
-
emit.data({ rows: [], note: SUBAGENT_NOTE });
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
1287
|
// Resolve self for relation column; best-effort, missing → "unknown" on every row.
|
|
936
1288
|
const myOwner = resolveOwner();
|
|
937
|
-
const myHb = myOwner ?
|
|
1289
|
+
const myHb = myOwner ? readLiveCoordinationRow(root, myOwner) : null;
|
|
938
1290
|
const mySession = myHb?.session_id ?? null;
|
|
939
1291
|
|
|
940
|
-
|
|
941
|
-
const heartbeats: Heartbeat[] = [];
|
|
942
|
-
for (const file of readdirSync(activeDir)) {
|
|
943
|
-
if (!file.endsWith(".json")) continue;
|
|
944
|
-
try {
|
|
945
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
946
|
-
const parsed = JSON.parse(raw);
|
|
947
|
-
if (parsed && typeof parsed.instance_id === "string") {
|
|
948
|
-
heartbeats.push(parsed as Heartbeat);
|
|
949
|
-
}
|
|
950
|
-
} catch {
|
|
951
|
-
// skip malformed
|
|
952
|
-
}
|
|
953
|
-
}
|
|
1292
|
+
const generations = readLiveCoordinationRows(root);
|
|
954
1293
|
|
|
955
1294
|
// Apply staleness filter unless --stale.
|
|
956
1295
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
957
1296
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
958
1297
|
const live = opts.stale
|
|
959
|
-
?
|
|
960
|
-
:
|
|
1298
|
+
? generations
|
|
1299
|
+
: generations.filter((h) => {
|
|
961
1300
|
const ts = Date.parse(h.last_heartbeat);
|
|
962
1301
|
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
963
1302
|
});
|
|
@@ -993,14 +1332,17 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
993
1332
|
session_id: h.session_id,
|
|
994
1333
|
kind: "transient",
|
|
995
1334
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
996
|
-
started_at: h.started_at,
|
|
1335
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
997
1336
|
last_heartbeat: h.last_heartbeat,
|
|
998
1337
|
files_touched: [...(h.files_touched ?? [])].sort(),
|
|
999
|
-
last_tool: h.last_tool ?? null,
|
|
1000
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
1001
1338
|
task: h.task ?? null,
|
|
1002
|
-
|
|
1003
|
-
|
|
1339
|
+
activity: activityOf(h),
|
|
1340
|
+
activity_updated_at: h.activity_updated_at ?? null,
|
|
1341
|
+
activity_source: h.activity_source ?? null,
|
|
1342
|
+
task_state: taskStateOf(h),
|
|
1343
|
+
task_state_scope: "current",
|
|
1344
|
+
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
1345
|
+
task_state_reason: h.task_state_reason ?? null,
|
|
1004
1346
|
platform: h.platform ?? "claude-code",
|
|
1005
1347
|
});
|
|
1006
1348
|
continue;
|
|
@@ -1018,26 +1360,28 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1018
1360
|
session_id: h.session_id,
|
|
1019
1361
|
kind: normalizeKind(h.kind),
|
|
1020
1362
|
relation: relationOf(h, myOwner ?? "", mySession),
|
|
1021
|
-
started_at: h.started_at,
|
|
1363
|
+
started_at: h.started_at ?? h.last_heartbeat,
|
|
1022
1364
|
last_heartbeat: h.last_heartbeat,
|
|
1023
1365
|
files_touched: files,
|
|
1024
|
-
last_tool: h.last_tool ?? null,
|
|
1025
|
-
last_tool_target: h.last_tool_target ?? null,
|
|
1026
1366
|
task: h.task ?? null,
|
|
1027
|
-
|
|
1028
|
-
|
|
1367
|
+
activity: activityOf(h),
|
|
1368
|
+
activity_updated_at: h.activity_updated_at ?? null,
|
|
1369
|
+
activity_source: h.activity_source ?? null,
|
|
1370
|
+
task_state: taskStateOf(h),
|
|
1371
|
+
task_state_scope: "current",
|
|
1372
|
+
task_state_updated_at: h.task_state_updated_at ?? null,
|
|
1373
|
+
task_state_reason: h.task_state_reason ?? null,
|
|
1029
1374
|
platform: h.platform ?? "claude-code",
|
|
1030
1375
|
});
|
|
1031
1376
|
}
|
|
1032
1377
|
|
|
1033
|
-
// Guard missing started_at
|
|
1034
|
-
//
|
|
1035
|
-
// what made `harn agents list --all --stale` crash.
|
|
1378
|
+
// Guard missing started_at so a partial observational projection cannot make
|
|
1379
|
+
// `harn agents list --all --stale` throw while sorting.
|
|
1036
1380
|
rows.sort((a, b) => (a.started_at ?? "").localeCompare(b.started_at ?? ""));
|
|
1037
1381
|
|
|
1038
1382
|
// Cross-machine presence (ADR 0016): append sessions on OTHER machines from
|
|
1039
1383
|
// the locally-fetched presence refs. Advisory rows (relation=remote) — they
|
|
1040
|
-
//
|
|
1384
|
+
// never participate in local claim blocking.
|
|
1041
1385
|
for (const rm of readRemoteMachines(root)) {
|
|
1042
1386
|
for (const a of rm.agents) {
|
|
1043
1387
|
rows.push({
|
|
@@ -1049,11 +1393,14 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1049
1393
|
started_at: a.started_at ?? "",
|
|
1050
1394
|
last_heartbeat: a.last_heartbeat ?? "",
|
|
1051
1395
|
files_touched: [...(a.files_touched ?? [])].sort(),
|
|
1052
|
-
last_tool: a.last_tool ?? null,
|
|
1053
|
-
last_tool_target: null,
|
|
1054
1396
|
task: a.task ?? null,
|
|
1055
|
-
|
|
1056
|
-
|
|
1397
|
+
activity: a.activity ?? "unknown",
|
|
1398
|
+
activity_updated_at: null,
|
|
1399
|
+
activity_source: null,
|
|
1400
|
+
task_state: a.task_state ?? "active",
|
|
1401
|
+
task_state_scope: "current",
|
|
1402
|
+
task_state_updated_at: null,
|
|
1403
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
1057
1404
|
platform: a.platform ?? "claude-code",
|
|
1058
1405
|
machine: rm.machine,
|
|
1059
1406
|
});
|
|
@@ -1068,7 +1415,7 @@ function runList(opts: { all?: boolean; stale?: boolean; json?: boolean }): void
|
|
|
1068
1415
|
}
|
|
1069
1416
|
|
|
1070
1417
|
function relationOf(
|
|
1071
|
-
peer:
|
|
1418
|
+
peer: Pick<LiveCoordinationRow, "instance_id" | "session_id">,
|
|
1072
1419
|
myOwner: string,
|
|
1073
1420
|
mySession: string | null,
|
|
1074
1421
|
): "self" | "group" | "blocks" | "unknown" {
|
|
@@ -1084,22 +1431,18 @@ function normalizeKind(kind: string | undefined | null): string {
|
|
|
1084
1431
|
}
|
|
1085
1432
|
|
|
1086
1433
|
/**
|
|
1087
|
-
* The `
|
|
1434
|
+
* The `no_live_generation` diagnostic, quoting the owner id in full.
|
|
1088
1435
|
*
|
|
1089
|
-
* This id is actionable: the reader
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
* here reads as complete, so it gets copy-pasted into `--owner` and mints a
|
|
1093
|
-
* heartbeat at a filename no reader resolves — an orphan that leaves the
|
|
1094
|
-
* session looking unhealable. Abbreviate ids for display elsewhere, never in
|
|
1095
|
-
* a message whose whole purpose is to hand the reader an id to pass back.
|
|
1436
|
+
* This id is actionable: the reader may pass it to cache repair or explicit
|
|
1437
|
+
* finalization. Abbreviate ids for display elsewhere, never in a diagnostic
|
|
1438
|
+
* whose purpose is to hand the caller a canonical identity.
|
|
1096
1439
|
*/
|
|
1097
|
-
function
|
|
1098
|
-
return `resolved owner=${owner} but no
|
|
1440
|
+
function noLiveGenerationMessage(owner: string): string {
|
|
1441
|
+
return `resolved owner=${owner} but no authority-safe live V3 generation exists for it`;
|
|
1099
1442
|
}
|
|
1100
1443
|
|
|
1101
1444
|
/**
|
|
1102
|
-
* The instance_id of a live
|
|
1445
|
+
* The instance_id of a live V3 generation that `prefix` strictly prefixes, or null.
|
|
1103
1446
|
*
|
|
1104
1447
|
* Used to recognize an abbreviated id handed back by a reader when the caller
|
|
1105
1448
|
* supplied no canonical id of their own. Ambiguity is treated as "no answer":
|
|
@@ -1107,12 +1450,9 @@ function noHeartbeatMessage(owner: string): string {
|
|
|
1107
1450
|
* and refusing with the wrong id in the message would be worse than the orphan.
|
|
1108
1451
|
*/
|
|
1109
1452
|
function liveIdWithPrefix(root: string, prefix: string): string | null {
|
|
1110
|
-
const activeDir = resolve(root, ".harnery", "active");
|
|
1111
|
-
if (!existsSync(activeDir)) return null;
|
|
1112
1453
|
const matches = new Set<string>();
|
|
1113
|
-
for (const
|
|
1114
|
-
|
|
1115
|
-
const id = file.slice(0, -".json".length);
|
|
1454
|
+
for (const row of readLiveCoordinationRows(root)) {
|
|
1455
|
+
const id = row.instance_id;
|
|
1116
1456
|
if (id !== prefix && id.startsWith(prefix)) matches.add(id);
|
|
1117
1457
|
}
|
|
1118
1458
|
return matches.size === 1 ? (matches.values().next().value as string) : null;
|
|
@@ -1127,82 +1467,60 @@ async function runWatch(pollMs: number): Promise<void> {
|
|
|
1127
1467
|
});
|
|
1128
1468
|
process.exit(1);
|
|
1129
1469
|
}
|
|
1130
|
-
const
|
|
1131
|
-
if (!existsSync(activeDir)) {
|
|
1132
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
1133
|
-
process.exit(1);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
const fs = await import("node:fs");
|
|
1137
|
-
const cache = new Map<string, Heartbeat>();
|
|
1470
|
+
const cache = new Map<string, LiveCoordinationRow>();
|
|
1138
1471
|
|
|
1139
1472
|
// Seed cache + print an initial roster line per live peer.
|
|
1140
|
-
const initial =
|
|
1141
|
-
process.stderr.write(
|
|
1473
|
+
const initial = listLiveGenerations(root);
|
|
1474
|
+
process.stderr.write("watching authoritative V3 coordination state (Ctrl-C to exit)\n"); // lint-ok-emission: banner goes to stderr, stdout is the live stream
|
|
1142
1475
|
for (const h of initial) {
|
|
1143
1476
|
cache.set(h.instance_id, h);
|
|
1144
1477
|
emitWatchLine(
|
|
1145
|
-
`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at))} old${h.task ? `, task: "${h.task}"` : ""})`,
|
|
1478
|
+
`agent-${h.name ?? "?"} present (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`,
|
|
1146
1479
|
);
|
|
1147
1480
|
}
|
|
1148
1481
|
|
|
1149
|
-
let scheduled: NodeJS.Timeout | null = null;
|
|
1150
1482
|
const rescan = () => {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (!current.has(id)) {
|
|
1160
|
-
emitWatchLine(`agent-${old.name ?? "?"} ended`);
|
|
1161
|
-
cache.delete(id);
|
|
1162
|
-
}
|
|
1483
|
+
const current = new Map<string, LiveCoordinationRow>();
|
|
1484
|
+
for (const h of listLiveGenerations(root)) current.set(h.instance_id, h);
|
|
1485
|
+
|
|
1486
|
+
// Removed agents.
|
|
1487
|
+
for (const [id, old] of cache) {
|
|
1488
|
+
if (!current.has(id)) {
|
|
1489
|
+
emitWatchLine(`agent-${old.name ?? "?"} ended`);
|
|
1490
|
+
cache.delete(id);
|
|
1163
1491
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
)
|
|
1171
|
-
|
|
1172
|
-
continue;
|
|
1173
|
-
}
|
|
1174
|
-
// Diff fields we care about.
|
|
1175
|
-
if ((prev.task ?? "") !== (h.task ?? "")) {
|
|
1176
|
-
emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
|
|
1177
|
-
}
|
|
1178
|
-
if (
|
|
1179
|
-
(prev.last_tool ?? "") !== (h.last_tool ?? "") ||
|
|
1180
|
-
(prev.last_tool_target ?? "") !== (h.last_tool_target ?? "")
|
|
1181
|
-
) {
|
|
1182
|
-
if (h.last_tool) {
|
|
1183
|
-
const target = h.last_tool_target ? ` ${truncate(h.last_tool_target, 80)}` : "";
|
|
1184
|
-
emitWatchLine(`agent-${h.name ?? "?"} ${h.last_tool}${target}`);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
// File additions/removals.
|
|
1188
|
-
const prevFiles = new Set(prev.files_touched ?? []);
|
|
1189
|
-
const currFiles = new Set(h.files_touched ?? []);
|
|
1190
|
-
for (const f of currFiles) {
|
|
1191
|
-
if (!prevFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
|
|
1192
|
-
}
|
|
1193
|
-
for (const f of prevFiles) {
|
|
1194
|
-
if (!currFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
|
|
1195
|
-
}
|
|
1492
|
+
}
|
|
1493
|
+
// Added or changed agents.
|
|
1494
|
+
for (const [id, h] of current) {
|
|
1495
|
+
const prev = cache.get(id);
|
|
1496
|
+
if (!prev) {
|
|
1497
|
+
emitWatchLine(
|
|
1498
|
+
`agent-${h.name ?? "?"} started (${formatAge(secondsSince(h.started_at ?? h.last_heartbeat))} old${h.task ? `, task: "${h.task}"` : ""})`,
|
|
1499
|
+
);
|
|
1196
1500
|
cache.set(id, h);
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
// Diff fields we care about.
|
|
1504
|
+
if ((prev.task ?? "") !== (h.task ?? "")) {
|
|
1505
|
+
emitWatchLine(`agent-${h.name ?? "?"} task: ${h.task ? `"${h.task}"` : "(cleared)"}`);
|
|
1506
|
+
}
|
|
1507
|
+
// File additions/removals.
|
|
1508
|
+
const prevFiles = new Set(prev.files_touched ?? []);
|
|
1509
|
+
const currFiles = new Set(h.files_touched ?? []);
|
|
1510
|
+
for (const f of currFiles) {
|
|
1511
|
+
if (!prevFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} +claim ${f}`);
|
|
1512
|
+
}
|
|
1513
|
+
for (const f of prevFiles) {
|
|
1514
|
+
if (!currFiles.has(f)) emitWatchLine(`agent-${h.name ?? "?"} -release ${f}`);
|
|
1197
1515
|
}
|
|
1198
|
-
|
|
1516
|
+
cache.set(id, h);
|
|
1517
|
+
}
|
|
1199
1518
|
};
|
|
1200
1519
|
|
|
1201
|
-
const watcher = fs.watch(activeDir, { persistent: true }, () => rescan());
|
|
1202
|
-
|
|
1203
1520
|
await new Promise<void>((resolveP) => {
|
|
1521
|
+
const timer = setInterval(rescan, Math.max(100, pollMs));
|
|
1204
1522
|
const stop = () => {
|
|
1205
|
-
|
|
1523
|
+
clearInterval(timer);
|
|
1206
1524
|
resolveP();
|
|
1207
1525
|
};
|
|
1208
1526
|
process.on("SIGINT", stop);
|
|
@@ -1210,23 +1528,13 @@ async function runWatch(pollMs: number): Promise<void> {
|
|
|
1210
1528
|
});
|
|
1211
1529
|
}
|
|
1212
1530
|
|
|
1213
|
-
function
|
|
1214
|
-
const out: Heartbeat[] = [];
|
|
1531
|
+
function listLiveGenerations(root: string): LiveCoordinationRow[] {
|
|
1215
1532
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1216
1533
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
const parsed = JSON.parse(raw) as Heartbeat;
|
|
1222
|
-
if (!parsed || typeof parsed.instance_id !== "string") continue;
|
|
1223
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
1224
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) out.push(parsed);
|
|
1225
|
-
} catch {
|
|
1226
|
-
// skip
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
return out;
|
|
1534
|
+
return readLiveCoordinationRows(root).filter((row) => {
|
|
1535
|
+
const ts = Date.parse(row.last_heartbeat);
|
|
1536
|
+
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
1537
|
+
});
|
|
1230
1538
|
}
|
|
1231
1539
|
|
|
1232
1540
|
function secondsSince(iso: string): number {
|
|
@@ -1248,29 +1556,14 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1248
1556
|
});
|
|
1249
1557
|
process.exit(1);
|
|
1250
1558
|
}
|
|
1251
|
-
|
|
1252
|
-
if (!existsSync(activeDir)) {
|
|
1253
|
-
emit.error({ code: "no_active_dir", message: ".harnery/active/ missing" });
|
|
1254
|
-
process.exit(1);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Read all heartbeats; match by name (case-insensitive). Apply freshness filter.
|
|
1258
|
-
const matches: Heartbeat[] = [];
|
|
1559
|
+
// Match authority-safe V3 generations by resolved display name.
|
|
1259
1560
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1260
1561
|
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1261
|
-
|
|
1262
|
-
if (
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (!parsed || typeof parsed.instance_id !== "string") continue;
|
|
1267
|
-
if ((parsed.name ?? "").toLowerCase() !== name.toLowerCase()) continue;
|
|
1268
|
-
const ts = Date.parse(parsed.last_heartbeat);
|
|
1269
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) matches.push(parsed);
|
|
1270
|
-
} catch {
|
|
1271
|
-
// skip malformed
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1562
|
+
const matches = readLiveCoordinationRows(root).filter((row) => {
|
|
1563
|
+
if ((row.name ?? "").toLowerCase() !== name.toLowerCase()) return false;
|
|
1564
|
+
const ts = Date.parse(row.last_heartbeat);
|
|
1565
|
+
return Number.isFinite(ts) && ts / 1000 >= cutoff;
|
|
1566
|
+
});
|
|
1274
1567
|
|
|
1275
1568
|
if (matches.length === 0) {
|
|
1276
1569
|
emit.error({
|
|
@@ -1302,7 +1595,7 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1302
1595
|
const report = null as PeerReport | null;
|
|
1303
1596
|
const bqError = null as string | null;
|
|
1304
1597
|
|
|
1305
|
-
const startedAtMs = Date.parse(hb.started_at);
|
|
1598
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1306
1599
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1307
1600
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1308
1601
|
: 0;
|
|
@@ -1319,12 +1612,15 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1319
1612
|
age_secs: ageSecs,
|
|
1320
1613
|
last_heartbeat_secs_ago: heartbeatAgeSecs,
|
|
1321
1614
|
task: hb.task ?? null,
|
|
1322
|
-
|
|
1323
|
-
|
|
1615
|
+
activity: activityOf(hb),
|
|
1616
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
1617
|
+
activity_source: hb.activity_source ?? null,
|
|
1618
|
+
task_state: taskStateOf(hb),
|
|
1619
|
+
task_state_scope: "current" as const,
|
|
1620
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
1621
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
1324
1622
|
title: report?.title ?? null,
|
|
1325
1623
|
files_held: hb.files_touched ?? [],
|
|
1326
|
-
last_tool: hb.last_tool ?? null,
|
|
1327
|
-
last_tool_target: hb.last_tool_target ?? null,
|
|
1328
1624
|
recent_prompts: report?.recent_prompts ?? [],
|
|
1329
1625
|
recent_tools: report?.recent_tools ?? [],
|
|
1330
1626
|
tool_counts: report?.tool_counts ?? [],
|
|
@@ -1350,10 +1646,10 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
|
|
|
1350
1646
|
` session ${formatAge(ageSecs)} old · kind=${data.kind} · session_id=${data.session_id.slice(0, 8)}…`,
|
|
1351
1647
|
);
|
|
1352
1648
|
lines.push(` last heartbeat: ${formatAge(heartbeatAgeSecs)} ago`);
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1649
|
+
lines.push(` activity: ${data.activity}`);
|
|
1650
|
+
lines.push(
|
|
1651
|
+
` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`,
|
|
1652
|
+
);
|
|
1357
1653
|
if (data.files_held.length > 0) {
|
|
1358
1654
|
lines.push(` holds ${data.files_held.length} file(s):`);
|
|
1359
1655
|
for (const f of data.files_held.slice(0, 10)) lines.push(` ${f}`);
|
|
@@ -1508,13 +1804,19 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1508
1804
|
// Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
|
|
1509
1805
|
// mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
|
|
1510
1806
|
// don't descend from a pid-map-registered anchor, so the walk can miss there.
|
|
1807
|
+
// Final fallback: the adapter/connector-stamped session id from the
|
|
1808
|
+
// environment. set-task is the session's REGISTRATION point — a fresh
|
|
1809
|
+
// (bridge) session has no heartbeat yet, so the heartbeat-validated resolver
|
|
1810
|
+
// returns null by design, and erroring here orphans the session's first
|
|
1811
|
+
// ritual command. The env id carries the same trust as an explicit
|
|
1812
|
+
// --session-id, and set-task mints the heartbeat exactly as that path does.
|
|
1511
1813
|
if (!opts?.sessionId) ensureCursorSession(root);
|
|
1512
|
-
const myOwner = opts?.sessionId ?? resolveOwner();
|
|
1814
|
+
const myOwner = opts?.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1513
1815
|
if (!myOwner) {
|
|
1514
1816
|
emit.error({
|
|
1515
1817
|
code: "no_pidmap_entry",
|
|
1516
1818
|
message:
|
|
1517
|
-
"not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1819
|
+
"not in an agent session; no session-id environment identity and the ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1518
1820
|
});
|
|
1519
1821
|
process.exit(1);
|
|
1520
1822
|
}
|
|
@@ -1525,7 +1827,12 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1525
1827
|
// naming window, and subagent/workflow kinds are never named — see
|
|
1526
1828
|
// heartbeat-writer.setTask. This call is the naming call exactly when the
|
|
1527
1829
|
// stamp appears across the mutation.
|
|
1528
|
-
const priorHb =
|
|
1830
|
+
const priorHb = readCurrentCoordinationRow(myOwner);
|
|
1831
|
+
const normalizedTask = task.length > 0 ? task : undefined;
|
|
1832
|
+
const lifecycleWarning =
|
|
1833
|
+
priorHb && (priorHb.task_state ?? "active") !== "active" && priorHb.task !== normalizedTask
|
|
1834
|
+
? `Task text changed while lifecycle is ${priorHb.task_state}; run \`${resolveBinName(root)} agents lifecycle active\` to reopen it explicitly.`
|
|
1835
|
+
: null;
|
|
1529
1836
|
|
|
1530
1837
|
// Heartbeat mutation goes through agent-coord (atomic temp+rename).
|
|
1531
1838
|
const helper = agentCoordOrExit(root);
|
|
@@ -1541,7 +1848,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1541
1848
|
process.exit(1);
|
|
1542
1849
|
}
|
|
1543
1850
|
|
|
1544
|
-
const hb =
|
|
1851
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1545
1852
|
|
|
1546
1853
|
// The naming call: the mutation just stamped `suggested_session_name` for
|
|
1547
1854
|
// the first time. `first_of_session: true` means exactly "this call produced
|
|
@@ -1553,19 +1860,6 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1553
1860
|
: null;
|
|
1554
1861
|
const firstOfSession = suggestedName !== null;
|
|
1555
1862
|
|
|
1556
|
-
emitCanonical({
|
|
1557
|
-
type: "state.task_set",
|
|
1558
|
-
owner: myOwner,
|
|
1559
|
-
session: hb?.session_id ?? myOwner,
|
|
1560
|
-
adapter: normalizeAdapter(hb?.platform),
|
|
1561
|
-
data: {
|
|
1562
|
-
task,
|
|
1563
|
-
cleared: !task || task.length === 0,
|
|
1564
|
-
first_of_session: firstOfSession,
|
|
1565
|
-
suggested_session_name: suggestedName,
|
|
1566
|
-
},
|
|
1567
|
-
});
|
|
1568
|
-
|
|
1569
1863
|
emit.data({
|
|
1570
1864
|
instance_id: myOwner,
|
|
1571
1865
|
name: hb?.name ?? null,
|
|
@@ -1573,11 +1867,199 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
|
|
|
1573
1867
|
cleared: !task || task.length === 0,
|
|
1574
1868
|
first_of_session: firstOfSession,
|
|
1575
1869
|
suggested_session_name: suggestedName,
|
|
1870
|
+
...(lifecycleWarning ? { warning: lifecycleWarning } : {}),
|
|
1576
1871
|
// Right-time instruction: the UserPromptSubmit nudge fires before the name
|
|
1577
1872
|
// exists; this result is the moment the model holds the string.
|
|
1578
1873
|
...(suggestedName
|
|
1579
1874
|
? {
|
|
1580
|
-
note:
|
|
1875
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1876
|
+
}
|
|
1877
|
+
: {}),
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function runLifecycle(rawState: string, opts: { reason?: string; sessionId?: string }): void {
|
|
1882
|
+
const state = rawState.trim().toLowerCase();
|
|
1883
|
+
if (state !== "active" && state !== "blocked" && state !== "done") {
|
|
1884
|
+
emit.error({
|
|
1885
|
+
code: "invalid_lifecycle_state",
|
|
1886
|
+
message: "lifecycle state must be one of: active, blocked, done",
|
|
1887
|
+
});
|
|
1888
|
+
process.exitCode = 1;
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const root = monorepoRoot();
|
|
1893
|
+
if (!root) {
|
|
1894
|
+
emit.error({
|
|
1895
|
+
code: "not_in_repo",
|
|
1896
|
+
message: "not in an agent session; coord_root() returned null",
|
|
1897
|
+
});
|
|
1898
|
+
process.exitCode = 1;
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
if (!opts.sessionId) ensureCursorSession(root);
|
|
1902
|
+
const myOwner = opts.sessionId ?? resolveOwner() ?? sessionIdentityFromEnv();
|
|
1903
|
+
if (!myOwner) {
|
|
1904
|
+
emit.error({
|
|
1905
|
+
code: "no_pidmap_entry",
|
|
1906
|
+
message:
|
|
1907
|
+
"not in an agent session; ppid walk found no pid-map entry (pass --session-id to bypass)",
|
|
1908
|
+
});
|
|
1909
|
+
process.exitCode = 1;
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
let hb = readCurrentCoordinationRow(myOwner);
|
|
1914
|
+
let reopenedGeneration: ReturnType<typeof reopenLiveCoordinationGenerationV3> | undefined;
|
|
1915
|
+
if (!hb) {
|
|
1916
|
+
if (state !== "active") {
|
|
1917
|
+
emit.error({
|
|
1918
|
+
code: "no_live_generation",
|
|
1919
|
+
message: `${noLiveGenerationMessage(myOwner)}; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation`,
|
|
1920
|
+
});
|
|
1921
|
+
process.exitCode = 1;
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
reopenedGeneration = reopenLiveCoordinationGenerationV3({
|
|
1926
|
+
coordRoot: root,
|
|
1927
|
+
owner: myOwner,
|
|
1928
|
+
nativeSessionId: nativeSessionIdentity(readHeartbeatCache(root, myOwner), myOwner),
|
|
1929
|
+
});
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
emit.error({
|
|
1932
|
+
code: "lifecycle_reopen_failed",
|
|
1933
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1934
|
+
});
|
|
1935
|
+
process.exitCode = 1;
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
hb = readCurrentCoordinationRow(myOwner);
|
|
1939
|
+
if (!hb) {
|
|
1940
|
+
emit.error({
|
|
1941
|
+
code: "lifecycle_reopen_failed",
|
|
1942
|
+
message: "the fresh generation was recorded but did not project as live",
|
|
1943
|
+
});
|
|
1944
|
+
process.exitCode = 1;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
if (hb.kind === "subagent" || hb.kind === "transient" || hb.workflow_run_id) {
|
|
1949
|
+
emit.error({
|
|
1950
|
+
code: "lifecycle_not_human_facing",
|
|
1951
|
+
message: "task lifecycle declarations are limited to human-facing sessions",
|
|
1952
|
+
});
|
|
1953
|
+
process.exitCode = 1;
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
const reason = opts.reason?.trim() || undefined;
|
|
1958
|
+
if (state === "blocked" && !reason) {
|
|
1959
|
+
emit.error({
|
|
1960
|
+
code: "blocked_reason_required",
|
|
1961
|
+
message: "lifecycle blocked requires --reason <text>",
|
|
1962
|
+
});
|
|
1963
|
+
process.exitCode = 1;
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
if (state === "done" && !hb.task) {
|
|
1967
|
+
emit.error({
|
|
1968
|
+
code: "task_required_for_done",
|
|
1969
|
+
message: "declare a current task with agents set-task before marking it done",
|
|
1970
|
+
});
|
|
1971
|
+
process.exitCode = 1;
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
const priorState: TaskState = hb.task_state ?? "active";
|
|
1976
|
+
const priorReason = hb.task_state_reason || undefined;
|
|
1977
|
+
if (reopenedGeneration) {
|
|
1978
|
+
emit.data({
|
|
1979
|
+
instance_id: myOwner,
|
|
1980
|
+
task_state: "active",
|
|
1981
|
+
prior_state: null,
|
|
1982
|
+
reason: null,
|
|
1983
|
+
changed: true,
|
|
1984
|
+
generation_reopened: true,
|
|
1985
|
+
prior_generation_id: reopenedGeneration.prior_generation_id,
|
|
1986
|
+
generation_id: reopenedGeneration.generation_id,
|
|
1987
|
+
name_reminted: false,
|
|
1988
|
+
suggested_session_name: null,
|
|
1989
|
+
git_finalization_checked: false,
|
|
1990
|
+
});
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (priorState === state && priorReason === reason) {
|
|
1994
|
+
emit.data({
|
|
1995
|
+
instance_id: myOwner,
|
|
1996
|
+
task_state: state,
|
|
1997
|
+
reason: reason ?? null,
|
|
1998
|
+
changed: false,
|
|
1999
|
+
name_reminted: false,
|
|
2000
|
+
git_finalization_checked: false,
|
|
2001
|
+
});
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
let finalization: GitFinalizationResult | null = null;
|
|
2006
|
+
if (state === "done") {
|
|
2007
|
+
const history = readSessionWriteClaims(
|
|
2008
|
+
root,
|
|
2009
|
+
hb.instance_id,
|
|
2010
|
+
nativeSessionIdentity(hb, myOwner),
|
|
2011
|
+
);
|
|
2012
|
+
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
2013
|
+
finalization = checkGitFinalization(root, touchedPaths, {
|
|
2014
|
+
claimHistoryComplete: history.complete,
|
|
2015
|
+
});
|
|
2016
|
+
if (!finalization.ok) {
|
|
2017
|
+
emit.error({
|
|
2018
|
+
code: "git_not_finalized",
|
|
2019
|
+
message: formatGitFinalizationFailure(finalization, resolveBinName(root)).replace(
|
|
2020
|
+
"the status box was not issued",
|
|
2021
|
+
"lifecycle was not changed",
|
|
2022
|
+
),
|
|
2023
|
+
});
|
|
2024
|
+
process.exitCode = 1;
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const suggestedName = buildLifecycleSuggestedName(hb.suggested_session_name, state);
|
|
2030
|
+
const nameReminted = suggestedName !== null && suggestedName !== hb.suggested_session_name;
|
|
2031
|
+
const emitted = emitEventV3({
|
|
2032
|
+
owner: myOwner,
|
|
2033
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
2034
|
+
adapter: normalizeAdapter(hb.platform),
|
|
2035
|
+
observation: {
|
|
2036
|
+
event_type: "coord.lifecycle_changed",
|
|
2037
|
+
new_state: state,
|
|
2038
|
+
...(reason ? { reason } : {}),
|
|
2039
|
+
...(nameReminted ? { suggested_session_name: suggestedName } : {}),
|
|
2040
|
+
},
|
|
2041
|
+
});
|
|
2042
|
+
if (!emitted) {
|
|
2043
|
+
emit.error({
|
|
2044
|
+
code: "lifecycle_event_failed",
|
|
2045
|
+
message: "the lifecycle event could not be recorded; heartbeat state was left unchanged",
|
|
2046
|
+
});
|
|
2047
|
+
process.exitCode = 1;
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
emit.data({
|
|
2052
|
+
instance_id: myOwner,
|
|
2053
|
+
task_state: state,
|
|
2054
|
+
prior_state: priorState,
|
|
2055
|
+
reason: reason ?? null,
|
|
2056
|
+
changed: true,
|
|
2057
|
+
name_reminted: nameReminted,
|
|
2058
|
+
suggested_session_name: nameReminted ? suggestedName : null,
|
|
2059
|
+
git_finalization_checked: finalization !== null,
|
|
2060
|
+
...(nameReminted
|
|
2061
|
+
? {
|
|
2062
|
+
note: SESSION_NAME_DISPLAY_NOTE,
|
|
1581
2063
|
}
|
|
1582
2064
|
: {}),
|
|
1583
2065
|
});
|
|
@@ -1609,7 +2091,7 @@ function runSuggestName(
|
|
|
1609
2091
|
process.exit(1);
|
|
1610
2092
|
}
|
|
1611
2093
|
|
|
1612
|
-
const hb =
|
|
2094
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1613
2095
|
const agentName = hb?.name || "unknown";
|
|
1614
2096
|
// Description resolution: an explicit arg wins; with no arg, fall back to the
|
|
1615
2097
|
// agent's current declared task, so a bare `suggest-name` reprints the running
|
|
@@ -1647,7 +2129,12 @@ function runSuggestName(
|
|
|
1647
2129
|
process.stdout.write(`${suggestedName}\n`); // lint-ok-emission: chat-paste path
|
|
1648
2130
|
}
|
|
1649
2131
|
|
|
1650
|
-
function runStatus(opts: {
|
|
2132
|
+
function runStatus(opts: {
|
|
2133
|
+
endTurn?: boolean;
|
|
2134
|
+
endSession?: boolean;
|
|
2135
|
+
json?: boolean;
|
|
2136
|
+
sessionId?: string;
|
|
2137
|
+
}): void {
|
|
1651
2138
|
const root = monorepoRoot();
|
|
1652
2139
|
if (!root) {
|
|
1653
2140
|
emit.error({
|
|
@@ -1656,6 +2143,14 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1656
2143
|
});
|
|
1657
2144
|
process.exit(1);
|
|
1658
2145
|
}
|
|
2146
|
+
if (opts.endSession && !opts.endTurn) {
|
|
2147
|
+
emit.error({
|
|
2148
|
+
code: "end_session_requires_end_turn",
|
|
2149
|
+
message: "--end-session requires --end-turn so Git finalization is verified first",
|
|
2150
|
+
});
|
|
2151
|
+
process.exitCode = 1;
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
1659
2154
|
|
|
1660
2155
|
// Identity resolution: prefer explicit --session-id (hook-friendly), fall
|
|
1661
2156
|
// back to ppid walk for interactive shell usage.
|
|
@@ -1670,18 +2165,30 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1670
2165
|
process.exit(1);
|
|
1671
2166
|
}
|
|
1672
2167
|
|
|
1673
|
-
const hb =
|
|
2168
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
1674
2169
|
if (!hb) {
|
|
1675
2170
|
emit.error({
|
|
1676
|
-
code: "
|
|
1677
|
-
message:
|
|
2171
|
+
code: "no_live_generation",
|
|
2172
|
+
message: noLiveGenerationMessage(myOwner),
|
|
1678
2173
|
});
|
|
1679
2174
|
process.exit(1);
|
|
1680
2175
|
}
|
|
1681
2176
|
|
|
2177
|
+
// Report-only run quality: cheap config/cursor due check first, then a
|
|
2178
|
+
// non-blocking lazy evaluation. Shadow mode deliberately exposes no severity.
|
|
2179
|
+
const qualityEvaluation = evaluateRunQualityIfDue(root, new Date(), myOwner);
|
|
2180
|
+
const quality = qualityForStatus(
|
|
2181
|
+
qualityEvaluation.config.requested_mode,
|
|
2182
|
+
qualityEvaluation.snapshot,
|
|
2183
|
+
);
|
|
2184
|
+
|
|
1682
2185
|
let finalization: GitFinalizationResult | null = null;
|
|
1683
2186
|
if (opts.endTurn) {
|
|
1684
|
-
const history = readSessionWriteClaims(
|
|
2187
|
+
const history = readSessionWriteClaims(
|
|
2188
|
+
root,
|
|
2189
|
+
hb.instance_id,
|
|
2190
|
+
nativeSessionIdentity(hb, myOwner),
|
|
2191
|
+
);
|
|
1685
2192
|
const touchedPaths = [...new Set([...(hb.files_touched ?? []), ...history.paths])];
|
|
1686
2193
|
finalization = checkGitFinalization(root, touchedPaths, {
|
|
1687
2194
|
claimHistoryComplete: history.complete,
|
|
@@ -1696,48 +2203,60 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1696
2203
|
}
|
|
1697
2204
|
}
|
|
1698
2205
|
|
|
1699
|
-
|
|
1700
|
-
type: "state.status_checked",
|
|
2206
|
+
emitEventV3({
|
|
1701
2207
|
owner: myOwner,
|
|
1702
|
-
session: hb
|
|
2208
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
1703
2209
|
adapter: normalizeAdapter(hb.platform),
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
agent_count: 0, // computed below, not yet available here; Phase 5 verdict reads owner-scope only
|
|
1708
|
-
included_self: true,
|
|
2210
|
+
observation: {
|
|
2211
|
+
event_type: "coord.status_observed",
|
|
2212
|
+
status: opts.endTurn ? "end_turn_checked" : opts.json ? "json_checked" : "box_checked",
|
|
1709
2213
|
},
|
|
1710
2214
|
});
|
|
1711
2215
|
|
|
1712
|
-
|
|
2216
|
+
let sessionEnd:
|
|
2217
|
+
| { state: "queued" | "already_requested"; request_id: string }
|
|
2218
|
+
| { state: "recorded" | "already_ended"; terminal_event_id?: string }
|
|
2219
|
+
| undefined;
|
|
2220
|
+
if (opts.endSession) {
|
|
2221
|
+
const canonicalInstanceId = liveInstanceIdV3(hb.instance_id);
|
|
2222
|
+
const records = listHookProducerStateRecordsV3(root, { includeTerminal: true }).filter(
|
|
2223
|
+
({ state }) => state.instance_id === canonicalInstanceId,
|
|
2224
|
+
);
|
|
2225
|
+
if (records.length !== 1 || !records[0]) {
|
|
2226
|
+
emit.error({
|
|
2227
|
+
code: "session_identity_ambiguous",
|
|
2228
|
+
message: `expected one V3 generation for ${canonicalInstanceId}; found ${records.length}`,
|
|
2229
|
+
});
|
|
2230
|
+
process.exitCode = 1;
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const state = records[0].state;
|
|
2234
|
+
const requested = requestSessionEndExplicitV3({
|
|
2235
|
+
coordRoot: root,
|
|
2236
|
+
instance_id: state.instance_id,
|
|
2237
|
+
generation_id: state.generation_id,
|
|
2238
|
+
outcome: "succeeded",
|
|
2239
|
+
coordination_finalized: true,
|
|
2240
|
+
});
|
|
2241
|
+
if (requested.state === "queued" || requested.state === "already_requested") {
|
|
2242
|
+
sessionEnd = { state: requested.state, request_id: requested.request.request_id };
|
|
2243
|
+
} else if (requested.state === "recorded") {
|
|
2244
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event.event_id };
|
|
2245
|
+
} else if (requested.state === "already_ended") {
|
|
2246
|
+
sessionEnd = { state: requested.state, terminal_event_id: requested.event_id };
|
|
2247
|
+
} else {
|
|
2248
|
+
emit.error({ code: "session_end_failed", message: JSON.stringify(requested) });
|
|
2249
|
+
process.exitCode = 1;
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
const startedAtMs = Date.parse(hb.started_at ?? hb.last_heartbeat);
|
|
1713
2255
|
const ageSecs = Number.isFinite(startedAtMs)
|
|
1714
2256
|
? Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000))
|
|
1715
2257
|
: 0;
|
|
1716
2258
|
|
|
1717
|
-
const
|
|
1718
|
-
const nowSec = Math.floor(Date.now() / 1000);
|
|
1719
|
-
const cutoff = nowSec - freshnessCutoffSecs();
|
|
1720
|
-
const livePeers: Heartbeat[] = [];
|
|
1721
|
-
let peersStale = 0;
|
|
1722
|
-
if (existsSync(activeDir)) {
|
|
1723
|
-
for (const file of readdirSync(activeDir)) {
|
|
1724
|
-
if (!file.endsWith(".json")) continue;
|
|
1725
|
-
try {
|
|
1726
|
-
const raw = readFileSync(resolve(activeDir, file), "utf8");
|
|
1727
|
-
const peer = JSON.parse(raw) as Heartbeat;
|
|
1728
|
-
if (!peer || typeof peer.instance_id !== "string") continue;
|
|
1729
|
-
if (peer.instance_id === myOwner) continue;
|
|
1730
|
-
const ts = Date.parse(peer.last_heartbeat);
|
|
1731
|
-
if (Number.isFinite(ts) && ts / 1000 >= cutoff) {
|
|
1732
|
-
livePeers.push(peer);
|
|
1733
|
-
} else {
|
|
1734
|
-
peersStale++;
|
|
1735
|
-
}
|
|
1736
|
-
} catch {
|
|
1737
|
-
// skip malformed
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
2259
|
+
const { livePeers, stale: peersStale } = collectStatusPeerHealth(root, myOwner);
|
|
1741
2260
|
|
|
1742
2261
|
// Sort: file-holders first (by file count desc), then idle peers by recency.
|
|
1743
2262
|
livePeers.sort((a, b) => {
|
|
@@ -1757,7 +2276,7 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1757
2276
|
const remoteMachines = readRemoteMachines(root);
|
|
1758
2277
|
const peersStr = formatPeers(livePeers, 4, peersStale, remoteMachines);
|
|
1759
2278
|
|
|
1760
|
-
const ctxUsage = readContextUsage(hb.session_id, hb.platform);
|
|
2279
|
+
const ctxUsage = readContextUsage(hb.native_session_id ?? hb.session_id, hb.platform);
|
|
1761
2280
|
let ctxStr: string;
|
|
1762
2281
|
if (!ctxUsage) {
|
|
1763
2282
|
ctxStr = "unavailable";
|
|
@@ -1790,6 +2309,13 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1790
2309
|
instance_id: hb.instance_id,
|
|
1791
2310
|
kind: normalizeKind(hb.kind),
|
|
1792
2311
|
session_age_secs: ageSecs,
|
|
2312
|
+
activity: activityOf(hb),
|
|
2313
|
+
activity_updated_at: hb.activity_updated_at ?? null,
|
|
2314
|
+
activity_source: hb.activity_source ?? null,
|
|
2315
|
+
task_state: taskStateOf(hb),
|
|
2316
|
+
task_state_scope: "current" as const,
|
|
2317
|
+
task_state_updated_at: hb.task_state_updated_at ?? null,
|
|
2318
|
+
task_state_reason: hb.task_state_reason ?? null,
|
|
1793
2319
|
files_held: filesHeld,
|
|
1794
2320
|
peers_live: livePeers.length,
|
|
1795
2321
|
peers_stale: peersStale,
|
|
@@ -1803,6 +2329,9 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1803
2329
|
agents: m.agents.map((a) => ({
|
|
1804
2330
|
name: a.name || "unnamed",
|
|
1805
2331
|
task: a.task ?? null,
|
|
2332
|
+
activity: a.activity ?? "unknown",
|
|
2333
|
+
task_state: a.task_state ?? "active",
|
|
2334
|
+
task_state_reason: a.task_state_reason ?? null,
|
|
1806
2335
|
files: a.files_touched?.length ?? 0,
|
|
1807
2336
|
})),
|
|
1808
2337
|
})),
|
|
@@ -1811,7 +2340,9 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1811
2340
|
context_window: ctxUsage?.window ?? null,
|
|
1812
2341
|
timestamp_iso: new Date().toISOString(),
|
|
1813
2342
|
timestamp_local: timeStr,
|
|
2343
|
+
...(quality ? { quality } : {}),
|
|
1814
2344
|
...(finalization ? { finalization } : {}),
|
|
2345
|
+
...(sessionEnd ? { session_end: sessionEnd } : {}),
|
|
1815
2346
|
};
|
|
1816
2347
|
|
|
1817
2348
|
if (opts.json) {
|
|
@@ -1822,15 +2353,14 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1822
2353
|
|
|
1823
2354
|
const rows: Array<[string, string]> = [
|
|
1824
2355
|
["session", formatAge(ageSecs)],
|
|
2356
|
+
["activity", activityOf(hb)],
|
|
2357
|
+
["lifecycle", lifecycleLabel(hb)],
|
|
1825
2358
|
["context", ctxStr],
|
|
1826
2359
|
["files", filesStr],
|
|
1827
2360
|
["peers", peersStr],
|
|
1828
2361
|
["time", timeStr],
|
|
1829
2362
|
];
|
|
1830
|
-
//
|
|
1831
|
-
if (hb.turn_summary && hb.turn_summary.length > 0) {
|
|
1832
|
-
rows.splice(1, 0, ["last turn", hb.turn_summary]);
|
|
1833
|
-
}
|
|
2363
|
+
// Task gets full text; formatBox word-wraps to MAX_BOX_CONTENT_WIDTH.
|
|
1834
2364
|
if (hb.task && hb.task.length > 0) {
|
|
1835
2365
|
rows.splice(1, 0, ["task", hb.task]);
|
|
1836
2366
|
}
|
|
@@ -1845,11 +2375,59 @@ function runStatus(opts: { endTurn?: boolean; json?: boolean; sessionId?: string
|
|
|
1845
2375
|
: `${pendingCouncils.length} pending (${pendingCouncils[0]}, +${pendingCouncils.length - 1})`;
|
|
1846
2376
|
rows.splice(idx, 0, ["council", summary]);
|
|
1847
2377
|
}
|
|
2378
|
+
if (quality) {
|
|
2379
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
2380
|
+
rows.splice(idx, 0, ["quality", formatRunQuality(quality.status, quality.signal_ids)]);
|
|
2381
|
+
}
|
|
2382
|
+
if (sessionEnd) {
|
|
2383
|
+
const idx = rows.findIndex((row) => row[0] === "time");
|
|
2384
|
+
rows.splice(idx, 0, [
|
|
2385
|
+
"session end",
|
|
2386
|
+
sessionEnd.state === "queued" || sessionEnd.state === "already_requested"
|
|
2387
|
+
? "queued after this turn"
|
|
2388
|
+
: "recorded",
|
|
2389
|
+
]);
|
|
2390
|
+
}
|
|
1848
2391
|
// Box rendering needs predictable stdout regardless of TTY/pipe detection:
|
|
1849
2392
|
// agent runs this via Bash (no TTY) and pastes captured stdout into chat.
|
|
1850
2393
|
process.stdout.write(`${formatBox(displayName, rows)}\n`); // lint-ok-emission: chat-paste path; emit.text() auto-suppresses non-TTY
|
|
1851
2394
|
}
|
|
1852
2395
|
|
|
2396
|
+
function qualityForStatus(
|
|
2397
|
+
mode: "off" | "shadow" | "report",
|
|
2398
|
+
snapshot: RunQualitySnapshot | null,
|
|
2399
|
+
): {
|
|
2400
|
+
status: RunQualityStatus;
|
|
2401
|
+
signal_ids: string[];
|
|
2402
|
+
evaluated_at: string | null;
|
|
2403
|
+
fresh: boolean;
|
|
2404
|
+
} | null {
|
|
2405
|
+
if (mode !== "report") return null;
|
|
2406
|
+
const fresh = !!snapshot && Date.parse(snapshot.expires_at) > Date.now();
|
|
2407
|
+
if (!snapshot || !fresh) {
|
|
2408
|
+
return {
|
|
2409
|
+
status: "unknown",
|
|
2410
|
+
signal_ids: [],
|
|
2411
|
+
evaluated_at: snapshot?.evaluated_at ?? null,
|
|
2412
|
+
fresh: false,
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
return {
|
|
2416
|
+
status: snapshot.status,
|
|
2417
|
+
signal_ids: snapshot.signals
|
|
2418
|
+
.filter((signal) => signal.state === "active" && signal.severity !== "none")
|
|
2419
|
+
.map((signal) => signal.id)
|
|
2420
|
+
.sort()
|
|
2421
|
+
.slice(0, 3),
|
|
2422
|
+
evaluated_at: snapshot.evaluated_at,
|
|
2423
|
+
fresh: true,
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function formatRunQuality(status: RunQualityStatus, signalIds: string[]): string {
|
|
2428
|
+
return signalIds.length > 0 ? `${status} (${signalIds.join(", ")})` : status;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
1853
2431
|
function formatList(items: string[], cap: number, emptyLabel: string): string {
|
|
1854
2432
|
if (items.length === 0) return emptyLabel;
|
|
1855
2433
|
if (items.length <= cap) return items.join(", ");
|
|
@@ -1858,7 +2436,7 @@ function formatList(items: string[], cap: number, emptyLabel: string): string {
|
|
|
1858
2436
|
}
|
|
1859
2437
|
|
|
1860
2438
|
function formatPeers(
|
|
1861
|
-
peers:
|
|
2439
|
+
peers: StatusPeerHeartbeat[],
|
|
1862
2440
|
cap: number,
|
|
1863
2441
|
staleCount: number,
|
|
1864
2442
|
remoteMachines: RemoteMachine[] = [],
|
|
@@ -1891,6 +2469,38 @@ function formatPeers(
|
|
|
1891
2469
|
return main;
|
|
1892
2470
|
}
|
|
1893
2471
|
|
|
2472
|
+
type StatusPeerHeartbeat = {
|
|
2473
|
+
instance_id: string;
|
|
2474
|
+
last_heartbeat: string;
|
|
2475
|
+
files_touched?: string[];
|
|
2476
|
+
name?: string;
|
|
2477
|
+
platform?: string;
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
export function collectStatusPeerHealth(
|
|
2481
|
+
root: string,
|
|
2482
|
+
myOwner: string,
|
|
2483
|
+
nowMs = Date.now(),
|
|
2484
|
+
): { livePeers: StatusPeerHeartbeat[]; stale: number } {
|
|
2485
|
+
let rows: StatusPeerHeartbeat[] = [];
|
|
2486
|
+
try {
|
|
2487
|
+
rows = readLiveCoordinationRows(root);
|
|
2488
|
+
} catch {
|
|
2489
|
+
// V3 authority failures must not resurrect disposable cache rows.
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
const cutoffMs = nowMs - freshnessCutoffSecs() * 1000;
|
|
2493
|
+
const livePeers: StatusPeerHeartbeat[] = [];
|
|
2494
|
+
let stale = 0;
|
|
2495
|
+
for (const peer of rows) {
|
|
2496
|
+
if (peer.instance_id === myOwner) continue;
|
|
2497
|
+
const observedAtMs = Date.parse(peer.last_heartbeat);
|
|
2498
|
+
if (Number.isFinite(observedAtMs) && observedAtMs >= cutoffMs) livePeers.push(peer);
|
|
2499
|
+
else stale += 1;
|
|
2500
|
+
}
|
|
2501
|
+
return { livePeers, stale };
|
|
2502
|
+
}
|
|
2503
|
+
|
|
1894
2504
|
function fmtTokens(n: number): string {
|
|
1895
2505
|
if (n < 1000) return `${n}`;
|
|
1896
2506
|
if (n < 1000000) return `${Math.round(n / 1000)}K`;
|
|
@@ -2140,46 +2750,69 @@ interface HealEvent {
|
|
|
2140
2750
|
platform: string;
|
|
2141
2751
|
}
|
|
2142
2752
|
|
|
2143
|
-
/**
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
*/
|
|
2147
|
-
interface CanonicalEvent {
|
|
2148
|
-
event_type: string;
|
|
2753
|
+
/** Bounded diagnostic event shape projected from the canonical V3 ledger. */
|
|
2754
|
+
export interface CanonicalEvent {
|
|
2755
|
+
event_type: EventTypeV3;
|
|
2149
2756
|
ts: string;
|
|
2150
2757
|
instance_id?: string;
|
|
2151
2758
|
adapter?: string;
|
|
2152
|
-
|
|
2759
|
+
payload?: Record<string, unknown>;
|
|
2153
2760
|
}
|
|
2154
2761
|
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2762
|
+
export interface AgentDiagnosticEventRead {
|
|
2763
|
+
source: "v3";
|
|
2764
|
+
authoritative: boolean;
|
|
2765
|
+
reason?: string;
|
|
2766
|
+
truncated: boolean;
|
|
2767
|
+
bytes: number;
|
|
2768
|
+
events: CanonicalEvent[];
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
/** Read validated canonical V3 events. */
|
|
2772
|
+
export function readAgentDiagnosticEventsInWindow(
|
|
2773
|
+
root: string,
|
|
2774
|
+
cutoffMs: number,
|
|
2775
|
+
): AgentDiagnosticEventRead {
|
|
2776
|
+
const control = readEventV3ControlState(root);
|
|
2777
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
2778
|
+
const ledger = readLedgerV3(root);
|
|
2779
|
+
if (!ledger.complete) {
|
|
2780
|
+
return {
|
|
2781
|
+
source: "v3",
|
|
2782
|
+
authoritative: false,
|
|
2783
|
+
reason: `V3 ledger validation failed: ${ledger.diagnostics.map((item) => item.code).join(", ") || "incomplete"}`,
|
|
2784
|
+
truncated: false,
|
|
2785
|
+
bytes: ledger.bytes,
|
|
2786
|
+
events: [],
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
const events: CanonicalEvent[] = [];
|
|
2790
|
+
for (const { event } of ledger.events) {
|
|
2791
|
+
const tsMs = Date.parse(event.time.recorded_at);
|
|
2176
2792
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs) continue;
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2793
|
+
events.push({
|
|
2794
|
+
event_type: event.event_type,
|
|
2795
|
+
ts: event.time.recorded_at,
|
|
2796
|
+
instance_id: event.scope.instance_id,
|
|
2797
|
+
payload: event.payload as Record<string, unknown>,
|
|
2798
|
+
});
|
|
2180
2799
|
}
|
|
2800
|
+
return {
|
|
2801
|
+
source: "v3",
|
|
2802
|
+
authoritative: true,
|
|
2803
|
+
truncated: false,
|
|
2804
|
+
bytes: ledger.bytes,
|
|
2805
|
+
events,
|
|
2806
|
+
};
|
|
2181
2807
|
}
|
|
2182
|
-
return
|
|
2808
|
+
return {
|
|
2809
|
+
source: "v3",
|
|
2810
|
+
authoritative: false,
|
|
2811
|
+
reason: `V3 control state is ${control.state}`,
|
|
2812
|
+
truncated: false,
|
|
2813
|
+
bytes: 0,
|
|
2814
|
+
events: [],
|
|
2815
|
+
};
|
|
2183
2816
|
}
|
|
2184
2817
|
|
|
2185
2818
|
/** Normalize adapter event data into the heartbeat platform value. */
|
|
@@ -2187,19 +2820,21 @@ function adapterToPlatform(adapter: string | undefined): string {
|
|
|
2187
2820
|
if (adapter === "claude-code") return "claude-code";
|
|
2188
2821
|
if (adapter === "cursor") return "cursor";
|
|
2189
2822
|
if (adapter === "codex") return "codex";
|
|
2190
|
-
return "
|
|
2823
|
+
return "unknown";
|
|
2191
2824
|
}
|
|
2192
2825
|
|
|
2193
2826
|
/** Project a canonical health.* event into the HealEvent shape the aggregators
|
|
2194
2827
|
* already consume. Returns null for non-heal events. instance_id → display name
|
|
2195
2828
|
* via `nameById` (full-UUID keyed). */
|
|
2196
2829
|
function canonicalToHealEvent(ev: CanonicalEvent, nameById: Map<string, string>): HealEvent | null {
|
|
2197
|
-
if (ev.event_type !== "health.
|
|
2830
|
+
if (ev.event_type !== "health.observed") {
|
|
2198
2831
|
return null;
|
|
2199
2832
|
}
|
|
2200
|
-
const
|
|
2201
|
-
const
|
|
2202
|
-
|
|
2833
|
+
const data = ev.payload ?? {};
|
|
2834
|
+
const subsystem = typeof data.subsystem === "string" ? data.subsystem : "";
|
|
2835
|
+
if (subsystem !== "pidmap" && subsystem !== "heartbeat") return null;
|
|
2836
|
+
const kind = subsystem;
|
|
2837
|
+
const reason: "missing" | "stale" = data.condition === "stale" ? "stale" : "missing";
|
|
2203
2838
|
const instanceId = ev.instance_id ?? "";
|
|
2204
2839
|
const name = nameById.get(instanceId);
|
|
2205
2840
|
const agent = name ? `agent-${name}` : `agent-${instanceId.slice(0, 8) || "unknown"}`;
|
|
@@ -2280,13 +2915,12 @@ function runHealEvents(opts: {
|
|
|
2280
2915
|
process.exit(1);
|
|
2281
2916
|
}
|
|
2282
2917
|
|
|
2283
|
-
// Heal telemetry
|
|
2284
|
-
// (health.pidmap_heal / health.heartbeat_heal), emitted by the writer on
|
|
2285
|
-
// actual self-heal writes.
|
|
2918
|
+
// Heal telemetry is read from the canonical V3 ledger.
|
|
2286
2919
|
const cutoffMs = Date.now() - sinceSecs * 1000;
|
|
2287
2920
|
const nameById = buildNameById(root);
|
|
2288
2921
|
const events: HealEvent[] = [];
|
|
2289
|
-
|
|
2922
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
2923
|
+
for (const ev of diagnosticEvents.events) {
|
|
2290
2924
|
const heal = canonicalToHealEvent(ev, nameById);
|
|
2291
2925
|
if (heal) events.push(heal);
|
|
2292
2926
|
}
|
|
@@ -2323,6 +2957,11 @@ function runHealEvents(opts: {
|
|
|
2323
2957
|
|
|
2324
2958
|
const data = {
|
|
2325
2959
|
since: opts.since,
|
|
2960
|
+
telemetry: {
|
|
2961
|
+
source: diagnosticEvents.source,
|
|
2962
|
+
authoritative: diagnosticEvents.authoritative,
|
|
2963
|
+
reason: diagnosticEvents.reason,
|
|
2964
|
+
},
|
|
2326
2965
|
total: events.length,
|
|
2327
2966
|
by_reason: byReason,
|
|
2328
2967
|
by_kind: byKind,
|
|
@@ -2334,7 +2973,17 @@ function runHealEvents(opts: {
|
|
|
2334
2973
|
|
|
2335
2974
|
if (opts.csv) {
|
|
2336
2975
|
emit.config({ format: "csv" });
|
|
2337
|
-
emit.data(
|
|
2976
|
+
emit.data(
|
|
2977
|
+
data.telemetry.authoritative
|
|
2978
|
+
? recent
|
|
2979
|
+
: [
|
|
2980
|
+
{
|
|
2981
|
+
status: "unavailable",
|
|
2982
|
+
source: data.telemetry.source,
|
|
2983
|
+
reason: data.telemetry.reason,
|
|
2984
|
+
},
|
|
2985
|
+
],
|
|
2986
|
+
);
|
|
2338
2987
|
return;
|
|
2339
2988
|
}
|
|
2340
2989
|
if (opts.json) {
|
|
@@ -2349,6 +2998,13 @@ function runHealEvents(opts: {
|
|
|
2349
2998
|
lines.push(
|
|
2350
2999
|
`Heal events: ${events.length} total in last ${opts.since} (health.pidmap_heal + health.heartbeat_heal)`,
|
|
2351
3000
|
);
|
|
3001
|
+
if (!data.telemetry.authoritative) {
|
|
3002
|
+
lines.push(
|
|
3003
|
+
` unavailable: ${data.telemetry.reason ?? "diagnostic event source is incomplete"}`,
|
|
3004
|
+
);
|
|
3005
|
+
emit.text(`${lines.join("\n")}\n`);
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
2352
3008
|
lines.push("");
|
|
2353
3009
|
if (events.length === 0) {
|
|
2354
3010
|
lines.push(" (none; pid-map/heartbeat drift is not happening in this window)");
|
|
@@ -2400,7 +3056,13 @@ function runHealEvents(opts: {
|
|
|
2400
3056
|
interface HealthReport {
|
|
2401
3057
|
since: string;
|
|
2402
3058
|
generated_at: string;
|
|
3059
|
+
event_telemetry: {
|
|
3060
|
+
source: "v3";
|
|
3061
|
+
authoritative: boolean;
|
|
3062
|
+
reason?: string;
|
|
3063
|
+
};
|
|
2403
3064
|
active_agents: {
|
|
3065
|
+
source: "event-ledger-v3";
|
|
2404
3066
|
total: number;
|
|
2405
3067
|
by_platform: Record<string, number>;
|
|
2406
3068
|
by_kind: Record<string, number>;
|
|
@@ -2430,11 +3092,19 @@ interface HealthReport {
|
|
|
2430
3092
|
// bug (e.g. a stop-projection crash that caused ~200 errors/day until it was fixed).
|
|
2431
3093
|
hook_errors: {
|
|
2432
3094
|
total: number;
|
|
3095
|
+
last_1h: number;
|
|
3096
|
+
latest_at: string | null;
|
|
2433
3097
|
by_phase: Record<string, number>;
|
|
3098
|
+
by_error: Record<string, number>;
|
|
2434
3099
|
top: Array<{ phase: string; count: number; sample: string }>;
|
|
3100
|
+
top_errors: Array<{ error: string; count: number; phase: string }>;
|
|
3101
|
+
recent_top_errors: Array<{ error: string; count: number; phase: string }>;
|
|
2435
3102
|
};
|
|
2436
3103
|
// Canonical event stream growth + drain lag.
|
|
2437
3104
|
stream: {
|
|
3105
|
+
source: "v3";
|
|
3106
|
+
authoritative: boolean;
|
|
3107
|
+
reason?: string;
|
|
2438
3108
|
bytes: number;
|
|
2439
3109
|
lines: number;
|
|
2440
3110
|
cursor_backlog: number;
|
|
@@ -2446,31 +3116,269 @@ interface HealthReport {
|
|
|
2446
3116
|
count: number;
|
|
2447
3117
|
samples: string[];
|
|
2448
3118
|
};
|
|
3119
|
+
// V3 event-ledger producer health: open tool spans, pending finalization
|
|
3120
|
+
// requests, intake/diagnostics spool depth, span-count pressure.
|
|
3121
|
+
event_ledger: EventLedgerHealthV3;
|
|
2449
3122
|
anomalies: string[];
|
|
2450
3123
|
}
|
|
2451
3124
|
|
|
3125
|
+
/** Open-span soft watermark per producer state. The producer-state reader
|
|
3126
|
+
* hard-caps at 256 spans (a state file beyond it fails to load), so surfacing
|
|
3127
|
+
* pressure at half that gives room to act before reads start failing. */
|
|
3128
|
+
const SPAN_PRESSURE_SOFT_WATERMARK = 128;
|
|
3129
|
+
|
|
3130
|
+
export type EventLedgerHealthV3 =
|
|
3131
|
+
| { state: "unavailable"; reason: string }
|
|
3132
|
+
| {
|
|
3133
|
+
state: "live";
|
|
3134
|
+
mode: "candidate" | "active";
|
|
3135
|
+
open_spans: {
|
|
3136
|
+
total: number;
|
|
3137
|
+
generations: Array<{
|
|
3138
|
+
instance_id: string;
|
|
3139
|
+
generation_id: string;
|
|
3140
|
+
adapter: string;
|
|
3141
|
+
span_count: number;
|
|
3142
|
+
/** False = spans are open with NO open turn: the orphan signature
|
|
3143
|
+
* (a turn ended without its tool spans being closed). */
|
|
3144
|
+
turn_open: boolean;
|
|
3145
|
+
}>;
|
|
3146
|
+
};
|
|
3147
|
+
pending_finalizations: Array<{
|
|
3148
|
+
request_id: string;
|
|
3149
|
+
trigger: string;
|
|
3150
|
+
generation_id: string;
|
|
3151
|
+
age_ms: number;
|
|
3152
|
+
allowed_open_span_count: number;
|
|
3153
|
+
}>;
|
|
3154
|
+
intake_spool: {
|
|
3155
|
+
total: number;
|
|
3156
|
+
groups: Array<{ adapter: string; session_hash: string; count: number }>;
|
|
3157
|
+
};
|
|
3158
|
+
diagnostics_spool: {
|
|
3159
|
+
total: number;
|
|
3160
|
+
last_24h: number;
|
|
3161
|
+
last_1h: number;
|
|
3162
|
+
latest_at: string | null;
|
|
3163
|
+
by_category: Record<string, { total: number; last_24h: number }>;
|
|
3164
|
+
recent_by_category: Record<string, { last_1h: number; latest_at: string | null }>;
|
|
3165
|
+
};
|
|
3166
|
+
span_pressure: Array<{ instance_id: string; generation_id: string; span_count: number }>;
|
|
3167
|
+
collection_errors: string[];
|
|
3168
|
+
};
|
|
3169
|
+
|
|
3170
|
+
function errorText(error: unknown): string {
|
|
3171
|
+
return error instanceof Error ? error.message : String(error);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/**
|
|
3175
|
+
* Read-only health counters for the V3 event ledger's producer surfaces. Never
|
|
3176
|
+
* mutates ledger state (no control repair, no spool drain) and never throws:
|
|
3177
|
+
* a non-live route returns `{ state: "unavailable" }`, and each sub-surface
|
|
3178
|
+
* that fails to read lands in `collection_errors` instead of aborting the rest.
|
|
3179
|
+
*/
|
|
3180
|
+
export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): EventLedgerHealthV3 {
|
|
3181
|
+
let control: ReturnType<typeof readEventV3ControlState>;
|
|
3182
|
+
try {
|
|
3183
|
+
control = readEventV3ControlState(root);
|
|
3184
|
+
} catch (error) {
|
|
3185
|
+
return { state: "unavailable", reason: `control read failed: ${errorText(error)}` };
|
|
3186
|
+
}
|
|
3187
|
+
if (control.state !== "candidate" && control.state !== "active") {
|
|
3188
|
+
return {
|
|
3189
|
+
state: "unavailable",
|
|
3190
|
+
reason: `${control.state}: ${control.reason}`,
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
const collectionErrors: string[] = [];
|
|
3195
|
+
|
|
3196
|
+
// 1) Open tool spans per live (non-terminal) generation + span-count pressure.
|
|
3197
|
+
const generations: Extract<EventLedgerHealthV3, { state: "live" }>["open_spans"]["generations"] =
|
|
3198
|
+
[];
|
|
3199
|
+
const spanPressure: Extract<EventLedgerHealthV3, { state: "live" }>["span_pressure"] = [];
|
|
3200
|
+
try {
|
|
3201
|
+
for (const { state } of listHookProducerStateRecordsV3(root)) {
|
|
3202
|
+
if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
|
|
3203
|
+
spanPressure.push({
|
|
3204
|
+
instance_id: state.instance_id,
|
|
3205
|
+
generation_id: state.generation_id,
|
|
3206
|
+
span_count: state.spans.length,
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
if (state.spans.length === 0) continue;
|
|
3210
|
+
generations.push({
|
|
3211
|
+
instance_id: state.instance_id,
|
|
3212
|
+
generation_id: state.generation_id,
|
|
3213
|
+
adapter: state.adapter,
|
|
3214
|
+
span_count: state.spans.length,
|
|
3215
|
+
turn_open: Boolean(state.current_turn_id),
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
collectionErrors.push(`producer states unreadable: ${errorText(error)}`);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// 2) Pending finalization requests with age + trigger.
|
|
3223
|
+
const pending: Extract<EventLedgerHealthV3, { state: "live" }>["pending_finalizations"] = [];
|
|
3224
|
+
try {
|
|
3225
|
+
for (const request of listSessionFinalizationRequestsV3(root)) {
|
|
3226
|
+
if (request.status !== "pending") continue;
|
|
3227
|
+
const observedMs = Date.parse(request.observed_at);
|
|
3228
|
+
pending.push({
|
|
3229
|
+
request_id: request.request_id,
|
|
3230
|
+
trigger: request.trigger,
|
|
3231
|
+
generation_id: request.generation_id,
|
|
3232
|
+
age_ms: Number.isFinite(observedMs) ? Math.max(0, nowMs - observedMs) : -1,
|
|
3233
|
+
allowed_open_span_count: request.allowed_open_span_ids?.length ?? 0,
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
} catch (error) {
|
|
3237
|
+
collectionErrors.push(`finalization requests unreadable: ${errorText(error)}`);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// 3) Intake spool depth (queued hook signals awaiting a lease-holder drain).
|
|
3241
|
+
let intakeTotal = 0;
|
|
3242
|
+
const intakeGroups: Extract<EventLedgerHealthV3, { state: "live" }>["intake_spool"]["groups"] =
|
|
3243
|
+
[];
|
|
3244
|
+
try {
|
|
3245
|
+
for (const group of listHookIntakeGroupsV3(root)) {
|
|
3246
|
+
const count = listHookIntakeRecordsV3(group.directory).length;
|
|
3247
|
+
if (count === 0) continue;
|
|
3248
|
+
intakeTotal += count;
|
|
3249
|
+
intakeGroups.push({ adapter: group.adapter, session_hash: group.session_hash, count });
|
|
3250
|
+
}
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
collectionErrors.push(`intake spool unreadable: ${errorText(error)}`);
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// 4) Diagnostics spool counts by category. Filenames only (never the
|
|
3256
|
+
// contents): `<category>-<orderkey>.json` where the orderkey leads with a
|
|
3257
|
+
// zero-padded epoch-ms, which also gives the last-24h split for free.
|
|
3258
|
+
const byCategory: Record<string, { total: number; last_24h: number }> = {};
|
|
3259
|
+
const recentByCategory: Extract<
|
|
3260
|
+
EventLedgerHealthV3,
|
|
3261
|
+
{ state: "live" }
|
|
3262
|
+
>["diagnostics_spool"]["recent_by_category"] = {};
|
|
3263
|
+
let diagnosticsTotal = 0;
|
|
3264
|
+
let diagnostics24h = 0;
|
|
3265
|
+
let diagnostics1h = 0;
|
|
3266
|
+
let latestDiagnosticMs = Number.NEGATIVE_INFINITY;
|
|
3267
|
+
try {
|
|
3268
|
+
const diagnosticsDir = join(resolve(root), EVENT_V3_LEDGER_RELATIVE_ROOT, "diagnostics");
|
|
3269
|
+
if (existsSync(diagnosticsDir)) {
|
|
3270
|
+
const dayAgoMs = nowMs - 24 * 60 * 60 * 1000;
|
|
3271
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
3272
|
+
for (const name of readdirSync(diagnosticsDir)) {
|
|
3273
|
+
const match = /^(.+)-(\d{15})-\d{20}-\d+-[0-9a-f-]+\.json$/.exec(name);
|
|
3274
|
+
const category = match?.[1];
|
|
3275
|
+
const epochMs = Number(match?.[2]);
|
|
3276
|
+
if (!category) continue;
|
|
3277
|
+
const entry = byCategory[category] ?? { total: 0, last_24h: 0 };
|
|
3278
|
+
const recent = recentByCategory[category] ?? { last_1h: 0, latest_at: null };
|
|
3279
|
+
byCategory[category] = entry;
|
|
3280
|
+
recentByCategory[category] = recent;
|
|
3281
|
+
entry.total += 1;
|
|
3282
|
+
diagnosticsTotal += 1;
|
|
3283
|
+
if (Number.isFinite(epochMs)) {
|
|
3284
|
+
const recordedAt = new Date(epochMs).toISOString();
|
|
3285
|
+
if (!recent.latest_at || epochMs > Date.parse(recent.latest_at)) {
|
|
3286
|
+
recent.latest_at = recordedAt;
|
|
3287
|
+
}
|
|
3288
|
+
latestDiagnosticMs = Math.max(latestDiagnosticMs, epochMs);
|
|
3289
|
+
if (epochMs >= dayAgoMs) {
|
|
3290
|
+
entry.last_24h += 1;
|
|
3291
|
+
diagnostics24h += 1;
|
|
3292
|
+
}
|
|
3293
|
+
if (epochMs >= hourAgoMs) {
|
|
3294
|
+
recent.last_1h += 1;
|
|
3295
|
+
diagnostics1h += 1;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
} catch (error) {
|
|
3301
|
+
collectionErrors.push(`diagnostics spool unreadable: ${errorText(error)}`);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
return {
|
|
3305
|
+
state: "live",
|
|
3306
|
+
mode: control.state,
|
|
3307
|
+
open_spans: {
|
|
3308
|
+
total: generations.reduce((sum, generation) => sum + generation.span_count, 0),
|
|
3309
|
+
generations,
|
|
3310
|
+
},
|
|
3311
|
+
pending_finalizations: pending,
|
|
3312
|
+
intake_spool: { total: intakeTotal, groups: intakeGroups },
|
|
3313
|
+
diagnostics_spool: {
|
|
3314
|
+
total: diagnosticsTotal,
|
|
3315
|
+
last_24h: diagnostics24h,
|
|
3316
|
+
last_1h: diagnostics1h,
|
|
3317
|
+
latest_at: Number.isFinite(latestDiagnosticMs)
|
|
3318
|
+
? new Date(latestDiagnosticMs).toISOString()
|
|
3319
|
+
: null,
|
|
3320
|
+
by_category: byCategory,
|
|
3321
|
+
recent_by_category: recentByCategory,
|
|
3322
|
+
},
|
|
3323
|
+
span_pressure: spanPressure,
|
|
3324
|
+
collection_errors: collectionErrors,
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
|
|
2452
3328
|
/** Tally agent-hook failures (.harnery/debug/agent-hook.errors.ndjson) in the
|
|
2453
|
-
* window
|
|
2454
|
-
*
|
|
2455
|
-
function readHookErrors(
|
|
3329
|
+
* window by exact error and phase, while separating current-hour failures from
|
|
3330
|
+
* historical rows. Each line is {ts, error, phase, ...}. */
|
|
3331
|
+
export function readHookErrors(
|
|
2456
3332
|
root: string,
|
|
2457
3333
|
cutoffMs: number,
|
|
3334
|
+
nowMs = Date.now(),
|
|
2458
3335
|
): {
|
|
2459
3336
|
total: number;
|
|
3337
|
+
last1h: number;
|
|
3338
|
+
latestAt: string | null;
|
|
2460
3339
|
byPhase: Record<string, number>;
|
|
3340
|
+
byError: Record<string, number>;
|
|
2461
3341
|
top: Array<{ phase: string; count: number; sample: string }>;
|
|
3342
|
+
topErrors: Array<{ error: string; count: number; phase: string }>;
|
|
3343
|
+
recentTopErrors: Array<{ error: string; count: number; phase: string }>;
|
|
2462
3344
|
} {
|
|
2463
3345
|
const p = resolve(root, ".harnery", "debug", "agent-hook.errors.ndjson");
|
|
2464
3346
|
const byPhase: Record<string, number> = {};
|
|
3347
|
+
const byError: Record<string, number> = {};
|
|
2465
3348
|
const sampleByPhase: Record<string, string> = {};
|
|
3349
|
+
const errorPhaseCounts = new Map<string, { error: string; phase: string; count: number }>();
|
|
3350
|
+
const recentErrorPhaseCounts = new Map<string, { error: string; phase: string; count: number }>();
|
|
2466
3351
|
let total = 0;
|
|
2467
|
-
|
|
3352
|
+
let last1h = 0;
|
|
3353
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
3354
|
+
if (!existsSync(p)) {
|
|
3355
|
+
return {
|
|
3356
|
+
total: 0,
|
|
3357
|
+
last1h: 0,
|
|
3358
|
+
latestAt: null,
|
|
3359
|
+
byPhase,
|
|
3360
|
+
byError,
|
|
3361
|
+
top: [],
|
|
3362
|
+
topErrors: [],
|
|
3363
|
+
recentTopErrors: [],
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
2468
3366
|
let raw: string;
|
|
2469
3367
|
try {
|
|
2470
3368
|
raw = readFileSync(p, "utf8");
|
|
2471
3369
|
} catch {
|
|
2472
|
-
return {
|
|
3370
|
+
return {
|
|
3371
|
+
total: 0,
|
|
3372
|
+
last1h: 0,
|
|
3373
|
+
latestAt: null,
|
|
3374
|
+
byPhase,
|
|
3375
|
+
byError,
|
|
3376
|
+
top: [],
|
|
3377
|
+
topErrors: [],
|
|
3378
|
+
recentTopErrors: [],
|
|
3379
|
+
};
|
|
2473
3380
|
}
|
|
3381
|
+
const hourAgoMs = nowMs - 60 * 60 * 1000;
|
|
2474
3382
|
for (const line of raw.split("\n")) {
|
|
2475
3383
|
if (!line.trim()) continue;
|
|
2476
3384
|
try {
|
|
@@ -2478,8 +3386,21 @@ function readHookErrors(
|
|
|
2478
3386
|
const tsMs = e.ts ? Date.parse(e.ts) : Number.NaN;
|
|
2479
3387
|
if (!Number.isFinite(tsMs) || tsMs < cutoffMs) continue;
|
|
2480
3388
|
const phase = e.phase ?? "(unknown)";
|
|
3389
|
+
const error = e.error ?? "(unknown)";
|
|
2481
3390
|
byPhase[phase] = (byPhase[phase] ?? 0) + 1;
|
|
3391
|
+
byError[error] = (byError[error] ?? 0) + 1;
|
|
2482
3392
|
if (!sampleByPhase[phase] && e.error) sampleByPhase[phase] = e.error;
|
|
3393
|
+
const errorPhaseKey = `${error}\0${phase}`;
|
|
3394
|
+
const errorPhase = errorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
3395
|
+
errorPhase.count += 1;
|
|
3396
|
+
errorPhaseCounts.set(errorPhaseKey, errorPhase);
|
|
3397
|
+
latestMs = Math.max(latestMs, tsMs);
|
|
3398
|
+
if (tsMs >= hourAgoMs) {
|
|
3399
|
+
last1h += 1;
|
|
3400
|
+
const recent = recentErrorPhaseCounts.get(errorPhaseKey) ?? { error, phase, count: 0 };
|
|
3401
|
+
recent.count += 1;
|
|
3402
|
+
recentErrorPhaseCounts.set(errorPhaseKey, recent);
|
|
3403
|
+
}
|
|
2483
3404
|
total++;
|
|
2484
3405
|
} catch {
|
|
2485
3406
|
/* skip malformed */
|
|
@@ -2489,124 +3410,181 @@ function readHookErrors(
|
|
|
2489
3410
|
.sort((a, b) => b[1] - a[1])
|
|
2490
3411
|
.slice(0, 5)
|
|
2491
3412
|
.map(([phase, count]) => ({ phase, count, sample: sampleByPhase[phase] ?? "" }));
|
|
2492
|
-
|
|
3413
|
+
const topErrors = [...errorPhaseCounts.values()]
|
|
3414
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
3415
|
+
.slice(0, 5);
|
|
3416
|
+
const recentTopErrors = [...recentErrorPhaseCounts.values()]
|
|
3417
|
+
.sort((left, right) => right.count - left.count || left.error.localeCompare(right.error))
|
|
3418
|
+
.slice(0, 5);
|
|
3419
|
+
return {
|
|
3420
|
+
total,
|
|
3421
|
+
last1h,
|
|
3422
|
+
latestAt: Number.isFinite(latestMs) ? new Date(latestMs).toISOString() : null,
|
|
3423
|
+
byPhase,
|
|
3424
|
+
byError,
|
|
3425
|
+
top,
|
|
3426
|
+
topErrors,
|
|
3427
|
+
recentTopErrors,
|
|
3428
|
+
};
|
|
2493
3429
|
}
|
|
2494
3430
|
|
|
2495
3431
|
/** Canonical event stream size + drain lag (events appended after the cursor). */
|
|
2496
|
-
function readStreamStats(root: string):
|
|
2497
|
-
const
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
const cursorPath = resolve(root, ".harnery", ".events-cursor");
|
|
2507
|
-
if (existsSync(cursorPath)) {
|
|
2508
|
-
try {
|
|
2509
|
-
cursor = readFileSync(cursorPath, "utf8").trim() || null;
|
|
2510
|
-
} catch {
|
|
2511
|
-
/* ignore */
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
let lines = 0;
|
|
2515
|
-
let backlog = 0;
|
|
2516
|
-
let seenCursor = cursor === null; // no cursor → everything is "backlog"
|
|
2517
|
-
try {
|
|
2518
|
-
// Bounded tail read: the ledger grows without bound and a whole-file
|
|
2519
|
-
// readFileSync throws past V8's ~512MB string limit. lines/backlog are then
|
|
2520
|
-
// scoped to the scanned window; `bytes` (full size, via statSync above) still
|
|
2521
|
-
// reflects the true stream size for the health rollup.
|
|
2522
|
-
const { text } = readStreamTailBounded(streamPath, STREAM_SCAN_CAP_BYTES);
|
|
2523
|
-
for (const line of text.split("\n")) {
|
|
2524
|
-
if (!line.trim()) continue;
|
|
2525
|
-
lines++;
|
|
2526
|
-
if (seenCursor) {
|
|
2527
|
-
backlog++;
|
|
2528
|
-
} else if (cursor && line.includes(`"event_id":"${cursor}"`)) {
|
|
2529
|
-
seenCursor = true;
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
} catch {
|
|
2533
|
-
/* ignore */
|
|
2534
|
-
}
|
|
2535
|
-
return { bytes, lines, cursor_backlog: backlog };
|
|
3432
|
+
function readStreamStats(root: string): HealthReport["stream"] {
|
|
3433
|
+
const read = readAgentDiagnosticEventsInWindow(root, 0);
|
|
3434
|
+
return {
|
|
3435
|
+
source: "v3",
|
|
3436
|
+
authoritative: read.authoritative,
|
|
3437
|
+
...(read.reason ? { reason: read.reason } : {}),
|
|
3438
|
+
bytes: read.bytes,
|
|
3439
|
+
lines: read.events.length,
|
|
3440
|
+
cursor_backlog: 0,
|
|
3441
|
+
};
|
|
2536
3442
|
}
|
|
2537
3443
|
|
|
2538
3444
|
/** One rendered line in a trace. */
|
|
2539
|
-
interface TraceEntry {
|
|
3445
|
+
export interface TraceEntry {
|
|
2540
3446
|
ts: string;
|
|
2541
3447
|
event_type: string;
|
|
2542
3448
|
detail: string;
|
|
2543
3449
|
}
|
|
2544
3450
|
|
|
3451
|
+
export function traceInstanceIdsForEventSource(
|
|
3452
|
+
nativeInstanceIds: readonly string[],
|
|
3453
|
+
source: "v3",
|
|
3454
|
+
): string[] {
|
|
3455
|
+
void source;
|
|
3456
|
+
return nativeInstanceIds.map((instanceId) => liveInstanceIdV3(instanceId));
|
|
3457
|
+
}
|
|
3458
|
+
|
|
2545
3459
|
/** Map a canonical event to a concise trace line, or null to drop it. */
|
|
2546
|
-
function traceLine(ev: CanonicalEvent, allTools: boolean): TraceEntry | null {
|
|
2547
|
-
const d = (ev.
|
|
3460
|
+
export function traceLine(ev: CanonicalEvent, allTools: boolean): TraceEntry | null {
|
|
3461
|
+
const d = (ev.payload ?? {}) as Record<string, unknown>;
|
|
2548
3462
|
const s = (k: string): string => (typeof d[k] === "string" ? (d[k] as string) : "");
|
|
3463
|
+
const object = (value: unknown): Record<string, unknown> =>
|
|
3464
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
3465
|
+
? (value as Record<string, unknown>)
|
|
3466
|
+
: {};
|
|
2549
3467
|
const clip = (v: string, n = 70): string => (v.length <= n ? v : `${v.slice(0, n - 1)}…`);
|
|
3468
|
+
const recovery =
|
|
3469
|
+
typeof d.recovery === "object" && d.recovery !== null
|
|
3470
|
+
? (d.recovery as { reason?: unknown })
|
|
3471
|
+
: null;
|
|
3472
|
+
const recoveryReason = typeof recovery?.reason === "string" ? recovery.reason : "";
|
|
3473
|
+
const recoveryDetail = recoveryReason ? ` · RECOVERY reason=${recoveryReason}` : "";
|
|
2550
3474
|
let detail = "";
|
|
2551
3475
|
switch (ev.event_type) {
|
|
2552
|
-
case "session.
|
|
2553
|
-
detail =
|
|
3476
|
+
case "session.started":
|
|
3477
|
+
detail = "generation started";
|
|
3478
|
+
break;
|
|
3479
|
+
case "session.resumed":
|
|
3480
|
+
detail = "generation resumed";
|
|
3481
|
+
break;
|
|
3482
|
+
case "session.ended":
|
|
3483
|
+
detail = `outcome=${s("outcome") || "unknown"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
3484
|
+
break;
|
|
3485
|
+
case "session.termination_observed":
|
|
3486
|
+
detail = `provisional ${s("observation") || "termination"}${s("reason") ? ` · reason=${s("reason")}` : ""}`;
|
|
3487
|
+
break;
|
|
3488
|
+
case "agent.delegated":
|
|
3489
|
+
detail = `delegated child=${s("child_generation_id") || "unknown"}`;
|
|
3490
|
+
break;
|
|
3491
|
+
case "agent.started":
|
|
3492
|
+
detail = `child started=${s("child_generation_id") || "unknown"}`;
|
|
3493
|
+
break;
|
|
3494
|
+
case "agent.completed":
|
|
3495
|
+
detail = `child completed=${s("child_generation_id") || "unknown"} · outcome=${s("outcome") || "unknown"}`;
|
|
3496
|
+
break;
|
|
3497
|
+
case "turn.started":
|
|
3498
|
+
detail = `intent=${s("intent_kind") || "unknown"}`;
|
|
3499
|
+
break;
|
|
3500
|
+
case "turn.completed":
|
|
3501
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
3502
|
+
break;
|
|
3503
|
+
case "tool.requested": {
|
|
3504
|
+
const tool = object(d.tool);
|
|
3505
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} requested (content omitted)${recoveryDetail}`;
|
|
3506
|
+
break;
|
|
3507
|
+
}
|
|
3508
|
+
case "tool.completed": {
|
|
3509
|
+
const tool = object(d.tool);
|
|
3510
|
+
detail = `${typeof tool.name === "string" ? tool.name : "tool"} · outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
3511
|
+
break;
|
|
3512
|
+
}
|
|
3513
|
+
case "command.started":
|
|
3514
|
+
if (!allTools) return null;
|
|
3515
|
+
detail = "command started (content omitted)";
|
|
3516
|
+
break;
|
|
3517
|
+
case "command.completed":
|
|
3518
|
+
if (!allTools && !recoveryReason) return null;
|
|
3519
|
+
detail = `outcome=${s("outcome") || "unknown"}${recoveryDetail}`;
|
|
3520
|
+
break;
|
|
3521
|
+
case "coord.task_changed":
|
|
3522
|
+
case "coord.lifecycle_changed":
|
|
3523
|
+
case "coord.presence_changed":
|
|
3524
|
+
detail = `${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}${s("reason") ? ` · ${clip(s("reason"), 55)}` : ""}`;
|
|
3525
|
+
break;
|
|
3526
|
+
case "coord.status_observed":
|
|
3527
|
+
detail = s("status") || "status observed";
|
|
2554
3528
|
break;
|
|
2555
|
-
case "
|
|
2556
|
-
|
|
3529
|
+
case "coord.claim_changed": {
|
|
3530
|
+
const target = object(d.target);
|
|
3531
|
+
detail = `${s("operation") || "changed"}${typeof target.display === "string" ? ` · ${clip(target.display)}` : ""}`;
|
|
2557
3532
|
break;
|
|
2558
|
-
|
|
2559
|
-
|
|
3533
|
+
}
|
|
3534
|
+
case "coord.message_observed":
|
|
3535
|
+
detail = `${s("direction") || "observed"} · peer=${s("peer_instance_id") || "unknown"}`;
|
|
2560
3536
|
break;
|
|
2561
|
-
case "
|
|
2562
|
-
detail =
|
|
3537
|
+
case "coord.identity_attested":
|
|
3538
|
+
detail = `${s("identity_id") || "identity"} · method=${s("method") || "unknown"}`;
|
|
2563
3539
|
break;
|
|
2564
|
-
case "
|
|
2565
|
-
detail =
|
|
3540
|
+
case "wait.started":
|
|
3541
|
+
detail = s("kind") || "wait started";
|
|
2566
3542
|
break;
|
|
2567
|
-
case "
|
|
2568
|
-
detail = `
|
|
3543
|
+
case "wait.ended":
|
|
3544
|
+
detail = `outcome=${s("outcome") || "unknown"}`;
|
|
2569
3545
|
break;
|
|
2570
|
-
case "
|
|
2571
|
-
|
|
3546
|
+
case "artifact.observed": {
|
|
3547
|
+
const artifact = object(d.artifact);
|
|
3548
|
+
detail = `operation=${s("operation") || "observed"}${typeof artifact.kind === "string" ? ` · kind=${artifact.kind}` : ""}`;
|
|
2572
3549
|
break;
|
|
2573
|
-
|
|
2574
|
-
|
|
3550
|
+
}
|
|
3551
|
+
case "progress.observed":
|
|
3552
|
+
detail = s("kind") || "progress";
|
|
2575
3553
|
break;
|
|
2576
|
-
case "
|
|
2577
|
-
detail = "
|
|
3554
|
+
case "health.observed":
|
|
3555
|
+
detail = `${s("subsystem") || "unknown"} · ${s("severity") || "unknown"} · ${s("condition") || "unknown"}`;
|
|
2578
3556
|
break;
|
|
2579
|
-
case "
|
|
2580
|
-
detail = `${s("
|
|
3557
|
+
case "council.state_changed":
|
|
3558
|
+
detail = `${s("council_id") || "council"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2581
3559
|
break;
|
|
2582
|
-
case "
|
|
2583
|
-
|
|
2584
|
-
case "claim.conflict":
|
|
2585
|
-
detail = clip(s("path"));
|
|
3560
|
+
case "decision.state_changed":
|
|
3561
|
+
detail = `${s("decision_id") || "decision"} · ${s("prior_state") || "unknown"} → ${s("new_state") || "unknown"}`;
|
|
2586
3562
|
break;
|
|
2587
|
-
case "
|
|
2588
|
-
|
|
2589
|
-
detail = `reason=${s("reason")}`;
|
|
3563
|
+
case "lifecycle.recovered":
|
|
3564
|
+
detail = `recovered=${s("recovery_kind") || "unknown"}`;
|
|
2590
3565
|
break;
|
|
2591
|
-
case "
|
|
2592
|
-
detail =
|
|
3566
|
+
case "lifecycle.sweep_observed":
|
|
3567
|
+
detail = `${s("observation") || "sweep"}${typeof d.age_ms === "number" ? ` · age=${Math.round(d.age_ms / 1000)}s` : ""}`;
|
|
2593
3568
|
break;
|
|
2594
3569
|
default:
|
|
2595
|
-
// Noise unless --all-tools: per-line command.* + tool.post_use.
|
|
2596
|
-
if (!allTools) return null;
|
|
2597
|
-
if (
|
|
2598
|
-
ev.event_type === "tool.post_use" ||
|
|
2599
|
-
ev.event_type === "tool.post_use_failure" ||
|
|
2600
|
-
ev.event_type.startsWith("command.")
|
|
2601
|
-
) {
|
|
2602
|
-
detail = s("tool_name") || "";
|
|
2603
|
-
break;
|
|
2604
|
-
}
|
|
2605
3570
|
return null;
|
|
2606
3571
|
}
|
|
2607
3572
|
return { ts: ev.ts, event_type: ev.event_type, detail };
|
|
2608
3573
|
}
|
|
2609
3574
|
|
|
3575
|
+
export function pendingFinalizationTraceEntries(
|
|
3576
|
+
requests: readonly SessionFinalizationRequestV3[],
|
|
3577
|
+
instanceId: string,
|
|
3578
|
+
): TraceEntry[] {
|
|
3579
|
+
return requests
|
|
3580
|
+
.filter((request) => request.status === "pending" && request.instance_id === instanceId)
|
|
3581
|
+
.map((request) => ({
|
|
3582
|
+
ts: request.observed_at,
|
|
3583
|
+
event_type: "session.finalization_pending",
|
|
3584
|
+
detail: `trigger=${request.trigger} · request=${request.request_id}${request.allowed_open_span_ids?.length ? ` · allowed_open_spans=${request.allowed_open_span_ids.length}` : ""}`,
|
|
3585
|
+
}));
|
|
3586
|
+
}
|
|
3587
|
+
|
|
2610
3588
|
function runTrace(
|
|
2611
3589
|
name: string,
|
|
2612
3590
|
opts: { since?: string; limit: string; allTools?: boolean; json?: boolean },
|
|
@@ -2635,7 +3613,7 @@ function runTrace(
|
|
|
2635
3613
|
if (candidates.length === 1) targetId = candidates[0]![0];
|
|
2636
3614
|
else if (candidates.length > 1)
|
|
2637
3615
|
targetId = candidates.map(([id]) => id).join("\x00"); // sentinel; resolved below
|
|
2638
|
-
else if (/^[0-9a-
|
|
3616
|
+
else if (/^(?:inst_)?[0-9a-z._-]{8,}$/i.test(wanted)) targetId = wanted;
|
|
2639
3617
|
}
|
|
2640
3618
|
if (!targetId) {
|
|
2641
3619
|
emit.error({
|
|
@@ -2647,37 +3625,16 @@ function runTrace(
|
|
|
2647
3625
|
|
|
2648
3626
|
const sinceMs = opts.since ? Date.now() - (parseWindowSecs(opts.since) ?? 0) * 1000 : 0;
|
|
2649
3627
|
const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 200);
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
// Bounded read: the ledger grows without bound and a whole-file readFileSync
|
|
2654
|
-
// throws past V8's ~512MB string limit. A trace of an agent whose events all
|
|
2655
|
-
// predate the window is reported as truncated rather than crashing.
|
|
2656
|
-
const streamPath = resolve(root, ".harnery", "events.ndjson");
|
|
3628
|
+
const diagnosticRead = readAgentDiagnosticEventsInWindow(root, sinceMs);
|
|
3629
|
+
const nativeCandidateIds = targetId.includes("\x00") ? targetId.split("\x00") : [targetId];
|
|
3630
|
+
const candidateIds = traceInstanceIdsForEventSource(nativeCandidateIds, diagnosticRead.source);
|
|
2657
3631
|
const byId = new Map<string, CanonicalEvent[]>();
|
|
2658
|
-
const
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
if (!line) continue;
|
|
2664
|
-
try {
|
|
2665
|
-
const ev = JSON.parse(line) as CanonicalEvent;
|
|
2666
|
-
if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
|
|
2667
|
-
if (sinceMs && Date.parse(ev.ts) < sinceMs) continue;
|
|
2668
|
-
const arr = byId.get(ev.instance_id) ?? [];
|
|
2669
|
-
arr.push(ev);
|
|
2670
|
-
byId.set(ev.instance_id, arr);
|
|
2671
|
-
} catch {
|
|
2672
|
-
/* skip */
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
if (streamTruncated) {
|
|
2676
|
-
process.stderr.write(
|
|
2677
|
-
`note: event ledger exceeds ${Math.round(STREAM_SCAN_CAP_BYTES / 1024 / 1024)}MB; traced only the most recent window (older events omitted)\n`,
|
|
2678
|
-
);
|
|
3632
|
+
for (const ev of diagnosticRead.events) {
|
|
3633
|
+
if (!ev.instance_id || !candidateIds.includes(ev.instance_id)) continue;
|
|
3634
|
+
const arr = byId.get(ev.instance_id) ?? [];
|
|
3635
|
+
arr.push(ev);
|
|
3636
|
+
byId.set(ev.instance_id, arr);
|
|
2679
3637
|
}
|
|
2680
|
-
|
|
2681
3638
|
// If the name mapped to multiple instances, trace the one with the latest event.
|
|
2682
3639
|
let resolvedId = candidateIds[0]!;
|
|
2683
3640
|
if (candidateIds.length > 1) {
|
|
@@ -2693,20 +3650,86 @@ function runTrace(
|
|
|
2693
3650
|
}
|
|
2694
3651
|
|
|
2695
3652
|
const events = byId.get(resolvedId) ?? [];
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
3653
|
+
let state = foldSessionState(events, { instance_id: resolvedId });
|
|
3654
|
+
let sessionState: "live" | "ended" = events.some((event) => event.event_type === "session.ended")
|
|
3655
|
+
? "ended"
|
|
3656
|
+
: "live";
|
|
3657
|
+
if (diagnosticRead.source === "v3" && diagnosticRead.authoritative) {
|
|
3658
|
+
try {
|
|
3659
|
+
const view = projectCoordinationViewV3(readLedgerV3(root));
|
|
3660
|
+
const liveGeneration = view.instances[resolvedId];
|
|
3661
|
+
const terminalGeneration = Object.values(view.terminal_generations).find(
|
|
3662
|
+
(candidate) => candidate.instance_id === resolvedId,
|
|
3663
|
+
);
|
|
3664
|
+
const generation = liveGeneration ?? terminalGeneration;
|
|
3665
|
+
if (generation) {
|
|
3666
|
+
sessionState = terminalGeneration && !liveGeneration ? "ended" : "live";
|
|
3667
|
+
const lifecycleState = generation.lifecycle_state;
|
|
3668
|
+
state = {
|
|
3669
|
+
activity: generation.activity === "terminal" ? "idle" : generation.activity,
|
|
3670
|
+
activity_updated_at: generation.last_observed_at,
|
|
3671
|
+
activity_source: "event-v3-coordination-view",
|
|
3672
|
+
task_state:
|
|
3673
|
+
lifecycleState === "active" || lifecycleState === "blocked" || lifecycleState === "done"
|
|
3674
|
+
? lifecycleState
|
|
3675
|
+
: "active",
|
|
3676
|
+
task_state_updated_at: generation.lifecycle_state_updated_at,
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
} catch {
|
|
3680
|
+
diagnosticRead.authoritative = false;
|
|
3681
|
+
diagnosticRead.reason = "V3 coordination projection is unavailable";
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
let pendingFinalizations: SessionFinalizationRequestV3[] = [];
|
|
3685
|
+
if (diagnosticRead.source === "v3") {
|
|
3686
|
+
try {
|
|
3687
|
+
pendingFinalizations = listSessionFinalizationRequestsV3(root).filter(
|
|
3688
|
+
(request) => request.status === "pending" && request.instance_id === resolvedId,
|
|
3689
|
+
);
|
|
3690
|
+
} catch {
|
|
3691
|
+
diagnosticRead.authoritative = false;
|
|
3692
|
+
diagnosticRead.reason = "V3 finalization requests are unreadable";
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
const lines = [
|
|
3696
|
+
...events
|
|
3697
|
+
.map((ev) => traceLine(ev, !!opts.allTools))
|
|
3698
|
+
.filter((l): l is TraceEntry => l !== null),
|
|
3699
|
+
...pendingFinalizationTraceEntries(pendingFinalizations, resolvedId),
|
|
3700
|
+
]
|
|
2699
3701
|
// Sort by timestamp, not file order: codex replays events (original ts,
|
|
2700
3702
|
// appended later), so append-order ≠ chronological order.
|
|
2701
3703
|
.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
2702
3704
|
const shown = lines.slice(-limit);
|
|
2703
|
-
const
|
|
3705
|
+
const nativeResolvedId =
|
|
3706
|
+
diagnosticRead.source === "v3"
|
|
3707
|
+
? nativeCandidateIds[candidateIds.indexOf(resolvedId)]
|
|
3708
|
+
: resolvedId;
|
|
3709
|
+
const displayName =
|
|
3710
|
+
nameById.get(resolvedId) ??
|
|
3711
|
+
(nativeResolvedId ? nameById.get(nativeResolvedId) : undefined) ??
|
|
3712
|
+
resolvedId.slice(0, 8);
|
|
2704
3713
|
|
|
2705
3714
|
const result = {
|
|
3715
|
+
event_source: {
|
|
3716
|
+
contract: diagnosticRead.source,
|
|
3717
|
+
authoritative: diagnosticRead.authoritative,
|
|
3718
|
+
reason: diagnosticRead.reason ?? null,
|
|
3719
|
+
},
|
|
2706
3720
|
name: displayName,
|
|
2707
3721
|
instance_id: resolvedId,
|
|
2708
3722
|
other_instances: candidateIds.filter((id) => id !== resolvedId),
|
|
3723
|
+
pending_finalizations: pendingFinalizations,
|
|
2709
3724
|
total_events: events.length,
|
|
3725
|
+
session_state: sessionState,
|
|
3726
|
+
activity: state.activity,
|
|
3727
|
+
activity_updated_at: state.activity_updated_at ?? null,
|
|
3728
|
+
activity_source: state.activity_source ?? null,
|
|
3729
|
+
task_state: state.task_state,
|
|
3730
|
+
task_state_scope: sessionState === "ended" ? "historical" : "current",
|
|
3731
|
+
task_state_updated_at: state.task_state_updated_at ?? null,
|
|
3732
|
+
task_state_reason: state.task_state_reason ?? null,
|
|
2710
3733
|
shown: shown.length,
|
|
2711
3734
|
entries: shown,
|
|
2712
3735
|
};
|
|
@@ -2718,6 +3741,12 @@ function runTrace(
|
|
|
2718
3741
|
emit.data(result);
|
|
2719
3742
|
const header = `Trace: agent-${displayName} (${resolvedId.slice(0, 8)}…) ${events.length} events${result.other_instances.length ? ` · ${result.other_instances.length} older instance(s) of this name` : ""}`;
|
|
2720
3743
|
process.stdout.write(`${header}\n`); // lint-ok-emission: human trace view
|
|
3744
|
+
process.stdout.write(
|
|
3745
|
+
` activity=${state.activity} · session=${sessionState} · lifecycle=${sessionState === "ended" ? `historical(${state.task_state})` : state.task_state}${sessionState !== "ended" && state.task_state === "blocked" && state.task_state_reason ? `: ${state.task_state_reason}` : ""}\n`,
|
|
3746
|
+
); // lint-ok-emission: human trace view
|
|
3747
|
+
if (!diagnosticRead.authoritative) {
|
|
3748
|
+
process.stdout.write(` unavailable: ${diagnosticRead.reason ?? "event source incomplete"}\n`); // lint-ok-emission: explicit no-fallback diagnostic
|
|
3749
|
+
}
|
|
2721
3750
|
if (shown.length === 0) {
|
|
2722
3751
|
process.stdout.write(" (no events)\n"); // lint-ok-emission: human trace view
|
|
2723
3752
|
return;
|
|
@@ -2728,6 +3757,73 @@ function runTrace(
|
|
|
2728
3757
|
}
|
|
2729
3758
|
}
|
|
2730
3759
|
|
|
3760
|
+
export interface ActiveAgentHealthSummary {
|
|
3761
|
+
source: "event-ledger-v3";
|
|
3762
|
+
total: number;
|
|
3763
|
+
by_platform: Record<string, number>;
|
|
3764
|
+
by_kind: Record<string, number>;
|
|
3765
|
+
by_schema_version: Record<string, number>;
|
|
3766
|
+
stale: number;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
type ActiveHealthHeartbeat = {
|
|
3770
|
+
instance_id: string;
|
|
3771
|
+
platform?: string;
|
|
3772
|
+
kind?: string;
|
|
3773
|
+
schema_version?: number;
|
|
3774
|
+
last_heartbeat?: string;
|
|
3775
|
+
};
|
|
3776
|
+
|
|
3777
|
+
/**
|
|
3778
|
+
* V3 generations, not disposable heartbeat caches, are the active-agent authority.
|
|
3779
|
+
*/
|
|
3780
|
+
export function collectActiveAgentHealth(
|
|
3781
|
+
root: string,
|
|
3782
|
+
nowMs = Date.now(),
|
|
3783
|
+
): ActiveAgentHealthSummary {
|
|
3784
|
+
const source: ActiveAgentHealthSummary["source"] = "event-ledger-v3";
|
|
3785
|
+
let heartbeats: ActiveHealthHeartbeat[] = [];
|
|
3786
|
+
try {
|
|
3787
|
+
const control = readEventV3ControlState(root);
|
|
3788
|
+
if (control.state === "candidate" || control.state === "active") {
|
|
3789
|
+
heartbeats = readLiveCoordinationRows(root);
|
|
3790
|
+
}
|
|
3791
|
+
} catch {
|
|
3792
|
+
// The event-ledger section reports the authority read failure. Do not
|
|
3793
|
+
// substitute stale cache rows for a live V3 route.
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
const byPlatform: Record<string, number> = {};
|
|
3797
|
+
const byKind: Record<string, number> = {};
|
|
3798
|
+
const bySchema: Record<string, number> = {};
|
|
3799
|
+
let stale = 0;
|
|
3800
|
+
for (const heartbeat of heartbeats) {
|
|
3801
|
+
const platform = formatPlatformLabel(heartbeat.platform);
|
|
3802
|
+
byPlatform[platform] = (byPlatform[platform] ?? 0) + 1;
|
|
3803
|
+
const kind = heartbeat.kind ?? "unknown";
|
|
3804
|
+
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
3805
|
+
// These rows exist only after an authority-safe V3 projection. The
|
|
3806
|
+
// disposable heartbeat file has its own cache-format version, which must
|
|
3807
|
+
// not be reported as the ledger schema version.
|
|
3808
|
+
bySchema.v3 = (bySchema.v3 ?? 0) + 1;
|
|
3809
|
+
const lastObservedMs = heartbeat.last_heartbeat
|
|
3810
|
+
? Date.parse(heartbeat.last_heartbeat)
|
|
3811
|
+
: Number.NaN;
|
|
3812
|
+
const ageMs = Number.isFinite(lastObservedMs)
|
|
3813
|
+
? nowMs - lastObservedMs
|
|
3814
|
+
: Number.POSITIVE_INFINITY;
|
|
3815
|
+
if (ageMs > freshnessCutoffSecs() * 1000) stale += 1;
|
|
3816
|
+
}
|
|
3817
|
+
return {
|
|
3818
|
+
source,
|
|
3819
|
+
total: heartbeats.length,
|
|
3820
|
+
by_platform: byPlatform,
|
|
3821
|
+
by_kind: byKind,
|
|
3822
|
+
by_schema_version: bySchema,
|
|
3823
|
+
stale,
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
|
|
2731
3827
|
function runHealth(opts: { since: string; json?: boolean }): void {
|
|
2732
3828
|
if (opts.json) emit.config({ format: "json" });
|
|
2733
3829
|
|
|
@@ -2749,12 +3845,13 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2749
3845
|
process.exit(1);
|
|
2750
3846
|
}
|
|
2751
3847
|
|
|
2752
|
-
const
|
|
3848
|
+
const nowMs = Date.now();
|
|
3849
|
+
const cutoffMs = nowMs - sinceSecs * 1000;
|
|
2753
3850
|
const activeDir = resolve(root, ".harnery/active");
|
|
2754
3851
|
const councilsDir = resolve(root, ".harnery/councils");
|
|
2755
3852
|
|
|
2756
|
-
// Coordination telemetry reads from the canonical
|
|
2757
|
-
// Heals come from health
|
|
3853
|
+
// Coordination telemetry reads from the canonical V3 ledger.
|
|
3854
|
+
// Heals come from health.observed; council activity from council.state_changed.
|
|
2758
3855
|
const heal: HealEvent[] = [];
|
|
2759
3856
|
let councilAdvanced = 0;
|
|
2760
3857
|
let councilClosed = 0;
|
|
@@ -2763,33 +3860,33 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2763
3860
|
const sweptByReason: Record<string, number> = {};
|
|
2764
3861
|
|
|
2765
3862
|
const nameById = buildNameById(root);
|
|
2766
|
-
|
|
3863
|
+
const diagnosticEvents = readAgentDiagnosticEventsInWindow(root, cutoffMs);
|
|
3864
|
+
for (const ev of diagnosticEvents.events) {
|
|
2767
3865
|
const healEv = canonicalToHealEvent(ev, nameById);
|
|
2768
3866
|
if (healEv) {
|
|
2769
3867
|
heal.push(healEv);
|
|
2770
3868
|
continue;
|
|
2771
3869
|
}
|
|
3870
|
+
if (ev.event_type === "council.state_changed") {
|
|
3871
|
+
const state = String(ev.payload?.new_state ?? "");
|
|
3872
|
+
if (state === "round_open") councilAdvanced++;
|
|
3873
|
+
if (state === "closed") councilClosed++;
|
|
3874
|
+
if (state === "archived") councilArchived++;
|
|
3875
|
+
continue;
|
|
3876
|
+
}
|
|
2772
3877
|
switch (ev.event_type) {
|
|
2773
|
-
case "
|
|
2774
|
-
councilAdvanced++;
|
|
2775
|
-
break;
|
|
2776
|
-
case "council.close":
|
|
2777
|
-
councilClosed++;
|
|
2778
|
-
break;
|
|
2779
|
-
case "council.archive":
|
|
2780
|
-
councilArchived++;
|
|
2781
|
-
break;
|
|
2782
|
-
case "health.heartbeat_swept": {
|
|
3878
|
+
case "lifecycle.sweep_observed": {
|
|
2783
3879
|
sweptTotal++;
|
|
2784
|
-
const reason = String(
|
|
3880
|
+
const reason = String(ev.payload?.observation ?? "unknown");
|
|
2785
3881
|
sweptByReason[reason] = (sweptByReason[reason] ?? 0) + 1;
|
|
2786
3882
|
break;
|
|
2787
3883
|
}
|
|
2788
3884
|
}
|
|
2789
3885
|
}
|
|
2790
3886
|
|
|
2791
|
-
const hookErrors = readHookErrors(root, cutoffMs);
|
|
3887
|
+
const hookErrors = readHookErrors(root, cutoffMs, nowMs);
|
|
2792
3888
|
const stream = readStreamStats(root);
|
|
3889
|
+
const eventLedger = collectEventLedgerHealthV3(root);
|
|
2793
3890
|
|
|
2794
3891
|
// Canonical health.* events carry the full instance_id, already resolved to
|
|
2795
3892
|
// `agent-<name>` (or `agent-<hex8>` fallback) by canonicalToHealEvent via
|
|
@@ -2809,19 +3906,13 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2809
3906
|
.slice(0, 5)
|
|
2810
3907
|
.map(([agent, count]) => ({ agent, count }));
|
|
2811
3908
|
|
|
2812
|
-
|
|
2813
|
-
const activeByPlatform: Record<string, number> = {};
|
|
2814
|
-
const activeByKind: Record<string, number> = {};
|
|
2815
|
-
const activeBySchema: Record<string, number> = {};
|
|
2816
|
-
let activeTotal = 0;
|
|
2817
|
-
let staleHeartbeats = 0;
|
|
3909
|
+
const activeAgents = collectActiveAgentHealth(root, nowMs);
|
|
2818
3910
|
// Zombies: files in active/ that are broken: unparseable, nameless, or an
|
|
2819
3911
|
// absurd (epoch-ish) last_heartbeat. These show as `agent-unknown` ghosts and
|
|
2820
3912
|
// mean dead files the sweep isn't reaping.
|
|
2821
3913
|
let zombieCount = 0;
|
|
2822
3914
|
const zombieSamples: string[] = [];
|
|
2823
3915
|
const ABSURD_AGE_MS = 24 * 60 * 60 * 1000; // > 1 day = clearly not a live, self-healing agent
|
|
2824
|
-
const nowMs = Date.now();
|
|
2825
3916
|
if (existsSync(activeDir)) {
|
|
2826
3917
|
for (const file of readdirSync(activeDir)) {
|
|
2827
3918
|
if (!file.endsWith(".json")) continue;
|
|
@@ -2838,17 +3929,8 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2838
3929
|
zombieSamples.push(`${idFromFile.slice(0, 12)} (unparseable/no-id)`);
|
|
2839
3930
|
continue;
|
|
2840
3931
|
}
|
|
2841
|
-
activeTotal++;
|
|
2842
|
-
const platform = formatPlatformLabel(hb.platform);
|
|
2843
|
-
activeByPlatform[platform] = (activeByPlatform[platform] ?? 0) + 1;
|
|
2844
|
-
const kind = hb.kind ?? "unknown";
|
|
2845
|
-
activeByKind[kind] = (activeByKind[kind] ?? 0) + 1;
|
|
2846
|
-
const sv = (hb as { schema_version?: number }).schema_version;
|
|
2847
|
-
const schemaKey = sv === undefined ? "v0" : `v${sv}`;
|
|
2848
|
-
activeBySchema[schemaKey] = (activeBySchema[schemaKey] ?? 0) + 1;
|
|
2849
3932
|
const lastHbMs = hb.last_heartbeat ? Date.parse(hb.last_heartbeat) : Number.NaN;
|
|
2850
3933
|
const ageMs = Number.isFinite(lastHbMs) ? nowMs - lastHbMs : Number.POSITIVE_INFINITY;
|
|
2851
|
-
if (ageMs > freshnessCutoffSecs() * 1000) staleHeartbeats++;
|
|
2852
3934
|
// Zombie heuristics on a parseable heartbeat: no name, or an age so large
|
|
2853
3935
|
// it can only be a broken/epoch timestamp (a real agent would have healed).
|
|
2854
3936
|
if (!hb.name || hb.name === "unknown" || ageMs > ABSURD_AGE_MS) {
|
|
@@ -2880,37 +3962,38 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2880
3962
|
);
|
|
2881
3963
|
}
|
|
2882
3964
|
}
|
|
2883
|
-
if (
|
|
3965
|
+
if (activeAgents.stale > 0) {
|
|
3966
|
+
const noun = activeAgents.source === "event-ledger-v3" ? "V3 generation" : "heartbeat";
|
|
3967
|
+
const action =
|
|
3968
|
+
activeAgents.source === "event-ledger-v3"
|
|
3969
|
+
? "lifecycle reconciliation may be delayed"
|
|
3970
|
+
: "heal mechanism may not be firing";
|
|
2884
3971
|
anomalies.push(
|
|
2885
|
-
`${
|
|
3972
|
+
`${activeAgents.stale} active ${noun}(s) without activity for ${Math.floor(freshnessCutoffSecs() / 60)}min; ${action}`,
|
|
2886
3973
|
);
|
|
2887
3974
|
}
|
|
2888
|
-
const
|
|
3975
|
+
const expectedSchema = "v3";
|
|
3976
|
+
const unexpectedSchemas = Object.keys(activeAgents.by_schema_version).filter(
|
|
3977
|
+
(schema) => schema !== expectedSchema,
|
|
3978
|
+
);
|
|
2889
3979
|
if (unexpectedSchemas.length > 0) {
|
|
2890
3980
|
anomalies.push(
|
|
2891
|
-
`Unexpected
|
|
3981
|
+
`Unexpected active-agent schema versions in use: ${unexpectedSchemas.join(", ")} (expected ${expectedSchema})`,
|
|
2892
3982
|
);
|
|
2893
3983
|
}
|
|
2894
|
-
//
|
|
2895
|
-
//
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2900
|
-
? `: top phase '${top.phase}' x${top.count}${top.sample ? ` (${top.sample.slice(0, 80)})` : ""}`
|
|
2901
|
-
: "";
|
|
2902
|
-
anomalies.push(`agent-hook errored ${hookErrors.total}x in ${opts.since}${detail}`);
|
|
3984
|
+
// Only recent hook failures are active anomalies. Historical rows remain in
|
|
3985
|
+
// the report with exact error signatures and their latest timestamp.
|
|
3986
|
+
if (hookErrors.last1h > 0) {
|
|
3987
|
+
const top = hookErrors.recentTopErrors[0];
|
|
3988
|
+
const detail = top ? `: top '${top.error.slice(0, 80)}' x${top.count} (${top.phase})` : "";
|
|
3989
|
+
anomalies.push(`agent-hook errored ${hookErrors.last1h}x in the last hour${detail}`);
|
|
2903
3990
|
}
|
|
2904
3991
|
if (stream.cursor_backlog > 500) {
|
|
2905
3992
|
anomalies.push(
|
|
2906
3993
|
`projection cursor is ${stream.cursor_backlog} events behind; drain lagging (stop projection may be failing)`,
|
|
2907
3994
|
);
|
|
2908
3995
|
}
|
|
2909
|
-
//
|
|
2910
|
-
// append-only ledger (names + forensics for the life of the log), and
|
|
2911
|
-
// consumeSince tail-reads it, so size no longer drives latency. The
|
|
2912
|
-
// bytes/lines still surface in the summary line for visibility;
|
|
2913
|
-
// cursor_backlog above is the real drain-lag signal.
|
|
3996
|
+
// Raw V3 stream size is not an anomaly; catalog rotation bounds readers.
|
|
2914
3997
|
if ((sweptByReason.unparseable ?? 0) > 0) {
|
|
2915
3998
|
anomalies.push(
|
|
2916
3999
|
`${sweptByReason.unparseable} heartbeat(s) swept as unparseable in ${opts.since}; possible corruption or a non-atomic writer`,
|
|
@@ -2921,16 +4004,46 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2921
4004
|
`${zombieCount} zombie heartbeat(s) in active/ (${zombieSamples.join(", ")}); broken files the sweep isn't reaping`,
|
|
2922
4005
|
);
|
|
2923
4006
|
}
|
|
4007
|
+
if (eventLedger.state === "live") {
|
|
4008
|
+
// Open spans whose generation has no open turn: the turn ended without its
|
|
4009
|
+
// tool spans closing, so an explicit end can only queue, never finalize.
|
|
4010
|
+
const orphanGenerations = eventLedger.open_spans.generations.filter(
|
|
4011
|
+
(generation) => !generation.turn_open,
|
|
4012
|
+
);
|
|
4013
|
+
if (orphanGenerations.length > 0) {
|
|
4014
|
+
const orphanSpans = orphanGenerations.reduce(
|
|
4015
|
+
(sum, generation) => sum + generation.span_count,
|
|
4016
|
+
0,
|
|
4017
|
+
);
|
|
4018
|
+
anomalies.push(
|
|
4019
|
+
`${orphanSpans} open tool span(s) across ${orphanGenerations.length} generation(s) with no open turn; orphaned spans block clean session end`,
|
|
4020
|
+
);
|
|
4021
|
+
}
|
|
4022
|
+
for (const pressure of eventLedger.span_pressure) {
|
|
4023
|
+
anomalies.push(
|
|
4024
|
+
`${pressure.instance_id} (${pressure.generation_id}) holds ${pressure.span_count} open spans (soft watermark ${SPAN_PRESSURE_SOFT_WATERMARK}; the reader's 256-span cap makes the state unreadable)`,
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
for (const collectionError of eventLedger.collection_errors) {
|
|
4028
|
+
anomalies.push(`event-ledger health: ${collectionError}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
2924
4031
|
|
|
2925
4032
|
const report: HealthReport = {
|
|
2926
4033
|
since: opts.since,
|
|
2927
4034
|
generated_at: new Date().toISOString(),
|
|
4035
|
+
event_telemetry: {
|
|
4036
|
+
source: diagnosticEvents.source,
|
|
4037
|
+
authoritative: diagnosticEvents.authoritative,
|
|
4038
|
+
...(diagnosticEvents.reason ? { reason: diagnosticEvents.reason } : {}),
|
|
4039
|
+
},
|
|
2928
4040
|
active_agents: {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
4041
|
+
source: activeAgents.source,
|
|
4042
|
+
total: activeAgents.total,
|
|
4043
|
+
by_platform: activeAgents.by_platform,
|
|
4044
|
+
by_kind: activeAgents.by_kind,
|
|
4045
|
+
by_schema_version: activeAgents.by_schema_version,
|
|
4046
|
+
stale: activeAgents.stale,
|
|
2934
4047
|
},
|
|
2935
4048
|
heal_events: {
|
|
2936
4049
|
total: heal.length,
|
|
@@ -2946,9 +4059,19 @@ function runHealth(opts: { since: string; json?: boolean }): void {
|
|
|
2946
4059
|
closed_in_window: councilClosed,
|
|
2947
4060
|
},
|
|
2948
4061
|
swept_events: { total: sweptTotal, by_reason: sweptByReason },
|
|
2949
|
-
hook_errors: {
|
|
4062
|
+
hook_errors: {
|
|
4063
|
+
total: hookErrors.total,
|
|
4064
|
+
last_1h: hookErrors.last1h,
|
|
4065
|
+
latest_at: hookErrors.latestAt,
|
|
4066
|
+
by_phase: hookErrors.byPhase,
|
|
4067
|
+
by_error: hookErrors.byError,
|
|
4068
|
+
top: hookErrors.top,
|
|
4069
|
+
top_errors: hookErrors.topErrors,
|
|
4070
|
+
recent_top_errors: hookErrors.recentTopErrors,
|
|
4071
|
+
},
|
|
2950
4072
|
stream,
|
|
2951
4073
|
zombies: { count: zombieCount, samples: zombieSamples },
|
|
4074
|
+
event_ledger: eventLedger,
|
|
2952
4075
|
anomalies,
|
|
2953
4076
|
};
|
|
2954
4077
|
|
|
@@ -2990,7 +4113,9 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
2990
4113
|
if (report.councils.archived_in_window > 0)
|
|
2991
4114
|
councilParts.push(`${report.councils.archived_in_window} archived`);
|
|
2992
4115
|
|
|
2993
|
-
const
|
|
4116
|
+
const activeSource =
|
|
4117
|
+
report.active_agents.source === "event-ledger-v3" ? "ledger" : "heartbeat cache";
|
|
4118
|
+
const activeStr = `${report.active_agents.total}${platforms ? ` (${platforms})` : ""}${schemas ? ` · ${schemas}` : ""}${report.active_agents.stale > 0 ? ` · ${report.active_agents.stale} stale` : ""} · ${activeSource}`;
|
|
2994
4119
|
|
|
2995
4120
|
const sweptReasonStr = Object.entries(report.swept_events.by_reason)
|
|
2996
4121
|
.map(([r, n]) => `${r} ${n}`)
|
|
@@ -2998,11 +4123,36 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
2998
4123
|
const hookErrStr =
|
|
2999
4124
|
report.hook_errors.total === 0
|
|
3000
4125
|
? "0"
|
|
3001
|
-
: `${report.hook_errors.total}
|
|
4126
|
+
: `${report.hook_errors.total} in window · ${report.hook_errors.last_1h} in 1h`;
|
|
3002
4127
|
const streamStr = `${(report.stream.bytes / 1048576).toFixed(1)}MB · ${report.stream.lines} lines · ${report.stream.cursor_backlog} behind`;
|
|
3003
4128
|
|
|
4129
|
+
const ledger = report.event_ledger;
|
|
4130
|
+
let ledgerStr: string;
|
|
4131
|
+
if (ledger.state !== "live") {
|
|
4132
|
+
ledgerStr = `unavailable (${ledger.reason})`;
|
|
4133
|
+
} else {
|
|
4134
|
+
const orphanGenerations = ledger.open_spans.generations.filter(
|
|
4135
|
+
(generation) => !generation.turn_open,
|
|
4136
|
+
).length;
|
|
4137
|
+
const pendingAges = ledger.pending_finalizations
|
|
4138
|
+
.map((request) =>
|
|
4139
|
+
request.age_ms < 0 ? "age?" : formatAge(Math.floor(request.age_ms / 1000)),
|
|
4140
|
+
)
|
|
4141
|
+
.join(", ");
|
|
4142
|
+
ledgerStr = [
|
|
4143
|
+
`open spans ${ledger.open_spans.total}${orphanGenerations > 0 ? ` (${orphanGenerations} gen turn-closed)` : ""}`,
|
|
4144
|
+
`pending ends ${ledger.pending_finalizations.length}${pendingAges ? ` (${pendingAges})` : ""}`,
|
|
4145
|
+
`intake ${ledger.intake_spool.total}`,
|
|
4146
|
+
`diagnostics ${ledger.diagnostics_spool.total} (${ledger.diagnostics_spool.last_1h} in 1h / ${ledger.diagnostics_spool.last_24h} in 24h)`,
|
|
4147
|
+
].join(" · ");
|
|
4148
|
+
}
|
|
4149
|
+
|
|
3004
4150
|
const rows: Array<[string, string]> = [
|
|
3005
4151
|
["window", `last ${report.since}`],
|
|
4152
|
+
[
|
|
4153
|
+
"event source",
|
|
4154
|
+
`${report.event_telemetry.source.toUpperCase()} · ${report.event_telemetry.authoritative ? "validated" : `unavailable (${report.event_telemetry.reason ?? "incomplete"})`}`,
|
|
4155
|
+
],
|
|
3006
4156
|
["active", activeStr],
|
|
3007
4157
|
[
|
|
3008
4158
|
"heals",
|
|
@@ -3012,6 +4162,7 @@ function renderHealthBox(report: HealthReport): void {
|
|
|
3012
4162
|
["swept", `${report.swept_events.total}${sweptReasonStr ? ` (${sweptReasonStr})` : ""}`],
|
|
3013
4163
|
["hook errors", hookErrStr],
|
|
3014
4164
|
["stream", streamStr],
|
|
4165
|
+
["event ledger", ledgerStr],
|
|
3015
4166
|
[
|
|
3016
4167
|
"zombies",
|
|
3017
4168
|
report.zombies.count === 0
|
|
@@ -3136,6 +4287,7 @@ function runAdapterProbe(
|
|
|
3136
4287
|
replayExitCode = result.exitCode;
|
|
3137
4288
|
data.samples = samples;
|
|
3138
4289
|
data.samples_summary = result.summary;
|
|
4290
|
+
if (result.note) data.samples_note = result.note;
|
|
3139
4291
|
}
|
|
3140
4292
|
|
|
3141
4293
|
emit.data(data);
|
|
@@ -3155,7 +4307,9 @@ function runAdapterProbe(
|
|
|
3155
4307
|
| undefined;
|
|
3156
4308
|
lines.push("");
|
|
3157
4309
|
if (samples.length === 0) {
|
|
3158
|
-
lines.push(
|
|
4310
|
+
lines.push(
|
|
4311
|
+
` Sample replay: ${String(data.samples_note ?? `no .json fixtures found under ${sampleDir}`)}`,
|
|
4312
|
+
);
|
|
3159
4313
|
} else {
|
|
3160
4314
|
lines.push(
|
|
3161
4315
|
` Sample replay (${samples.length} fixture${samples.length === 1 ? "" : "s"}):`,
|
|
@@ -3212,6 +4366,7 @@ function replayAdapterSamples(
|
|
|
3212
4366
|
samples: SampleReplayResult[];
|
|
3213
4367
|
exitCode: number;
|
|
3214
4368
|
summary: { total: number; pass: number; fail: number; skipped: number };
|
|
4369
|
+
note?: string;
|
|
3215
4370
|
} {
|
|
3216
4371
|
const sampleDir = resolve(root, relativeSampleDir);
|
|
3217
4372
|
if (!existsSync(sampleDir)) {
|
|
@@ -3219,6 +4374,7 @@ function replayAdapterSamples(
|
|
|
3219
4374
|
samples: [],
|
|
3220
4375
|
exitCode: 0,
|
|
3221
4376
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
4377
|
+
note: `sample directory not found: ${relativeSampleDir} — replay verified nothing`,
|
|
3222
4378
|
};
|
|
3223
4379
|
}
|
|
3224
4380
|
|
|
@@ -3232,6 +4388,7 @@ function replayAdapterSamples(
|
|
|
3232
4388
|
samples: [],
|
|
3233
4389
|
exitCode: 0,
|
|
3234
4390
|
summary: { total: 0, pass: 0, fail: 0, skipped: 0 },
|
|
4391
|
+
note: `no .json fixtures under ${relativeSampleDir} — replay verified nothing`,
|
|
3235
4392
|
};
|
|
3236
4393
|
}
|
|
3237
4394
|
|
|
@@ -3457,11 +4614,26 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
|
|
|
3457
4614
|
});
|
|
3458
4615
|
process.exit(1);
|
|
3459
4616
|
}
|
|
3460
|
-
const myHb =
|
|
4617
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3461
4618
|
const fromName = myHb?.name ?? "anonymous";
|
|
3462
4619
|
const body = `from agent-${fromName}: ${message.trim()}`;
|
|
3463
4620
|
const doc = appendEntry(peerOwner, "handoff", body);
|
|
3464
4621
|
|
|
4622
|
+
// Canonical delivery record: sender is the envelope owner, recipient rides
|
|
4623
|
+
// in data, so read-only observers can render the communication without
|
|
4624
|
+
// joining journal files.
|
|
4625
|
+
emitEventV3({
|
|
4626
|
+
owner: myOwner,
|
|
4627
|
+
session: nativeSessionIdentity(myHb, myOwner),
|
|
4628
|
+
adapter: normalizeAdapter(myHb?.platform),
|
|
4629
|
+
observation: {
|
|
4630
|
+
event_type: "coord.message_observed",
|
|
4631
|
+
direction: "sent",
|
|
4632
|
+
subject: peerOwner,
|
|
4633
|
+
body: message.trim(),
|
|
4634
|
+
},
|
|
4635
|
+
});
|
|
4636
|
+
|
|
3465
4637
|
const data = {
|
|
3466
4638
|
peer: name,
|
|
3467
4639
|
peer_instance_id: peerOwner,
|
|
@@ -3519,7 +4691,7 @@ async function runWait(
|
|
|
3519
4691
|
|
|
3520
4692
|
let lastProgressMs = 0;
|
|
3521
4693
|
while (true) {
|
|
3522
|
-
const hb =
|
|
4694
|
+
const hb = readCurrentCoordinationRow(peerOwner);
|
|
3523
4695
|
const now = Date.now();
|
|
3524
4696
|
const elapsedMs = now - startMs;
|
|
3525
4697
|
|
|
@@ -3559,9 +4731,8 @@ async function runWait(
|
|
|
3559
4731
|
const progressGapMs = Math.max(pollMs, 30_000);
|
|
3560
4732
|
if (!opts.quiet && now - lastProgressMs >= progressGapMs) {
|
|
3561
4733
|
lastProgressMs = now;
|
|
3562
|
-
const lastTool = hb.last_tool ? `, last=${hb.last_tool}` : "";
|
|
3563
4734
|
const elapsedStr = formatAge(Math.floor(elapsedMs / 1000));
|
|
3564
|
-
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking
|
|
4735
|
+
const progress = ` [${elapsedStr}] ${stillBlocking.length} file(s) blocking\n`;
|
|
3565
4736
|
process.stderr.write(progress); // lint-ok-emission: per-poll progress heartbeat to stderr
|
|
3566
4737
|
}
|
|
3567
4738
|
|
|
@@ -3602,6 +4773,7 @@ function runHeal(opts: {
|
|
|
3602
4773
|
owner: string;
|
|
3603
4774
|
kind: string;
|
|
3604
4775
|
sessionId?: string;
|
|
4776
|
+
adapter?: string;
|
|
3605
4777
|
pid?: string;
|
|
3606
4778
|
json?: boolean;
|
|
3607
4779
|
}): void {
|
|
@@ -3613,10 +4785,22 @@ function runHeal(opts: {
|
|
|
3613
4785
|
emit.error({ code: "missing_owner", message: "--owner is required" });
|
|
3614
4786
|
process.exit(1);
|
|
3615
4787
|
}
|
|
3616
|
-
if (kind !== "pidmap" && kind !== "
|
|
4788
|
+
if (kind !== "pidmap" && kind !== "cache") {
|
|
3617
4789
|
emit.error({
|
|
3618
4790
|
code: "bad_kind",
|
|
3619
|
-
message: "--kind must be one of: pidmap,
|
|
4791
|
+
message: "--kind must be one of: pidmap, cache",
|
|
4792
|
+
});
|
|
4793
|
+
process.exit(1);
|
|
4794
|
+
}
|
|
4795
|
+
if (
|
|
4796
|
+
opts.adapter &&
|
|
4797
|
+
opts.adapter !== "claude-code" &&
|
|
4798
|
+
opts.adapter !== "cursor" &&
|
|
4799
|
+
opts.adapter !== "codex"
|
|
4800
|
+
) {
|
|
4801
|
+
emit.error({
|
|
4802
|
+
code: "bad_adapter",
|
|
4803
|
+
message: "--adapter must be one of: claude-code, cursor, codex",
|
|
3620
4804
|
});
|
|
3621
4805
|
process.exit(1);
|
|
3622
4806
|
}
|
|
@@ -3630,10 +4814,9 @@ function runHeal(opts: {
|
|
|
3630
4814
|
process.exit(1);
|
|
3631
4815
|
}
|
|
3632
4816
|
|
|
3633
|
-
const action =
|
|
3634
|
-
kind === "pidmap" ? "heal-pidmap" : kind === "heartbeat" ? "heal-heartbeat" : "kill-heartbeat";
|
|
4817
|
+
const action = kind === "pidmap" ? "heal-pidmap" : "repair-coordination-cache";
|
|
3635
4818
|
|
|
3636
|
-
// Refuse to
|
|
4819
|
+
// Refuse to materialize a V3 cache at a truncated owner id.
|
|
3637
4820
|
//
|
|
3638
4821
|
// Heartbeats are keyed by the whole instance_id, so healing at an
|
|
3639
4822
|
// abbreviated id writes `.harnery/active/<prefix>.json` while every reader
|
|
@@ -3653,19 +4836,18 @@ function runHeal(opts: {
|
|
|
3653
4836
|
// whose instance_id this id is a prefix of settles it just as well, and is
|
|
3654
4837
|
// present in precisely the case that matters (the session the reader was
|
|
3655
4838
|
// trying to heal is registered, just not under the abbreviated name).
|
|
3656
|
-
if (kind === "
|
|
4839
|
+
if (kind === "cache" && !readHeartbeatCache(root, owner)) {
|
|
3657
4840
|
const canonical =
|
|
3658
4841
|
opts.sessionId && opts.sessionId.trim() !== owner && opts.sessionId.trim().startsWith(owner)
|
|
3659
4842
|
? opts.sessionId.trim()
|
|
3660
4843
|
: liveIdWithPrefix(root, owner);
|
|
3661
4844
|
if (canonical) {
|
|
3662
|
-
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live
|
|
4845
|
+
const source = opts.sessionId?.trim() === canonical ? "--session-id" : "a live V3 generation";
|
|
3663
4846
|
emit.error({
|
|
3664
4847
|
code: "truncated_owner",
|
|
3665
4848
|
message:
|
|
3666
|
-
`--owner ${owner} is a prefix of ${canonical} (${source})
|
|
3667
|
-
`
|
|
3668
|
-
`resolves. Re-run with --owner ${canonical}.`,
|
|
4849
|
+
`--owner ${owner} is a prefix of ${canonical} (${source}). Cache repair requires ` +
|
|
4850
|
+
`the canonical instance id; re-run with --owner ${canonical}.`,
|
|
3669
4851
|
});
|
|
3670
4852
|
process.exit(1);
|
|
3671
4853
|
}
|
|
@@ -3673,14 +4855,13 @@ function runHeal(opts: {
|
|
|
3673
4855
|
|
|
3674
4856
|
// Build positional args. agent-coord's arg layout:
|
|
3675
4857
|
// heal-pidmap <instance_id> [<pid>]
|
|
3676
|
-
//
|
|
3677
|
-
// kill-heartbeat <instance_id>
|
|
4858
|
+
// repair-coordination-cache <instance_id> [<session_id>]
|
|
3678
4859
|
const helperArgs: string[] = [action, owner];
|
|
3679
4860
|
if (kind === "pidmap" && opts.pid) helperArgs.push(opts.pid);
|
|
3680
|
-
if (kind === "
|
|
4861
|
+
if (kind === "cache" && opts.sessionId) helperArgs.push(opts.sessionId);
|
|
4862
|
+
if (kind === "cache" && opts.adapter) helperArgs.push(`--adapter=${opts.adapter}`);
|
|
3681
4863
|
|
|
3682
|
-
//
|
|
3683
|
-
// bundled agent-coord binary.
|
|
4864
|
+
// Both recovery actions are handled by the bundled agent-coord binary.
|
|
3684
4865
|
const helper = agentCoordOrExit(root);
|
|
3685
4866
|
const proc = spawnSync(helper, helperArgs, {
|
|
3686
4867
|
encoding: "utf8",
|
|
@@ -3695,27 +4876,22 @@ function runHeal(opts: {
|
|
|
3695
4876
|
process.exit(1);
|
|
3696
4877
|
}
|
|
3697
4878
|
|
|
3698
|
-
//
|
|
4879
|
+
// Read the derived cache to surface post-action state.
|
|
3699
4880
|
let after: Heartbeat | null = null;
|
|
3700
4881
|
try {
|
|
3701
|
-
after =
|
|
4882
|
+
after = readHeartbeatCache(root, owner);
|
|
3702
4883
|
} catch {
|
|
3703
4884
|
after = null;
|
|
3704
4885
|
}
|
|
3705
4886
|
|
|
3706
|
-
// Outcome semantics differ per kind. kill-heartbeat targets the heartbeat
|
|
3707
|
-
// file directly; heal-heartbeat upserts it; heal-pidmap touches a pid-map
|
|
3708
|
-
// row whose existence is independent of the heartbeat. Reporting on
|
|
3709
|
-
// "heartbeat present after" for the pidmap path was misleading: it's
|
|
3710
|
-
// unrelated to whether the heal succeeded.
|
|
3711
4887
|
const outcome =
|
|
3712
4888
|
kind === "pidmap"
|
|
3713
4889
|
? proc.status === 0
|
|
3714
4890
|
? "ok"
|
|
3715
4891
|
: "failed"
|
|
3716
4892
|
: after
|
|
3717
|
-
? "
|
|
3718
|
-
: "
|
|
4893
|
+
? "cache_present"
|
|
4894
|
+
: "cache_absent";
|
|
3719
4895
|
|
|
3720
4896
|
emit.data({
|
|
3721
4897
|
rows: [
|
|
@@ -3738,36 +4914,40 @@ function runHeal(opts: {
|
|
|
3738
4914
|
if (kind === "pidmap") {
|
|
3739
4915
|
emit.text(`agent-coord ${action} ok\n`);
|
|
3740
4916
|
} else {
|
|
3741
|
-
emit.text(`agent-coord ${action} ok:
|
|
4917
|
+
emit.text(`agent-coord ${action} ok: cache ${after ? "present" : "absent"} after\n`);
|
|
3742
4918
|
}
|
|
3743
4919
|
}
|
|
3744
4920
|
|
|
3745
|
-
// Canonical health.* emission is owned by the writer
|
|
3746
|
-
//
|
|
4921
|
+
// Canonical health.* emission is owned by the cache writer, so it fires
|
|
4922
|
+
// inside the agent-coord subprocess
|
|
3747
4923
|
// above on actual writes only: write-only telemetry, no double-emit, no
|
|
3748
4924
|
// event when an already-correct heal no-ops. (Previously emitted here
|
|
3749
4925
|
// unconditionally on every `harn agents heal`, which over-counted no-op heals.)
|
|
3750
4926
|
}
|
|
3751
4927
|
|
|
3752
4928
|
/**
|
|
3753
|
-
*
|
|
3754
|
-
*
|
|
3755
|
-
* through silently if no session (CI / direct invocation).
|
|
4929
|
+
* Emit one V3 council audit transition keyed to the exact durable manifest.
|
|
4930
|
+
* Falls through silently when no live session can attest the observation.
|
|
3756
4931
|
*/
|
|
3757
4932
|
function emitCouncilStateEvent(
|
|
3758
|
-
type: string,
|
|
3759
4933
|
manifest: CouncilManifest,
|
|
3760
|
-
|
|
4934
|
+
newState: string,
|
|
4935
|
+
priorState?: string,
|
|
3761
4936
|
): void {
|
|
3762
4937
|
const myOwner = resolveOwner();
|
|
3763
4938
|
if (!myOwner) return;
|
|
3764
|
-
const hb =
|
|
3765
|
-
|
|
3766
|
-
type,
|
|
4939
|
+
const hb = readCurrentCoordinationRow(myOwner);
|
|
4940
|
+
emitEventV3({
|
|
3767
4941
|
owner: myOwner,
|
|
3768
|
-
session: hb
|
|
4942
|
+
session: nativeSessionIdentity(hb, myOwner),
|
|
3769
4943
|
adapter: normalizeAdapter(hb?.platform),
|
|
3770
|
-
|
|
4944
|
+
observation: {
|
|
4945
|
+
event_type: "council.state_changed",
|
|
4946
|
+
council_id: manifest.council_id,
|
|
4947
|
+
...(priorState ? { prior_state: priorState } : {}),
|
|
4948
|
+
new_state: newState,
|
|
4949
|
+
record: manifest,
|
|
4950
|
+
},
|
|
3771
4951
|
});
|
|
3772
4952
|
}
|
|
3773
4953
|
|
|
@@ -3824,7 +5004,7 @@ function runCouncilCreate(
|
|
|
3824
5004
|
if (opts.createdBy?.trim()) {
|
|
3825
5005
|
createdBy = normalizeAgentName(opts.createdBy);
|
|
3826
5006
|
} else if (myOwner) {
|
|
3827
|
-
const myHb =
|
|
5007
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3828
5008
|
if (myHb?.name) {
|
|
3829
5009
|
createdBy = normalizeAgentName(myHb.name);
|
|
3830
5010
|
}
|
|
@@ -3885,17 +5065,16 @@ function runCouncilCreate(
|
|
|
3885
5065
|
writeManifest(manifest);
|
|
3886
5066
|
|
|
3887
5067
|
if (myOwner) {
|
|
3888
|
-
const myHbForEmit =
|
|
3889
|
-
|
|
3890
|
-
type: "council.open",
|
|
5068
|
+
const myHbForEmit = readCurrentCoordinationRow(myOwner);
|
|
5069
|
+
emitEventV3({
|
|
3891
5070
|
owner: myOwner,
|
|
3892
|
-
session: myHbForEmit
|
|
5071
|
+
session: nativeSessionIdentity(myHbForEmit, myOwner),
|
|
3893
5072
|
adapter: normalizeAdapter(myHbForEmit?.platform),
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
5073
|
+
observation: {
|
|
5074
|
+
event_type: "council.state_changed",
|
|
5075
|
+
council_id: manifest.council_id,
|
|
5076
|
+
new_state: "active",
|
|
5077
|
+
record: manifest,
|
|
3899
5078
|
},
|
|
3900
5079
|
});
|
|
3901
5080
|
}
|
|
@@ -3908,7 +5087,7 @@ function runCouncilCreate(
|
|
|
3908
5087
|
const skippedMembers: string[] = [];
|
|
3909
5088
|
for (const memberName of members) {
|
|
3910
5089
|
const bareName = memberName.replace(/^agent-/, "");
|
|
3911
|
-
if (myOwner &&
|
|
5090
|
+
if (myOwner && readCurrentCoordinationRow(myOwner)?.name === bareName) {
|
|
3912
5091
|
// Convener is themselves a member; skip the self-ping
|
|
3913
5092
|
continue;
|
|
3914
5093
|
}
|
|
@@ -3973,7 +5152,7 @@ function runCouncilList(opts: { status?: string; mine?: boolean; json?: boolean
|
|
|
3973
5152
|
if (opts.mine) {
|
|
3974
5153
|
const myOwner = resolveOwner();
|
|
3975
5154
|
if (myOwner) {
|
|
3976
|
-
const myHb =
|
|
5155
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
3977
5156
|
if (myHb?.name) myName = normalizeAgentName(myHb.name);
|
|
3978
5157
|
}
|
|
3979
5158
|
if (!myName) {
|
|
@@ -4150,7 +5329,7 @@ function runCouncilClose(id: string, opts: { json?: boolean }): void {
|
|
|
4150
5329
|
closed_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
4151
5330
|
};
|
|
4152
5331
|
writeManifest(next);
|
|
4153
|
-
emitCouncilStateEvent("
|
|
5332
|
+
emitCouncilStateEvent(next, "closed", manifest.status);
|
|
4154
5333
|
|
|
4155
5334
|
// Build the transcript: every round's contributions in order.
|
|
4156
5335
|
const transcript = buildTranscript(next);
|
|
@@ -4195,7 +5374,7 @@ function runCouncilArchive(id: string, opts: { json?: boolean }): void {
|
|
|
4195
5374
|
// updated status. moveToArchive then physically relocates the artifacts.
|
|
4196
5375
|
writeManifest(next);
|
|
4197
5376
|
moveToArchive(next.council_id);
|
|
4198
|
-
emitCouncilStateEvent("
|
|
5377
|
+
emitCouncilStateEvent(next, "archived", manifest.status);
|
|
4199
5378
|
|
|
4200
5379
|
emit.data({
|
|
4201
5380
|
rows: [
|
|
@@ -4254,7 +5433,7 @@ function runCouncilUnarchive(id: string, opts: { json?: boolean }): void {
|
|
|
4254
5433
|
// (allows re-running for testing).
|
|
4255
5434
|
moveFromArchive(next.council_id);
|
|
4256
5435
|
writeManifest(next);
|
|
4257
|
-
emitCouncilStateEvent(
|
|
5436
|
+
emitCouncilStateEvent(next, restoredStatus, "archived");
|
|
4258
5437
|
|
|
4259
5438
|
emit.data({
|
|
4260
5439
|
rows: [
|
|
@@ -4316,7 +5495,7 @@ function runCouncilDelete(id: string, opts: { yes?: boolean; json?: boolean }):
|
|
|
4316
5495
|
|
|
4317
5496
|
const removed = deleteArchivedCouncil(manifest.council_id);
|
|
4318
5497
|
if (removed) {
|
|
4319
|
-
emitCouncilStateEvent("
|
|
5498
|
+
emitCouncilStateEvent(manifest, "deleted", "archived");
|
|
4320
5499
|
}
|
|
4321
5500
|
|
|
4322
5501
|
emit.data({
|
|
@@ -4506,7 +5685,7 @@ function runCouncilContribute(
|
|
|
4506
5685
|
try {
|
|
4507
5686
|
const myOwner = resolveOwner();
|
|
4508
5687
|
if (myOwner) {
|
|
4509
|
-
const myHb =
|
|
5688
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4510
5689
|
if (myHb?.name) actualName = normalizeAgentName(myHb.name);
|
|
4511
5690
|
}
|
|
4512
5691
|
} catch {
|
|
@@ -4522,7 +5701,7 @@ function runCouncilContribute(
|
|
|
4522
5701
|
});
|
|
4523
5702
|
process.exit(1);
|
|
4524
5703
|
}
|
|
4525
|
-
const myHb =
|
|
5704
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4526
5705
|
if (!myHb?.name) {
|
|
4527
5706
|
emit.error({
|
|
4528
5707
|
code: "no_self_name",
|
|
@@ -4575,11 +5754,7 @@ function runCouncilContribute(
|
|
|
4575
5754
|
}
|
|
4576
5755
|
|
|
4577
5756
|
const path = writeContribution(manifest.council_id, manifest.current_round, myName, body);
|
|
4578
|
-
emitCouncilStateEvent("
|
|
4579
|
-
round_no: manifest.current_round,
|
|
4580
|
-
member: myName,
|
|
4581
|
-
body_summary: body.length > 1000 ? `${body.slice(0, 997)}...` : body,
|
|
4582
|
-
});
|
|
5757
|
+
emitCouncilStateEvent(manifest, "contribution_recorded", manifest.round_status);
|
|
4583
5758
|
|
|
4584
5759
|
// Update manifest: if all members have now contributed, flip round_status.
|
|
4585
5760
|
const contributorsNow = contributorsInRound(manifest.council_id, manifest.current_round);
|
|
@@ -4589,14 +5764,10 @@ function runCouncilContribute(
|
|
|
4589
5764
|
if (allIn) {
|
|
4590
5765
|
nextManifest = { ...manifest, round_status: "collected" };
|
|
4591
5766
|
writeManifest(nextManifest);
|
|
4592
|
-
emitCouncilStateEvent("
|
|
4593
|
-
round_no: nextManifest.current_round,
|
|
4594
|
-
});
|
|
5767
|
+
emitCouncilStateEvent(nextManifest, "round_closed", manifest.round_status);
|
|
4595
5768
|
if (manifest.auto_advance) {
|
|
4596
5769
|
nextManifest = advanceCouncil(nextManifest, /*force=*/ false);
|
|
4597
|
-
emitCouncilStateEvent("
|
|
4598
|
-
round_no: nextManifest.current_round,
|
|
4599
|
-
});
|
|
5770
|
+
emitCouncilStateEvent(nextManifest, "round_open", "round_closed");
|
|
4600
5771
|
autoAdvanced = true;
|
|
4601
5772
|
}
|
|
4602
5773
|
}
|
|
@@ -4687,7 +5858,7 @@ function runCouncilPrompt(
|
|
|
4687
5858
|
try {
|
|
4688
5859
|
const myOwner = resolveOwner();
|
|
4689
5860
|
if (myOwner) {
|
|
4690
|
-
const myHb =
|
|
5861
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4691
5862
|
if (myHb?.name) actualName = normalizeAgentName(myHb.name);
|
|
4692
5863
|
}
|
|
4693
5864
|
} catch {
|
|
@@ -4703,7 +5874,7 @@ function runCouncilPrompt(
|
|
|
4703
5874
|
});
|
|
4704
5875
|
process.exit(1);
|
|
4705
5876
|
}
|
|
4706
|
-
const myHb =
|
|
5877
|
+
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4707
5878
|
if (!myHb?.name) {
|
|
4708
5879
|
emit.error({
|
|
4709
5880
|
code: "no_self_name",
|
|
@@ -4868,12 +6039,8 @@ function runCouncilAdvance(id: string, opts: { force?: boolean; json?: boolean }
|
|
|
4868
6039
|
}
|
|
4869
6040
|
|
|
4870
6041
|
const next = advanceCouncil(manifest, !!opts.force);
|
|
4871
|
-
emitCouncilStateEvent("
|
|
4872
|
-
|
|
4873
|
-
});
|
|
4874
|
-
emitCouncilStateEvent("council.round_open", next, {
|
|
4875
|
-
round_no: next.current_round,
|
|
4876
|
-
});
|
|
6042
|
+
emitCouncilStateEvent(manifest, "round_closed", manifest.round_status);
|
|
6043
|
+
emitCouncilStateEvent(next, "round_open", "round_closed");
|
|
4877
6044
|
|
|
4878
6045
|
emit.data({
|
|
4879
6046
|
rows: [
|
|
@@ -4919,7 +6086,7 @@ function advanceCouncil(manifest: CouncilManifest, force: boolean): CouncilManif
|
|
|
4919
6086
|
// (Convener already knows; we skip pinging them if they convened it from
|
|
4920
6087
|
// their own session.)
|
|
4921
6088
|
const myOwner = resolveOwner();
|
|
4922
|
-
const myName = myOwner ? normalizeAgentName(
|
|
6089
|
+
const myName = myOwner ? normalizeAgentName(readCurrentCoordinationRow(myOwner)?.name ?? "") : "";
|
|
4923
6090
|
for (const memberName of next.members) {
|
|
4924
6091
|
if (memberName === myName) continue;
|
|
4925
6092
|
const bareName = memberName.replace(/^agent-/, "");
|
|
@@ -4943,7 +6110,7 @@ function advanceCouncil(manifest: CouncilManifest, force: boolean): CouncilManif
|
|
|
4943
6110
|
// Hard cap on box width. Values longer than the per-row budget word-wrap to
|
|
4944
6111
|
// continuation lines (blank key column, value resumes indented). Picked to
|
|
4945
6112
|
// stay readable in narrow terminals + chat clients while giving long
|
|
4946
|
-
//
|
|
6113
|
+
// Task values need room to breathe.
|
|
4947
6114
|
const MAX_BOX_CONTENT_WIDTH = 100;
|
|
4948
6115
|
|
|
4949
6116
|
function formatBox(title: string, rows: Array<[string, string]>): string {
|