session-orchestrator 5.2.0 → 5.3.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/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -30,29 +30,43 @@
|
|
|
30
30
|
* newest-across-all, not genuine-first" for why the
|
|
31
31
|
* two kinds are maxed rather than ranked.
|
|
32
32
|
* - `cutoff` = the CURRENT session's `session.lock`
|
|
33
|
-
* `started_at` (via `readLock()`)
|
|
34
|
-
*
|
|
33
|
+
* `started_at` (via `readLock()`), pulled back to
|
|
34
|
+
* that lock id's FIRST `lock.acquired` event
|
|
35
|
+
* (`ownGenesisMs()`, a compact re-acquire rewrites
|
|
36
|
+
* `started_at`); when no lock is readable,
|
|
37
|
+
* `cutoff = now` (all events count).
|
|
35
38
|
* - `lastForeignEventAt`= the NEWEST `events.jsonl` line whose `timestamp`
|
|
36
39
|
* is STRICTLY BEFORE `cutoff` — this structurally
|
|
37
40
|
* excludes the current session's own events without
|
|
38
41
|
* needing a `session_id` filter (most event lines
|
|
39
42
|
* don't carry one; see the mission-log sample in
|
|
40
|
-
* `.orchestrator/metrics/events.jsonl`).
|
|
43
|
+
* `.orchestrator/metrics/events.jsonl`). A previous
|
|
44
|
+
* session's own teardown events are skipped
|
|
45
|
+
* (`CLOSING_DIAGNOSTIC_EVENTS`): they postdate its
|
|
46
|
+
* ledger record by construction.
|
|
41
47
|
* - `deltaHours` = (lastForeignEventAt − lastLedgerAt) / 1h, only
|
|
42
48
|
* meaningful when > 0 (foreign activity happened
|
|
43
49
|
* AFTER the last ledger entry).
|
|
44
50
|
*
|
|
45
|
-
* Backfill-stub self-erasure fix — the anchor axis: a backfill
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
51
|
+
* Backfill-stub self-erasure fix — the anchor axis: a backfill STUB
|
|
52
|
+
* (`status: 'abandoned'`, see `session-close-backfill.mjs` `synthesizeRecord()`)
|
|
53
|
+
* is a reconstruction, not a measured close. Before #914 R1 its `completed_at`
|
|
54
|
+
* could be the BACKFILL RUN's own wall-clock; since #914 R1 it is
|
|
55
|
+
* events-derived (terminal event, else last life-sign, else `started_at`) and
|
|
56
|
+
* flagged in `_backfill_incomplete_fields` whenever it is an estimate. A stub's
|
|
57
|
+
* `completed_at` is still refused as an anchor: an abandoned session proves
|
|
58
|
+
* the ledger alive only at its start. Anchoring on it once let a backfill run
|
|
59
|
+
* retroactively erase a multi-day staleness gap just by writing a stub today
|
|
60
|
+
* (observed: a 92.5h gap to the last GENUINE record collapsed to 0.6h).
|
|
61
|
+
*
|
|
62
|
+
* A `state-md-completed` backfill (status `completed`, #429/#1068) is NOT a
|
|
63
|
+
* stub: its status is the session's own truth claim, and its timestamps are
|
|
64
|
+
* events-derived like any other. Treating every `_backfill_source` as a stub
|
|
65
|
+
* kept the probe warning after a stub had been superseded, and recommended a
|
|
66
|
+
* backfill run with nothing left to do (live 2026-09-18). Independently of the
|
|
67
|
+
* kind, a timestamp named in a record's `_backfill_incomplete_fields` was not
|
|
68
|
+
* measured (e.g. `started_at` fell back to the backfill run's `now`) and is
|
|
69
|
+
* never an anchor candidate.
|
|
56
70
|
*
|
|
57
71
|
* Two axes were available to fix this: (a) skip stub records when scanning
|
|
58
72
|
* for the ledger anchor, keeping `completed_at` as the anchor field; or (b)
|
|
@@ -63,10 +77,9 @@
|
|
|
63
77
|
* (a session's own mid-session events would newly count as "after" the
|
|
64
78
|
* anchor), reintroducing false positives on the opposite side. (a) is
|
|
65
79
|
* chosen: a stub's `completed_at` is NEVER an anchor candidate. Stub
|
|
66
|
-
* recognition
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* degrades gracefully (still catches it), AND does not.
|
|
80
|
+
* recognition: `status: 'abandoned'` alone suffices, and so does a
|
|
81
|
+
* `_backfill_source` on any record whose status is not `completed` — OR, not
|
|
82
|
+
* AND, so a future backfill variant that drops one marker is still caught.
|
|
70
83
|
*
|
|
71
84
|
* #1125 — newest-across-ALL records, not genuine-first: excluding the stub's
|
|
72
85
|
* `completed_at` (above) is correct; excluding the whole STUB from the anchor
|
|
@@ -146,6 +159,64 @@ export const WARN_THRESHOLD_HOURS = DEFAULT_TTL_HOURS * 2;
|
|
|
146
159
|
/** alert threshold: a full day behind is a strong close-through signal. */
|
|
147
160
|
export const ALERT_THRESHOLD_HOURS = 24;
|
|
148
161
|
|
|
162
|
+
/**
|
|
163
|
+
* A session's own CLOSING diagnostics — events emitted at teardown, i.e. AFTER
|
|
164
|
+
* that session's `/close` has already written its ledger record. They measure
|
|
165
|
+
* when the process exited, not when work happened, so they are never "foreign
|
|
166
|
+
* activity": counting them turned every terminal left open after `/close` into
|
|
167
|
+
* a close-through warning (live 2026-09-19: a 9.6h warn whose only post-ledger
|
|
168
|
+
* events were one SessionEnd's `wave.final_refused`, `session.ended`,
|
|
169
|
+
* `session.backfill_completed` and `telemetry.flush`, while
|
|
170
|
+
* `backfill-abandoned-sessions.mjs --dry-run` found 0 sessions to backfill).
|
|
171
|
+
* Map value = the `emitted_by` an event must carry to be
|
|
172
|
+
* skipped; `null` = skipped from every emitter.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately NOT listed, and still counted: `orchestrator.turn.stopped`, its
|
|
175
|
+
* deprecated alias `orchestrator.session.stopped`, `orchestrator.agent.stopped`
|
|
176
|
+
* and every other event. A process killed by OOM or `kill -9` never runs its
|
|
177
|
+
* SessionEnd hook but has left those behind — that is the close-through gap
|
|
178
|
+
* this probe exists to catch. Skipping teardown events loses nothing there:
|
|
179
|
+
* a session that exits WITHOUT `/close` still leaves its turn/agent events,
|
|
180
|
+
* and only the idle tail between its last turn and the exit goes uncounted.
|
|
181
|
+
*/
|
|
182
|
+
const CLOSING_DIAGNOSTIC_EVENTS = new Map([
|
|
183
|
+
// Only emitter: hooks/on-session-end.mjs — the SessionEnd breadcrumb itself.
|
|
184
|
+
['orchestrator.session.ended', null],
|
|
185
|
+
// Only emitter: hooks/on-session-end.mjs — the outcome of the exit-time telemetry flush.
|
|
186
|
+
['orchestrator.telemetry.flush', null],
|
|
187
|
+
// hooks/on-session-end.mjs + scripts/backfill-abandoned-sessions.mjs — a backfill run
|
|
188
|
+
// is only ever ABOUT an already-dead session, and writes that session's ledger stub itself.
|
|
189
|
+
['orchestrator.session.backfill_completed', null],
|
|
190
|
+
// Lock-teardown breadcrumbs. Every emitter is a session teardown: hooks/on-session-end.mjs,
|
|
191
|
+
// scripts/lib/session-transition.mjs (#1069 process boundary), and
|
|
192
|
+
// scripts/lib/autopilot/worktree-pipeline.mjs (release_failed/released only).
|
|
193
|
+
['orchestrator.session.lock.released', null],
|
|
194
|
+
['orchestrator.session.lock.release_failed', null],
|
|
195
|
+
// Only emitter: hooks/on-session-end.mjs — reading the lock during teardown.
|
|
196
|
+
['orchestrator.session.lock.read_anomaly', null],
|
|
197
|
+
// Wave lifecycle: hooks/post-tool-batch-wave-signal.mjs emits wave.completed MID-session
|
|
198
|
+
// (real activity, still counted); only the SessionEnd final-wave stamp is a closing
|
|
199
|
+
// diagnostic. final_refused has on-session-end as its sole emitter today, keyed the
|
|
200
|
+
// same way so a future mid-session emitter stays counted by default.
|
|
201
|
+
['orchestrator.wave.completed', 'on-session-end'],
|
|
202
|
+
['orchestrator.wave.final_refused', 'on-session-end'],
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* True when an events.jsonl record is one of the session's own closing
|
|
207
|
+
* diagnostics (`CLOSING_DIAGNOSTIC_EVENTS`). An unknown or missing `event`
|
|
208
|
+
* counts as activity — the failure direction stays "warn", never "silence".
|
|
209
|
+
* Caller guarantees `record` is already a non-null object.
|
|
210
|
+
*
|
|
211
|
+
* @param {object} record
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function isClosingDiagnostic(record) {
|
|
215
|
+
if (typeof record.event !== 'string' || !CLOSING_DIAGNOSTIC_EVENTS.has(record.event)) return false;
|
|
216
|
+
const requiredEmitter = CLOSING_DIAGNOSTIC_EVENTS.get(record.event);
|
|
217
|
+
return requiredEmitter === null || record.emitted_by === requiredEmitter;
|
|
218
|
+
}
|
|
219
|
+
|
|
149
220
|
/**
|
|
150
221
|
* Read a JSONL file's non-empty lines. Returns `null` when the file is
|
|
151
222
|
* absent or unreadable, `[]` when it exists but has no non-empty lines.
|
|
@@ -166,26 +237,44 @@ function readJsonlLines(filePath) {
|
|
|
166
237
|
}
|
|
167
238
|
|
|
168
239
|
/**
|
|
169
|
-
* True when a `sessions.jsonl` record
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
240
|
+
* True when a `sessions.jsonl` record is a backfill STUB — a reconstruction
|
|
241
|
+
* whose `completed_at` is never an anchor candidate (module header,
|
|
242
|
+
* "Backfill-stub self-erasure fix").
|
|
243
|
+
*
|
|
244
|
+
* `isRealSession()` (`./session-schema/filters.mjs`) catches `status:
|
|
245
|
+
* 'abandoned'`, the canonical phantom marker. A `_backfill_source` counts only
|
|
246
|
+
* when the status is not `completed`: the `state-md-completed` supersede record
|
|
247
|
+
* is the session's own completion claim. Same exclusion as
|
|
248
|
+
* `isSupersedableStub()` in `session-close-backfill.mjs` (which keeps
|
|
249
|
+
* `completed` out of its stub statuses); not imported from there because that
|
|
250
|
+
* predicate demands BOTH markers before a record may be OVERWRITTEN, where
|
|
251
|
+
* this one accepts EITHER — a record only has to be doubtful to be
|
|
252
|
+
* disqualified as a time anchor. The two therefore disagree by design on
|
|
253
|
+
* `{_backfill_source, status: 'interrupted'}`, and the names say which
|
|
254
|
+
* question each answers.
|
|
255
|
+
* Caller guarantees `record` is already a non-null object.
|
|
182
256
|
*
|
|
183
257
|
* @param {object} record
|
|
184
258
|
* @returns {boolean}
|
|
185
259
|
*/
|
|
186
|
-
function
|
|
260
|
+
function isNonAnchorStub(record) {
|
|
187
261
|
if (!isRealSession(record)) return true;
|
|
188
|
-
return typeof record._backfill_source === 'string' && record._backfill_source.length > 0
|
|
262
|
+
return typeof record._backfill_source === 'string' && record._backfill_source.length > 0
|
|
263
|
+
&& record.status !== 'completed';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Anchor candidate for one timestamp field of a record — `null` when the
|
|
268
|
+
* backfill engine listed that field in `_backfill_incomplete_fields` (it was
|
|
269
|
+
* not measured, e.g. a `started_at` that fell back to the backfill run's now).
|
|
270
|
+
*
|
|
271
|
+
* @param {object} record
|
|
272
|
+
* @param {'started_at'|'completed_at'} field
|
|
273
|
+
* @returns {{iso: string, ms: number}|null}
|
|
274
|
+
*/
|
|
275
|
+
function fieldCandidate(record, field) {
|
|
276
|
+
if (Array.isArray(record._backfill_incomplete_fields) && record._backfill_incomplete_fields.includes(field)) return null;
|
|
277
|
+
return tsCandidate(record[field]);
|
|
189
278
|
}
|
|
190
279
|
|
|
191
280
|
/**
|
|
@@ -225,15 +314,16 @@ function keepNewer(current, candidate) {
|
|
|
225
314
|
*
|
|
226
315
|
* Which timestamp each record kind contributes, and why (full reasoning in the
|
|
227
316
|
* module header, "#1125 — newest-across-ALL records"):
|
|
228
|
-
* - GENUINE (`!
|
|
317
|
+
* - GENUINE (`!isNonAnchorStub()`) → `completed_at`, written by the real
|
|
229
318
|
* session-end path; falling back to `started_at` when `completed_at` is
|
|
230
319
|
* absent or unparseable, so a truncated/in-flight record still contributes
|
|
231
320
|
* the coverage it does prove. Taking the newer of the two can only move the
|
|
232
321
|
* anchor forward, never backward, so it does not inflate `deltaHours`.
|
|
233
|
-
* - BACKFILL STUB → `started_at` ONLY.
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
322
|
+
* - BACKFILL STUB → `started_at` ONLY. An abandoned session proves the
|
|
323
|
+
* ledger alive at its start, not at a reconstructed end; anchoring on the
|
|
324
|
+
* stub's `completed_at` would let a backfill run erase a real gap.
|
|
325
|
+
* - Either kind: a field listed in `_backfill_incomplete_fields` was not
|
|
326
|
+
* measured and contributes nothing (`fieldCandidate()`).
|
|
237
327
|
*
|
|
238
328
|
* The two kinds are MAXED, not ranked: an older genuine record must not outrank
|
|
239
329
|
* a newer stub (that priority order was the #1125 false-alert defect). When the
|
|
@@ -258,13 +348,13 @@ function lastLedgerEntry(records) {
|
|
|
258
348
|
for (const record of records) {
|
|
259
349
|
if (!record || typeof record !== 'object') continue;
|
|
260
350
|
|
|
261
|
-
if (
|
|
262
|
-
newestStub = keepNewer(newestStub,
|
|
351
|
+
if (isNonAnchorStub(record)) {
|
|
352
|
+
newestStub = keepNewer(newestStub, fieldCandidate(record, 'started_at'));
|
|
263
353
|
continue;
|
|
264
354
|
}
|
|
265
355
|
|
|
266
|
-
let own =
|
|
267
|
-
const started =
|
|
356
|
+
let own = fieldCandidate(record, 'completed_at');
|
|
357
|
+
const started = fieldCandidate(record, 'started_at');
|
|
268
358
|
// `completed_at` is the normal anchor and wins ties; `started_at` only
|
|
269
359
|
// takes over when it is genuinely newer or `completed_at` is unusable.
|
|
270
360
|
if (started !== null && (own === null || started.ms > own.ms)) own = started;
|
|
@@ -284,7 +374,23 @@ function lastLedgerEntry(records) {
|
|
|
284
374
|
* current session's own events (all >= cutoff, since cutoff is this
|
|
285
375
|
* session's lock `started_at`) are structurally excluded without needing a
|
|
286
376
|
* `session_id` filter. Malformed lines and lines with a missing/invalid
|
|
287
|
-
* `timestamp` are skipped
|
|
377
|
+
* `timestamp` are skipped, and so is a previous session's own teardown output
|
|
378
|
+
* (`isClosingDiagnostic()`), which postdates that session's ledger record by
|
|
379
|
+
* construction.
|
|
380
|
+
*
|
|
381
|
+
* Known ceiling (BV-004) — post-close TURNS still count: a session that keeps
|
|
382
|
+
* talking after its `/close` wrote the ledger record emits
|
|
383
|
+
* `orchestrator.turn.stopped` (and the deprecated `orchestrator.session.stopped`)
|
|
384
|
+
* at the end of every further turn, so a post-close conversation spanning more
|
|
385
|
+
* than `WARN_THRESHOLD_HOURS` warns although that session IS recorded. Kept on
|
|
386
|
+
* purpose: those same events are the only trace an OOM-/`kill -9`-ended session
|
|
387
|
+
* leaves. Size (W1 census 2026-09-19 over this repo's events.jsonl(.1), not
|
|
388
|
+
* re-measured here): 4 of 10 resolvable firings were this class. Revisit trigger: the HR-105 firing-rate
|
|
389
|
+
* audit shows this class alone above ~10% of sessions that ran the probe
|
|
390
|
+
* (HR-101) — then skip a stop event whose `semantic_session_id` matches a
|
|
391
|
+
* GENUINE ledger record (that session is recorded, so its post-close turns are
|
|
392
|
+
* not a close-through gap), which needs `newestForeignEvent` to receive the
|
|
393
|
+
* canonical records.
|
|
288
394
|
*
|
|
289
395
|
* @param {string[]} lines
|
|
290
396
|
* @param {number} cutoffMs
|
|
@@ -303,6 +409,7 @@ function newestForeignEvent(lines, cutoffMs) {
|
|
|
303
409
|
const ms = Date.parse(record.timestamp);
|
|
304
410
|
if (!Number.isFinite(ms)) continue;
|
|
305
411
|
if (ms >= cutoffMs) continue; // not "foreign" — belongs to (or postdates) the current session
|
|
412
|
+
if (isClosingDiagnostic(record)) continue; // a previous session's teardown, not activity
|
|
306
413
|
if (best === null || ms > best.ms) best = { iso: record.timestamp, ms };
|
|
307
414
|
}
|
|
308
415
|
return best;
|
|
@@ -325,18 +432,53 @@ function newestForeignEvent(lines, cutoffMs) {
|
|
|
325
432
|
*
|
|
326
433
|
* @param {string} repoRoot
|
|
327
434
|
* @param {number} nowMs
|
|
328
|
-
* @returns {number}
|
|
435
|
+
* @returns {{cutoffMs: number, ownSessionId: string|null}}
|
|
329
436
|
*/
|
|
330
|
-
function
|
|
437
|
+
function resolveCutoff(repoRoot, nowMs) {
|
|
331
438
|
let lock;
|
|
332
439
|
try {
|
|
333
440
|
lock = readLock({ repoRoot });
|
|
334
441
|
} catch {
|
|
335
|
-
return nowMs;
|
|
442
|
+
return { cutoffMs: nowMs, ownSessionId: null };
|
|
443
|
+
}
|
|
444
|
+
if (!lock) return { cutoffMs: nowMs, ownSessionId: null };
|
|
445
|
+
const ownSessionId = typeof lock.session_id === 'string' && lock.session_id !== '' ? lock.session_id : null;
|
|
446
|
+
const parsed = typeof lock.started_at === 'string' ? Date.parse(lock.started_at) : NaN;
|
|
447
|
+
return { cutoffMs: Number.isFinite(parsed) ? parsed : nowMs, ownSessionId };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Pull the cutoff back to the FIRST `orchestrator.session.lock.acquired` event
|
|
452
|
+
* carrying the current lock's raw `session_id`. A re-acquire under the same id
|
|
453
|
+
* (the SessionStart hook on a context compact) rewrites `started_at`, which
|
|
454
|
+
* moved the cutoff past the session's own earlier events — and not all of them
|
|
455
|
+
* carry a `session_id` to filter on (`secret_masker.applied` does not).
|
|
456
|
+
* Measured 2026-09-19: a 2h15m session warned "11.9h behind" about itself on
|
|
457
|
+
* compact. Raw id only, never the semantic label (two sessions can mint the
|
|
458
|
+
* same label, #1066). Linear scan over events.jsonl — the same pass
|
|
459
|
+
* `newestForeignEvent()` already makes.
|
|
460
|
+
*
|
|
461
|
+
* @param {string[]} lines
|
|
462
|
+
* @param {string|null} ownSessionId
|
|
463
|
+
* @param {number} cutoffMs
|
|
464
|
+
* @returns {number}
|
|
465
|
+
*/
|
|
466
|
+
function ownGenesisMs(lines, ownSessionId, cutoffMs) {
|
|
467
|
+
if (ownSessionId === null) return cutoffMs;
|
|
468
|
+
let genesis = cutoffMs;
|
|
469
|
+
for (const line of lines) {
|
|
470
|
+
if (!line.includes(ownSessionId)) continue;
|
|
471
|
+
let record;
|
|
472
|
+
try {
|
|
473
|
+
record = JSON.parse(line);
|
|
474
|
+
} catch {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (record?.event !== 'orchestrator.session.lock.acquired' || record.session_id !== ownSessionId) continue;
|
|
478
|
+
const ms = Date.parse(record.timestamp);
|
|
479
|
+
if (Number.isFinite(ms) && ms < genesis) genesis = ms;
|
|
336
480
|
}
|
|
337
|
-
|
|
338
|
-
const parsed = Date.parse(lock.started_at);
|
|
339
|
-
return Number.isFinite(parsed) ? parsed : nowMs;
|
|
481
|
+
return genesis;
|
|
340
482
|
}
|
|
341
483
|
|
|
342
484
|
/**
|
|
@@ -386,9 +528,9 @@ export function checkSessionsStaleness({ repoRoot, now = Date.now() } = {}) {
|
|
|
386
528
|
const eventLines = readJsonlLines(path.join(repoRoot, EVENTS_PATH));
|
|
387
529
|
if (eventLines === null || eventLines.length === 0) return null;
|
|
388
530
|
|
|
389
|
-
const cutoffMs =
|
|
531
|
+
const { cutoffMs, ownSessionId } = resolveCutoff(repoRoot, nowMs);
|
|
390
532
|
|
|
391
|
-
const foreign = newestForeignEvent(eventLines, cutoffMs);
|
|
533
|
+
const foreign = newestForeignEvent(eventLines, ownGenesisMs(eventLines, ownSessionId, cutoffMs));
|
|
392
534
|
if (foreign === null) return null;
|
|
393
535
|
|
|
394
536
|
const deltaMs = foreign.ms - ledger.ms;
|