Add /graph G2: resumable budget, GraphUpdated status chip, PTY + turn-level coverage
BudgetLimited is now a resumable state: a budget trip demotes in-flight
nodes to Ready (a resource stop, not a verdict — no forever-Running node is
ever persisted) and '/graph resume --budget <tokens>' re-arms the graph with
fresh headroom (new budget = spent-so-far + extra). The tripped node's
partial burn is charged into tokens_spent_nodes at BOTH cascade sites before
the demotion clears current_node, so the top-up arithmetic never runs on an
under-counted ledger. Any input starting with 'resume' resolves to a resume
(case-insensitive; malformed top-ups surface the usage hint) and setup_graph
refuses to replace any non-Complete graph — a typo can no longer silently
destroy a resumable graph. An explicit --budget on a merely-paused graph is
rejected loudly instead of silently discarded; all trip-time messages now
advertise the top-up.
The pager gains a graph status chip: a new GraphUpdated wire variant
(extensions/notification.rs, old pagers degrade via #[serde(other)]) is
emitted from the single persist_graph_state chokepoint — every transition is
both a checkpoint and a badge tick — with a 'cleared' sentinel on /graph
clear and a one-shot re-emit after session restore (the replayed updates log
otherwise shows the pre-shutdown Active state that from_snapshot just
demoted in memory). TUI side: GraphDisplayState, session-notification arm,
and a goal-idiom chip with node progress, clamped current-node title, and
budget-aware spend. Pre-session command availability now advertises /graph
from the flags (it was fail-closed to the in-session path only, so the
welcome-screen slash menu never showed it).
Coverage: GraphUpdated wire round-trip + minimal-payload + unknown-tag
tests; PTY scenarios graph_slash_presession{,_disabled}.yaml (both run
green against the real pager binary); handle_prompt-level e2e for terminal
slash outcomes (/graph status|resume|pause, /goal refusals while the graph
owns the engine); budget top-up e2e driving a BudgetLimited diamond back to
Complete. Not shimmed: pre-G2 persisted snapshots with budget-Failed nodes
(the KIGI_GRAPH flag has never shipped enabled, so none exist).
kigi-shell 4927 and kigi-tui 6610 lib tests green; workspace clippy clean.
This commit is contained in:
1 parent
4d1e4fdc52
commit
1579558b56
20 files changed
+878
-57
No files matched your search
@@ -263,7 +263,9 @@ pub(super) const BUILTIN_COMMANDS: &[BuiltinCommand] = &[
|
||||
BuiltinCommand {
|
||||
name: "graph",
|
||||
description: "Decompose an objective into a dependency graph of autonomous goals",
|
||||
argument_hint: Some("<objective> [--budget <tokens>] | status | pause | resume | clear"),
|
||||
argument_hint: Some(
|
||||
"<objective> [--budget <tokens>] | status | pause | resume [--budget <tokens>] | clear",
|
||||
),
|
||||
aliases: &[],
|
||||
gate: BuiltinGate::Graph,
|
||||
resolve: |args| {
|
||||
@@ -273,9 +275,27 @@ pub(super) const BUILTIN_COMMANDS: &[BuiltinCommand] = &[
|
||||
// it is an alias for the status tree.
|
||||
"" | "status" | "show" => BuiltinAction::GraphStatus,
|
||||
"pause" => BuiltinAction::GraphPause,
|
||||
"resume" => BuiltinAction::GraphResume,
|
||||
"resume" => BuiltinAction::GraphResume { extra_budget: None },
|
||||
"clear" => BuiltinAction::GraphClear,
|
||||
_ => {
|
||||
// ANY input starting with `resume` is a resume attempt
|
||||
// and must NEVER fall through to GraphSet — a typo'd
|
||||
// top-up would otherwise silently replace a resumable
|
||||
// BudgetLimited graph. Well-formed `resume --budget
|
||||
// <tokens>` (case-insensitive keywords) carries the
|
||||
// top-up; malformed variants resolve to a plain
|
||||
// resume, whose BudgetLimited arm prints the usage.
|
||||
let lower = trimmed.to_lowercase();
|
||||
if let Some(rest) = lower.strip_prefix("resume") {
|
||||
let extra_budget = rest
|
||||
.trim()
|
||||
.strip_prefix("--budget")
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()))
|
||||
.and_then(|v| v.parse::<i64>().ok())
|
||||
.filter(|extra| *extra > 0);
|
||||
return BuiltinAction::GraphResume { extra_budget };
|
||||
}
|
||||
let (objective, token_budget) = parse_goal_budget(trimmed);
|
||||
BuiltinAction::GraphSet {
|
||||
objective,
|
||||
@@ -700,7 +720,9 @@ pub(super) enum BuiltinAction {
|
||||
},
|
||||
GraphStatus,
|
||||
GraphPause,
|
||||
GraphResume,
|
||||
GraphResume {
|
||||
extra_budget: Option<i64>,
|
||||
},
|
||||
GraphClear,
|
||||
}
|
||||
|
||||
@@ -737,7 +759,7 @@ impl BuiltinAction {
|
||||
BuiltinAction::GraphSet { .. }
|
||||
| BuiltinAction::GraphStatus
|
||||
| BuiltinAction::GraphPause
|
||||
| BuiltinAction::GraphResume
|
||||
| BuiltinAction::GraphResume { .. }
|
||||
| BuiltinAction::GraphClear => "graph",
|
||||
}
|
||||
}
|
||||
@@ -772,10 +794,10 @@ impl BuiltinAction {
|
||||
| BuiltinAction::GoalResume
|
||||
| BuiltinAction::GoalClear => false,
|
||||
BuiltinAction::GraphSet { .. } => true,
|
||||
BuiltinAction::GraphStatus
|
||||
| BuiltinAction::GraphPause
|
||||
| BuiltinAction::GraphResume
|
||||
| BuiltinAction::GraphClear => false,
|
||||
BuiltinAction::GraphResume { extra_budget } => extra_budget.is_some(),
|
||||
BuiltinAction::GraphStatus | BuiltinAction::GraphPause | BuiltinAction::GraphClear => {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1714,7 +1736,26 @@ mod tests {
|
||||
));
|
||||
assert!(matches!(
|
||||
resolve_builtin("graph", "resume"),
|
||||
Some(BuiltinAction::GraphResume)
|
||||
Some(BuiltinAction::GraphResume { extra_budget: None })
|
||||
));
|
||||
assert!(matches!(
|
||||
resolve_builtin("graph", "resume --budget 800"),
|
||||
Some(BuiltinAction::GraphResume {
|
||||
extra_budget: Some(800)
|
||||
})
|
||||
));
|
||||
// Malformed top-ups resolve to a PLAIN resume — never to
|
||||
// GraphSet, which would silently replace a resumable graph; the
|
||||
// BudgetLimited resume arm then prints the usage hint.
|
||||
assert!(matches!(
|
||||
resolve_builtin("graph", "resume --budget nope"),
|
||||
Some(BuiltinAction::GraphResume { extra_budget: None })
|
||||
));
|
||||
assert!(matches!(
|
||||
resolve_builtin("graph", "Resume --Budget 800"),
|
||||
Some(BuiltinAction::GraphResume {
|
||||
extra_budget: Some(800)
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
resolve_builtin("graph", "clear"),
|
||||
|
||||
Reference in new issue
Block a user