feat(x/core): session layer with write-through index (stage 3)

SessionsImpl per session-design.md, constructor-injected and tested
entirely against a mocked ITurnRuntime:

- sendMessage: per-session lock, typed TurnNotSettledError while the
  latest turn is non-terminal (terminal statuses including failed/
  cancelled allow continuation), context as { previousTurnId } ref,
  turn-file-first write ordering (a failed session append leaves a
  benign orphan turn and no advance), denormalized agent/model on
  turn_appended, default title from the first message.
- Dedicated respondToAskHuman endpoint (async_tool_result wrapper);
  respondToPermission / deliverAsyncToolResult pass turn-runtime
  rejections through. stopTurn aborts a live advance or cancels an
  at-rest turn; resumeTurn re-enters idle turns (never at startup).
- In-memory index: startup scan (session files + each latest turn for
  status; corrupt files yield errored entries without aborting),
  write-through updates, deletion guard against late-settle
  resurrection. Bus events (turn-event / index-changed) defined in
  @x/shared as the renderer IPC contract.
- FSSessionRepo: date-partitioned append-only JSONL + list/delete;
  deleteSession removes the session file only.
- runHeadlessTurn: standalone turns (sessionId null, auto permission,
  no human) for background/knowledge/scheduled callers.

29 new tests covering the session-design §13 matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-02 09:57:20 +05:30
parent 9e1d9b81ce
commit ccf38c6a0f
12 changed files with 1760 additions and 1 deletions

View file

@ -1,5 +1,9 @@
import { z } from "zod";
import { ModelDescriptor, type TurnStatus } from "./turns.js";
import {
ModelDescriptor,
type TurnStatus,
type TurnStreamEvent,
} from "./turns.js";
// Durable session contract for the session layer (see
// packages/core/docs/session-design.md). A session is an append-only chain
@ -167,6 +171,22 @@ export interface SessionIndexEntry {
error?: string;
}
// What the renderer's single feed consumer receives over IPC: live turn
// stream events (durable events + ephemeral deltas) and index updates.
// entry: null signals deletion.
export type SessionBusEvent =
| {
kind: "turn-event";
sessionId: string;
turnId: string;
event: TurnStreamEvent;
}
| {
kind: "index-changed";
sessionId: string;
entry: SessionIndexEntry | null;
};
export function sessionIndexEntry(
state: SessionState,
latestTurnStatus: SessionLatestTurnStatus,