Add /graph G1: parallel fan-out with worktree isolation and merge-back
With KIGI_GRAPH_CONCURRENCY > 1 (default 3, clamp [1,8]) and >=2 Ready nodes, drive_graph — the single dispatch loop shared by setup/advance/resume — runs parallel batches: each node executes as a bounded worker<->verifier subagent loop (KIGI_GRAPH_NODE_ROUNDS, default 3; general-purpose children with the full implementer toolset; worktree isolation on round 1, resume keeps context and worktree on later rounds; NODE_RESULT/NODE_VERDICT terminal contracts parsed fail-closed with fence-stripping and line anchoring). Achieved nodes merge back SEQUENTIALLY via kigi_workspace apply_worktree in Merge mode; a conflict fails the node, block_dependents fires, and surviving chains keep going. gn-final always runs serially on the full goal engine. Concurrency=1 is byte-identical to the serial G0 path; non-git projects degrade to serial. Merge primitive hardened for real use (kigi-workspace): the 3-way apply is now byte-safe (binary files no longer read as UTF-8 and silently deleted) and gains the identical-content rule (ours==theirs => already present, not a conflict) — without it, any dirty file inherited via PreserveWorkingTree false-conflicted every node merge and multi-wave graphs self-poisoned. Adversarial review pass (16 confirmed findings, all fixed): in-session resume demotes orphaned Running nodes instead of wedge-pausing forever after a mid-batch Esc; cancel re-sweeps subagents AFTER the turn abort so workers spawned in the cancel window die too; empty child ids are never adopted as resume targets (no unisolated escape to the shared tree); a successful isolated round returning no worktree fails the node (soft-fallback can no longer put N writers in one tree); main-HEAD movement during a batch aborts merges instead of reverse-applying external commits; failed nodes still charge the token budget; budget trips terminally fail the in-flight node; runaway (>600s) rounds are cancelled by spawn id and retried via resume; worker summaries are marker-sanitized before verifier embedding. Merged worktrees are removed immediately (storage discipline); failed nodes keep theirs for postmortem. Tests: 4922 kigi-shell lib tests green (58 graph-specific), including fan-out proven by a held-reply gate, real-git batch merge with cleanup assertions, budget charging across verdicts, cap trimming, resume-after- cancelled-batch, and backgrounded-round cancel semantics.
This commit is contained in:
1 parent
4361b03259
commit
4d1e4fdc52
20 files changed
+1986
-68
No files matched your search
@@ -628,6 +628,42 @@ impl GraphTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Demote in-flight (`Running`/`Verifying`) nodes whose executor is
|
||||
/// gone back to `Ready`, keeping `keep` (the node whose goal still
|
||||
/// lives in the engine, if any). Used by the IN-SESSION resume path:
|
||||
/// a cancel during a parallel batch aborts the batch future after
|
||||
/// nodes were marked Running, and — unlike a restart, where
|
||||
/// `from_snapshot` sanitizes — nothing else would ever demote them,
|
||||
/// wedging every subsequent resume. Re-running is safe by design
|
||||
/// (verifier-gated).
|
||||
pub fn demote_orphaned_in_flight(&mut self, keep: Option<&str>) {
|
||||
let Some(state) = self.state.as_mut() else {
|
||||
return;
|
||||
};
|
||||
for node in &mut state.nodes {
|
||||
if matches!(node.status, NodeStatus::Running | NodeStatus::Verifying)
|
||||
&& keep != Some(node.id.as_str())
|
||||
{
|
||||
node.status = NodeStatus::Ready;
|
||||
}
|
||||
}
|
||||
state.current_node = keep.map(str::to_owned);
|
||||
self.recompute_ready();
|
||||
}
|
||||
|
||||
/// Charge a node's spend against the graph budget and stamp it on
|
||||
/// the node, independent of verdict — a FAILED node's tokens were
|
||||
/// still spent (budget integrity).
|
||||
pub fn charge_node_tokens(&mut self, id: &str, tokens: i64) {
|
||||
let Some(state) = self.state.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some(node) = state.nodes.iter_mut().find(|n| n.id == id) {
|
||||
node.tokens_used = tokens;
|
||||
}
|
||||
state.tokens_spent_nodes = state.tokens_spent_nodes.saturating_add(tokens);
|
||||
}
|
||||
|
||||
/// Promote every `Waiting` node whose deps are all `Achieved` to
|
||||
/// `Ready`.
|
||||
pub fn recompute_ready(&mut self) {
|
||||
|
||||
Reference in new issue
Block a user