Skip to content

feat(session): session handle contract for cross-process resume - #104

Merged
divo12 merged 7 commits into
mainfrom
feat/session-resume-contract
Aug 9, 2026
Merged

divo12 merged 7 commits into
mainfrom
feat/session-resume-contract

Conversation

@divo12

@divo12 divo12 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #92. Makes dream drivable the way a control plane drives a coding CLI: dream owns the transcript, the caller keeps only a handle.

  • SessionHandle from save_session: id, path, working_dir, schema_version, usage delta + running total
  • start_session(session_id=…) so chorus can mint task-{uuid} and agree with the harness without a round-trip
  • SessionResumeError with typed reason + should_clear_handle
  • run_role(session_id=…) + run_task(session_scope=…) — one resumable thread per role under a scope ({scope}-planner, -generator, -evaluator)
  • Plan-time acceptance criteria — dropped negotiator heads (3 heads instead of 5)
  • Fix: hyphen-separated role session ids (colon separator broke production — checked_task_id rejects : in sidecar directory names)

Chorus counterpart: #89 stores the handle row; passes session_scope=dream_session_key_for_task(task_id).

Merge order

  1. feat(session): durable save/resume via FileSessionStore #92 (FileSessionStore + save/resume)
  2. feat(session): session handle contract for cross-process resume #104 (this PR)
  3. chorus feat(api): native response_format + typed structured complete #89

Test plan

  • uv run pytest tests/test_services/test_session_store.py tests/test_runner/test_task_session_scope.py tests/test_runner/test_role_session_resume.py -q
  • uv run pytest -q — full suite green
  • Live with chorus scheduler: 2-beat timeout resume, session threads persist under task-{uuid}-{role}.json

Make the harness drivable by a control plane that runs it in short
windows: save_session returns a SessionHandle (id, path, working dir,
per-save usage delta and running total), start_session accepts a
caller-minted id so a scheduler's task-keyed record and the harness
agree without a round-trip, and reset_session drops a spent snapshot.

Resume failures are now typed via SessionResumeError.reason
(missing / corrupt / schema_mismatch / working_dir_mismatch) with
should_clear_handle, so a caller can retry fresh instead of parsing
messages. Snapshots record working_dir and a resume into a different
directory is refused unless explicitly allowed, since replaying a
transcript about other files is worse than starting over.

The transcript stays in dream's own store; callers keep only the handle.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codeant-ai

codeant-ai Bot commented Aug 8, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed e5d4159 Aug 09, 2026 · 06:16 06:16
✅ Reviewed your PR 8b49bfb Aug 08, 2026 · 12:20 12:23

@codeant-ai

codeant-ai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Aug 8, 2026
Comment thread src/dream/services/session_store.py Outdated
# The directory the session did its work in. A resume into a different
# working directory replays a transcript about other files, so the harness
# refuses it unless the caller opts in. ``None`` for engine-less sessions.
working_dir: str | None = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Appending working_dir before the existing metadata field changes the positional constructor contract of the public SessionSnapshot dataclass. Existing consumers that pass metadata positionally will bind it to working_dir, causing invalid snapshot data or silently losing metadata; append new fields after metadata or provide a compatibility-preserving constructor. [api mismatch]

Severity Level: Major ⚠️
- ❌ Existing positional snapshot consumers cannot round-trip snapshots.
- ⚠️ Persisted metadata is silently bound to the wrong field.
- ⚠️ Resume fails with a corrupt-snapshot error for affected callers.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/dream/services/session_store.py
**Line:** 134:134
**Comment:**
	*Api Mismatch: Appending `working_dir` before the existing `metadata` field changes the positional constructor contract of the public `SessionSnapshot` dataclass. Existing consumers that pass `metadata` positionally will bind it to `working_dir`, causing invalid snapshot data or silently losing metadata; append new fields after `metadata` or provide a compatibility-preserving constructor.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce4b168. Rather than reorder the tail, the optional fields (max_turns, working_dir, metadata) are now kw_only=True — each of them is something the harness learned to persist after the fact, so the next one would recreate the same trap. test_snapshot_optional_fields_are_keyword_only pins it.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds durable session handles and role-scoped session continuation across processes while replacing sprint-contract negotiation with plan-time acceptance criteria.

  • Adds caller-defined session IDs, typed resume failures, usage accounting, and working-directory binding.
  • Adds resumable planner, generator, and evaluator threads beneath a task session scope.
  • Simplifies task execution to three role heads and derives sprint contracts from planner criteria.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; both previously reported session-resume issues are fixed in the current code.

Important Files Changed

Filename Overview
src/dream/services/session_store.py Adds versioned session-handle serialization and rejects legacy snapshots before restoration.
src/dream/harness.py Adds named session lifecycle operations, typed resume handling, working-directory checks, and session-handle persistence.
src/dream/runner/_role_session.py Adds resumable role sessions while preserving snapshots that belong to another working directory.
src/dream/runner/_run.py Adds task-scoped role session IDs and simplifies orchestration around plan-derived sprint contracts.
src/dream/sprint/_plan_contract.py Builds sprint contracts directly from plan-time acceptance criteria and unresolved evaluator feedback.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[run_role with session_id] --> B{Saved snapshot exists?}
    B -- No --> C[Start named session]
    B -- Yes --> D{Snapshot resumable?}
    D -- Yes --> E[Resume named session]
    D -- Spent or incompatible --> F[Reset and start named session]
    D -- Working directory mismatch --> G[Start anonymous non-owning session]
    C --> H[Run role]
    E --> H
    F --> H
    G --> H
    H --> I{Owns named session ID?}
    I -- Yes --> J[Save and return SessionHandle]
    I -- No --> K[Do not save; preserve existing snapshot]
Loading

Reviews (7): Last reviewed commit: "fix(session): refuse resume when working..." | Re-trigger Greptile

Comment thread src/dream/services/session_store.py
Comment thread src/dream/harness.py
await self._ensure_open()
opts = options or SessionOptions()
session_id = uuid.uuid4().hex
resolved_id = uuid.uuid4().hex if session_id is None else checked_session_id(session_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Caller-supplied IDs are used directly as the session storage key without checking whether a snapshot already exists or coordinating ownership. Two scheduler tasks can select the same ID, create independent sessions, and later overwrite each other's snapshots, losing one transcript while both callers retain apparently valid handles. Reject an existing ID for start_session, or require an explicit resume/replace operation with ownership or version checks. [state collision]

Severity Level: Major ⚠️
- ❌ Concurrent scheduler tasks can lose one conversation transcript.
- ⚠️ Handles can point to silently replaced session state.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/dream/harness.py
**Line:** 190:190
**Comment:**
	*State Collision: Caller-supplied IDs are used directly as the session storage key without checking whether a snapshot already exists or coordinating ownership. Two scheduler tasks can select the same ID, create independent sessions, and later overwrite each other's snapshots, losing one transcript while both callers retain apparently valid handles. Reject an existing ID for `start_session`, or require an explicit resume/replace operation with ownership or version checks.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ce4b168. start_session now refuses a caller-minted id that already names a saved snapshot, pointing at resume_session to continue it or reset_session to discard it. The check is skipped when no store is configured, since an id collision costs nothing with nowhere to persist. test_start_session_refuses_an_id_that_already_has_a_snapshot covers it.

divo12 and others added 2 commits August 8, 2026 17:58
A role head previously opened a fresh session every call, so a caller
running the harness in short windows lost the conversation between them.
Naming the thread now resumes it: run_role restores that snapshot when
one is readable and returns its SessionHandle, with the usage delta
scoped to the run.

An unusable snapshot never strands the role. Missing, corrupt, foreign
schema, or a snapshot taken under another working directory all start
the thread over under the same name — the same fallback a coding CLI
makes when --resume is refused — and a spent snapshot is dropped so
later runs stop re-paying the failed attempt.

The transcript is persisted before the mid-stream error check, so a
session that failed still leaves the history explaining why for the
next run of the thread.

Co-authored-by: Cursor <cursoragent@cursor.com>
run_role can now name a thread, but a control plane drives run_task, whose
heads each opened their own unnamed session. session_scope threads one key
per task through the autowired heads: every role gets a resumable thread
beneath it ({scope}:planner and so on), so a later run_task under the same
scope continues those conversations instead of restarting them.

Roles stay separate — a planner and an evaluator are different
conversations — but heads bound to the same role deliberately share one
thread. The generator negotiates the sprint contract and then builds
against terms it remembers agreeing to; the evaluator judges against
criteria it proposed itself.

Explicitly supplied heads are left alone, and omitting the scope keeps
today's behaviour of persisting nothing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/dream/runner/_role_session.py Outdated
divo12 and others added 3 commits August 8, 2026 18:46
…iation

Naming a step and naming what "done" means for it is one judgement, so the
planner does both and the sprint reads the criteria off the ledger. The
propose/respond exchange between evaluator and generator cost two role
sessions per sprint and six at the cap, all before a line of code existed,
and it is the one part of the loop that had two agents negotiating actions
rather than contributing intelligence.

run_task now takes three heads instead of five. A needs-changes verdict still
steers the retry: its unresolved items fold into the next contract's criteria.

Co-authored-by: Cursor <cursoragent@cursor.com>
Review findings on the session-handle work, all variations on one thing: a
session id is a name two callers can pick, and the harness treated the second
one as the owner.

- start_session refuses an id that already has a saved snapshot. Two scheduler
  tasks on the same key would otherwise save over each other while both kept a
  handle that looked fine. resume_session continues it, reset_session discards
  it; either is a decision the caller makes out loud.
- run_role no longer saves over a snapshot from another working directory.
  should_clear_handle already called that snapshot reusable, and then the
  fallback started a fresh session under the same name and saved it. The run
  now goes on unnamed and returns no handle, leaving the transcript resumable
  from the workspace that wrote it.
- SCHEMA_VERSION 2. A version-1 snapshot predates working_dir, so it decoded
  as None and skipped the directory check entirely — an old file was a way to
  resume a transcript into any workspace. It now reads as a foreign schema.
- SessionSnapshot's optional fields are keyword-only. Each was learned after
  the fact and the next one lands beside them; positional callers would bind
  metadata to whatever arrived last.

Co-authored-by: Cursor <cursoragent@cursor.com>
Derived session ids used a colon separator that checked_task_id rejects when
the scope becomes a directory name under ~/.dream/data/sessions. Switch to
hyphen separation and validate at derivation time so chorus task-{uuid} scopes
work in production.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codeant-ai

codeant-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XL This PR changes 500-999 lines, ignoring generated files labels Aug 9, 2026
v2 snapshots with a null working_dir cannot prove they belong in the
current workspace, so resume now requires explicit opt-in rather than
silently skipping the directory binding check.

Co-authored-by: Cursor <cursoragent@cursor.com>
@divo12
divo12 changed the base branch from feat/session-save-resume to main August 9, 2026 06:27
@divo12
divo12 merged commit 4c3af9a into main Aug 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant