LearnNewsExamplesServices
Frontmatter
id11520
titleLane D of #11503 — annotate PrimaryRepoSyncService.runKbSync cascade as observable kbSync task
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-4-7
createdAtMay 17, 2026, 3:53 AM
updatedAtMay 17, 2026, 8:23 AM
githubUrlhttps://github.com/neomjs/neo/issues/11520
authorneo-opus-4-7
commentsCount0
parentIssue11503
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 17, 2026, 8:23 AM

Lane D of #11503 — annotate PrimaryRepoSyncService.runKbSync cascade as observable kbSync task

Closedenhancementaiarchitecture
neo-opus-4-7
neo-opus-4-7 commented on May 17, 2026, 3:53 AM

Authored by Claude Opus 4.7 (Claude Code). Session f662d055-a35b-446a-83ff-5fc859604722.

Sub of #11503 umbrella. Filed per @neo-gpt's lead-call MESSAGE:fb84293e (2026-05-17T01:39Z): "Do NOT skip Lane D entirely. #11503 AC8 still says the nested KB cascade must stop being hidden as primary-dev-sync only. The narrow D shape is kbSync TaskStateService + HealthService annotation with {parent: 'primary-dev-sync'} or equivalent."

FAIR-band: in-band [5/30] — narrow observability work; small ticket.

V-B-A on Lane D scope: posted as #11503 comment IC_kwDODSospM8AAAABClwhYQ (full source-trace of PrimaryRepoSyncService.runKbSync() confirming cascade DOES route through Lane C-wrapped syncKnowledgeBase.mjs — coverage layer is closed; observability layer is the remaining gap per umbrella AC8).

Context

#11503 umbrella's Problem statement #4 explicitly named: "Primary dev sync has a nested KB-sync path. PrimaryRepoSyncService.runKbSync() shells out to npm run ai:sync-kb inside the primary-dev-sync task. The outer service task is serialized, but the nested KB sync is not observable as the kbSync child task."

V-B-A confirms this is still true:

  • PrimaryRepoSyncService.mjs:483-491 shells out via execFileSyncFn(npmBin, ['run', 'ai:sync-kb'], ...) → routes through buildScripts/ai/syncKnowledgeBase.mjs
  • During this cascade, TaskStateService.taskState.kbSync.running stays false (only primary-dev-sync.running is true)
  • HealthService.recordTaskOutcome is never called for the cascade kbSync — operator sees only primary-dev-sync events
  • Monitoring agents looking at TaskStateService blindspot: cascade kbSync invisible

The Problem

Two operational consequences:

  1. Operator monitoring blindspot: a long cascade kbSync looks like a long primary-dev-sync task; no per-stage visibility
  2. Health timeline gap: HealthService.recordTaskOutcome timeline lacks the cascade kbSync entry; post-incident forensics conflates parent + nested work

Architectural Reality

  • ai/daemons/services/PrimaryRepoSyncService.mjs:483-491runKbSync() shell-out
  • ai/daemons/services/TaskStateService.mjs — owns markStarted / markCompleted / markFailed for task lifecycle
  • ai/services/memory-core/HealthService.mjs — owns recordTaskOutcome for observability
  • Orchestrator.mjs:704-718 — wraps PrimaryRepoSyncService.runTask with executeMaintenanceTask for primary-dev-sync lifecycle

The Fix

Modify PrimaryRepoSyncService.runKbSync() to bracket the cascade spawn with TaskStateService + HealthService annotations:

runKbSync(primaryRoot, execFileSyncFn, {taskStateService, healthService, parentTaskName = 'primary-dev-sync'} = {}) {
    const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
    const reason = `cascaded-from-${parentTaskName}`;
    
    taskStateService?.markStarted?.('kbSync', reason);
    healthService?.recordTaskOutcome?.('kbSync', 'running', {
        reason: {reason},
        parent: parentTaskName,
        startedAt: new Date().toISOString()
    });
    
    try {
        execFileSyncFn(npmBin, ['run', 'ai:sync-kb'], {
            cwd: primaryRoot,
            encoding: 'utf8',
            stdio: ['ignore', 'pipe', 'pipe']
        });
        taskStateService?.markCompleted?.('kbSync');
        healthService?.recordTaskOutcome?.('kbSync', 'completed', {
            parent: parentTaskName,
            completedAt: new Date().toISOString()
        });
    } catch (e) {
        taskStateService?.markFailed?.('kbSync', e.status || 1);
        healthService?.recordTaskOutcome?.('kbSync', 'failed', {
            parent: parentTaskName,
            error: e.message,
            failedAt: new Date().toISOString()
        });
        throw e;
    }
}

The parent: parentTaskName annotation is the load-bearing observability signal — distinguishes cascade kbSync from orchestrator-spawned kbSync.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
runKbSync TaskStateService annotation PrimaryRepoSyncService.mjs:483 + this ticket Marks kbSync.running = true before cascade + markCompleted/markFailed after Optional-chained injection — backward-compatible with callers not passing taskStateService JSDoc on method Spec asserts state transitions via taskStateService mock
runKbSync HealthService annotation Same Calls recordTaskOutcome('kbSync', 'running'/'completed'/'failed', {parent: parentTaskName, ...}) Optional-chained — no-op when healthService not injected JSDoc Spec asserts outcome events with parent annotation
Cascade-vs-orchestrator distinction New reason: 'cascaded-from-primary-dev-sync' string Operator dashboards can filter parent field to distinguish cascade kbSync from orchestrator kbSync Reason string includes cascaded-from- prefix as durable convention Reason-string convention in TaskDefinitions or service JSDoc Spec asserts reason string format

Acceptance Criteria

  • AC1: PrimaryRepoSyncService.runKbSync() calls taskStateService.markStarted('kbSync', 'cascaded-from-primary-dev-sync') before cascade spawn
  • AC2: runKbSync() calls healthService.recordTaskOutcome('kbSync', 'running'|'completed'|'failed', {parent: 'primary-dev-sync', ...}) at cascade lifecycle points
  • AC3: runKbSync() calls markCompleted on cascade success + markFailed on cascade error (rethrows error to preserve current caller semantics)
  • AC4: TaskStateService + HealthService injections are optional-chained — callers not passing them get current behavior (no breaking change)
  • AC5: Orchestrator.poll() primary-dev-sync invocation passes both services to runKbSync() via the runTask plumbing
  • AC6: Spec test: mock taskStateService + healthService injected; assert cascade triggers markStarted('kbSync', ...) BEFORE execFileSyncFn call + markCompleted('kbSync') AFTER
  • AC7: Spec test: cascade-failure path — execFileSyncFn throws → markFailed('kbSync', ...) called + recordTaskOutcome('kbSync', 'failed', ...) called + error rethrown
  • AC8: Spec test: outcome events carry parent: 'primary-dev-sync' annotation
  • AC9: No regression in existing PrimaryRepoSyncService.spec.mjs or Orchestrator.spec.mjs
  • AC10: Documentation: brief JSDoc on runKbSync calling out the cascade annotation contract + parent field semantic

Out of Scope

  • Cross-daemon orchestrator-side lease adoption — companion ticket filed alongside this one
  • Lease-inheritance env-var mechanism — handled by companion ticket
  • TaskStateService schema changes — none needed; existing markStarted(name, reason) shape carries the cascade reason in the reason field
  • HealthService schema changes — none needed; existing recordTaskOutcome extra-fields shape carries parent
  • Logging output format — no operator-facing log changes; observability flows through TaskStateService + HealthService surfaces only

Avoided Traps

  • Mutating TaskStateService schema to add a parent field: rejected — existing details-object passed to recordTaskOutcome already carries arbitrary fields; no schema change needed
  • Spawning kbSync through orchestrator's full task pipeline instead: rejected — would require routing the cascade through ProcessSupervisorService.runTask which would re-enter heavy-maintenance backpressure (cascade would self-defer behind its own parent primary-dev-sync). The cross-daemon ticket's env-var inheritance is the right mechanism for that direction; THIS ticket is observability annotation only.
  • Adding cascade kbSync as a separate task definition in TaskDefinitions.mjs: rejected — semantically it IS the same kbSync task class; the annotation captures the source-of-invocation distinction without taxonomy growth
  • Synthetic lastReason mutation only (no TaskStateService start/complete calls): rejected — observability mid-cascade requires running: true state for monitoring tooling that polls TaskStateService

Related

  • Parent umbrella: #11503 (umbrella AC8: "PrimaryRepoSyncService.runKbSync() no longer hides unguarded KB work inside the primary-dev-sync lane")
  • Companion ticket: cross-daemon lease coverage + inheritance (parallel-filed)
  • Substrate primitives:
    • TaskStateService (existing — owns task-lifecycle state)
    • HealthService (existing — owns observability outcomes)
  • Design dialogue: #11503 comment IC_kwDODSospM8AAAABClwhYQ (V-B-A trace) + IC_kwDODSospM8AAAABCly8-w (peer-role design)
  • Lead-call authority: @neo-gpt MESSAGE:fb84293e (2026-05-17T01:39:36Z)

Handoff Retrieval Hints

  • Retrieval Hint: PrimaryRepoSyncService runKbSync cascade annotation kbSync observability parent
  • Retrieval Hint: Commit SHA a5c638069 (Lane C merge; baseline for this work)
  • Retrieval Hint: cascaded-from-primary-dev-sync (the durable reason-string convention)

Origin Session ID: f662d055-a35b-446a-83ff-5fc859604722

tobiu referenced in commit 55dadc7 - "feat(ai): annotate runKbSync cascade as kbSync lifecycle (#11520) (#11521) on May 17, 2026, 8:23 AM
tobiu closed this issue on May 17, 2026, 8:23 AM