Skip to content

feat(sessions): durable WorkingContext grounding#598

Merged
Aaronontheweb merged 2 commits into
devfrom
working-context-grounding
Apr 11, 2026
Merged

feat(sessions): durable WorkingContext grounding#598
Aaronontheweb merged 2 commits into
devfrom
working-context-grounding

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

Stacks on #597 (compaction-rework).

Adds a WorkingContext field to SessionState that 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:

Tier Storage Update path
Skills stateless how-to reference pre-loaded on session start
Memory cross-session facts memory store, recalled per-turn
WorkingContext (new) in-session mutable state tool execution hooks + observer parsing
Conversation turn history append-on-turn

WorkingContext shape

public sealed record WorkingContext
{
    public ImmutableList<string> RecentFiles { get; init; }    // bounded ring, 10 entries
    public ImmutableList<string> OpenGoals { get; init; }
    public ImmutableList<string> ProgressMarkers { get; init; }
}
  • RecentFiles: bounded ring buffer of 10 entries, most-recent-first, deduped on repeat access. Updated automatically by LlmSessionActor whenever a file-taking tool (file_read, file_write, file_edit, grep_files, etc.) completes. The path is extracted from the tool call's ArgumentsJson by WorkingContextUpdater (tests cover the extraction from path / file_path / filePath / file / filename field 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

WorkingContext is emitted as a [working-context] block via InjectDynamicContextLayers on every LLM call when non-empty, adjacent to the existing [session] block. The block is suppressed entirely when WorkingContext.IsEmpty so the model doesn't see a barren header.

Precedent

Cline's TaskState.currentFocusChainChecklist is 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"). Our WorkingContext is the Netclaw equivalent, stored on SessionState rather than a standalone file.

Explicitly NOT in this change

Both were deliberately carved out to keep this PR focused on durable task state.

Journal compatibility

WorkingContext is ProtoMember(6) on SessionSnapshot and ProtoMember(7) on SessionCompacted, both nullable. Old snapshots and events without the field deserialize to WorkingContext.Empty via SessionState.FromSnapshot / Apply(SessionCompacted). No migration required.

OpenSpec

Full OpenSpec change at openspec/changes/working-context-grounding/ (proposal, design, netclaw-session delta spec with new "Durable working context grounding" requirement, tasks).

Test plan

  • dotnet build — zero warnings
  • dotnet test909/909 pass (19 new tests added)
  • dotnet slopwatch analyze — clean
  • WorkingContextTests.cs: ring buffer, dedupe-on-repeat, immutable update, empty defaults
  • WorkingContextUpdaterTests.cs: field-name probing, orphan tool result handling, multi-call batches, dedupe across reads
  • Manual reproduction: run a local daemon, read several files, trigger compaction, confirm [working-context] block appears in the next turn's system message with expected recent_files entries (deferred to integration testing against a running instance)

Review notes

This is a small PR in terms of lines-of-code risk:

  • Two new source files (WorkingContext.cs, WorkingContextUpdater.cs), both small and self-contained
  • Four edits to existing files (SessionState, SessionSnapshot, Events, LlmSessionActor) — all additive, no signature changes visible outside the actor package
  • Two new test files (WorkingContextTests.cs, WorkingContextUpdaterTests.cs)

The concept is the scope — not the code. Worth reviewing the OpenSpec design doc first if you want the architectural framing.

@Aaronontheweb Aaronontheweb added this to the 0.12 milestone Apr 11, 2026
@Aaronontheweb Aaronontheweb added context-pipeline LLM context assembly: prompt layers, dynamic injection, memory recall, temporal grounding enhancement New feature or request sessions LLM session actor, turn lifecycle, pipelines labels Apr 11, 2026
@Aaronontheweb Aaronontheweb force-pushed the working-context-grounding branch from 9f7c1a0 to 2690d25 Compare April 11, 2026 15:48
@Aaronontheweb Aaronontheweb force-pushed the working-context-grounding branch from 2690d25 to c977169 Compare April 11, 2026 16:49
Base automatically changed from compaction-rework to dev April 11, 2026 17:36
@Aaronontheweb Aaronontheweb force-pushed the working-context-grounding branch 3 times, most recently from e367ac1 to dfdbedf Compare April 11, 2026 18:24
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
…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).
@Aaronontheweb Aaronontheweb merged commit 8e16531 into dev Apr 11, 2026
3 checks passed
@Aaronontheweb Aaronontheweb deleted the working-context-grounding branch April 11, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

context-pipeline LLM context assembly: prompt layers, dynamic injection, memory recall, temporal grounding enhancement New feature or request sessions LLM session actor, turn lifecycle, pipelines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant