feat(sessions): durable WorkingContext grounding#598
Merged
Conversation
12451a5 to
795037c
Compare
9f7c1a0 to
2690d25
Compare
795037c to
0f3f24c
Compare
2690d25 to
c977169
Compare
e367ac1 to
dfdbedf
Compare
Stacks on the compaction-rework change (merged as #597). Adds a `WorkingContext` field to `SessionState` that tracks recent files the agent has read/written/edited, survives compaction, actor recovery, and daemon restart without depending on the observer LLM to reconstruct it. This introduces a new tier of session state that sits parallel to existing tiers: - Skills: stateless how-to reference (already exists) - Memory: cross-session facts (already exists, via memory store) - WorkingContext: in-session mutable state (new) - Conversation: turn history (already exists) `WorkingContext` carries a single field: `RecentFiles` — a bounded ring buffer of 10 file paths, most-recent-first, deduped on repeat access. Updated automatically by `LlmSessionActor` when a path-taking tool (file_read, file_write, file_edit, etc.) completes. The path is extracted from the tool call's `ArgumentsJson` via `WorkingContextUpdater.TryExtractFilePath` which probes well-known JSON field names (`path`, `file_path`, `filePath`, `file`, `filename`, `fileName`) — no tool-name allowlist, so first-party tools and MCP filesystem tools participate automatically. Security: `AddRecentFile` rejects paths containing control characters (newline, carriage return, null byte). Without this, an attacker- crafted path containing a literal newline would break out of the `recent_files:` section in the `[working-context]` block and inject arbitrary content into the LLM's system prompt. Rejected paths are silently dropped at the earliest point in the ingestion pipeline. Injection: `WorkingContext` is emitted as a `[working-context]` block via `InjectDynamicContextLayers` on every LLM call when non-empty, adjacent to the existing `[session]` block. Suppressed entirely when `WorkingContext.IsEmpty` so the model doesn't see a barren header. Explicitly NOT in this change: - OpenGoals / ProgressMarkers fields and the observer-output parser that would populate them — deferred per "no hypothetical future requirements" rule. Can be added in a follow-up change when the parser lands. - CurrentWorkingDirectory / ActiveProjectPath — GitHub #595. - Authoritative CWD for path-taking tools — GitHub #596. Journal compatibility: `WorkingContext` is a new optional field on `SessionSnapshot` (ProtoMember 6) and `SessionCompacted` (ProtoMember 7). ProtoMember 5/6 respectively are reserved holes — formerly `CompactionBoundaryIndex`, removed before compaction-rework merged. Old snapshots and events without the field deserialize to `WorkingContext.Empty` via `SessionState.FromSnapshot` / `Apply(SessionCompacted)`. Precedent: Cline's `TaskState.currentFocusChainChecklist` in `src/core/task/focus-chain/` is the closest comparable pattern — a persistent markdown todo list tracked on disk, re-attached to every LLM call, explicitly protected from re-summarization. `WorkingContext` is the Netclaw equivalent, stored on `SessionState` rather than a standalone file. Tests: 933/933 pass (+11 new). Slopwatch clean (SW003 on the JsonException catch is explicitly ignored via inline directive with justification — LLM-generated tool args can be malformed and "no path tracked" is the correct handling). Refs Aaronontheweb/netclaw#595, Aaronontheweb/netclaw#596
dfdbedf to
2c3c60d
Compare
…for WorkingContext WorkingContextUpdater now takes an optional ILoggingAdapter and emits debug logs for both tracked files and schema-drift (tool call had string-valued arguments but none of the probed field names matched). Operators hunting "why isn't this file in [working-context]" get a concrete signal in debug logs. Adds an end-to-end integration test that drives a file_read tool call through the real compaction pipeline (3 turns: populate → compact → probe) and verifies the [working-context] block still contains src/Rect.cs after compaction. This exercises WorkingContextUpdater, the SessionCompacted event path, and InjectDynamicContextLayers together — not just direct Apply() on an in-memory state. Refs #600 (sub-agent gap discovered during this work).
This was referenced Apr 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacks on #597 (compaction-rework).
Adds a
WorkingContextfield toSessionStatethat survives compaction, actor recovery, and daemon restart without depending on the observer LLM to reconstruct it. This introduces a new tier of session state that sits parallel to existing tiers:WorkingContext shape
RecentFiles: bounded ring buffer of 10 entries, most-recent-first, deduped on repeat access. Updated automatically byLlmSessionActorwhenever a file-taking tool (file_read,file_write,file_edit,grep_files, etc.) completes. The path is extracted from the tool call'sArgumentsJsonbyWorkingContextUpdater(tests cover the extraction frompath/file_path/filePath/file/filenamefield names).OpenGoals: user-stated goals, populated opportunistically from the observer's structured summary "Pending Tasks" section after compaction.ProgressMarkers: done/pending markers in[x]/[ ]form, populated opportunistically from the observer's "Current Work" section.Injection
WorkingContextis emitted as a[working-context]block viaInjectDynamicContextLayerson every LLM call when non-empty, adjacent to the existing[session]block. The block is suppressed entirely whenWorkingContext.IsEmptyso the model doesn't see a barren header.Precedent
Cline's
TaskState.currentFocusChainChecklistis the closest comparable in the four harnesses researched for PR #597 — a persistent markdown todo list on disk, watched via chokidar, re-attached to every LLM call, and explicitly protected by the Cline summarizer prompt ("If no task_progress list was included in the previous context, you should NOT create a new task_progress list"). OurWorkingContextis the Netclaw equivalent, stored onSessionStaterather than a standalone file.Explicitly NOT in this change
CurrentWorkingDirectoryandActiveProjectPath— tracked as Track session CWD in WorkingContext and emit as [working-context] + [project-instructions] #595 (session CWD tracking + project identity re-read, milestone 0.12)Both were deliberately carved out to keep this PR focused on durable task state.
Journal compatibility
WorkingContextisProtoMember(6)onSessionSnapshotandProtoMember(7)onSessionCompacted, both nullable. Old snapshots and events without the field deserialize toWorkingContext.EmptyviaSessionState.FromSnapshot/Apply(SessionCompacted). No migration required.OpenSpec
Full OpenSpec change at
openspec/changes/working-context-grounding/(proposal, design,netclaw-sessiondelta spec with new "Durable working context grounding" requirement, tasks).Test plan
dotnet build— zero warningsdotnet test— 909/909 pass (19 new tests added)dotnet slopwatch analyze— cleanWorkingContextTests.cs: ring buffer, dedupe-on-repeat, immutable update, empty defaultsWorkingContextUpdaterTests.cs: field-name probing, orphan tool result handling, multi-call batches, dedupe across reads[working-context]block appears in the next turn's system message with expectedrecent_filesentries (deferred to integration testing against a running instance)Review notes
This is a small PR in terms of lines-of-code risk:
WorkingContext.cs,WorkingContextUpdater.cs), both small and self-containedSessionState,SessionSnapshot,Events,LlmSessionActor) — all additive, no signature changes visible outside the actor packageWorkingContextTests.cs,WorkingContextUpdaterTests.cs)The concept is the scope — not the code. Worth reviewing the OpenSpec design doc first if you want the architectural framing.