Add /graph G4: project-level shared graph in .kigi/graph.jsonl
The graph now follows the REPOSITORY, not the session. Every checkpoint projects the orchestration to .kigi/graph.jsonl at the git root in a beads-style, line-mergeable shape: line 1 is the header (orchestration minus nodes — omitted entirely, and a header carrying inline nodes is rejected on load rather than silently duplicating the per-line entries), then one content-hash-id node per line. The session tracker stays the single source of truth; projection failures warn loudly but never block progress. kigi only writes the file — committing it stays a user decision. Single-writer discipline via an fs2 flock on a sidecar .lock: the session that creates or resumes a graph owns the projection; other instances get a read-only /graph status view rendered from the file and an explicit refusal on resume. Cross-session revive: /graph resume in a fresh session loads the file UNDER the lock (locking after reading raced the owner's final checkpoint and could resurrect a just-cleared graph), sanitizes it with the from_snapshot demotions, and re-dispatches. Holding the lock proves nobody writes NOW — not that the file's content is yours. Every lock-then-mutate site therefore identity-checks the projected graph_id: /graph <objective> refuses to overwrite a foreign non-Complete projection (revive-or-clear guidance, mirroring the session-level guard); session-restored graphs claim writership on resume (and best-effort at spawn re-emit, so the shared file learns the demoted truth immediately) but refuse when the projection belongs to a different graph; /graph clear skips projection teardown entirely when no session graph exists, leaves foreign projections in place, and warns instead of swallowing lock errors. Resume arms validate their flags before taking the lock. Tests: projection round-trip pinning the one-line-per-node shape, inline-nodes rejection, malformed-content loud errors, exclusive-lock semantics across handles, and an e2e driving create → checkpoint projection → second-instance read-only refusal → owner death → fresh- session revive (demoted node relaunches) → clear removing the projection. kigi-shell 4941 lib tests green; workspace clippy clean.
This commit is contained in:
@@ -431,6 +431,8 @@ pub(crate) async fn spawn_session_actor(
|
||||
};
|
||||
Arc::new(parking_lot::Mutex::new(tracker))
|
||||
};
|
||||
let graph_project_dir =
|
||||
crate::session::graph_project::project_graph_dir(tool_context.cwd.as_path());
|
||||
let graph_tracker = {
|
||||
let session_dir = crate::session::persistence::session_dir(&session_info);
|
||||
let tracker = if let Some(snapshot) = persisted_graph_mode {
|
||||
@@ -1102,6 +1104,8 @@ pub(crate) async fn spawn_session_actor(
|
||||
graph_concurrency: effective_config.resolve_graph_concurrency(),
|
||||
graph_node_rounds: effective_config.resolve_graph_node_rounds(),
|
||||
graph_replan_cap: effective_config.resolve_graph_replan_cap(),
|
||||
graph_project_dir,
|
||||
graph_project_lock: std::cell::RefCell::new(None),
|
||||
goal_turn_task_ids: parking_lot::Mutex::new(std::collections::HashSet::new()),
|
||||
goal_continuation_streak: std::sync::atomic::AtomicU32::new(0),
|
||||
goal_blocked_streak: std::sync::atomic::AtomicU32::new(0),
|
||||
@@ -1248,8 +1252,13 @@ pub(crate) async fn spawn_session_actor(
|
||||
// A restored graph was demoted (Active→UserPaused, Running→Ready) IN
|
||||
// MEMORY after the updates-log replay, whose last GraphUpdated still
|
||||
// shows the pre-shutdown Active state. Re-emit truth once so a
|
||||
// reattached pager never renders a stale self-driving chip.
|
||||
// reattached pager never renders a stale self-driving chip — and
|
||||
// best-effort reclaim project writership so the shared file gets the
|
||||
// demoted truth too (Busy = another instance owns it; skip quietly).
|
||||
if session.graph_tracker.lock().snapshot().is_some() {
|
||||
if let Some(msg) = session.claim_project_graph_for_resume() {
|
||||
tracing::info!(%msg, "graph restore: project writership not reclaimed");
|
||||
}
|
||||
session.persist_graph_state();
|
||||
}
|
||||
if let Some(ref display_cwd) = prompt_display_cwd {
|
||||
|
||||
Reference in New Issue
Block a user