session-orchestrator 5.12.0 → 5.13.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 +2 -2
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +162 -4
- package/README.md +126 -127
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +1 -1
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +90 -17
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +49 -15
- package/docs/session-config-template.md +30 -22
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +139 -49
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -2
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +39 -27
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +2 -2
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +4 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +72 -9
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +49 -5
- package/scripts/lib/so-hud/runtime.mjs +15 -4
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +14 -5
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +19 -7
- package/scripts/lib/vault-mirror/process.mjs +56 -27
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +3 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +79 -0
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +2 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +10 -17
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +29 -6
- package/scripts/state-md-idle-reset.mjs +2 -1
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/skills/_shared/fleet-protocol.md +142 -1
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +3 -2
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +94 -11
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-documentation-updates.md +14 -12
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +4 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +9 -3
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -2
- 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 +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -339,7 +339,8 @@ async function pushRecord(record, opts) {
|
|
|
339
339
|
if (!acquired.ok) return acquired;
|
|
340
340
|
|
|
341
341
|
try {
|
|
342
|
-
|
|
342
|
+
// Retention trim (#1633) — the reader drops the same entries, see LIFECYCLE_RETENTION_MS.
|
|
343
|
+
const map = withoutExpired(readCurrentMap(currentFile), typeof opts?.now === 'number' ? opts.now : Date.now());
|
|
343
344
|
map[record.agentId] = record;
|
|
344
345
|
const written = writeJsonAtomicSync(currentFile, map, { tmpPrefix: '.agent-status-current' });
|
|
345
346
|
if (!written.ok) return { ok: false, reason: 'fs-error', error: written.error };
|
|
@@ -415,6 +416,113 @@ export async function setProgress(agentId, progress = {}, opts = {}) {
|
|
|
415
416
|
return pushRecord(record, opts);
|
|
416
417
|
}
|
|
417
418
|
|
|
419
|
+
// Lifecycle entries (#1580) carry a fresh harness agent_id per subagent, so —
|
|
420
|
+
// unlike the wave-executor's reused ids — the cache grew by one ~200-byte entry
|
|
421
|
+
// per subagent, and the lock wait of a write grew with it (#1633: 18 parallel
|
|
422
|
+
// writers, 10,000 entries → p50 468 ms / max 910 ms against a 1 s timeout).
|
|
423
|
+
// Pruning the cache ALONE is wrong: readCurrentStatus() folds cache ∪ ledger
|
|
424
|
+
// tail, and an id present only in the ledger tail flips the whole read to
|
|
425
|
+
// `rebuilt-log` (cache-behind-ledger). So the retention bound below is applied
|
|
426
|
+
// on BOTH sides with one predicate: every write drops expired lifecycle entries
|
|
427
|
+
// from the cache, and every read drops them from the cache view AND the ledger
|
|
428
|
+
// view before the fold. Expiry is monotone in time (expired at write time ⇒
|
|
429
|
+
// expired at any later read), so a trimmed cache reads exactly like the
|
|
430
|
+
// untrimmed one. The ledger itself is never rewritten.
|
|
431
|
+
// CEILING: 14 days of subagents stay in the cache (#1633 measured 725 starts in
|
|
432
|
+
// 31 days here ≈ 330 entries per 14 days); a repo with no subagent for 14 days
|
|
433
|
+
// reads as "no lifecycle record" (HUD "?") until the next start.
|
|
434
|
+
// REVISIT-TRIGGER: a session that legitimately lives longer than 14 days, or a
|
|
435
|
+
// repo starting more than ~5,000 subagents per 14 days (the cache is back in the
|
|
436
|
+
// measured slow range) — then shorten the window or trim per session.
|
|
437
|
+
const LIFECYCLE_STATES = new Set(['running', 'done']);
|
|
438
|
+
export const LIFECYCLE_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
439
|
+
|
|
440
|
+
// A `running` lifecycle entry without a SubagentStop (#1633: 2 of 725 agents
|
|
441
|
+
// since 2026-09-15, 0.28 %) would otherwise count as running forever. Bound
|
|
442
|
+
// measured 2026-10-16 on this repo's `.orchestrator/metrics/subagents.jsonl`
|
|
443
|
+
// (start→stop wall clock per agent_id): since 2026-09-15 n=810, p50 5.7 min,
|
|
444
|
+
// p99 47 min, max 715.5 min; all time n=3017, p99 55.1 min, max 1042.9 min
|
|
445
|
+
// (17.4 h — a batch of agents stopped together at a session resume; the hook's
|
|
446
|
+
// own duration_ms max is 132 min). 24 h sits above every observed pair.
|
|
447
|
+
// CEILING: a subagent genuinely running longer than 24 h is reported as decayed,
|
|
448
|
+
// not running. REVISIT-TRIGGER: any start→stop pair above ~20 h in subagents.jsonl.
|
|
449
|
+
export const LIFECYCLE_DECAY_MS = 24 * 60 * 60 * 1000;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Whether a record is a lifecycle entry older than the retention window
|
|
453
|
+
* (`LIFECYCLE_RETENTION_MS`). Records without a parsable `ts` never expire.
|
|
454
|
+
* @param {object} rec
|
|
455
|
+
* @param {number} now Epoch millis.
|
|
456
|
+
* @returns {boolean}
|
|
457
|
+
*/
|
|
458
|
+
function isLifecycleExpired(rec, now) {
|
|
459
|
+
const ms = rec?.kind === 'lifecycle' ? recordTsMs(rec) : null;
|
|
460
|
+
return ms !== null && now - ms > LIFECYCLE_RETENTION_MS;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Whether a lifecycle entry still says `running` but is older than
|
|
465
|
+
* `LIFECYCLE_DECAY_MS` — its SubagentStop most likely never arrived (#1633).
|
|
466
|
+
* Such an agent is neither running nor done: report it as decayed.
|
|
467
|
+
* @param {object} rec
|
|
468
|
+
* @param {number} now Epoch millis.
|
|
469
|
+
* @returns {boolean}
|
|
470
|
+
*/
|
|
471
|
+
export function isLifecycleDecayed(rec, now) {
|
|
472
|
+
if (rec?.kind !== 'lifecycle' || rec.status !== 'running') return false;
|
|
473
|
+
const ms = recordTsMs(rec);
|
|
474
|
+
return ms !== null && now - ms > LIFECYCLE_DECAY_MS;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Copy of a map without its expired lifecycle entries (null-prototype).
|
|
479
|
+
* @param {Record<string, object>} entries
|
|
480
|
+
* @param {number} now
|
|
481
|
+
* @returns {Record<string, object>}
|
|
482
|
+
*/
|
|
483
|
+
function withoutExpired(entries, now) {
|
|
484
|
+
const out = Object.create(null);
|
|
485
|
+
for (const [id, rec] of Object.entries(entries)) {
|
|
486
|
+
if (!isLifecycleExpired(rec, now)) out[id] = rec;
|
|
487
|
+
}
|
|
488
|
+
return out;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Record a subagent lifecycle transition from the SubagentStart/SubagentStop
|
|
493
|
+
* hooks (#1580): `running` on start, `done` on stop, bound to the parent
|
|
494
|
+
* session so the HUD counts only its own session's agents. No-throw,
|
|
495
|
+
* best-effort — same result contract as `setStatus`.
|
|
496
|
+
*
|
|
497
|
+
* @param {string} agentId Harness agent id (LWW map key).
|
|
498
|
+
* @param {{ status: 'running'|'done', sessionId: string, agentType?: string|null }} lifecycle
|
|
499
|
+
* @param {{ repoRoot?: string, timeoutMs?: number, now?: number }} [opts]
|
|
500
|
+
* @returns {Promise<{ ok: true } | { ok: false, reason: 'invalid-input'|'timeout'|'fs-error', error?: string }>}
|
|
501
|
+
*/
|
|
502
|
+
export async function setLifecycle(agentId, lifecycle = {}, opts = {}) {
|
|
503
|
+
if (typeof agentId !== 'string' || agentId.trim().length === 0 || UNSAFE_MAP_KEYS.has(agentId)) {
|
|
504
|
+
return { ok: false, reason: 'invalid-input', error: 'agentId must be a non-empty, safe string' };
|
|
505
|
+
}
|
|
506
|
+
const { status, sessionId, agentType } = lifecycle ?? {};
|
|
507
|
+
if (!LIFECYCLE_STATES.has(status)) {
|
|
508
|
+
return { ok: false, reason: 'invalid-input', error: 'status must be running or done' };
|
|
509
|
+
}
|
|
510
|
+
if (typeof sessionId !== 'string' || sessionId.trim().length === 0) {
|
|
511
|
+
return { ok: false, reason: 'invalid-input', error: 'sessionId must be a non-empty string' };
|
|
512
|
+
}
|
|
513
|
+
const now = typeof opts?.now === 'number' ? opts.now : Date.now();
|
|
514
|
+
const record = {
|
|
515
|
+
agentId,
|
|
516
|
+
kind: 'lifecycle',
|
|
517
|
+
status,
|
|
518
|
+
session_id: sessionId,
|
|
519
|
+
ts: new Date(now).toISOString(),
|
|
520
|
+
};
|
|
521
|
+
if (typeof agentType === 'string' && agentType.trim().length > 0) record.agent_type = truncate(agentType);
|
|
522
|
+
|
|
523
|
+
return pushRecord(record, opts);
|
|
524
|
+
}
|
|
525
|
+
|
|
418
526
|
/**
|
|
419
527
|
* Rebuild the current-status map from a bounded TAIL of the append-only ledger
|
|
420
528
|
* (#1342). The ledger is the source of truth; this is how a consumer reads it
|
|
@@ -599,14 +707,24 @@ export function rebuildCurrentFromLedger(opts = {}) {
|
|
|
599
707
|
* per-agentId, a `rebuilt-log` view can hold BOTH kinds. A consumer must treat
|
|
600
708
|
* `binding` as optional — its ABSENCE means "from the cache", never "bound".
|
|
601
709
|
*
|
|
602
|
-
*
|
|
710
|
+
* Retention (#1633): lifecycle entries older than `LIFECYCLE_RETENTION_MS`
|
|
711
|
+
* (relative to `opts.now`, default `Date.now()`) are absent from the view.
|
|
712
|
+
*
|
|
713
|
+
* @param {{ repoRoot?: string, maxBytes?: number, now?: number }} [opts]
|
|
603
714
|
* @returns {{ entries: Record<string, object>, source: 'live-map'|'rebuilt-log'|'stale-cache'|'absent', at: string|null, degraded?: object }}
|
|
604
715
|
*/
|
|
605
716
|
export function readCurrentStatus(opts = {}) {
|
|
606
|
-
const
|
|
717
|
+
const now = typeof opts?.now === 'number' ? opts.now : Date.now();
|
|
718
|
+
const cacheRead = readCurrentMapDetailed(currentPathFor(opts?.repoRoot));
|
|
607
719
|
const rebuilt = rebuildCurrentFromLedger(opts);
|
|
720
|
+
// Both views lose their expired lifecycle entries BEFORE the fold (#1633), so a
|
|
721
|
+
// cache trimmed at write time reads exactly like the untrimmed one.
|
|
722
|
+
const cache = { ...cacheRead, entries: withoutExpired(cacheRead.entries, now) };
|
|
723
|
+
const ledgerEntries = withoutExpired(rebuilt.entries, now);
|
|
608
724
|
|
|
609
725
|
const cacheNewest = newestTsMs(cache.entries);
|
|
726
|
+
// Provenance checks read the UNTRIMMED ledger: trimming can only lower the
|
|
727
|
+
// cache's newest stamp, so comparing against the full ledger stays exact.
|
|
610
728
|
const ledgerNewest = rebuilt.at === null ? null : Date.parse(rebuilt.at);
|
|
611
729
|
const ledgerUnusable =
|
|
612
730
|
rebuilt.degraded !== null &&
|
|
@@ -626,7 +744,7 @@ export function readCurrentStatus(opts = {}) {
|
|
|
626
744
|
let at;
|
|
627
745
|
|
|
628
746
|
const cacheIds = Object.keys(cache.entries);
|
|
629
|
-
const ledgerIds = Object.keys(
|
|
747
|
+
const ledgerIds = Object.keys(ledgerEntries);
|
|
630
748
|
|
|
631
749
|
if (ledgerUnusable && cacheIds.length === 0) {
|
|
632
750
|
// Nothing on disk at all — no ledger to verify against AND no cached entry
|
|
@@ -648,8 +766,8 @@ export function readCurrentStatus(opts = {}) {
|
|
|
648
766
|
const cached = Object.prototype.hasOwnProperty.call(cache.entries, id)
|
|
649
767
|
? cache.entries[id]
|
|
650
768
|
: undefined;
|
|
651
|
-
const logged = Object.prototype.hasOwnProperty.call(
|
|
652
|
-
?
|
|
769
|
+
const logged = Object.prototype.hasOwnProperty.call(ledgerEntries, id)
|
|
770
|
+
? ledgerEntries[id]
|
|
653
771
|
: undefined;
|
|
654
772
|
|
|
655
773
|
if (cached === undefined) {
|
|
@@ -682,7 +800,7 @@ export function readCurrentStatus(opts = {}) {
|
|
|
682
800
|
}
|
|
683
801
|
|
|
684
802
|
at = newestTsMs(entries);
|
|
685
|
-
if (tookFromLedger || (!cache.ok &&
|
|
803
|
+
if (tookFromLedger || (!cache.ok && Object.keys(rebuilt.entries).length > 0)) {
|
|
686
804
|
source = 'rebuilt-log';
|
|
687
805
|
if (cache.reason !== 'ok') reasons.unshift(cache.reason);
|
|
688
806
|
else reasons.unshift('cache-behind-ledger');
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-usage-census.mjs — which agent types ran, on which model, at what cost
|
|
3
|
+
* shape (#1617). Read-only, stdlib only.
|
|
4
|
+
*
|
|
5
|
+
* ## Why the transcripts and not the ledger
|
|
6
|
+
*
|
|
7
|
+
* `.orchestrator/metrics/subagents.jsonl` sees only dispatches from sessions in
|
|
8
|
+
* THIS repo, carries no observed model on most records and no effort at all, and
|
|
9
|
+
* ~87% of its stop records are the #939 phantom class with no type. The fleet
|
|
10
|
+
* dispatches from 20+ repos. The Claude Code transcripts are the source that
|
|
11
|
+
* holds all of it:
|
|
12
|
+
*
|
|
13
|
+
* <root>/<project>/<session>/subagents/**\/agent-<id>.meta.json → agentType
|
|
14
|
+
* <root>/<project>/<session>/subagents/**\/agent-<id>.jsonl → message.model + usage
|
|
15
|
+
*
|
|
16
|
+
* (`**` because Workflow agents nest one level deeper, under
|
|
17
|
+
* `subagents/workflows/<wf-id>/`.)
|
|
18
|
+
*
|
|
19
|
+
* ## Definitions (the numbers are only comparable if these hold)
|
|
20
|
+
*
|
|
21
|
+
* run one `agent-<id>.jsonl` whose FIRST turn falls inside the window.
|
|
22
|
+
* Window membership by first turn, not by file mtime, so a run is
|
|
23
|
+
* never split across two weekly censuses.
|
|
24
|
+
* turn one API response. Streaming writes several assistant lines per
|
|
25
|
+
* response; they are collapsed on `requestId`, else `message.id`, last
|
|
26
|
+
* usage block wins — the same recipe as hooks/subagent-telemetry.mjs
|
|
27
|
+
* extractTranscriptUsage() (#950/#963). A line carrying neither key is
|
|
28
|
+
* its own turn. The turn's timestamp is its FIRST line (request time).
|
|
29
|
+
* Only lines with a `message.model` count as turns. The raw line
|
|
30
|
+
* count is reported beside it as `assistant_lines` /
|
|
31
|
+
* `models_by_lines` — the unit the #1617 discovery measured.
|
|
32
|
+
* rebuild a cache re-creation after an idle gap: the gap to the previous turn
|
|
33
|
+
* exceeds GAP_SECONDS AND this turn's cache_creation exceeds
|
|
34
|
+
* REBUILD_MIN_CREATION AND its cache_read is below its cache_creation.
|
|
35
|
+
* start input + cache_creation + cache_read of the run's first turn.
|
|
36
|
+
*
|
|
37
|
+
* ## Unreadable lines
|
|
38
|
+
*
|
|
39
|
+
* Every line is parsed; a line that does not parse is skipped AND counted in
|
|
40
|
+
* `malformed_lines` (measurement-discipline.md § Unlesbare JSONL-Zeilen). A
|
|
41
|
+
* meta.json that is missing or unreadable puts the run under `(no-meta)` /
|
|
42
|
+
* `(unreadable-meta)` rather than dropping it.
|
|
43
|
+
*
|
|
44
|
+
* Memory: transcripts are streamed line by line (readline). Per file only the
|
|
45
|
+
* turn map of that one run is held, so a multi-GB window stays bounded by the
|
|
46
|
+
* largest single run.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import fs from 'node:fs';
|
|
50
|
+
import path from 'node:path';
|
|
51
|
+
import readline from 'node:readline';
|
|
52
|
+
|
|
53
|
+
export const SCHEMA = 'agent-usage-census/1';
|
|
54
|
+
export const GAP_SECONDS = 300;
|
|
55
|
+
export const REBUILD_MIN_CREATION = 20_000;
|
|
56
|
+
export const DEFAULT_DAYS = 14;
|
|
57
|
+
|
|
58
|
+
const AGENT_FILE_RE = /^agent-[A-Za-z0-9_-]+\.jsonl$/;
|
|
59
|
+
|
|
60
|
+
const clamp = (v) => (Number.isInteger(v) && v >= 0 ? v : 0);
|
|
61
|
+
|
|
62
|
+
/** Median of a numeric array; null for an empty one. */
|
|
63
|
+
export function median(values) {
|
|
64
|
+
if (values.length === 0) return null;
|
|
65
|
+
const s = [...values].sort((a, b) => a - b);
|
|
66
|
+
const mid = s.length >> 1;
|
|
67
|
+
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* List every subagent transcript under `root` whose mtime is at or after
|
|
72
|
+
* `cutoffMs` (a cheap pre-filter: a file untouched since the cutoff cannot hold
|
|
73
|
+
* a first turn inside the window). Unreadable directories are skipped and
|
|
74
|
+
* counted.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} root
|
|
77
|
+
* @param {number} cutoffMs
|
|
78
|
+
* @returns {{ files: string[], unreadable_dirs: number }}
|
|
79
|
+
*/
|
|
80
|
+
export function findSubagentTranscripts(root, cutoffMs) {
|
|
81
|
+
const files = [];
|
|
82
|
+
let unreadableDirs = 0;
|
|
83
|
+
const list = (dir) => {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
86
|
+
} catch {
|
|
87
|
+
unreadableDirs++;
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const walkSubagents = (dir) => {
|
|
92
|
+
for (const e of list(dir)) {
|
|
93
|
+
const p = path.join(dir, e.name);
|
|
94
|
+
if (e.isDirectory()) walkSubagents(p);
|
|
95
|
+
else if (e.isFile() && AGENT_FILE_RE.test(e.name)) {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.statSync(p).mtimeMs >= cutoffMs) files.push(p);
|
|
98
|
+
} catch {
|
|
99
|
+
// vanished between readdir and stat — not part of the window
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
for (const project of list(root)) {
|
|
105
|
+
if (!project.isDirectory()) continue;
|
|
106
|
+
for (const session of list(path.join(root, project.name))) {
|
|
107
|
+
if (!session.isDirectory()) continue;
|
|
108
|
+
const sub = path.join(root, project.name, session.name, 'subagents');
|
|
109
|
+
// lstat, not existsSync: a `subagents` symlink is skipped like every
|
|
110
|
+
// other symlink in this walk (Dirent types do not follow links).
|
|
111
|
+
let subStat;
|
|
112
|
+
try {
|
|
113
|
+
subStat = fs.lstatSync(sub);
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (subStat.isDirectory()) walkSubagents(sub);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
files.sort();
|
|
121
|
+
return { files, unreadable_dirs: unreadableDirs };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Agent type from the sibling meta.json.
|
|
126
|
+
* @param {string} jsonlPath
|
|
127
|
+
* @returns {string} agentType, or '(no-meta)' / '(unreadable-meta)'
|
|
128
|
+
*/
|
|
129
|
+
export function readAgentType(jsonlPath) {
|
|
130
|
+
const metaPath = jsonlPath.replace(/\.jsonl$/, '.meta.json');
|
|
131
|
+
let raw;
|
|
132
|
+
try {
|
|
133
|
+
raw = fs.readFileSync(metaPath, 'utf8');
|
|
134
|
+
} catch {
|
|
135
|
+
return '(no-meta)';
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const t = JSON.parse(raw)?.agentType;
|
|
139
|
+
return typeof t === 'string' && t.trim() ? t : '(no-meta)';
|
|
140
|
+
} catch {
|
|
141
|
+
return '(unreadable-meta)';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Stream one subagent transcript into a run summary.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} jsonlPath
|
|
149
|
+
* @returns {Promise<{ turns: Array<{ model: string, ts: number|null, input: number,
|
|
150
|
+
* cacheCreation: number, cacheRead: number, output: number }>,
|
|
151
|
+
* lines: Record<string, number>, malformed: number }>}
|
|
152
|
+
*/
|
|
153
|
+
export async function readRunTurns(jsonlPath) {
|
|
154
|
+
const byKey = new Map();
|
|
155
|
+
const unkeyed = [];
|
|
156
|
+
/** Raw assistant lines per model, BEFORE the streaming dedup. */
|
|
157
|
+
const lines = {};
|
|
158
|
+
let malformed = 0;
|
|
159
|
+
const rl = readline.createInterface({
|
|
160
|
+
input: fs.createReadStream(jsonlPath, { encoding: 'utf8' }),
|
|
161
|
+
crlfDelay: Infinity,
|
|
162
|
+
});
|
|
163
|
+
for await (const line of rl) {
|
|
164
|
+
if (!line.trim()) continue;
|
|
165
|
+
let obj;
|
|
166
|
+
try {
|
|
167
|
+
obj = JSON.parse(line);
|
|
168
|
+
} catch {
|
|
169
|
+
malformed++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (obj?.type !== 'assistant') continue;
|
|
173
|
+
const msg = obj.message;
|
|
174
|
+
const model = typeof msg?.model === 'string' && msg.model ? msg.model : null;
|
|
175
|
+
if (model === null) continue;
|
|
176
|
+
lines[model] = (lines[model] ?? 0) + 1;
|
|
177
|
+
const u = msg.usage && typeof msg.usage === 'object' ? msg.usage : {};
|
|
178
|
+
const ts = Date.parse(obj.timestamp);
|
|
179
|
+
const snap = {
|
|
180
|
+
model,
|
|
181
|
+
input: clamp(u.input_tokens),
|
|
182
|
+
cacheCreation: clamp(u.cache_creation_input_tokens),
|
|
183
|
+
cacheRead: clamp(u.cache_read_input_tokens),
|
|
184
|
+
output: clamp(u.output_tokens),
|
|
185
|
+
};
|
|
186
|
+
const key =
|
|
187
|
+
typeof obj.requestId === 'string' && obj.requestId
|
|
188
|
+
? `r:${obj.requestId}`
|
|
189
|
+
: typeof msg.id === 'string' && msg.id
|
|
190
|
+
? `m:${msg.id}`
|
|
191
|
+
: null;
|
|
192
|
+
if (key === null) {
|
|
193
|
+
unkeyed.push({ ...snap, ts: Number.isFinite(ts) ? ts : null });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const prev = byKey.get(key);
|
|
197
|
+
// Last usage block wins; the timestamp stays the FIRST one (request time).
|
|
198
|
+
byKey.set(key, { ...snap, ts: prev ? prev.ts : Number.isFinite(ts) ? ts : null });
|
|
199
|
+
}
|
|
200
|
+
return { turns: [...byKey.values(), ...unkeyed], lines, malformed };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Derive the per-run metrics from its turns (in file order).
|
|
205
|
+
* @param {Array<{ model: string, ts: number|null, input: number, cacheCreation: number,
|
|
206
|
+
* cacheRead: number, output: number }>} turns
|
|
207
|
+
*/
|
|
208
|
+
export function summarizeRun(turns) {
|
|
209
|
+
const ordered = [...turns].sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
210
|
+
const models = {};
|
|
211
|
+
let output = 0;
|
|
212
|
+
let rebuilds = 0;
|
|
213
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
214
|
+
const t = ordered[i];
|
|
215
|
+
models[t.model] = (models[t.model] ?? 0) + 1;
|
|
216
|
+
output += t.output;
|
|
217
|
+
const prev = ordered[i - 1];
|
|
218
|
+
if (prev && prev.ts !== null && t.ts !== null && (t.ts - prev.ts) / 1000 > GAP_SECONDS
|
|
219
|
+
&& t.cacheCreation > REBUILD_MIN_CREATION && t.cacheRead < t.cacheCreation) {
|
|
220
|
+
rebuilds++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const first = ordered[0];
|
|
224
|
+
return {
|
|
225
|
+
firstTs: first?.ts ?? null,
|
|
226
|
+
turns: ordered.length,
|
|
227
|
+
models,
|
|
228
|
+
output,
|
|
229
|
+
rebuilds,
|
|
230
|
+
startContext: first ? first.input + first.cacheCreation + first.cacheRead : null,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const shareMap = (counts, total, unit = 'turns') =>
|
|
235
|
+
Object.fromEntries(
|
|
236
|
+
Object.entries(counts)
|
|
237
|
+
.sort((a, b) => b[1] - a[1])
|
|
238
|
+
.map(([m, n]) => [m, { [unit]: n, share: total ? +(n / total).toFixed(4) : 0 }]),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Fold run summaries into the per-type census.
|
|
243
|
+
* @param {Array<{ agentType: string } & ReturnType<typeof summarizeRun>>} runs
|
|
244
|
+
*/
|
|
245
|
+
export function aggregate(runs) {
|
|
246
|
+
const groups = new Map();
|
|
247
|
+
const allModels = {};
|
|
248
|
+
let totalTurns = 0;
|
|
249
|
+
for (const r of runs) {
|
|
250
|
+
let g = groups.get(r.agentType);
|
|
251
|
+
if (!g) {
|
|
252
|
+
g = { runs: [], models: {} };
|
|
253
|
+
groups.set(r.agentType, g);
|
|
254
|
+
}
|
|
255
|
+
g.runs.push(r);
|
|
256
|
+
for (const [m, n] of Object.entries(r.models)) {
|
|
257
|
+
g.models[m] = (g.models[m] ?? 0) + n;
|
|
258
|
+
allModels[m] = (allModels[m] ?? 0) + n;
|
|
259
|
+
totalTurns += n;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const byType = [...groups.entries()].map(([agentType, g]) => {
|
|
263
|
+
const turns = g.runs.reduce((s, r) => s + r.turns, 0);
|
|
264
|
+
return {
|
|
265
|
+
agent_type: agentType,
|
|
266
|
+
dispatches: g.runs.length,
|
|
267
|
+
turns,
|
|
268
|
+
models: shareMap(g.models, turns),
|
|
269
|
+
turns_per_run_median: median(g.runs.map((r) => r.turns)),
|
|
270
|
+
output_tokens_per_run_median: median(g.runs.map((r) => r.output)),
|
|
271
|
+
cache_rebuilds: g.runs.reduce((s, r) => s + r.rebuilds, 0),
|
|
272
|
+
runs_with_cache_rebuild: g.runs.filter((r) => r.rebuilds > 0).length,
|
|
273
|
+
start_context_median: median(g.runs.map((r) => r.startContext).filter((v) => v !== null)),
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
byType.sort((a, b) => b.turns - a.turns || a.agent_type.localeCompare(b.agent_type));
|
|
277
|
+
return { turns: totalTurns, models: shareMap(allModels, totalTurns), by_agent_type: byType };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Run the census over a transcript root.
|
|
282
|
+
*
|
|
283
|
+
* @param {{ root: string, days?: number, now?: number, concurrency?: number }} opts
|
|
284
|
+
*/
|
|
285
|
+
export async function runCensus({ root, days = DEFAULT_DAYS, now = Date.now(), concurrency = 4 }) {
|
|
286
|
+
const cutoffMs = now - days * 86_400_000;
|
|
287
|
+
const { files, unreadable_dirs } = findSubagentTranscripts(root, cutoffMs);
|
|
288
|
+
const runs = [];
|
|
289
|
+
let malformed = 0;
|
|
290
|
+
let unreadableFiles = 0;
|
|
291
|
+
let outsideWindow = 0;
|
|
292
|
+
let emptyRuns = 0;
|
|
293
|
+
const lineModels = {};
|
|
294
|
+
let next = 0;
|
|
295
|
+
const worker = async () => {
|
|
296
|
+
while (next < files.length) {
|
|
297
|
+
const file = files[next++];
|
|
298
|
+
let res;
|
|
299
|
+
try {
|
|
300
|
+
res = await readRunTurns(file);
|
|
301
|
+
} catch {
|
|
302
|
+
unreadableFiles++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
malformed += res.malformed;
|
|
306
|
+
if (res.turns.length === 0) {
|
|
307
|
+
emptyRuns++;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const s = summarizeRun(res.turns);
|
|
311
|
+
if (s.firstTs === null || s.firstTs < cutoffMs || s.firstTs > now) {
|
|
312
|
+
outsideWindow++;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
runs.push({ agentType: readAgentType(file), ...s });
|
|
316
|
+
for (const [m, n] of Object.entries(res.lines)) lineModels[m] = (lineModels[m] ?? 0) + n;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker));
|
|
320
|
+
return {
|
|
321
|
+
schema: SCHEMA,
|
|
322
|
+
measured_at: new Date(now).toISOString(),
|
|
323
|
+
root,
|
|
324
|
+
window_days: days,
|
|
325
|
+
cutoff: new Date(cutoffMs).toISOString(),
|
|
326
|
+
definition: {
|
|
327
|
+
turn: 'assistant response with message.model, deduped on requestId else message.id (last usage wins)',
|
|
328
|
+
run_in_window: 'first turn timestamp >= cutoff',
|
|
329
|
+
cache_rebuild: `gap > ${GAP_SECONDS}s, cache_creation > ${REBUILD_MIN_CREATION}, cache_read < cache_creation`,
|
|
330
|
+
start_context: 'input + cache_creation + cache_read of the first turn',
|
|
331
|
+
},
|
|
332
|
+
files_scanned: files.length,
|
|
333
|
+
runs: runs.length,
|
|
334
|
+
runs_outside_window: outsideWindow,
|
|
335
|
+
runs_without_turns: emptyRuns,
|
|
336
|
+
unreadable_files: unreadableFiles,
|
|
337
|
+
unreadable_dirs,
|
|
338
|
+
malformed_lines: malformed,
|
|
339
|
+
...aggregate(runs),
|
|
340
|
+
// Raw streaming lines, not API turns: the #1617 discovery counted these
|
|
341
|
+
// (349,519 in 14 days); one response is ~1.9 lines. Kept for comparability.
|
|
342
|
+
assistant_lines: Object.values(lineModels).reduce((a, b) => a + b, 0),
|
|
343
|
+
models_by_lines: shareMap(lineModels, Object.values(lineModels).reduce((a, b) => a + b, 0), 'lines'),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const pct = (s) => `${(s * 100).toFixed(1)}%`;
|
|
348
|
+
const fmt = (v) => (v === null ? '-' : Number.isInteger(v) ? String(v) : v.toFixed(1));
|
|
349
|
+
|
|
350
|
+
/** Human-readable table of a census result. */
|
|
351
|
+
export function formatTable(result, { top = 25 } = {}) {
|
|
352
|
+
const out = [];
|
|
353
|
+
out.push(
|
|
354
|
+
`agent-usage-census: ${result.runs} runs / ${result.turns} turns, last ${result.window_days} days ` +
|
|
355
|
+
`(${result.files_scanned} files, malformed_lines ${result.malformed_lines}, ` +
|
|
356
|
+
`unreadable_files ${result.unreadable_files})`,
|
|
357
|
+
);
|
|
358
|
+
out.push(
|
|
359
|
+
'models: ' +
|
|
360
|
+
Object.entries(result.models)
|
|
361
|
+
.map(([m, v]) => `${m} ${pct(v.share)} (${v.turns})`)
|
|
362
|
+
.join(' · '),
|
|
363
|
+
);
|
|
364
|
+
out.push(
|
|
365
|
+
`raw assistant lines ${result.assistant_lines}: ` +
|
|
366
|
+
Object.entries(result.models_by_lines)
|
|
367
|
+
.slice(0, 3)
|
|
368
|
+
.map(([m, v]) => `${m} ${pct(v.share)}`)
|
|
369
|
+
.join(' · '),
|
|
370
|
+
);
|
|
371
|
+
out.push('');
|
|
372
|
+
const head = ['agent_type', 'runs', 'turns', 'models', 'turns/run', 'out/run', 'rebuilds', 'start_ctx'];
|
|
373
|
+
const rows = result.by_agent_type.slice(0, top).map((t) => [
|
|
374
|
+
t.agent_type,
|
|
375
|
+
String(t.dispatches),
|
|
376
|
+
String(t.turns),
|
|
377
|
+
Object.entries(t.models)
|
|
378
|
+
.slice(0, 3)
|
|
379
|
+
.map(([m, v]) => `${m.replace(/^claude-/, '')} ${pct(v.share)}`)
|
|
380
|
+
.join(', '),
|
|
381
|
+
fmt(t.turns_per_run_median),
|
|
382
|
+
fmt(t.output_tokens_per_run_median),
|
|
383
|
+
`${t.cache_rebuilds}/${t.runs_with_cache_rebuild}`,
|
|
384
|
+
fmt(t.start_context_median),
|
|
385
|
+
]);
|
|
386
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
387
|
+
const line = (r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
|
|
388
|
+
out.push(line(head));
|
|
389
|
+
for (const r of rows) out.push(line(r));
|
|
390
|
+
if (result.by_agent_type.length > top) out.push(`… ${result.by_agent_type.length - top} more types (--json for all)`);
|
|
391
|
+
out.push('');
|
|
392
|
+
out.push('rebuilds = cache rebuild events / runs with ≥1; medians per run.');
|
|
393
|
+
return out.join('\n');
|
|
394
|
+
}
|
|
@@ -36,7 +36,11 @@ import { randomUUID } from 'node:crypto';
|
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
import { atomicWriteText, envelopeToError } from './io.mjs';
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { canonicalizeSessions, isMeasuredSession } from './sessions-canonical.mjs';
|
|
40
|
+
import { resolveEventsLedgerRoot } from './platform.mjs';
|
|
41
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
42
|
+
import { withVisibleInboxText } from './learnings/surface.mjs';
|
|
43
|
+
import { inboxPathFor } from './learnings/store-role.mjs';
|
|
40
44
|
|
|
41
45
|
// ---------------------------------------------------------------------------
|
|
42
46
|
// Constants
|
|
@@ -82,11 +86,11 @@ function consumedDirPath(repoRoot) {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
function sessionsJsonlPath(repoRoot) {
|
|
85
|
-
return path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
89
|
+
return path.join(resolveEventsLedgerRoot(repoRoot), '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
function learningsJsonlPath(repoRoot) {
|
|
89
|
-
return
|
|
93
|
+
return resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', repoRoot);
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
// ---------------------------------------------------------------------------
|
|
@@ -163,11 +167,10 @@ export async function readDialecticSignals({ repoRoot } = {}) {
|
|
|
163
167
|
continue; // malformed line — skip silently
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const realEntries = filterRealSessions(entries);
|
|
170
|
+
// Canonical identities precede the shared measured-work predicate.
|
|
171
|
+
// Keep ID-less legacy rows: this cadence reader has always accepted them.
|
|
172
|
+
const realEntries = canonicalizeSessions(entries, { keepUnidentified: true })
|
|
173
|
+
.filter(isMeasuredSession);
|
|
171
174
|
for (const entry of realEntries) {
|
|
172
175
|
const startedAt = entry.started_at;
|
|
173
176
|
if (typeof startedAt !== 'string' || startedAt.length === 0) continue;
|
|
@@ -181,10 +184,14 @@ export async function readDialecticSignals({ repoRoot } = {}) {
|
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
// Learnings
|
|
187
|
+
// The visible inbox records count too (#1516, #1685): shared visibility rule,
|
|
188
|
+
// also when the live store is absent (inbox host); an unreadable inbox never
|
|
189
|
+
// costs the live count (`withVisibleInboxText` swallows its own errors).
|
|
184
190
|
const learningsPath = learningsJsonlPath(repoRoot);
|
|
185
|
-
if (existsSync(learningsPath)) {
|
|
191
|
+
if (existsSync(learningsPath) || existsSync(inboxPathFor(learningsPath))) {
|
|
186
192
|
try {
|
|
187
|
-
const
|
|
193
|
+
const live = existsSync(learningsPath) ? await readFile(learningsPath, 'utf8') : '';
|
|
194
|
+
const raw = await withVisibleInboxText(learningsPath, live);
|
|
188
195
|
const lines = raw.split('\n').filter((l) => l.length > 0);
|
|
189
196
|
for (const line of lines) {
|
|
190
197
|
let entry;
|