session-orchestrator 5.3.0 → 5.5.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/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -64,6 +64,9 @@ import { serializeSessionLineChecked as defaultSerialize } from './session-schem
|
|
|
64
64
|
import { resolveStateMdPath as defaultResolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
65
65
|
import { parseStateMd as defaultParseStateMd } from './state-md/yaml-parser.mjs';
|
|
66
66
|
import { canonicalizeSessions } from './sessions-canonical.mjs';
|
|
67
|
+
// Already in the hook import set (`emitEvent` lives there): the rotation-aware
|
|
68
|
+
// reader adds no new file to the SessionStart/SessionEnd hook graph.
|
|
69
|
+
import { scanEventsBackwards } from './events.mjs';
|
|
67
70
|
// Leaf constants module (no imports of its own) and ALREADY in the hook import
|
|
68
71
|
// set via session-schema/validator.mjs — importing it here adds no new file to
|
|
69
72
|
// the SessionStart/SessionEnd hook graph. The profile enum must not be
|
|
@@ -326,6 +329,117 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
|
|
|
326
329
|
};
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Events-ledger window for the #1401 census (`events-retention-banner.mjs`):
|
|
334
|
+
* `null` — {@link gatherSessionEvents} reads back as far as a candidate's own
|
|
335
|
+
* `session.started`, however old, so no day count bounds it. A cut history
|
|
336
|
+
* leaves the candidate's oldest events unread: its record then lists
|
|
337
|
+
* `started_at` among `_backfill_incomplete_fields` — the same answer as before
|
|
338
|
+
* this reader existed, never a guessed time.
|
|
339
|
+
*/
|
|
340
|
+
export const REQUIRED_EVENTS_WINDOW_DAYS = null;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Every record of the ledger — rotated archives included — whose raw line
|
|
344
|
+
* contains `id`, oldest first. A backward scan in the existing rotation-aware
|
|
345
|
+
* reader (`scanEventsBackwards`), reversed: one scan's order is global, so the
|
|
346
|
+
* file order {@link collectSessionEvents} depends on survives.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} eventsPath
|
|
349
|
+
* @param {string} id
|
|
350
|
+
* @param {number|null} deadlineMs — epoch ms the scan must end by; `null` = unbounded
|
|
351
|
+
* @returns {Array<object>|null} `null` when the deadline had passed before the
|
|
352
|
+
* scan or cut it short — a partial history is never handed on as a whole one.
|
|
353
|
+
*/
|
|
354
|
+
function recordsMentioning(eventsPath, id, deadlineMs) {
|
|
355
|
+
let budgetMs;
|
|
356
|
+
if (deadlineMs !== null) {
|
|
357
|
+
budgetMs = deadlineMs - Date.now();
|
|
358
|
+
if (!(budgetMs > 0)) return null;
|
|
359
|
+
}
|
|
360
|
+
const newestFirst = [];
|
|
361
|
+
const { truncated, unreadable } = scanEventsBackwards({
|
|
362
|
+
filePath: eventsPath,
|
|
363
|
+
filter: id,
|
|
364
|
+
budgetMs,
|
|
365
|
+
onRecord: (record) => {
|
|
366
|
+
newestFirst.push(record);
|
|
367
|
+
return false;
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
// #1505.5b: a source the walk could not read cut the history exactly like a
|
|
371
|
+
// deadline does — its records are simply absent — so it defers the candidate
|
|
372
|
+
// the same way. Ceiling (BV-004): a PERMANENTLY unreadable archive defers its
|
|
373
|
+
// candidates on every run (`skipped-history-unread`, visible, never a record
|
|
374
|
+
// dated NOW). `gaps` (pruned/unindexed archives) are deliberately NOT treated
|
|
375
|
+
// so: the walk reports every gap of the whole ledger, mostly far older than
|
|
376
|
+
// the candidate, and deferring on any of them would skip the candidate
|
|
377
|
+
// forever — telling a gap inside the session's window apart needs its
|
|
378
|
+
// first_ts/last_ts against the session's range, a follow-up.
|
|
379
|
+
const cut = truncated || (Array.isArray(unreadable) && unreadable.length > 0);
|
|
380
|
+
return cut ? null : newestFirst.reverse();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* {@link collectSessionEvents} over the ACTIVE events.jsonl, and — when that
|
|
385
|
+
* holds no `session.started` for the candidate — over the whole rotated ledger.
|
|
386
|
+
*
|
|
387
|
+
* #1498: `planSessions` reads every archive since #1414, but this core read only
|
|
388
|
+
* the active file, so a candidate whose events had rotated away gathered
|
|
389
|
+
* nothing: no `lastEventMs`, hence never dead-by-age and skipped behind a live
|
|
390
|
+
* foreign lock on every run — or, without a lock, written with `started_at` and
|
|
391
|
+
* `completed_at` set to NOW. The active read stays first and cheap; the scan
|
|
392
|
+
* runs only for such a candidate, and once its record is written the CLI's
|
|
393
|
+
* sessions.jsonl pre-filter skips it on every later run.
|
|
394
|
+
*
|
|
395
|
+
* Cost (#1498 review): ≈0.55-0.8 ms per MB of ledger per such candidate —
|
|
396
|
+
* measured 2026-10-02: 25 candidates over a synthetic 58 MB ledger of six
|
|
397
|
+
* sources took 1.17-1.20 s, active reads included, at load average ~20; the
|
|
398
|
+
* review measured +0.80-0.87 s for the scans alone at 60 MB and load ~7. That
|
|
399
|
+
* does not fit the 5 s SessionStart hook unbounded, so a caller may pass
|
|
400
|
+
* `deadlineMs`: ONE epoch-ms deadline shared by every candidate of a run. A
|
|
401
|
+
* scan the deadline skipped or cut returns `historyUnread: true` instead of a
|
|
402
|
+
* partial answer, and the caller defers the candidate — never the active-file
|
|
403
|
+
* answer, which for such a candidate is a record dated NOW (see above) that the
|
|
404
|
+
* dedupe would then keep forever.
|
|
405
|
+
*
|
|
406
|
+
* One filtered scan keeps the order exact: a native UUID appears on every event
|
|
407
|
+
* of its session. Without one, the semantic label is scanned for the UUID
|
|
408
|
+
* bridge, then that UUID's events. Named ceiling (BV-004): a label bridging to
|
|
409
|
+
* SEVERAL UUIDs would need several scans merged without a shared order, so it
|
|
410
|
+
* keeps the active-file answer — revisit if a backfill record shows
|
|
411
|
+
* `started_at` among its incomplete fields while the archive holds it.
|
|
412
|
+
*
|
|
413
|
+
* @param {Function} readFileSync
|
|
414
|
+
* @param {string} eventsPath
|
|
415
|
+
* @param {{ sessionId: string|null, semanticSessionId: string|null }} ids
|
|
416
|
+
* @param {number|null} [deadlineMs=null] — shared scan deadline (epoch ms); `null` = unbounded
|
|
417
|
+
* @returns {ReturnType<typeof collectSessionEvents> & { historyUnread?: true }}
|
|
418
|
+
*/
|
|
419
|
+
function gatherSessionEvents(readFileSync, eventsPath, ids, deadlineMs = null) {
|
|
420
|
+
const gathered = collectSessionEvents(readJsonlSafe(readFileSync, eventsPath), ids);
|
|
421
|
+
if (gathered.startedAt !== null) return gathered;
|
|
422
|
+
try {
|
|
423
|
+
let uuid = isUuid(ids.sessionId) ? ids.sessionId : null;
|
|
424
|
+
if (uuid === null) {
|
|
425
|
+
const label = ids.semanticSessionId || ids.sessionId;
|
|
426
|
+
if (!label) return gathered;
|
|
427
|
+
const bridge = recordsMentioning(eventsPath, label, deadlineMs);
|
|
428
|
+
if (bridge === null) return { ...gathered, historyUnread: true };
|
|
429
|
+
const bridged = collectSessionEvents(bridge, ids).uuids;
|
|
430
|
+
if (bridged.size !== 1) return gathered;
|
|
431
|
+
[uuid] = bridged;
|
|
432
|
+
}
|
|
433
|
+
const history = recordsMentioning(eventsPath, uuid, deadlineMs);
|
|
434
|
+
if (history === null) return { ...gathered, historyUnread: true };
|
|
435
|
+
// Empty only when the scan found no source at all — e.g. a reader seam the
|
|
436
|
+
// filesystem does not back; the active answer stands.
|
|
437
|
+
return history.length > 0 ? collectSessionEvents(history, ids) : gathered;
|
|
438
|
+
} catch {
|
|
439
|
+
return gathered;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
329
443
|
/**
|
|
330
444
|
* Determine whether a candidate should be treated as dead-by-age DESPITE a
|
|
331
445
|
* live FOREIGN session.lock (#731 — the historical migration CLI blocks
|
|
@@ -724,6 +838,11 @@ function checkAlreadyRecorded(readFileSync, sessionsPath, { recordId, sessionId
|
|
|
724
838
|
* `lastEventMs` strictly predates this value bypasses a foreign live lock
|
|
725
839
|
* regardless of `relaxDeadByAge`. Corresponds to the CLI's
|
|
726
840
|
* `--assume-dead-before <ISO>` flag.
|
|
841
|
+
* @param {number|null} [args.archiveScanDeadlineMs=null]
|
|
842
|
+
* #1498 — epoch ms by which a scan of the ROTATED ledger must end; one value
|
|
843
|
+
* shared by every candidate of a run (see `gatherSessionEvents`). A candidate
|
|
844
|
+
* whose history the deadline left unread returns `skipped-history-unread`
|
|
845
|
+
* and is retried by the next run. `null` (CLI, SessionEnd) = unbounded.
|
|
727
846
|
* @param {object} [args.deps] DI overrides (fs, appendJsonl, readLock, …)
|
|
728
847
|
* @returns {Promise<object>}
|
|
729
848
|
*/
|
|
@@ -736,6 +855,7 @@ export async function backfillAbandonedSession({
|
|
|
736
855
|
relaxDeadByAge = false,
|
|
737
856
|
ownSessionIsEnding = false,
|
|
738
857
|
assumeDeadBeforeMs = null,
|
|
858
|
+
archiveScanDeadlineMs = null,
|
|
739
859
|
deps = {},
|
|
740
860
|
} = {}) {
|
|
741
861
|
const {
|
|
@@ -795,8 +915,14 @@ export async function backfillAbandonedSession({
|
|
|
795
915
|
}
|
|
796
916
|
|
|
797
917
|
// -- Read events (needed to synthesize, and to bridge UUID→semantic) ----
|
|
798
|
-
const
|
|
799
|
-
|
|
918
|
+
const gathered = gatherSessionEvents(
|
|
919
|
+
readFileSync, eventsPath, { sessionId, semanticSessionId }, archiveScanDeadlineMs,
|
|
920
|
+
);
|
|
921
|
+
// Deferred, never answered from the active file alone: for this candidate
|
|
922
|
+
// that answer is a record dated NOW, which the dedupe keeps forever.
|
|
923
|
+
if (gathered.historyUnread) {
|
|
924
|
+
return { action: 'skipped-history-unread', sessionId: recordId ?? sessionId };
|
|
925
|
+
}
|
|
800
926
|
|
|
801
927
|
// -- Resolve a deferred id from the lock bridge or a synthetic mint ------
|
|
802
928
|
if (recordId === null) {
|
|
@@ -1153,8 +1279,7 @@ export async function backfillCompletedFromStateMd({
|
|
|
1153
1279
|
|
|
1154
1280
|
// -- Derive whatever is derivable from events.jsonl (never STATE.md body) -
|
|
1155
1281
|
const eventsPath = path.join(repoRoot, ...EVENTS_REL);
|
|
1156
|
-
const
|
|
1157
|
-
const gathered = collectSessionEvents(events, { sessionId: nativeId, semanticSessionId });
|
|
1282
|
+
const gathered = gatherSessionEvents(readFileSync, eventsPath, { sessionId: nativeId, semanticSessionId });
|
|
1158
1283
|
|
|
1159
1284
|
// -- Synthesize + validate (round-trip gate) BEFORE any disk mutation ----
|
|
1160
1285
|
const record = synthesizeRecord({
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
* @property {string} [error] - error message, `reason === 'error'` only.
|
|
46
46
|
* @property {number} [scanned] - entries read from the active store (ran only).
|
|
47
47
|
* @property {number} [archived] - entries moved to the archive sidecar (ran only).
|
|
48
|
+
* @property {number} [malformed] - unparseable store lines kept verbatim, never
|
|
49
|
+
* archived (ran only; a measured `0` when none).
|
|
48
50
|
* @property {string} [archivePath] - the archive sidecar written to (ran only).
|
|
49
51
|
*/
|
|
50
52
|
|
|
@@ -88,10 +90,10 @@ function findPhaseDecision(plan, phase) {
|
|
|
88
90
|
* happens to resolve to (#941 — that mistake once landed a test record in the
|
|
89
91
|
* operator's real fleet ledger).
|
|
90
92
|
*
|
|
91
|
-
* @param {{repoRoot: string, scanned: number, archived: number}} ctx
|
|
93
|
+
* @param {{repoRoot: string, scanned: number, archived: number, malformed: number}} ctx
|
|
92
94
|
* @returns {Promise<void>}
|
|
93
95
|
*/
|
|
94
|
-
async function emitSweepApplied({ repoRoot, scanned, archived }) {
|
|
96
|
+
async function emitSweepApplied({ repoRoot, scanned, archived, malformed }) {
|
|
95
97
|
if (typeof repoRoot !== 'string' || repoRoot.trim() === '') {
|
|
96
98
|
process.stderr.write(
|
|
97
99
|
`tail-runner: skipped ${SWEEP_EVENT} — no repoRoot given; ` +
|
|
@@ -103,7 +105,7 @@ async function emitSweepApplied({ repoRoot, scanned, archived }) {
|
|
|
103
105
|
const { emitEvent, sessionAttribution } = await import('../events.mjs');
|
|
104
106
|
await emitEvent(
|
|
105
107
|
SWEEP_EVENT,
|
|
106
|
-
{ scanned, archived, source: SWEEP_SOURCE, ...sessionAttribution(repoRoot) },
|
|
108
|
+
{ scanned, archived, malformed, source: SWEEP_SOURCE, ...sessionAttribution(repoRoot) },
|
|
107
109
|
{ repoRoot },
|
|
108
110
|
);
|
|
109
111
|
} catch {
|
|
@@ -151,9 +153,12 @@ export async function runExpiredSweep({ repoRoot, plan, now, graceDays, emit = t
|
|
|
151
153
|
|
|
152
154
|
const scanned = res?.scanned ?? 0;
|
|
153
155
|
const archived = res?.archived ?? 0;
|
|
154
|
-
|
|
156
|
+
// The sweep result omits `malformed` when the read found none, so absent IS
|
|
157
|
+
// a measured 0 here — carried as a number like its two siblings (HR-105).
|
|
158
|
+
const malformed = res?.malformed ?? 0;
|
|
159
|
+
if (emit) await emitSweepApplied({ repoRoot, scanned, archived, malformed });
|
|
155
160
|
|
|
156
|
-
return { ran: true, scanned, archived, archivePath: res?.archivePath ?? archivePath };
|
|
161
|
+
return { ran: true, scanned, archived, malformed, archivePath: res?.archivePath ?? archivePath };
|
|
157
162
|
} catch (err) {
|
|
158
163
|
// Fail CLOSED (see the module header): a maintenance sweep must never be
|
|
159
164
|
// able to block a session close.
|
|
@@ -25,9 +25,12 @@
|
|
|
25
25
|
* @param {unknown} obj — a value already parsed from JSON.
|
|
26
26
|
* @returns {boolean} true when `obj` carries all six required lock fields with
|
|
27
27
|
* the right primitive types. `semantic_session_id` and `last_heartbeat` are
|
|
28
|
-
* deliberately NOT required
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* deliberately NOT required. `semantic_session_id` is absent on harnesses
|
|
29
|
+
* that resolve no semantic id. A lock without `last_heartbeat` must stay
|
|
30
|
+
* shape-valid so it remains VISIBLE: since the #595 sunset (2026-10-02)
|
|
31
|
+
* `isLockLive()` calls it not live and `acquire()` classifies it
|
|
32
|
+
* `stale-heartbeat`, so the bootstrap reclaims it. Requiring the field here
|
|
33
|
+
* would hide it from every `readLock()` caller instead.
|
|
31
34
|
*/
|
|
32
35
|
export function isLockShape(obj) {
|
|
33
36
|
return (
|