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:
- Operator monitoring blindspot: a long cascade kbSync looks like a long
primary-dev-sync task; no per-stage visibility
- 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-491 — runKbSync() 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
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
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-synconly. The narrow D shape iskbSyncTaskStateService + 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-wrappedsyncKnowledgeBase.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 tonpm run ai:sync-kbinside theprimary-dev-synctask. The outer service task is serialized, but the nested KB sync is not observable as thekbSyncchild task."V-B-A confirms this is still true:
PrimaryRepoSyncService.mjs:483-491shells out viaexecFileSyncFn(npmBin, ['run', 'ai:sync-kb'], ...)→ routes throughbuildScripts/ai/syncKnowledgeBase.mjsTaskStateService.taskState.kbSync.runningstaysfalse(onlyprimary-dev-sync.runningistrue)HealthService.recordTaskOutcomeis never called for the cascade kbSync — operator sees onlyprimary-dev-synceventsThe Problem
Two operational consequences:
primary-dev-synctask; no per-stage visibilityHealthService.recordTaskOutcometimeline lacks the cascade kbSync entry; post-incident forensics conflates parent + nested workArchitectural Reality
ai/daemons/services/PrimaryRepoSyncService.mjs:483-491—runKbSync()shell-outai/daemons/services/TaskStateService.mjs— ownsmarkStarted/markCompleted/markFailedfor task lifecycleai/services/memory-core/HealthService.mjs— ownsrecordTaskOutcomefor observabilityOrchestrator.mjs:704-718— wrapsPrimaryRepoSyncService.runTaskwithexecuteMaintenanceTaskfor primary-dev-sync lifecycleThe 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: parentTaskNameannotation is the load-bearing observability signal — distinguishes cascade kbSync from orchestrator-spawned kbSync.Contract Ledger Matrix
runKbSyncTaskStateService annotationPrimaryRepoSyncService.mjs:483+ this ticketkbSync.running = truebefore cascade +markCompleted/markFailedafterrunKbSyncHealthService annotationrecordTaskOutcome('kbSync', 'running'/'completed'/'failed', {parent: parentTaskName, ...})parentannotationreason: 'cascaded-from-primary-dev-sync'stringparentfield to distinguish cascade kbSync from orchestrator kbSynccascaded-from-prefix as durable conventionAcceptance Criteria
PrimaryRepoSyncService.runKbSync()callstaskStateService.markStarted('kbSync', 'cascaded-from-primary-dev-sync')before cascade spawnrunKbSync()callshealthService.recordTaskOutcome('kbSync', 'running'|'completed'|'failed', {parent: 'primary-dev-sync', ...})at cascade lifecycle pointsrunKbSync()callsmarkCompletedon cascade success +markFailedon cascade error (rethrows error to preserve current caller semantics)Orchestrator.poll()primary-dev-syncinvocation passes both services torunKbSync()via therunTaskplumbingtaskStateService+healthServiceinjected; assert cascade triggersmarkStarted('kbSync', ...)BEFOREexecFileSyncFncall +markCompleted('kbSync')AFTERexecFileSyncFnthrows →markFailed('kbSync', ...)called +recordTaskOutcome('kbSync', 'failed', ...)called + error rethrownparent: 'primary-dev-sync'annotationPrimaryRepoSyncService.spec.mjsorOrchestrator.spec.mjsrunKbSynccalling out the cascade annotation contract +parentfield semanticOut of Scope
markStarted(name, reason)shape carries the cascade reason in thereasonfieldrecordTaskOutcomeextra-fields shape carriesparentAvoided Traps
parentfield: rejected — existingdetails-object passed torecordTaskOutcomealready carries arbitrary fields; no schema change neededProcessSupervisorService.runTaskwhich would re-enter heavy-maintenance backpressure (cascade would self-defer behind its own parentprimary-dev-sync). The cross-daemon ticket's env-var inheritance is the right mechanism for that direction; THIS ticket is observability annotation only.lastReasonmutation only (no TaskStateService start/complete calls): rejected — observability mid-cascade requiresrunning: truestate for monitoring tooling that polls TaskStateServiceRelated
PrimaryRepoSyncService.runKbSync()no longer hides unguarded KB work inside theprimary-dev-synclane")TaskStateService(existing — owns task-lifecycle state)HealthService(existing — owns observability outcomes)Handoff Retrieval Hints
PrimaryRepoSyncService runKbSync cascade annotation kbSync observability parenta5c638069(Lane C merge; baseline for this work)cascaded-from-primary-dev-sync(the durable reason-string convention)Origin Session ID: f662d055-a35b-446a-83ff-5fc859604722