Neutron N5 — Executable Task Graph and Subagents Maintainer Brief
Status
Slice 1 implemented in @intentloom/application/neutron-scheduler (graph execution validation, deterministic scheduling classification/selection, pure state transitions). Slice 2 implemented — executeNeutronTaskNode runs exactly one scheduler-selected ready node through N3 → N2 → N4 under a read-only capability clamp and returns an application-level result wrapping N1 NeutronSubagentResult. Slice 3 implemented — local-first leases and one bounded concurrent scheduling wave (executeReadyNeutronTaskNodes). Slice 4 implemented — bounded retry, cancellation propagation, timeout recovery, and stale-attempt protection on that wave. Slice 5 implemented — deterministic graph aggregation, stale project/checkpoint/profile detection, and parent-child/attempt/tool/context provenance on a one-wave reconciliation boundary. N5 runtime milestone complete for the authorized read-only scheduler. No graph runner loop. Mutation Slice 5 later attached graph-linked proposal/review evidence without giving N5 Apply authority.
Mutation-routing brief is NEUTRON_MUTATION_ROUTING_BRIEF.md. Mutation implementation remains deferred.
Evidence baseline: origin/main @ 957756e12c6de488a943f49735816eb6ac2e498a (2026-09-04; legitimate advancement over N5 handoff 279eacd — Dependabot deps only). N5 handoff merge: 279eacd4fddcd1a08f51e1b32b19e79eb6e1a94c (#444).
Maintainer decision for this increment: choose N5 before mutation routing. N5 must prove controlled scheduling and subagent execution using current read-only authority boundaries.
Authoritative roadmap gate: NEUTRON_RUNTIME_ROADMAP.md §N5.
1. Current baseline
| Item | Evidence |
|---|---|
Post-N4 handoff main SHA | 3b504d143146b95a1215529208d6ce75f565b2c2 (#442) |
| Legitimate later advancement | d28baf570a70a60ab536c10228a0de3f51e41e5e (#433 deps only) |
| N4 Slice 1 | PR #439 @ 5d8c6e16427466e6b1627a0320f1d96a12c5dc43 — router + inspect |
| N4 Slice 2 | PR #441 @ e19d50edf3d2bb013f129c838725472c7195dd7a — read-only catalog |
| N3 runtime milestone | Complete (Slices 1–4); optional Slice 5 unauthorized |
| Routed read-only tools | inspect, doctor, memorySearch, timeline, conformance, securityAudit, projectDiff |
N1–N4 capability summary
| Milestone | Package surface | Role today |
|---|---|---|
| N1 | @intentloom/protocol/neutron-runtime, @intentloom/validator/neutron-runtime, prepareNeutronRuntimeContractSnapshot | Versioned contracts: session, adapter, context bundle, tool envelope, task graph, subagent result, usage budget, runtime event |
| N2 | @intentloom/application/neutron-n2 — runNeutronN2ReadOnlyLoop, OllamaModelAdapter | One read-only model turn loop; fingerprint proof; AbortSignal; one in-flight turn per session |
| N3 | @intentloom/application/neutron-context-assembly — assembleNeutronContext; N2 hook prepareNeutronN2ModelPrompt | Deterministic context assembly with budget, provenance, profile/task/memory integration |
| N4 | @intentloom/application/neutron-tool-router — routeNeutronToolInvocation | Fail-closed capability-scoped routing over seven read-only tools; no parallel execution path |
2. Existing foundations inventory
2.1 Task graph contracts (N1 — schema only, no scheduler)
Location: packages/protocol/src/neutron-runtime.ts, packages/validator/src/neutron-runtime-records.ts
| Field / concept | Current state |
|---|---|
| URN | urn:intentloom:schema:neutron-task-graph:1 |
NeutronTaskGraph | root, sessionId, nodes[] |
NeutronTaskNode | taskId, parentId, dependencies[], role, requiredCapabilities[], state, expectedOutput |
Lifecycle states (NEUTRON_TASK_STATES) | pending, ready, running, blocked, cancelled, timed-out, failed, completed |
| Budget / retry on node | Not present in N1 node schema — N5 adds scheduler metadata additively |
| Validator | validateNeutronTaskGraph, validateNode — shape only; no cycle or dependency semantics |
| Fixture | tests/fixtures/neutron-runtime/contract-snapshot.v1.json — single completed node |
| Execution | None — graph is contract snapshot, not a running scheduler |
Gap: N5 must add graph validation (references, cycles, dependency semantics) and scheduler-owned state transitions without renaming N1 states.
2.2 Subagent records (persisted orchestration foundation)
Two related but distinct records:
A. Legacy persisted task records
Location: packages/application/src/index.ts — spawnNeutronSubagentTask, getNeutronSubagentTask, listNeutronSubagentTasks
| Aspect | Evidence |
|---|---|
| Protocol type | NeutronSubagentTaskRecord (schemaVersion: "1") |
| Roles | research, arch-checker, test-runner, conformance-auditor, custom |
| Statuses | pending, running, completed, failed |
| Persistence | .aif/neutron/subagents/{taskId}.json |
| Parent relationship | None — flat records, no graph linkage |
| Current behavior | Stub: spawn writes completed immediately with synthetic resultOutput |
| CLI | `intentloom neutron subagent spawn |
| Tests | tests/neutron-orchestration.test.ts |
B. N1 subagent execution result envelope
Location: packages/protocol/src/neutron-runtime.ts
| Field | Purpose |
|---|---|
taskId, sessionId, root | Binding |
status | completed | failed | cancelled |
outputDigest | sha256:… over normalized output |
mutationAttempted | Must be false |
Gap: N5 bridges graph nodes → subagent execution → enriched NeutronSubagentResult with provenance, usage, tool envelopes, and attempts. Legacy NeutronSubagentTaskRecord remains the CLI-facing persistence shape until a migration slice is justified.
2.3 Checkpoints (L5 — separate from Neutron task graph)
Location: packages/application/src/index.ts
| Operation | Persists to | States |
|---|---|---|
createTaskCheckpoint | .aif/memory/checkpoints/{id}.json | initial active |
pauseTask | same | paused |
cancelTask | same | cancelled |
redirectTask | same | redirected |
resumeTask | same | resumed |
deleteTaskCheckpoint | removes file | — |
listTaskCheckpoints, getTaskCheckpoint | read | — |
N3 integration: assembleNeutronContext reads latest checkpoint by taskId for context excerpts (neutron-context-state-collectors.ts). N3 never mutates checkpoints.
N5 relationship: Checkpoints are user/task orchestration state, not scheduler lease state. N5 may read checkpoint metadata for stale detection when a graph node references taskId, but must not conflate TaskCheckpoint.state with NeutronTaskState. Scheduler lifecycle persistence lives under .aif/neutron/scheduler/ (proposed below).
2.4 Delegation and capability clamping
Location: delegateTaskRole in packages/application/src/index.ts
| Aspect | Behavior |
|---|---|
| Input | DelegationRequest with profileName, role, optional requestedCapabilities |
| Profile lookup | getProfile — throws if missing |
| Role validation | Role must be in profile.activeRoles |
| Read-only enforcement | context-scout and reviewer force readOnly: true; cannot request readOnly: false |
| Effective capabilities | Intersection/clamp: readOnly, allowedPaths, allowedTools, maxBudget, allowNetwork |
| Persistence | .aif/memory/delegations/{delegationId}.json |
| N4 reuse | AgentRoleCapabilities passed to routeNeutronToolInvocation; N4 requires readOnly: true, allowNetwork: false, tool in allowedTools |
N5 effective grant formula:
effectiveCapabilities =
sessionCapabilities
∩ profileCapabilities(profileName)
∩ delegationResult.effectiveCapabilities (when delegateTaskRole used)
∩ nodeRequiredCapabilities(requiredCapabilities)
∩ readOnlyCatalogConstraint (N5 initial: all nodes read-only)N5 must call delegateTaskRole (or equivalent pure clamp function extracted from it) before node execution; never inherit parent capabilities implicitly.
2.5 Neutron runtime session
Location: packages/protocol/src/neutron-runtime.ts, N2 loop
| Aspect | Evidence |
|---|---|
| Session states | created, discussing, inspecting, planning, cancelled, timed-out, failed, completed |
mutationAllowed | Always false in N1–N4 |
| N2 concurrency guard | inFlightSessions Set — one in-flight N2 turn per sessionId |
| Cancellation | AbortSignal checked in N2 and N4 authorization |
| Fingerprint proof | N2 compares before/after project fingerprint; throws on mutation |
| Timeout | N4: per-invocation timeoutMs, optional deadlineMs; session timed-out blocks tools |
N5 session coordination extends this: graph-level cancellation must propagate to child node AbortSignal, N2 turns, and N4 tool calls.
2.6 N4 router boundary (subagents must reuse, not bypass)
Location: packages/application/src/neutron-tool-router.ts
Subagent tool execution path:
scheduler node worker
→ build NeutronToolInvocation + NeutronRuntimeSession + AgentRoleCapabilities
→ routeNeutronToolInvocation({ dispatch: createNeutronReadOnlyDispatch(fs) })
→ existing application operationForbidden: Direct calls to inspectProject, doctorProject, etc. from scheduler workers bypassing routeNeutronToolInvocation. Forbidden: A second tool registry or shell dispatch for subagents.
Checks already enforced: root/session binding, active session lifecycle, deadline, AbortSignal, read-only tool definition, capability clamp, result byte limits, normalized error envelopes.
2.7 Workspace synchronization
Location: syncLocalWorkspaceState in packages/application/src/index.ts
| Aspect | Evidence |
|---|---|
| Shared project root | Yes — all subagents share the selected root |
| Isolation | Metadata only under .aif/neutron/subagents/; no git worktrees |
| Fingerprint | Uses inspectProject readiness + doctor findings + security score |
| Read-only N5 | No worktree or mutation-isolation machinery |
Stale detection compares scheduler-start fingerprint vs pre-execution fingerprint.
3. N5 definition — precise objective
N5 extends persisted Neutron subagent foundations into a deterministic, bounded, cancellable execution scheduler that coordinates already-authorized N2/N3/N4 capabilities across a validated task dependency graph.
What N5 is
| Layer | N5 responsibility |
|---|---|
| Persisted task graph | Validate structure, dependencies, roles; persist scheduler state |
| Scheduler | Deterministic ready-node selection, concurrency limits, leases, retries, cancellation propagation — outside model weights |
| Worker / subagent execution | Per-node: capability clamp → N3 context → N2 model turn → N4 tools → structured result |
| Model turns | N2 adapter boundary; one bounded turn sequence per node (reuse N2 loop composition) |
| Routed tool execution | N4 only; same path as parent |
| Task result aggregation | Deterministic parent merge from graph order, not completion timing |
| Checkpoints | Read for stale context; scheduler writes separate execution records |
| Mutation approval | Out of scope — no Apply, no write tools, no project bytes changed |
What N5 is not
- Autonomous mutation or mutation routing
- Generic shell or parallel tool execution path
- Model-as-scheduler (the model may propose graphs via future APIs; the scheduler executes validated graphs deterministically)
- Desktop UI (N6)
- New provider support
- Replacement of L5 checkpoints or legacy subagent CLI (initial slices compose alongside)
N5 exit gate
Deterministic multi-task fixtures prove: dependency handling, cancellation, timeout recovery, budget enforcement, provenance, stable aggregation, and no hidden background mutation (project fingerprint unchanged under read-only roles).
4. Runtime ownership decision
Option A — Continue in @intentloom/application (recommended for N5 Slice 1–3)
Pros:
- N1–N4 already live in application subpaths (
neutron-n2,neutron-context-assembly,neutron-tool-router); N5 composes them directly - No new package consumer exists yet (Desktop N6 deferred)
- Roadmap: dedicated package only when contract and real consumer justify boundary
- Tests already inject
FileSystemthrough application APIs - Dependency direction preserved: application → protocol/validator, not reverse
Cons:
packages/application/src/index.tsis already oversized; N5 modules must not land in the monolith — dedicatedneutron-scheduler-*.tsmodules with ≤250-line budget- Risk of orchestration monolith if slices skip extraction
Option B — Introduce packages/neutron-runtime (deferred)
Pros:
- Clean boundary for execution-session coordination named in roadmap decomposition
- Future Desktop/daemon consumer could depend on runtime without full application surface
- Enforces dependency direction earlier
Cons:
- No second consumer yet; would duplicate or re-export N2/N3/N4 composition boundaries
- Violates "introduce only when justified" without N6 or daemon RPC consumer
- Large migration cost for existing test imports
Decision
Option A for N5 foundation slices. Extract cohesive modules under packages/application/src/neutron-scheduler-*.ts and export via @intentloom/application/neutron-scheduler subpath.
Re-evaluate Option B when N6 Desktop Neutron Workspace needs a runtime consumer or when scheduler modules exceed sustainable file count (>6 production modules or repeated cross-package import pressure from apps/desktop).
Do not create packages/neutron-runtime in N5 Slice 1.
5. Task-node state machine
Reuse NEUTRON_TASK_STATES verbatim. Do not invent succeeded (use completed), paused (use blocked with scheduler pause reason metadata), or stale (transition to failed with stale-state error code in result metadata).
| State | Meaning |
|---|---|
pending | Registered; dependencies not yet satisfied |
ready | All dependencies terminal-success; eligible for scheduling |
running | Lease held; worker executing N3/N2/N4 |
blocked | Dependency failed/cancelled, budget exhausted awaiting parent decision, or explicit pause |
cancelled | Cancel propagated or node explicitly cancelled |
timed-out | Node, lease, or model/tool deadline exceeded |
failed | Non-retryable error or retries exhausted (includes stale-state rejection) |
completed | Terminal success; result persisted |
Transitions
| From | To | Trigger | Persisted evidence | Cancel behavior | Dependency effect |
|---|---|---|---|---|---|
pending | ready | All deps completed | readyAt, dep snapshot digest | — | — |
pending | blocked | Any dep failed/cancelled/timed-out | blockedReason: dependency-{state} | — | Blocks descendants to blocked |
pending | cancelled | Session/node cancel before start | cancelledAt, cancelSource | Immediate | Propagate block to dependents |
ready | running | Lease acquired, worker started | leaseId, attempt, startedAt | Pre-start cancel → cancelled | — |
ready | cancelled | Cancel while queued | cancelledAt | — | Dependents → blocked or cancelled per policy |
running | completed | Worker success | NeutronSubagentResult, usage, tool envelopes | — | Unblock dependents → evaluate ready |
running | failed | Non-retryable error | Normalized error, attempts | Abort in-flight work | Dependents → blocked |
running | timed-out | Deadline/lease expiry | Timeout kind metadata | Abort in-flight | Dependents → blocked; may retry if policy allows |
running | cancelled | Cancel during execution | cancelledAt | AbortSignal to N2/N4 | Dependents → blocked/cancelled |
running | ready | Retryable failure, attempts remain | Increment attempt, clear lease | — | — |
failed/completed/terminal | — | No outbound transitions | Final result immutable | — | — |
Scheduler pause (maintainer cancel of scheduling without cancelling session): moves ready → blocked with blockedReason: scheduler-paused; resume re-evaluates deps.
6. Dependency semantics
Graph validation runs before any execution (validateNeutronTaskGraphForExecution).
| Case | Semantics |
|---|---|
| Zero dependencies | Node may reach ready immediately after graph activation |
| Multiple dependencies | All must reach completed before ready |
| Dependency success | Dependent transitions pending → ready (deterministic batch) |
| Dependency failure | Dependent → blocked with reason; never auto-run |
| Cancelled dependency | Dependent → blocked or cancelled (config: default blocked with cancel cascade optional) |
| Timed-out dependency | Treat as failure → blocked downstream |
| Stale dependency result | If dep completed but fingerprint/provenance stale flag set → dependent → failed at schedule time |
| Cyclic graph | Reject at validation with validation-failed / cycle path in error |
| Missing dependency ID | Reject at validation |
| Duplicate taskId | Reject at validation |
| parentId | Provenance and aggregation ordering only; not an execution dependency unless also listed in dependencies |
Deterministic cycle detection: DFS with nodes sorted by taskId (code-point order); first cycle found is reported in stable order.
7. Runnable-node selection and concurrency
Ready selection (deterministic)
When capacity available, select runnable nodes in order:
- Nodes in state
readywith valid lease slot - Sort by: (a) optional explicit
priorityif added to scheduler metadata — lower number first; (b)taskIdcode-point ascending (default when no priority) - Never use filesystem order, completion timing, or model output for ordering
Concurrency limits
| Source | Default | Maximum |
|---|---|---|
Scheduler option maxConcurrentNodes | 1 | 4 (hard cap for N5 initial) |
Session / profile maxBudget | May further restrict parallel token use | — |
| Per-parent limit | Default: unlimited within global cap; optional maxConcurrentChildren on graph metadata | 2 |
| Per-role limit | Optional map role → maxConcurrent | 1 for same role initially |
When capacity opens: Re-run ready selection on next scheduler tick; fill slots in deterministic order. No Promise.all over unbounded ready set.
Conservative default: maxConcurrentNodes: 1 for Slice 1–2 tests; concurrency slice proves cap at 2–4 with fixtures.
8. Execution leases and heartbeats
Local-first, single daemon/process assumption. No distributed consensus.
| Concept | Definition |
|---|---|
| Lease owner ID | {sessionId}:{taskId}:{attempt} |
| Acquisition | Atomic write of lease record; fails if unexpired lease exists |
| Lease TTL | Default nodeTimeoutMs or 120_000 ms, whichever is smaller |
| Heartbeat | Worker renews lease every ttl/3 while running |
| Expiry | If heartbeat missing at TTL → node → timed-out, lease cleared, retry if allowed |
| Lost worker recovery | On scheduler tick, expired lease → timed-out → retry or failed |
| Duplicate execution prevention | Second acquirer gets lease-held; must not enter N2/N4 |
Persistence: .aif/neutron/scheduler/leases/{leaseId}.json
9. Retry and timeout model
Retry policy
| Failure class | Retryable | Notes |
|---|---|---|
permission-denied, capability-denied | No | Clamp error |
validation-failed, root-mismatch, invalid schema | No | |
unsupported-tool | No | |
cancelled | No | |
budget-exceeded | No | |
timeout (transient provider) | Yes | Max attempts default 2 |
operation-failed (transient) | Yes | Bounded |
| Worker interruption / expired lease | Yes | If attempts remain |
Defaults: maxAttempts: 2 (initial attempt + 1 retry). Scheduler metadata per node may lower, not raise above graph-level maxAttempts cap (default 3 absolute max).
Timeout layers (do not collapse)
| Layer | Owner | Default |
|---|---|---|
| Model turn timeout | N2 adapter / node worker | 60_000 ms |
| Tool invocation timeout | N4 invocation.timeoutMs | 30_000 ms |
| Node execution timeout | Scheduler worker wraps full N3+N2+N4 | 120_000 ms |
| Lease timeout | Scheduler | ≤ node execution timeout |
| Scheduler/session timeout | Graph-level optional graphDeadlineMs | unset = no graph timeout |
Interaction: inner timeouts fire first; node worker maps to normalized NeutronErrorCode; cancellation aborts all layers via shared AbortSignal.
10. Cancellation propagation
| Scope | Behavior |
|---|---|
| Cancel one node | If pending/ready → cancelled; if running → abort signal + cancelled; dependents → blocked |
| Cancel parent node | Optional cascade to child nodes (default: cancel children) |
| Cancel whole session | All non-terminal nodes → cancelled; session state → cancelled; no new leases |
| Cancelled dependency | Dependents blocked/cancelled per §6 |
| Running child | AbortSignal checked before N3, before each N2 turn, passed to N4 |
| Queued children | Never start; transition to cancelled on session cancel |
AbortSignal flow:
scheduleCancel(sessionId | taskId)
→ set session/node cancel flag + AbortController.abort()
→ scheduler tick skips cancelled ready nodes
→ running worker catches abort → normalized cancelled result
→ N2 executeTurn(signal) + N4 routeNeutronToolInvocation(signal)
→ retry loop checks signal before retryGuarantee: After session cancel completes scheduler drain, no in-flight N2/N4 for that session. Tests assert zero tool operations after cancel acknowledgment.
11. Context and token budgets
Reuse NeutronUsageBudget and N3 assembleNeutronContext limits.
| Budget | Source | Exhaustion behavior |
|---|---|---|
| Per-node context tokens | Node metadata maxContextTokens or default 4000 | Node → failed, budget-exceeded |
| Per-node model I/O | Accumulate from N2 adapter usage | Same |
| Whole-graph token budget | Graph metadata graphTokenBudget optional | Remaining nodes → blocked; running node completes or fails per policy |
| Tool result bytes | N4 NEUTRON_TOOL_MAX_RESULT_BYTES | Normalized budget-exceeded |
| Graph node count | Validation cap default 32 nodes | Reject at validation |
Accounting: After each node, merge usage into graph accumulator persisted at .aif/neutron/scheduler/sessions/{sessionId}/usage.json. Failures are auditable in result provenance.
12. Role and capability clamp
Every node execution:
- Resolve
profileNamefrom graph/session metadata (required when role ≠ default) - Call
delegateTaskRoleor pureclampCapabilitiesForNode(parent, profile, node) - Pass result
effectiveCapabilitiesto N4 and N3 (rolefilter) - Verify
requiredCapabilities⊆ granted tools/capabilities - Force
readOnly: true,allowNetwork: falsefor N5 initial implementation
Child cannot exceed parent: effectiveChild = intersect(parentEffective, …) when parent node executed with known grant; root session starts from session baseline (read-only catalog only).
13. Read-only guarantee
Allowed tools: the seven N4 read-only tools only.
Forbidden for any child/subagent:
- Write / Apply / memory mutation / checkpoint mutation via model tools
- Generic shell / git mutation
- Direct application mutation operations
Allowed scheduler persistence (not project mutation):
- Task state, lease, heartbeat, attempt count, result metadata, cancellation flags under
.aif/neutron/scheduler/ - Legacy subagent record updates under
.aif/neutron/subagents/
Proof: Project fingerprint (N2-style) before graph start and after graph terminal; must match for read-only roles. Existing runNeutronN2ReadOnlyLoop fingerprint check reused per node.
14. Parent-child provenance
Extend NeutronSubagentResult additively (Slice 5) or wrap in application NeutronNodeExecutionRecord (preferred initially to avoid protocol bump):
| Field | Source |
|---|---|
graphId / session graph digest | Scheduler |
nodeId (taskId) | Graph node |
parentNodeId | parentId |
sessionId, root | Session |
role, profileName | Node + delegation |
providerKind, modelId | N2 adapter capability |
contextBundleProvenance | N3 source IDs + digests |
toolsInvoked | N4 envelope invocation IDs |
attempts, timing | Scheduler lease records |
finalState | NeutronTaskState |
childResultDigests | Aggregation slice |
Do not persist chain-of-thought or raw model hidden reasoning.
15. Result contract and deterministic aggregation
Node result
Prefer wrapping existing NeutronSubagentResult with scheduler envelope:
interface NeutronNodeExecutionResult {
readonly subagent: NeutronSubagentResult;
readonly taskState: NeutronTaskState;
readonly usage: NeutronUsageBudget;
readonly toolEnvelopes: readonly NeutronToolEnvelope[];
readonly provenance: NeutronNodeProvenance;
readonly attempts: number;
readonly warnings: readonly string[];
}Parent aggregation
| Input | Rule |
|---|---|
| Child ordering | Sort by taskId ascending (graph structure order) |
| Successful children | Include output digest + summary in parent payload |
| Failed/cancelled children | Include status + error code; do not omit |
| Partial graph | Parent may completed with warnings if policy allows partial success; default: parent failed if any required child failed |
| Concatenation | Never order by completedAt timestamp |
16. Stale-state detection
| Stale kind | Detection | Action |
|---|---|---|
| Project fingerprint changed | Compare scheduler-start vs pre-node fingerprint | Node → failed, stale reason |
| Checkpoint changed | updatedAt or checksum vs context assembly snapshot | Reject or re-assemble once; then fail |
| Workspace sync stale | syncLocalWorkspaceState.syncedAt older than threshold | Warning or block |
| Parent state changed | Parent not terminal when child completes | Child result flagged invalid |
| Lease expired | §8 | timed-out path |
| Profile/capabilities changed | Re-delegate before run; mismatch → fail | No silent widen |
Reuse createdSnapshotChecksum from checkpoints and N2 fingerprint helpers.
17. Persistence and crash recovery
Allowed persistence layout
.aif/neutron/scheduler/
graphs/{graphExecutionId}.json # graph + node states
leases/{leaseId}.json
sessions/{sessionId}/usage.json
results/{taskId}.json # NeutronNodeExecutionResultRecovery on process restart
| Situation | Recovery |
|---|---|
running + valid unexpired lease | Ambiguous — treat as expired after TTL; do not assume worker alive |
| Expired lease | → timed-out, retry if policy allows |
completed + result file | Idempotent; do not re-execute |
| Partial attempt (no result) | Increment attempt or fail deterministically |
Guarantee statement: At-most-once node execution per {taskId, attempt} under single-process scheduler. Not exactly-once across crashes. Retries may produce duplicate model calls; dedupe by attempt ID in provenance.
18. Proposed runtime flow
User / parent API
→ validate graph (cycles, refs, caps)
→ persist graph execution record (all nodes pending)
→ scheduler tick loop
→ select ready nodes (deterministic order, concurrency cap)
→ acquire lease
→ clamp capabilities (delegateTaskRole)
→ fingerprint check (stale gate)
→ assembleNeutronContext (N3)
→ runNeutronN2ReadOnlyLoop or deterministic test worker (N2)
→ routeNeutronToolInvocation (N4) for each tool call
→ persist NeutronNodeExecutionResult
→ release lease; update node state
→ propagate dependency releases
→ aggregate parent results (deterministic)
→ terminal graph state + fingerprint proofThe scheduler tick is pure TypeScript control flow (testable with fake clock and injected workers). The model never selects execution order.
19. Scheduler API (minimal)
Application subpath @intentloom/application/neutron-scheduler:
| Operation | Purpose |
|---|---|
validateNeutronTaskGraphForExecution(graph) | Pre-flight validation |
createGraphExecution(input) | Persist initial execution record |
tickGraphExecution(executionId, options) | Run one scheduler step (ready select + start workers up to cap) |
cancelGraphExecution(executionId, scope) | Cancel session/node |
getGraphExecutionStatus(executionId) | Inspect states |
recoverGraphExecution(executionId) | Post-crash lease cleanup + resume tick |
First vertical slice: validateNeutronTaskGraphForExecution + selectReadyNodes + pure state transition functions — no model execution.
20. N2/N3/N4 reuse (mandatory composition)
| Milestone | N5 usage |
|---|---|
| N3 | assembleNeutronContext per node with taskId, profileName, role |
| N2 | runNeutronN2ReadOnlyLoop with createNeutronReadOnlyDispatch(fs) as runTool; or slim deterministic worker in tests |
| N4 | All tools via routeNeutronToolInvocation |
N5 modules must not duplicate context assembly, adapter protocol, or authorization logic.
21. Threat analysis
| Threat | Mitigation |
|---|---|
| Task amplification | Graph node cap; session node budget; validation rejects oversized graphs |
| Runaway concurrency | Hard cap maxConcurrentNodes ≤ 4; default 1 |
| Infinite retry | maxAttempts absolute cap; non-retryable error taxonomy |
| Child capability escalation | Intersect clamp; N4 read-only enforcement |
| Stale authorization | Re-delegate before each run; fingerprint gate |
| Cancelled work continuing | AbortSignal + session cancel drain + tests |
| Duplicate execution | Lease acquire fails closed |
| Cross-project context | Root binding on every layer (N1 contract rule) |
| Cross-profile memory | N3 projectId isolation unchanged |
| Malicious parent task | Graph validation; capability clamp; no shell |
| Tool-call loops | N2 loop bounded turns per node; tool count cap per node |
| Budget exhaustion | Normalized budget-exceeded; graph budget stops scheduling |
| Result injection between children | Signed digests; aggregation from persisted results only |
22. Implementation slices
Slice 1 — Graph validation and deterministic scheduling core
| Item | Detail |
|---|---|
| Status | Implemented on @intentloom/application/neutron-scheduler |
| Objective | Validate graphs; deterministic ready selection; pure state transitions |
| Modules | neutron-scheduler-validate.ts, neutron-scheduler-select.ts, neutron-scheduler-transitions.ts, neutron-scheduler-errors.ts, neutron-scheduler-sort.ts |
| APIs | validateNeutronTaskGraphForExecution, planNeutronTaskScheduling, selectReadyNodes, validateNeutronTaskStateTransition, applyNeutronTaskStateTransition |
| Reused APIs | validateNeutronTaskGraph, NEUTRON_TASK_STATES |
| Tests | tests/neutron-n5-task-graph.test.ts, tests/neutron-n5-scheduling.test.ts |
| Decisions | parentId is provenance only; duplicate dependency IDs rejected; ready order is taskId code-point ascending; priority deferred; scheduling classification (ready/waiting/blocked) is separate from protocol node state; default maxConcurrency 1, hard cap 4; cycle paths reported in DFS discovery order |
| Risks | Confusion with L5 checkpoints — document separation |
| Non-goals | Model execution, N3 assembly, N4 tool calls, persistence, leases, retry loops, concurrency workers |
| Exit gate | Pure functions prove ready order and transitions on fixture graphs (met) |
Slice 2 — Single-worker node execution (N3/N2/N4 composition)
| Item | Detail |
|---|---|
| Objective | Execute one ready node through full read-only stack |
| Modules | neutron-scheduler-worker.ts, neutron-scheduler-execute.ts |
| Reused APIs | assembleNeutronContext, runNeutronN2ReadOnlyLoop, routeNeutronToolInvocation, delegateTaskRole |
| Tests | tests/neutron-n5-scheduler-execute.test.ts — deterministic adapter, fingerprint unchanged |
| Risks | N2 in-flight session guard vs graph concurrency — use distinct session IDs per node |
| Non-goals | Parallel nodes, retries |
| Exit gate | Single-node linear graph executes with provenance + no mutation |
Slice 3 — Leases and bounded concurrency
| Item | Detail |
|---|---|
| Objective | Lease acquire/renew/expire; maxConcurrentNodes |
| Modules | neutron-scheduler-lease.ts, persistence under .aif/neutron/scheduler/ |
| Tests | tests/neutron-n5-scheduler-lease.test.ts, concurrency cap fixtures |
| Exit gate | No duplicate active lease; cap enforced deterministically |
Slice 4 — Retry, cancellation, timeout recovery
| Item | Detail |
|---|---|
| Objective | Bounded retries; cancel propagation; layered timeouts |
| Tests | tests/neutron-n5-scheduler-cancel.test.ts, retry/timeout fixtures |
| Exit gate | Cancel mid-run aborts N2/N4; retries respect taxonomy |
Slice 5 — Aggregation, stale-state, provenance enrichment
| Item | Detail |
|---|---|
| Status | Implemented on @intentloom/application/neutron-scheduler |
| Objective | Deterministic graph aggregation; fingerprint/checkpoint/profile stale gate; full provenance record |
| Modules | neutron-scheduler-graph-result.ts, neutron-scheduler-stale.ts, neutron-scheduler-provenance.ts, neutron-scheduler-aggregate.ts |
| APIs | aggregateNeutronTaskGraphResults, detectNeutronGraphStaleness, reconcileNeutronTaskGraphExecution |
| Tests | tests/neutron-n5-aggregation.test.ts, tests/neutron-n5-stale-state.test.ts |
| Exit gate | Multi-node graph completes with stable aggregation; stale project/checkpoint/profile rejected without rerun (met) |
| Non-goals | Graph runner loop, result persistence, mutation routing, N6, N3 Slice 5, generic shell |
23. First recommended implementation slice
Exactly one: Slice 1 — Graph validation and deterministic scheduling core.
Rationale: Smallest testable increment without concurrent model execution; confirms dependency semantics and state machine against existing N1 types; unblocks all later slices.
Authorization requested after brief merge: explicit maintainer sign-off for N5 Slice 1 only.
24. Test strategy
| Category | Cases |
|---|---|
| Graph | Linear chain, diamond, independent tasks, cycle rejection, failed/cancelled/timed-out dependency |
| Scheduling | Deterministic ready order, concurrency cap, no duplicate execution |
| Lease | Acquire, renew, expire, recovery |
| Retry | Retryable vs non-retryable, max attempts |
| Cancellation | Node, parent, session, propagation to N2/N4 |
| Budget | Per-node limit, graph limit, exhaustion |
| Capabilities | Parent/child clamp, read-only child, denied tool |
| Stale state | Fingerprint, profile, checkpoint change |
| Aggregation | Deterministic ordering, partial failure, cancellation |
| Safety | Project bytes unchanged (MemoryFileSystem + fingerprint) |
All tests use injected FileSystem, fake clock, and deterministic model adapter — no live Ollama required for scheduler proofs.
25. Performance considerations
| Concern | N5 initial bound |
|---|---|
| Graph size | ≤32 nodes default validation cap |
| Runnable nodes per tick | ≤4 |
| Scheduler tick | O(n log n) sort on ready set; n ≤32 |
| Persisted writes | One lease + one state file per transition; batch where safe |
| Context/model cost | Dominates; scheduler overhead must stay <5% of node wall time in fixtures |
No queues, worker pools, or external services until evidence requires them.
26. Module boundaries (file-size planning)
| Module | Responsibility | Target lines |
|---|---|---|
neutron-scheduler-validate.ts | Graph validation, cycle detection | ≤200 |
neutron-scheduler-select.ts | Ready selection ordering | ≤120 |
neutron-scheduler-transitions.ts | Pure state machine | ≤200 |
neutron-scheduler-lease.ts | Lease acquire/renew/expire | ≤200 |
neutron-scheduler-worker.ts | N3/N2/N4 node worker | ≤250 |
neutron-scheduler-execute.ts | Tick loop orchestration | ≤250 |
neutron-scheduler-aggregate.ts | Parent result merge | ≤180 |
neutron-scheduler-persist.ts | .aif/neutron/scheduler/ I/O | ≤200 |
No monolithic neutron-scheduler.ts. Extract before any file exceeds 300 effective lines.
27. Acceptance criteria (N5 milestone)
- [x] Deterministic graph scheduling with stable ready order
- [x] Dependency correctness including failure/cancel propagation
- [x] Bounded concurrency with configurable cap (default 1, max 4)
- [x] No duplicate active lease for same node
- [x] Bounded retries with non-retryable taxonomy enforced
- [x] Cancellation propagates to N2 model turns and N4 tools
- [x] Timeout recovery per layered timeout model
- [x] Context/token budgets enforced and auditable
- [x] Child capability clamp ⊆ parent/session/profile/node
- [x] Parent-child provenance on every node result
- [x] Stable aggregation order by
taskId - [x] Stale-state rejection (fingerprint, profile, checkpoint)
- [x] Project fingerprint unchanged under read-only roles
- [x]
pnpm verifygreen; cross-platform compatible
(pnpm verify on this Slice 5 branch: 294 files, 2551 passed, 3 skipped.)
28. Mutation-routing decision
MUTATION ROUTING REMAINS OUTSIDE N5 APPLY AUTHORITY.
N5 must not Apply, retry mutation, or design around write tools. Mutation Slice 5 (PR #507, merge 0eb326986537f125abfefc573e6709824914b37c) composes graph proposal/review evidence onto existing host Apply; Slice 5.1 (PR #510) hardened stale proposal materialization and production proposal-capability clamping. Additive to the scheduler: does not alter task states, lease semantics, retry semantics, the node mutationAttempted invariant, or read-only project execution. Host Apply remains outside scheduler node execution. Desktop Approve/Apply UX still requires separate maintainer authorization. Scheduling alone is not sufficient justification for mutation.
29. Open decisions
| # | Decision | Recommendation | Blocker for |
|---|---|---|---|
| 1 | Add priority field to scheduler metadata vs taskId-only order | Deferred — Slice 1 uses taskId code-point ascending only | Slice 2+ |
| 2 | Unify NeutronSubagentTaskRecord with graph node persistence | Keep separate; link by taskId in Slice 2 | Slice 2 |
| 3 | Protocol bump for enriched NeutronSubagentResult | Resolved in Slice 5 — application NeutronGraphExecutionResult; no protocol bump | — |
| 4 | Graph-level partial success policy | Resolved in Slice 5 — strict graph status; partial is observational only | — |
| 5 | When to introduce packages/neutron-runtime | Re-evaluate at N6 Desktop consumer | N6 |
30. Recommendation
N5 RUNTIME MILESTONE COMPLETE — Slice 5 aggregation, stale-state detection, and provenance completion are implemented. Mutation routing and N6 still require separate maintainer authorization. No graph runner loop.
31. Slice 2 implementation record
Evidence baseline: origin/main @ 6a5c17aee9f9ae04b38f6df4d497a8503d44f410 (N5 Slice 1 handoff #446). Explicit maintainer authorization covered Slice 2 only.
| Decision | Record |
|---|---|
| Execution API | executeNeutronTaskNode on @intentloom/application/neutron-scheduler |
| Inputs | graph, taskId, active session, projectId, model adapter, filesystem, session capabilities, fingerprint, optional profile/signal/budget |
| Outputs | in-memory result: updated graph/node, N1 NeutronSubagentResult, attempt 1, capabilities, N3 context/usage, N4 tool envelope, N2 adapter, fingerprints, normalized error |
| N3/N2/N4 composition | N2 runNeutronN2ReadOnlyLoop calls assembleNeutronContext via the existing pre-turn hook; model tool calls go through routeNeutronToolInvocation |
| Node objective | N1 expectedOutput is the only evidence-backed intent field; it is the N2 prompt and N3 query |
| Role / capability | resolveNeutronNodeCapabilities intersects session, optional profile grant, parent requiredCapabilities, node requiredCapabilities, and the N4 read-only catalog; readOnly=true, allowNetwork=false |
| State transitions | pending→ready when classified ready, then ready→running; success running→completed; failure running→failed; cancel→cancelled; timeout→timed-out |
| Result contract | N1 NeutronSubagentResult reused unchanged (completed/failed/cancelled; timed-out nodes map subagent status to failed); richer audit fields stay on the application wrapper (brief §29 #3) |
| Errors | scheduling/not-runnable fail closed with executed: false and no provider call; post-start failures return executed: true with one attempt and a staged error |
| No persistence | no .aif/neutron/scheduler/ writes |
| No retry | attempt = 1; provider/tool/context failures terminate the node |
| No concurrency | one operation executes one requested ready node; capacity forced to 1; a running peer blocks start |
delegateTaskRole is not called: it writes delegation files. Clamp is pure.
32. Slice 3 implementation record
Evidence baseline: origin/main @ 1165d044f461ddbaf90297aa74b1be13c11982ed (N5 Slice 2 handoff #448). Explicit maintainer authorization covered Slice 3 only.
| Decision | Record |
|---|---|
| Batch API | executeReadyNeutronTaskNodes on @intentloom/application/neutron-scheduler — one scheduling wave, not a graph runner |
| Admission | Reuses Slice 1 planNeutronTaskScheduling; taskId code-point ascending; availableCapacity = maxConcurrency - runningCount; default 1, hard cap 4 |
| Lease identity | {sessionId}:{taskId}:{attempt}; default attempt 1; attempt is not incremented |
| Owner | Injectable ownerId; default scheduler:{sessionId}; renewal and release require the current owner |
| TTL / heartbeat | Default TTL min(nodeTimeoutMs, 120_000) ms (120s when unset); renew every ttl/3; injected clock; no leftover timers after the wave |
| Acquisition | Atomic-enough local acquire via in-process lock + .aif/neutron/scheduler/leases/{urlencoded(leaseId)}.json; active/released → lease-held; expired → lease-expired (no silent re-acquire or retry) |
| Release | Owner release on completion, failure, and cancellation; released records remain for audit |
| Ordering | Acquire sequentially in admitted taskId order, then execute concurrently; outcomes sorted by taskId regardless of completion order |
| Slice 2 reuse | Each admitted node calls executeNeutronTaskNode with allowConcurrentPeers; distinct N3/N2/N4 capability clamps; N2 in-flight key is sessionId + taskId |
| State transitions | Lease acquired before ready → running; Slice 2 owns running → terminal; failed lease acquire does not mark the node running |
| Persistence boundary | Scheduler lease metadata under .aif/neutron/scheduler/leases/ only; project source fingerprint must ignore that prefix |
| No retry | Expired lease is fail-closed; caller decides; no attempt increment, backoff, or retry queue |
| No graph runner | One call executes at most availableCapacity ready nodes and stops |
| Mutation routing | Remains deferred |
Clock, lease, heartbeat, and batch modules stay in @intentloom/application. No protocol version bump.
33. Slice 4 implementation record
Evidence baseline: origin/main @ c06dbd4f6599e02265c46153dd4b96ba8e5fb0e1 (N5 Slice 3 handoff #450). Explicit maintainer authorization covered Slice 4 only.
| Decision | Record |
|---|---|
| Retry API | classifyNeutronRetry — typed error codes only; model cannot request retries |
| Max attempts | 2 total (attempt 1 + one retry). Absolute cap is also 2 |
| Retryable | operation-failed, timeout, expired lease, lease-lost / renewal invalid-owner |
| Non-retryable | validation, capability/permission denied, root mismatch, unsupported tool, cancellation, budget exceeded, malformed/context failures |
| Attempt identity | Attempt 2 uses {sessionId}:{taskId}:2; attempt 1 lease is never reused |
| Evidence | Application-level NeutronAttemptEvidence on the wave outcome; N1 NeutronSubagentResult unchanged |
| Cancellation | Session signal or session.state === "cancelled" admits nothing; node AbortSignal is local; session signal cancels all running nodes; no retry after cancel |
| Timeout layers | Model/tool remain N2/N4; optional nodeTimeoutMs wrapper; lease TTL stays min(nodeTimeoutMs, 120_000); no graph deadline |
| Expired lease | Same-attempt re-acquire still lease-expired; recovery acquires attempt 2 only |
| Renewal failure | Heartbeat onError aborts the attempt (lease-lost); retry policy decides |
| Stale-attempt | Authority invalidates attempt 1 before attempt 2; late success after timeout/lease-loss is rejected |
| Concurrency | Retry stays in the same wave slot; no extra worker; default 1 / hard cap 4 unchanged |
| Capabilities | Attempt 2 recomputes clamp and intersects attempt-1 ceiling; never widens |
| Context / N4 | Fresh N3 via Slice 2; each attempt still routes tools through N4 |
| Persistence | Lease files only; no .aif/neutron/scheduler/results/ |
| No graph runner | One call remains one wave, with in-wave retries for admitted nodes only |
| Mutation routing | Remains deferred |
Tests: tests/neutron-n5-retry.test.ts, tests/neutron-n5-cancellation-timeout.test.ts.
34. Slice 5 implementation record
Evidence baseline: origin/main @ 426858b5d687930ff9fbbe38861b8b83a24dedab (N5 Slice 4 handoff #452). Explicit maintainer authorization covered Slice 5 only.
| Decision | Record |
|---|---|
| Aggregation API | aggregateNeutronTaskGraphResults — pure merge over graph nodes + optional wave outcomes |
| Stale API | detectNeutronGraphStaleness — project fingerprint, optional checkpoint authority, optional profile authority |
| Reconcile API | reconcileNeutronTaskGraphExecution — detect then aggregate; fail-closed; never reruns |
| Graph contract | Application NeutronGraphExecutionResult. No protocol bump. N1 NeutronSubagentResult remains node-level |
| Graph status | Precedence: stale → incomplete (pending/ready/running) → cancelled → timed-out → failed (includes blocked) → completed |
| Partial-success | Strict: accepted only when status === "completed". partial is observational (some completed and not fully successful). Cancellation/timeout are not hidden behind sibling success |
| Ordering | Nodes by taskId code-point ascending. Attempts by attempt number ascending. Digests use canonical key order and omit observational timestamps |
| Dependencies | Blocked descendants keep blockingDependencyIds and scheduling reasons. They are not executed and contribute no model usage |
| Attempts | Full Slice 4 attempt history retained. Authoritative output is the last non-stale attempt. Superseded late results remain audit evidence only |
| Project stale | Baseline vs current source fingerprint. .aif/neutron/scheduler/ lease metadata is excluded by existing isNeutronSchedulerStatePath fingerprint helpers |
| Checkpoint stale | Compare id, taskId, state, updatedAt, createdSnapshotChecksum when the caller supplies checkpoint authority |
| Profile stale | Compare profile name + capability fingerprint (allowedTools, roles, read-only/network/budget/paths). Created-at is not authority |
| Provenance | graphId, parentId, dependencies, role, requested vs effective capabilities, lease IDs, adapter, N3 source IDs/digests/warnings, N4 tool envelopes as digests, usage, mutationAttempted: false. No chain-of-thought |
| Usage | Sum existing NeutronUsageBudget fields across attempts. budget-exceeded is preserved as the node error code |
| Persistence | None added. Lease files remain the only scheduler persistence |
| Read-only | Aggregation and stale detection never write source project bytes. No apply, shell, or mutation tools |
| No graph runner | Callers may invoke one or more existing waves, then reconcile. Slice 5 does not loop |
| Mutation routing | Remains deferred. N5 evidence is a prerequisite, not authorization |
Tests: tests/neutron-n5-aggregation.test.ts, tests/neutron-n5-stale-state.test.ts.