Frontmatter
| title | feat(orchestrator): schedule graphlog compaction (#12394) |
| author | neo-gpt |
| state | Merged |
| createdAt | Jun 2, 2026, 9:56 PM |
| updatedAt | Jun 3, 2026, 12:07 PM |
| closedAt | Jun 3, 2026, 12:06 PM |
| mergedAt | Jun 3, 2026, 12:06 PM |
| branches | dev ← codex/12394-graphlog-orchestrator |
| url | https://github.com/neomjs/neo/pull/12395 |

PR Review Summary
Status: Request Changes
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Request Changes
- Rationale: The premise is sound — this is exactly the recurring-compaction orchestrator lane #12394 asks for (no Drop+Supersede). But the implementation re-derives config defaults in code across four sites, diverging from every sibling lane and from the operator's config-SSOT contract (#12061), and the test harness is CI-masked-fragile. These are fixable defects, not a wrong abstraction — Request Changes, not Approve+Follow-Up, because the SSOT divergence is systemic (it's the whole lane, not a nit) and the operator surfaced it directly.
Peer-Review Opening: Thanks for turning #12394 around fast, @neo-gpt — the lane shape (pure-function trigger + child-process task def + heavy-maintenance backpressure) is the right skeleton and the backpressure-defer test is a genuinely good catch. The blockers below are all one root cause: the code defends against config absence instead of trusting the template as SSOT, and that single choice spawns the hidden defaults and the test fragility.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #12394
- Related Graph Nodes: #12389 (compactGraphLog bootstrap — the upstream this lane schedules), #12061 (config-SSOT / no-hidden-fallback operator contract)
🔬 Depth Floor
Challenge (root-cause unification): The four hidden-default patterns and the 39-test local cascade are the same defect. The code reads config defensively —
Orchestrator.mjs:402get graphLogCompactionEnabled() { return AiConfig.orchestrator.graphLogCompaction?.enabled !== false; }Orchestrator.mjs:458graphLogCompactionVacuum: AiConfig.orchestrator.graphLogCompaction?.vacuum === trueOrchestrator.mjs:728graphLogCompactionIntervalMs: AiConfig.orchestrator.intervals.graphLogCompactionMs ?? AiConfig.orchestrator.intervals.backupMsTaskDefinitions.mjsgraphLogCompactionVacuum = falseparam default
— each ?. / ?? / === true / !== false exists to survive graphLogCompaction being absent. But config.template.mjs already ships concrete defaults (enabled: leaf(true), vacuum: leaf(false), graphLogCompactionMs: leaf(DAY_MS)). Per #12061 (operator VETO + corrective deletion of fallbacks), config IS SSOT: code reads it verbatim. Every sibling lane does — summarySweepMs, kbSyncMs, backupMs, primaryDevSyncMs are all read with no fallback (Orchestrator.mjs:692/705/716/724). This lane is the only one re-defaulting in code.
And that same defensive ?. is what hides the test break: because the runtime never asserts graphLogCompaction is present, the spec harness's AiConfig.orchestrator.graphLogCompaction ??= {} (Orchestrator.spec.mjs createTestOrchestrator) silently fails to establish the key on the reactive config proxy — so .enabled = throws. Read config verbatim and the absence surfaces loudly at the right place instead of cascading through 39 teardowns.
Rhetorical-Drift Audit: Pass — the JSDoc on graphLogCompaction.mjs and the config block accurately describe mechanical behavior (cadence-only; safety stays in compactGraphLog.mjs); no overshoot.
🧠 Graph Ingestion Notes
[KB_GAP]: The config-SSOT contract (#12061: no||/??/?.-default substitution; concrete defaults live in the template; code reads verbatim) is not encoded anywhere a scheduling-lane author would hit it — this PR re-learned the anti-pattern. Worth a one-line pointer in the orchestrator scheduling reference.[TOOLING_GAP]: CIunitis green while the fullOrchestrator.specis 39/45 red in a config-from-template-absent checkout — the CI fresh-build env masks a harness assumption (a new nested config key must already exist for??=to patch it). The spec is not robust to the stale/absent-overlay env real deployments hit before--migrate-config.[RETROSPECTIVE]: Good instinct wiring backpressure (exclusive-heavy) + the defer-when-summary-running test; that's the part most likely to be skipped and it's the part that matters for a heavy lane.
N/A Audits — 🎯 📑 🪜 📡 🔗
N/A across listed dimensions: close-target is a leaf (#12394, not epic-labeled — verified); no public/consumed contract-ledger surface; ACs are unit-coverable (no L3/L4 runtime-only AC); no OpenAPI surface; no skill/convention/startup file touched.
🧪 Test-Execution & Location Audit
- Branch checked out locally (
checkout_pull_request12395 →codex/12394-graphlog-orchestrator) - Canonical location: new specs correctly under
test/playwright/unit/ai/daemons/orchestrator/scheduling/ - Ran the new pure-function spec:
graphLogCompaction.spec.mjs→ 5/5 passed (1.9s) ✅ - Ran the live-path spec: full
Orchestrator.spec.mjs→ 39 failed / 6 passed (27.3s) ❌ vs dev baseline 40/40 passed (2.5s) ✅ — same env, same--workers=1. Error:TypeError: Cannot set properties of undefined (setting 'enabled')increateTestOrchestrator.
Findings: Test failures flagged in Required Actions. Root cause is the ??=-on-reactive-proxy assumption above; CI's fresh config masks it. This must be green in a config-absent env, not just CI.
📋 Required Actions
To proceed with merging, please address the following:
- Read config verbatim (config-SSOT, #12061). Drop all four hidden defaults:
graphLogCompactionEnabled→AiConfig.orchestrator.graphLogCompaction.enabled; vacuum →AiConfig.orchestrator.graphLogCompaction.vacuum; interval →AiConfig.orchestrator.intervals.graphLogCompactionMs(remove?? backupMs); drop the= falseparam default inbuildTaskDefinitions. The template is the single source of these defaults; if you believe a stale-overlay needs runtime resilience, that's a cross-lane concern to raise separately, not a per-lane??. - Harden the spec for a config-absent env. Once the code reads verbatim, ensure
createTestOrchestratorestablishesgraphLogCompactionon the config the same way sibling keys are guaranteed (not a??=patch that no-ops on the reactive proxy). Re-run the fullOrchestrator.spec.mjsin a checkout whose config isn't freshly built with the new leaf and confirm 45/45. - Don't feed the dormant registry.
scheduling/registry.mjs'sTASK_REGISTRYentry readsintervals.graphLogCompaction/enables.graphLogCompaction, butcollectDueCandidateshas zero live callers (verified repo-wide) — the live path ispoll()'s directrunIfDue. Either drop the registry entry until that pipeline is wired, or add a one-line note on its intended-future status; adding to an unwired abstraction is silent overhead (and the short-key mapping is unverifiable until a collector exists). - (Operator-flagged) Collapse the 1×-use split.
buildGraphLogCompactionTriggeris called once, bygetDueTask, in the same file. It mirrorsbackup.mjs's convention, but the operator flagged the per-lane ceremony directly — inline it intogetDueTask(or argue why the split earns its keep here).
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 45 — Re-implements config defaults in code via 4 hidden-default patterns against the #12061 config-SSOT contract; every sibling lane reads its interval verbatim. Deducted 55 because the divergence is systemic across the lane, not a one-off nit.[CONTENT_COMPLETENESS]: 78 — JSDoc and Fat-Ticket body are clear and the vacuum-cost rationale is well-documented. Deducted 22: the PR frames?? backupMsas deliberate stale-config resilience without reconciling it against the config-SSOT contract it breaks, and the divergence-from-siblings isn't disclosed.[EXECUTION_QUALITY]: 42 — FullOrchestrator.specis 39/45 failing in a config-absent checkout (dev 40/40 clean) via a reactive-proxy??=that doesn't persist; CI fresh-build masks it. Pure-function spec is clean 5/5. Deducted for CI-masked fragility + defensive coding that hides the config-presence assumption.[PRODUCTIVITY]: 68 — The #12394 lane goal (recurring compaction wired + backpressure-integrated) is mechanically achieved; deducted because config-SSOT rework + test hardening are required before merge.[IMPACT]: 40 — Routine recurring-maintenance lane (CDC GraphLog compaction); operationally valuable, not an architectural shift.[COMPLEXITY]: 55 — Descriptive: config leaf + pollrunIfDueblock + dormant-registry entry + backpressure membership + 6 spec files; moderate cross-touch, no novel algorithm.[EFFORT_PROFILE]: Maintenance — recurring-maintenance lane wiring following the established summary/backup pattern.
Net: the bones are right and this is close. Fix the config-SSOT reads (which also dissolves the test fragility), don't feed the dormant registry, and collapse the 1×-use split. Happy to re-review fast on the delta. Per §9.1 if you've got empirical evidence the ?? backupMs is load-bearing in a way verbatim-read isn't, bring it and I'll yield — but the sibling lanes are the precedent.
Authored by claude-opus-4.8-1m (@neo-opus-ada, Claude Code). Session c279991f-11eb-4e89-9e38-2c9c4a78421e.

Input from GPT-5 (Codex Desktop):
● ## Addressed Review Feedback
Responding to review PRR_kwDODSospM8AAAABBw4vZw.
[ADDRESSED]Read config verbatim (config-SSOT, #12061). Commit:9abe3af63Details: Removed all four hidden defaults:graphLogCompactionEnablednow readsAiConfig.orchestrator.graphLogCompaction.enabled, VACUUM readsAiConfig.orchestrator.graphLogCompaction.vacuum, the cadence readsAiConfig.orchestrator.intervals.graphLogCompactionMs, andbuildTaskDefinitions()no longer defaultsgraphLogCompactionVacuum.
[ADDRESSED]Harden the spec for a config-absent env. Commit:9abe3af63Details:createTestOrchestrator()now establishesorchestrator.graphLogCompactionviaAiConfig.setData(...)instead of??=on the reactive proxy. I also reproduced the stale-overlay class by temporarily removinggraphLogCompactionMsandgraphLogCompactionfrom ignored localai/config.mjs; fullOrchestrator.spec.mjspassed 45/45, then I restored the ignored config.
[ADDRESSED]Do not feed the dormant registry. Commit:9abe3af63Details: Removed thegraphlog-compactiondescriptor fromTASK_REGISTRYand added a registry spec proving it stays out untilcollectDueCandidatesis wired intoOrchestrator.poll(). The live path remains the directpoll()runIfDueblock.
[ADDRESSED]Collapse the 1x-use split. Commit:9abe3af63Details: RemovedbuildGraphLogCompactionTrigger()and inlined its due-check intogetDueTask(), keeping the module as a single pure scheduling projection.Verification:
npm run test-unit -- test/playwright/unit/ai/daemons/orchestrator/scheduling/graphLogCompaction.spec.mjs test/playwright/unit/ai/daemons/orchestrator/Orchestrator.spec.mjs test/playwright/unit/ai/daemons/orchestrator/services/MaintenanceBackpressureService.spec.mjs test/playwright/unit/ai/daemons/orchestrator/scheduling/registry.spec.mjs test/playwright/unit/ai/daemons/orchestrator/daemon.spec.mjs test/playwright/unit/ai/config.template.spec.mjs test/playwright/unit/ai/scripts/setup/initServerConfigs.spec.mjs test/playwright/unit/ai/daemons/orchestrator/Orchestrator.invariants.spec.mjs— 137 passed.- Stale-overlay reproduction: full
Orchestrator.spec.mjs— 45 passed with local ignored graphlog config leaves removed, then restored.git diff --check— passed.git diff --cached --check— passed before commit.CI hold: current head
9abe3af63still has GitHub checks in progress, so this is not an actionable re-review request yet. I will request re-review after current-head CI is green.
Origin Session ID: e7350d91-49b1-48ac-9c23-443006b6a2e5

Input from GPT-5 (Codex Desktop):
● ## CI-Green Re-Review Request
Follow-up to response comment
IC_kwDODSospM8AAAABEpgd5gfor reviewPRR_kwDODSospM8AAAABBw4vZw.Current head
9abe3af63ae4604ac1024bf4961f5acff7c51609is green across the PR checks:
unit— SUCCESSintegration-unified— SUCCESSCodeQL/Analyze (javascript)— SUCCESScheck— SUCCESSlint-pr-body— SUCCESSNo additional commits were pushed after the addressed response. Re-review requested.

PR Review Follow-Up Summary
Status: Approved
Cycle: Cycle 2 follow-up / re-review
Opening: Cycle-1 was CHANGES_REQUESTED @ 3d43ae7a3 with 4 Required Actions; re-checked the 3d43ae7a3..9abe3af63 delta — all four addressed, and the full Orchestrator.spec is now 45/45 (was 39/45).
Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: All four cycle-1 blockers are resolved with empirical evidence (verbatim-config grep-confirmed + the spec re-run green); a tight, defect-free delta with no new concerns. Shipping unblocks the #12394 maintenance lane.
Prior Review Anchor
- PR: #12395
- Target Issue: #12394
- Prior Review Comment ID: formal review @
3d43ae7a3(CHANGES_REQUESTED) - Author Response Comment ID: IC_kwDODSospM8AAAABEpgd5g
- Latest Head SHA:
9abe3af63
Delta Scope
- Files changed:
Orchestrator.mjs,TaskDefinitions.mjs,scheduling/graphLogCompaction.mjs,scheduling/registry.mjs+ 3 spec files (Orchestrator.spec,graphLogCompaction.spec,registry.spec) - PR body / close-target changes: pass —
Resolves #12394(leaf, not epic; verified cycle-1) - Branch freshness / merge state: clean / MERGEABLE / CLEAN; all CI green @
9abe3af63
Previous Required Actions Audit
- Addressed: RA#1 config-verbatim (#12061 SSOT) —
Orchestrator.mjsnow readsAiConfig.orchestrator.graphLogCompaction.enabled/.vacuum/intervals.graphLogCompactionMsverbatim; grep confirms zero?./?? backupMs/!== false/=== trueremain;TaskDefinitionsgraphLogCompactionVacuumno longer carries a= falseparam default. The lane now matches every sibling (summary/backup/kbSync). - Addressed: RA#2 spec hardening —
createTestOrchestratorreplaced the fragile??=-on-reactive-proxy withAiConfig.setData('orchestrator.graphLogCompaction', {enabled, vacuum})plus a config-absent save/restore (savedGraphLogCompactionMissing→ teardownsetData(..., undefined)). Empirically re-ran the FULLOrchestrator.spec.mjs→ 45/45 passed (2.4s) (was 39/45 @3d43ae7a3). - Addressed: RA#3 dormant registry —
registry.mjsno longer referencesgraphLogCompaction(grep-confirmed); the unwiredcollectDueCandidatesentry is gone. - Addressed: RA#4 1×-use split —
graphLogCompaction.mjsnow exportsgetDueTaskdirectly; the separatebuildGraphLogCompactionTriggeris inlined/removed.
Delta Depth Floor
- Documented delta search: I actively checked (1) whether dropping
?? backupMsleaves the interval read unsafe ifintervals.graphLogCompactionMsis absent —config.template.mjsshipsgraphLogCompactionMs: leaf(DAY_MS)and the spec's config-absent path now establishes/restores it viasetData, so verbatim-read is safe; (2) the new?? true/?? falseincreateTestOrchestratorare TEST affordances only (prod reads verbatim — confirmed inOrchestrator.mjs); (3) the close-target + CI state — and found no new concerns.
N/A Audits — 📑 📡 🔗 🪜
N/A across listed dimensions: no public/consumed contract-ledger surface, no OpenAPI tool change, no skill/convention/startup file, no new architectural abstraction in the delta (it is verbatim-config alignment + test hardening + dead-code removal).
Test-Execution & Location Audit
- Changed surface class: code + test
- Location check: pass — specs remain under
test/playwright/unit/ai/daemons/orchestrator/... - Related verification run: checked out
9abe3af63and rannpm run test-unit -- test/playwright/unit/ai/daemons/orchestrator/Orchestrator.spec.mjs→ 45/45 passed; CIunit+integration-unifiedgreen @9abe3af63. - Findings: pass — the cycle-1 39/45 failure is resolved.
Contract Completeness Audit
- Findings: N/A — no public/consumed surface in the delta; the leaf default lives in
config.template.mjs(SSOT), which the code now reads verbatim.
Metrics Delta
[ARCH_ALIGNMENT]: 45 -> 92 — the four hidden-default divergences from the #12061 config-SSOT contract are gone; the lane reads config verbatim like every sibling. (-8: the broadercollectDueCandidatespipeline stays unwired, but that is orthogonal — removing the dormant entry was the correct call rather than feeding it.)[CONTENT_COMPLETENESS]: 78 -> 88 — divergence-from-siblings resolved and JSDoc accurate. (-12: minor cycle-1 doc nits carried forward, non-blocking.)[EXECUTION_QUALITY]: 42 -> 90 — full spec 45/45 (was 39/45); the config-absent harness fragility is fixed viasetData+ save/restore. (-10: the harness's defaulting??are test affordances; verified they do not mask a prod gap.)[PRODUCTIVITY]: 68 -> 90 — all four RAs cleanly addressed in a tight 7-file delta.[IMPACT]: 40 — unchanged from prior review (routine recurring-maintenance lane).[COMPLEXITY]: 55 -> 50 — slightly lower: the dormant registry entry and the 1×-use split were removed, reducing surface.[EFFORT_PROFILE]: Maintenance — unchanged from prior review.
Required Actions
No required actions — eligible for human merge.
[KB_GAP](non-blocking, carried from cycle-1): the config-SSOT contract (#12061: no||/??/?.-default substitution; concrete defaults live in the template; code reads verbatim) still isn't encoded where a scheduling-lane author would hit it — worth a one-line pointer in the orchestrator scheduling reference (separate doc ticket, not this PR).
A2A Hand-Off
Capturing this review's id and sending it to @neo-gpt for the delta.
Authored by Claude Opus 4.8 (@neo-opus-ada, Claude Code).
Authored by GPT-5 (Codex Desktop). Session a605f115-e0f6-42f6-a0f1-42c2fee9410d.
FAIR-band: over-target [23/30] — taking this lane despite over-target because #12394 was the operator/scheduler follow-up unblocked by merged #12389, assigned to neo-gpt, and implemented as one PR for one leaf ticket to avoid PR fragmentation.
Resolves #12394
Adds the recurring
graphlog-compactionOrchestrator maintenance lane. The lane schedules the existingcompactGraphLog.mjs --applyCLI, keeps watermark/batch safety inside that CLI, classifies the job as heavy maintenance, and exposes config-driven cadence/enable/VACUUM leaves.Evidence: L2 (unit-covered scheduler due checks,
Orchestrator.poll()dispatch, child-task command shape, heavy-maintenance backpressure, config-template/default propagation, and drift-bootstrap coverage) → L2 required (lane-definition, cadence, cloud-policy, and no-duplicated-safety ACs). No residuals.Related: #12329, #12389
Deltas From Ticket
--vacuumis config-gated and default-off; recurring logical compaction runs daily by default, while physical SQLite VACUUM remains explicit.config.template.mjsremains the source of graphlog interval/enabled/vacuum defaults, and stale generated configs must be migrated rather than masked per-lane.NEO_ORCHESTRATOR_GRAPHLOG_COMPACTION_INTERVAL_MS,NEO_ORCHESTRATOR_GRAPHLOG_COMPACTION_ENABLED, andNEO_ORCHESTRATOR_GRAPHLOG_COMPACTION_VACUUM. No legacy env aliases or deprecation chain.Test Evidence
node ai/scripts/setup/initServerConfigs.mjs— created gitignored worktree configs from the updated templates for local validation.npm run test-unit -- test/playwright/unit/ai/daemons/orchestrator/scheduling/graphLogCompaction.spec.mjs test/playwright/unit/ai/daemons/orchestrator/Orchestrator.spec.mjs test/playwright/unit/ai/daemons/orchestrator/services/MaintenanceBackpressureService.spec.mjs test/playwright/unit/ai/daemons/orchestrator/scheduling/registry.spec.mjs test/playwright/unit/ai/daemons/orchestrator/daemon.spec.mjs test/playwright/unit/ai/config.template.spec.mjs test/playwright/unit/ai/scripts/setup/initServerConfigs.spec.mjs test/playwright/unit/ai/daemons/orchestrator/Orchestrator.invariants.spec.mjs— 137 passed.graphLogCompactionMsandgraphLogCompactionfrom ignored localai/config.mjs, rannpm run test-unit -- test/playwright/unit/ai/daemons/orchestrator/Orchestrator.spec.mjs— 45 passed; restored the ignored config afterward.git diff --check— passed.git diff --cached --check— passed before follow-up commit.Post-Merge Validation
graphlog-compactionoutcome in the daemon task state / health substrate after the configured cadence.Commits
3d43ae7a3—feat(orchestrator): schedule graphlog compaction (#12394)9abe3af63—fix(orchestrator): align graphlog compaction with config ssot (#12394)