session-orchestrator 3.21.0 → 3.23.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +11 -2
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +10 -10
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +269 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/memory-proposal-collector.md +6 -4
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/eli5.md +33 -0
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +52 -5
- package/docs/session-config-reference.md +57 -12
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +26 -16
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +12 -2
- package/hooks/on-session-end.mjs +237 -104
- package/hooks/on-session-start.mjs +404 -110
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +133 -21
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/monitors/monitors.json +6 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +12 -8
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +221 -68
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +84 -11
- package/scripts/lib/gates/gate-helpers.mjs +93 -21
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +284 -31
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +104 -4
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +165 -7
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +159 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +111 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +467 -72
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +59 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +478 -0
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +6 -0
- package/scripts/validate-wave-scope.mjs +73 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -25
- package/skills/_shared/parallel-aware-preamble.md +35 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +61 -8
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +5 -4
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/discovery/SKILL.md +28 -24
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +18 -6
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +44 -4
- package/skills/session-end/SKILL.md +70 -24
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +48 -5
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +96 -48
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +129 -24
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/templates/_shared/journey-manifest.md +110 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
- package/templates/_shared/rules/parallel-sessions.md +0 -77
package/hooks/on-session-end.mjs
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* `session.started` had no terminal partner.
|
|
11
11
|
*
|
|
12
12
|
* JSONL format (`.orchestrator/metrics/events.jsonl`):
|
|
13
|
-
* {"timestamp":<ISO>,"event":"orchestrator.session.ended","session_id":"...","reason":"<reason>","duration_ms":<int>}
|
|
13
|
+
* {"timestamp":<ISO>,"event":"orchestrator.session.ended","session_id":"...","semantic_session_id":"...","reason":"<reason>","duration_ms":<int>}
|
|
14
|
+
* (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.)
|
|
14
15
|
*
|
|
15
16
|
* Exit codes: 0 always (informational hook — must never block session teardown).
|
|
16
17
|
* stdin: optional JSON { hook_event_name:"SessionEnd", session_id?, reason?, cwd? }.
|
|
@@ -30,16 +31,20 @@ if (!shouldRunHook('on-session-end')) process.exit(0);
|
|
|
30
31
|
|
|
31
32
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
32
33
|
import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
backfillAbandonedSession,
|
|
36
|
+
backfillCompletedFromStateMd,
|
|
37
|
+
} from '../scripts/lib/session-close-backfill.mjs';
|
|
34
38
|
import {
|
|
35
39
|
readLockDetailed,
|
|
36
40
|
release,
|
|
37
|
-
isLockLive,
|
|
38
41
|
loadOwnerProof,
|
|
39
|
-
isLockOwnedByProof,
|
|
40
42
|
OWNER_PROOF_RELPATH,
|
|
41
43
|
} from '../scripts/lib/session-lock.mjs';
|
|
44
|
+
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
42
45
|
import { deregisterSelf, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
46
|
+
import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
|
|
47
|
+
import { flush } from '../scripts/lib/telemetry/sync.mjs';
|
|
43
48
|
import { attemptLockReconciliation } from './_lib/lock-reconcile.mjs';
|
|
44
49
|
|
|
45
50
|
// ---------------------------------------------------------------------------
|
|
@@ -74,29 +79,40 @@ async function readStdinJson() {
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
|
-
* Resolve this session's id + duration + semantic id.
|
|
78
|
-
* otherwise fall back to
|
|
79
|
-
* on-session-start.mjs).
|
|
80
|
-
* is
|
|
81
|
-
* mismatched / unknown session.
|
|
82
|
+
* Resolve this session's id + duration + semantic id. A stdin session_id wins
|
|
83
|
+
* ONLY when it parses as a UUID; otherwise fall back to
|
|
84
|
+
* `.orchestrator/current-session.json` (written by on-session-start.mjs).
|
|
85
|
+
* duration_ms is only computed when the ENDING session is the one recorded in
|
|
86
|
+
* current-session.json — never fabricated for a mismatched / unknown session.
|
|
87
|
+
*
|
|
88
|
+
* #1091 / Kanevry#66 — WRITER/READER SYMMETRY. `on-session-start.mjs`
|
|
89
|
+
* (`resolveSessionId`, :316-317) accepts a stdin raw id only when
|
|
90
|
+
* `parseSessionId(fromStdin)?.format === 'uuid'` and otherwise mints a
|
|
91
|
+
* `randomUUID()`; `current-session.json`, `session.lock` and the host registry
|
|
92
|
+
* are therefore ALWAYS keyed by a UUID. This reader used to accept ANY
|
|
93
|
+
* non-empty stdin string, so a harness that passed a non-UUID id
|
|
94
|
+
* (`{"session_id":"not-a-uuid"}`) resolved a key that matches nothing written
|
|
95
|
+
* at start: the raw-ID ownership compare below (:330) fails, the lock is
|
|
96
|
+
* neither released nor reconciled, and it LEAKS until its TTL expires. Same
|
|
97
|
+
* for `deregisterSelf()`, whose registry file is named after the id
|
|
98
|
+
* `registerSelf()` used. Mirroring the writer's rule here makes the fallback
|
|
99
|
+
* (which reads exactly those artifacts) the single source of the identity.
|
|
100
|
+
* The ownership compare itself stays an exact string `===` — this changes
|
|
101
|
+
* WHICH id is compared, never HOW.
|
|
82
102
|
*
|
|
83
103
|
* `semanticSessionId` is read from current-session.json (present since #587):
|
|
84
104
|
* it is the SEMANTIC id (`<branch>-<date>-<mode>-<n>`) that sessions.jsonl is
|
|
85
105
|
* keyed by, and the id the backfill (C1 #724) uses when the stdin session_id is
|
|
86
|
-
* a harness UUID with no lock.acquired bridge.
|
|
106
|
+
* a harness UUID with no lock.acquired bridge. It is never a lock-release
|
|
107
|
+
* ownership identity; only the raw/native `sessionId` may release a live lock.
|
|
87
108
|
*
|
|
88
109
|
* #863 defect (c) — `semanticSessionId` is gated by the SAME "ending session
|
|
89
110
|
* IS the recorded one" check as `durationMs` above. `current-session.json` is
|
|
90
111
|
* a single repo-global file: it always reflects whichever session most
|
|
91
112
|
* recently ran SessionStart, which may be a DIFFERENT, still-live session
|
|
92
|
-
* when multiple windows share this repo.
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* session's `semantic_session_id` unconditionally — main()'s `ownBySemantic`
|
|
96
|
-
* check then matched that OTHER, still-running session's lock and released
|
|
97
|
-
* it. Gating this field closes that window: an unrelated/mismatched ending
|
|
98
|
-
* session now resolves `semanticSessionId: null`, so `ownBySemantic` can
|
|
99
|
-
* never accidentally fire on someone else's identity.
|
|
113
|
+
* when multiple windows share this repo. An unrelated/mismatched ending
|
|
114
|
+
* session therefore resolves `semanticSessionId: null` rather than inheriting
|
|
115
|
+
* another live session's backfill identity.
|
|
100
116
|
*
|
|
101
117
|
* @param {object|null} input
|
|
102
118
|
* @param {string} projectRoot
|
|
@@ -104,7 +120,8 @@ async function readStdinJson() {
|
|
|
104
120
|
*/
|
|
105
121
|
async function resolveSession(input, projectRoot) {
|
|
106
122
|
const fromStdin = input?.session_id ?? input?.sessionId ?? null;
|
|
107
|
-
|
|
123
|
+
// UUID-only, exactly as the writer decides it (see the docblock above).
|
|
124
|
+
let sessionId = parseSessionId(fromStdin)?.format === 'uuid' ? fromStdin : null;
|
|
108
125
|
|
|
109
126
|
let recordedId = null;
|
|
110
127
|
let semanticSessionId = null;
|
|
@@ -144,6 +161,152 @@ async function resolveSession(input, projectRoot) {
|
|
|
144
161
|
return { sessionId, semanticSessionId: resolvedSemanticSessionId, durationMs };
|
|
145
162
|
}
|
|
146
163
|
|
|
164
|
+
/**
|
|
165
|
+
* POST budget for the close-time telemetry flush (ms).
|
|
166
|
+
*
|
|
167
|
+
* NAMED CEILING, and the reason it is BELOW the module default POST_TIMEOUT_MS
|
|
168
|
+
* (3000): the two timeouts fail differently.
|
|
169
|
+
*
|
|
170
|
+
* - This one expiring is LOSSLESS. `flush()` catches the abort and routes the
|
|
171
|
+
* record into the bounded offline queue; the daily fallback
|
|
172
|
+
* (`shouldDailyFlush`) drains it on a later session.
|
|
173
|
+
* - The HARNESS timeout expiring is LOSSY. Claude Code kills the hook process
|
|
174
|
+
* mid-flight, so the enqueue never runs and the record is simply gone —
|
|
175
|
+
* which is the exact failure #1138 exists to remove.
|
|
176
|
+
*
|
|
177
|
+
* So the internal bound must stay comfortably under the harness bound, not
|
|
178
|
+
* merely below it. Budget (hooks/hooks.json `SessionEnd.timeout: 10` s, raised
|
|
179
|
+
* from 5 with this change): up to 500 ms stdin + a backfill measured at a
|
|
180
|
+
* ~845 ms median (its TAIL, not its median, is what would collide) + lock
|
|
181
|
+
* release + deregistration, then this 2 s. That leaves several seconds of slack
|
|
182
|
+
* for the backfill tail while still covering a normal round-trip many times
|
|
183
|
+
* over.
|
|
184
|
+
*
|
|
185
|
+
* Revisit if the hooks.json SessionEnd timeout changes — the pin lives in
|
|
186
|
+
* tests/hooks/on-session-end.test.mjs.
|
|
187
|
+
*/
|
|
188
|
+
const TELEMETRY_FLUSH_TIMEOUT_MS = 2000;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Read `persistence` from the repo's Session Config. Defaults to `true` — the
|
|
192
|
+
* same default `scripts/lib/config.mjs` applies — so an unreadable or absent
|
|
193
|
+
* CLAUDE.md (or its Codex alias AGENTS.md) never silently disables the flush.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} projectRoot
|
|
196
|
+
* @returns {Promise<boolean>}
|
|
197
|
+
*/
|
|
198
|
+
async function readPersistence(projectRoot) {
|
|
199
|
+
try {
|
|
200
|
+
const md = await readConfigFile(projectRoot);
|
|
201
|
+
return parseSessionConfig(md).persistence !== false;
|
|
202
|
+
} catch {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Reduce a `flush()` result to the two-field breadcrumb the event carries.
|
|
209
|
+
*
|
|
210
|
+
* `reason` is normalised to its head token because `flush()` may return
|
|
211
|
+
* `build-error: <message>`, and a raw error message is unbounded free text in a
|
|
212
|
+
* stream whose whole purpose is aggregation by class.
|
|
213
|
+
*
|
|
214
|
+
* @param {{sent?: boolean, queued?: boolean, reason?: string}|null|undefined} res
|
|
215
|
+
* @returns {{outcome: 'sent'|'queued'|'gated'|'skipped', reason: string}}
|
|
216
|
+
*/
|
|
217
|
+
function classifyFlush(res) {
|
|
218
|
+
const reason = String(res?.reason ?? 'unknown').split(':')[0];
|
|
219
|
+
if (res?.sent === true) return { outcome: 'sent', reason };
|
|
220
|
+
if (res?.queued === true) return { outcome: 'queued', reason };
|
|
221
|
+
if (reason === 'gated') return { outcome: 'gated', reason };
|
|
222
|
+
return { outcome: 'skipped', reason };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* #1138 — the MECHANICAL telemetry flush.
|
|
227
|
+
*
|
|
228
|
+
* Until now the close-time flush existed only as prose in
|
|
229
|
+
* `skills/session-end/SKILL.md` § Phase 3.45, i.e. it ran only when the
|
|
230
|
+
* coordinator LLM happened to execute that phase. Measured 2026-08-23: 588
|
|
231
|
+
* session closes across 13 repos produced 82 ingest records (~14%). A hook is
|
|
232
|
+
* the only caller that fires on EVERY close, including the ones that never
|
|
233
|
+
* reach `/close` at all.
|
|
234
|
+
*
|
|
235
|
+
* Strictly best-effort: never throws, and bounded by TELEMETRY_FLUSH_TIMEOUT_MS.
|
|
236
|
+
* The consent gate lives INSIDE `flush()` (`resolveConsent()` is its first
|
|
237
|
+
* statement) — this function deliberately does not re-implement it, so there is
|
|
238
|
+
* exactly one place where "may we send?" is decided.
|
|
239
|
+
*
|
|
240
|
+
* Always emits `orchestrator.telemetry.flush` with `{ outcome, reason }` and
|
|
241
|
+
* NOTHING else — no payload, no anon_id. Per `.claude/rules/host-resources.md`
|
|
242
|
+
* HR-105, a mechanism whose firing rate nothing records cannot be falsified;
|
|
243
|
+
* this event is what makes the flush rate measurable next time.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} projectRoot
|
|
246
|
+
* @returns {Promise<void>}
|
|
247
|
+
*/
|
|
248
|
+
async function flushTelemetry(projectRoot) {
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = (await readPersistence(projectRoot))
|
|
252
|
+
? classifyFlush(await flush({
|
|
253
|
+
metricsDir: path.join(projectRoot, '.orchestrator', 'metrics'),
|
|
254
|
+
timeoutMs: TELEMETRY_FLUSH_TIMEOUT_MS,
|
|
255
|
+
}))
|
|
256
|
+
// `persistence: false` means this session leaves no durable local trace;
|
|
257
|
+
// a telemetry ping is a durable record too, so it honours the same switch.
|
|
258
|
+
: { outcome: 'skipped', reason: 'persistence-disabled' };
|
|
259
|
+
} catch {
|
|
260
|
+
result = { outcome: 'skipped', reason: 'error' };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// `result` is passed through verbatim: it holds EXACTLY {outcome, reason}, so
|
|
264
|
+
// no payload field can leak into the event by accident.
|
|
265
|
+
try {
|
|
266
|
+
await emitEvent('orchestrator.telemetry.flush', result);
|
|
267
|
+
} catch { /* observability is best-effort */ }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* #1068 AC2 — emit ONE canonically queryable outcome per backfill attempt.
|
|
272
|
+
*
|
|
273
|
+
* Until now a backfill result reached exactly one place:
|
|
274
|
+
* `.orchestrator/metrics/session-close-backfill.log`, a side-log no lifecycle
|
|
275
|
+
* consumer reads and no staleness/integrity check joins against. The event
|
|
276
|
+
* stream is where every other lifecycle fact already lives, so the outcome goes
|
|
277
|
+
* there too — carrying IDENTITY (`session_id` raw UUID + the record id the
|
|
278
|
+
* backfill acted on + the attested semantic id), ACTION, and, when the action
|
|
279
|
+
* was an error, its REASON. The side-log is unchanged and remains the verbose
|
|
280
|
+
* copy; this is the queryable one.
|
|
281
|
+
*
|
|
282
|
+
* `kind` distinguishes the two backfill classes, which share the log file and
|
|
283
|
+
* would otherwise be indistinguishable in aggregate: `'abandoned'`
|
|
284
|
+
* (`backfillAbandonedSession`) vs `'state-md-completed'`
|
|
285
|
+
* (`backfillCompletedFromStateMd`).
|
|
286
|
+
*
|
|
287
|
+
* Every id key is OMITTED when unknown — never `""`, never a guess (#1068 AC1).
|
|
288
|
+
* Best-effort throughout: observability must never block teardown.
|
|
289
|
+
*
|
|
290
|
+
* @param {'abandoned'|'state-md-completed'} kind
|
|
291
|
+
* @param {{action?: string, sessionId?: string|null, supersedes?: string, error?: string}|null|undefined} result
|
|
292
|
+
* @param {{sessionId: string|null, semanticSessionId: string|null}} ids
|
|
293
|
+
* @returns {Promise<void>}
|
|
294
|
+
*/
|
|
295
|
+
async function emitBackfillOutcome(kind, result, { sessionId, semanticSessionId }) {
|
|
296
|
+
try {
|
|
297
|
+
const recordId = typeof result?.sessionId === 'string' ? result.sessionId : null;
|
|
298
|
+
await emitEvent('orchestrator.session.backfill_completed', {
|
|
299
|
+
kind,
|
|
300
|
+
action: typeof result?.action === 'string' ? result.action : 'unknown',
|
|
301
|
+
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
302
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
303
|
+
...(recordId !== null ? { record_id: recordId } : {}),
|
|
304
|
+
...(typeof result?.supersedes === 'string' ? { supersedes: result.supersedes } : {}),
|
|
305
|
+
...(typeof result?.error === 'string' ? { reason: result.error } : {}),
|
|
306
|
+
});
|
|
307
|
+
} catch { /* observability is best-effort */ }
|
|
308
|
+
}
|
|
309
|
+
|
|
147
310
|
// ---------------------------------------------------------------------------
|
|
148
311
|
// main
|
|
149
312
|
// ---------------------------------------------------------------------------
|
|
@@ -158,8 +321,20 @@ async function main() {
|
|
|
158
321
|
|
|
159
322
|
// Single emission path: emitEvent writes the canonical {timestamp, event, ...payload}
|
|
160
323
|
// JSONL record AND fires the optional Clank webhook with the SAME event name.
|
|
324
|
+
//
|
|
325
|
+
// #1068 AC1 — the terminal event carries the raw UUID *and* the semantic id
|
|
326
|
+
// whenever the latter is ATTESTED, so a lifecycle outcome is joinable by
|
|
327
|
+
// identity from events.jsonl alone (previously only `session.lock.acquired`
|
|
328
|
+
// carried both, and only ~1/3 of sessions emit one). The key is OMITTED, never
|
|
329
|
+
// written as `""` or `null`, when `resolveSession()` could not attest it —
|
|
330
|
+
// "identity unresolved" must stay visibly unresolved rather than become a
|
|
331
|
+
// guessed id (#1068 AC1's explicit "niemals eine geratene ID"). Note the
|
|
332
|
+
// attestation bar is the #863 defect (c) guard inside `resolveSession()`: an
|
|
333
|
+
// ending session that is NOT the one current-session.json describes resolves
|
|
334
|
+
// `semanticSessionId: null` and therefore emits no key here.
|
|
161
335
|
await emitEvent('orchestrator.session.ended', {
|
|
162
336
|
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
337
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
163
338
|
reason,
|
|
164
339
|
duration_ms: durationMs,
|
|
165
340
|
});
|
|
@@ -178,27 +353,32 @@ async function main() {
|
|
|
178
353
|
// historical migration CLI) is NEVER passed here: a foreign lock that is live
|
|
179
354
|
// at hook-time is, by definition, a real active session, not stale history.
|
|
180
355
|
try {
|
|
181
|
-
await backfillAbandonedSession({ repoRoot: projectRoot, sessionId, semanticSessionId });
|
|
356
|
+
const res = await backfillAbandonedSession({ repoRoot: projectRoot, sessionId, semanticSessionId });
|
|
357
|
+
await emitBackfillOutcome('abandoned', res, { sessionId, semanticSessionId });
|
|
182
358
|
} catch { /* best-effort — never block teardown */ }
|
|
183
359
|
|
|
184
|
-
// (
|
|
185
|
-
//
|
|
186
|
-
//
|
|
360
|
+
// (a2) #429 — STATE.md `status: completed` self-heal. Orthogonal to (a)
|
|
361
|
+
// above and to THIS session's own id: it reads STATE.md directly and
|
|
362
|
+
// repairs a PAST session whose `status: completed` was set (by hand or
|
|
363
|
+
// otherwise) without session-end's Phase 3.7 ever writing the matching
|
|
364
|
+
// sessions.jsonl record — the exact state `commands/close.md`'s
|
|
365
|
+
// Pre-Check then reads as "already finalized" forever after. Cheap
|
|
366
|
+
// no-op on the overwhelmingly common path (STATE.md status is
|
|
367
|
+
// 'active'/'paused'/'idle', or the record already exists).
|
|
368
|
+
try {
|
|
369
|
+
const res = await backfillCompletedFromStateMd({ repoRoot: projectRoot });
|
|
370
|
+
await emitBackfillOutcome('state-md-completed', res, { sessionId, semanticSessionId });
|
|
371
|
+
} catch { /* best-effort — never block teardown */ }
|
|
372
|
+
|
|
373
|
+
// (b) Deterministic lock release — ONLY a lock whose raw/native session_id
|
|
374
|
+
// exactly matches this ending session's raw/native sessionId. Semantic
|
|
375
|
+
// IDs remain lifecycle/backfill metadata and persisted proofs remain an
|
|
376
|
+
// additional delete defense, but neither can heal a raw-ID mismatch.
|
|
187
377
|
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// (2) when the harness UUID rotates (clear/compact/resume) and
|
|
193
|
-
// current-session.json's semantic_session_id is null/stale, BOTH
|
|
194
|
-
// ownership checks are false and this branch never runs release()
|
|
195
|
-
// at all — the lock survives until the NEXT session-start's own
|
|
196
|
-
// reaper sweep discovers it.
|
|
197
|
-
// Both are addressed below without loosening the strict
|
|
198
|
-
// never-release-a-foreign-lock invariant: release() only ever runs when
|
|
199
|
-
// ownership matched; the reconciliation fallback only ever runs through
|
|
200
|
-
// reapRepoLock(), which independently NEVER reaps a live lease (and only
|
|
201
|
-
// ever reaps on this same host with a dead recorded PID).
|
|
378
|
+
// A mismatch routes only through reconciliation. The reaper independently
|
|
379
|
+
// never touches a live lease, a cross-host lease, or a lease whose
|
|
380
|
+
// recorded PID is still alive on this host; this retains dead/stale-lock
|
|
381
|
+
// recovery without granting a SessionEnd hook foreign-lock ownership.
|
|
202
382
|
try {
|
|
203
383
|
// readLockDetailed (additive, see its JSDoc in session-lock.mjs) replaces
|
|
204
384
|
// readLock() here so an unreadable/corrupt lock file is DISTINGUISHABLE
|
|
@@ -219,61 +399,15 @@ async function main() {
|
|
|
219
399
|
} catch { /* observability is best-effort */ }
|
|
220
400
|
} else if (lockDetail.status === 'ok') {
|
|
221
401
|
const lock = lockDetail.lock;
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const ownBySemanticFallback =
|
|
232
|
-
semanticSessionId !== null && lock.session_id === semanticSessionId;
|
|
233
|
-
const ownBySemantic = ownBySemanticStrict || ownBySemanticFallback;
|
|
234
|
-
|
|
235
|
-
// #987 Part 2 — persisted ownership proof (pid + host + started_at,
|
|
236
|
-
// written at lock genesis by bootstrapLock Step 2b, see
|
|
237
|
-
// writeOwnerProof() in session-lock.mjs). loadOwnerProof() is
|
|
238
|
-
// fail-closed: a missing/corrupt proof file yields null, and a null
|
|
239
|
-
// proof presented to isLockOwnedByProof() yields false — the hook then
|
|
240
|
-
// degrades to exactly the pre-#987 (proof-less) behaviour below.
|
|
241
|
-
const proof = loadOwnerProof({ repoRoot: projectRoot });
|
|
242
|
-
const ownByProof = isLockOwnedByProof(lock, proof);
|
|
243
|
-
|
|
244
|
-
// #906-class fix: the semantic session id
|
|
245
|
-
// (`<branch>-<date>-<mode>-<n>`) is NOT globally unique — the
|
|
246
|
-
// id-counter can hand out the SAME id to two different session
|
|
247
|
-
// processes (live-observed twice on 2026-07-29: `main-2026-07-29-deep-1`
|
|
248
|
-
// and `main-2026-07-29-session-1`, each assigned to two distinct
|
|
249
|
-
// sessions). A live lock matched ONLY via a collidable semantic id
|
|
250
|
-
// (strict OR fallback, never corroborated by a non-collidable factor)
|
|
251
|
-
// is therefore NEVER trusted for release.
|
|
252
|
-
//
|
|
253
|
-
// #987 Part 2 — the discrimination the #906-class fix escalated as
|
|
254
|
-
// "structurally unobservable" is now OBSERVABLE via the persisted
|
|
255
|
-
// owner proof: a genuine same-session UUID rotation (clear/compact)
|
|
256
|
-
// and a foreign same-day semantic collision used to produce the
|
|
257
|
-
// IDENTICAL shape here (UUID mismatch + semantic match on a live
|
|
258
|
-
// lock), so the previous fix conservatively left BOTH un-released
|
|
259
|
-
// (bounded by the ttl_hours reaper). The proof written at lock genesis
|
|
260
|
-
// (pid + host + started_at — presence-at-genesis, never the collidable
|
|
261
|
-
// session_id/semantic id, see the FACTOR CHOICE docblock on
|
|
262
|
-
// isLockOwnedByProof()) is exactly the discriminator that RCR-007
|
|
263
|
-
// escalation named: on a self-rotation the on-disk lock is FROZEN at
|
|
264
|
-
// its pre-rotation values (bootstrapLock's shouldForce is false for a
|
|
265
|
-
// rotated UUID, so it bails without touching the lock), which are the
|
|
266
|
-
// SAME values the proof captured at genesis → ownByProof is true → the
|
|
267
|
-
// lock is released correctly. A foreign same-day collision wrote its
|
|
268
|
-
// lock in a DIFFERENT process at a DIFFERENT millisecond → ownByProof
|
|
269
|
-
// is false → semanticOnlyLive stays true → no release, the foreign
|
|
270
|
-
// lease survives (the never-release-a-foreign-lock invariant is
|
|
271
|
-
// untouched). No proof on disk (pre-#987 sessions, failed proof
|
|
272
|
-
// write) → ownByProof false → the conservative pre-#987 behaviour.
|
|
273
|
-
const semanticOnlyLive = !ownByUuid && !ownByProof && ownBySemantic && isLockLive(lock);
|
|
274
|
-
const releaseEligible = (ownByUuid || ownBySemantic) && !semanticOnlyLive;
|
|
275
|
-
|
|
276
|
-
if (releaseEligible) {
|
|
402
|
+
const ownByRawId = sessionId !== null && lock.session_id === sessionId;
|
|
403
|
+
|
|
404
|
+
if (ownByRawId) {
|
|
405
|
+
// The proof is deliberately passed to release() only after this raw-ID
|
|
406
|
+
// check. release() validates a supplied proof against its fresh on-disk
|
|
407
|
+
// read, preserving it as a second defense for the raw-owned path without
|
|
408
|
+
// allowing proof equality to establish ownership by itself.
|
|
409
|
+
const proof = loadOwnerProof({ repoRoot: projectRoot });
|
|
410
|
+
|
|
277
411
|
// Defense-in-depth (#987): when a persisted proof exists, hand it to
|
|
278
412
|
// release() so the delete is double-gated (session_id match AND
|
|
279
413
|
// proof match) at the fs layer too. `proof` is `null` whenever
|
|
@@ -284,7 +418,7 @@ async function main() {
|
|
|
284
418
|
// A 'proof-mismatch' result flows into the existing release_failed
|
|
285
419
|
// breadcrumb below (releaseResult.reason surfaces verbatim).
|
|
286
420
|
const releaseResult = release({
|
|
287
|
-
sessionId
|
|
421
|
+
sessionId,
|
|
288
422
|
repoRoot: projectRoot,
|
|
289
423
|
proof,
|
|
290
424
|
});
|
|
@@ -337,7 +471,7 @@ async function main() {
|
|
|
337
471
|
await emitEvent('orchestrator.session.lock.released', {
|
|
338
472
|
session_id: sessionId,
|
|
339
473
|
lock_session_id: lock.session_id,
|
|
340
|
-
semantic_session_id: semanticSessionId,
|
|
474
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
341
475
|
end_reason: reason,
|
|
342
476
|
caller: 'on-session-end',
|
|
343
477
|
outcome: benignAlreadyGone ? 'already-gone' : 'deleted',
|
|
@@ -355,16 +489,9 @@ async function main() {
|
|
|
355
489
|
} catch { /* best-effort — a leftover proof is self-invalidating */ }
|
|
356
490
|
}
|
|
357
491
|
} else {
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// is still live (semanticOnlyLive). attemptLockReconciliation() is
|
|
362
|
-
// the extracted, DI-testable seam (Issue #748) — it internally
|
|
363
|
-
// no-ops when the lease is still live (isLockLive), which is exactly
|
|
364
|
-
// what makes it safe to route the semanticOnlyLive case here too: it
|
|
365
|
-
// is otherwise best-effort, and reapRepoLock() never touches a live
|
|
366
|
-
// lease, a cross-host lease, or a lease whose recorded PID is still
|
|
367
|
-
// alive on this host.
|
|
492
|
+
// A raw-ID mismatch never grants release ownership. Reconciliation is
|
|
493
|
+
// the only remaining cleanup path; it no-ops for a live lock and safely
|
|
494
|
+
// reaps only eligible dead/stale locks.
|
|
368
495
|
await attemptLockReconciliation({ repoRoot: projectRoot, sessionId, lock });
|
|
369
496
|
}
|
|
370
497
|
}
|
|
@@ -416,6 +543,12 @@ async function main() {
|
|
|
416
543
|
logSweepEvent({ event: 'deregister-failed', session_id: sessionId, error: err?.message ?? String(err) });
|
|
417
544
|
}
|
|
418
545
|
}
|
|
546
|
+
|
|
547
|
+
// (d) #1138 — mechanical telemetry flush. Deliberately LAST: it is the only
|
|
548
|
+
// step here that may touch the network, so every local-fs guarantee above
|
|
549
|
+
// (backfill, lock release, deregistration) is already durable before the
|
|
550
|
+
// hook spends any of its remaining budget on a POST.
|
|
551
|
+
await flushTelemetry(projectRoot);
|
|
419
552
|
}
|
|
420
553
|
|
|
421
554
|
// Exit 0 always — informational hook must never block session teardown.
|