Merge remote-tracking branch 'origin/master' into worktree/provider-routed-llm-adapters

# Conflicts:
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	packages/context/time-context/tests/time-context.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/session/README.md
#	packages/core/session/src/types.ts
#	packages/core/session/tests/surface.spec.ts
#	packages/examples/acp-demo/tests/acp-agent.spec.ts
#	packages/examples/stdio-demo/tests/stdio-agent.spec.ts
#	packages/hooks/hooks-claude/tests/coverage.spec.ts
#	packages/hooks/hooks-codex/tests/coverage.spec.ts
#	packages/session-query/session-query/tests/session-query.spec.ts
#	packages/support/invariants/tests/invariants.spec.ts
#	packages/ui/acp/tests/harness.ts
This commit is contained in:
Tianyi Cui
2026-07-17 21:56:10 +08:00
358 changed files with 18578 additions and 2877 deletions

View File

@@ -8,6 +8,7 @@ The persisted unit IS the existing `SessionEvent` (event-sourced model — the l
| Method | Contract |
|---|---|
| `locate(meta): SessionLocation \| undefined` | Resolve an absolute per-session artifact target without I/O or materialization. Backends without an independent local artifact return `undefined`. |
| `create(meta): Promise<void>` | Register a new session's metadata. MAY defer the physical write until the first `append` (lazy materialization). |
| `append(id, events): Promise<void>` | Durably persist a batch (from the `session/flush` drain). Append-only; first event `seq` == stored next-seq after any repair; rejects non-JSON-serializable data naming the offending type. |
| `load(id): Promise<{ meta; events }>` | Reload meta + log. Preserves an interrupted (unclosed) final turn and closes it with synthetic closers — an error `tool/result` per unanswered `tool-call`, then `step/end?`+`turn/end {interrupted}` (a turn can be huge — never truncated); only a torn tail fragment is dropped. Events contiguous (`events[i].seq === i`); rejects a committed-region gap/parse error or unknown `version`. |
@@ -24,6 +25,8 @@ The persisted unit IS the existing `SessionEvent` (event-sourced model — the l
`PersistenceCoordinator` owns per-id state, write-behind buffers and serialization, the `session/event``session/flush` drain, lazy materialization, crash-tail repair, session adoption, and quiescent disposal. A first-party backend composes one, implements the small `PersistenceBackend` storage hook interface, and delegates its four public service methods. JSONL and SQLite therefore share lifecycle correctness while retaining different storage primitives; see the [coordinator RFC](../../../docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md).
The side-effect-free `locate` query remains backend-owned because it describes storage topology rather than write orchestration.
The `PersistenceBackend<TornMarker>` hooks (the only seam between the coordinator and storage):
| Hook | Role |
@@ -44,9 +47,9 @@ Import `runPersistenceContract` from `tests/contract.ts` (the public-API contrac
Three backends run these suites: an in-memory reference (in `tests/`), `dsh-session-persistence-jsonl` (append-only file log) and `dsh-session-persistence-sqlite` (`node:sqlite`, each `SessionEvent` one row `(session_id, seq, type, time, data, source_event_seqs, surface_op)`). All passing the same contract + coordinator suite is the proof that the seam is genuinely backend-agnostic — lazy materialization, crash-tail-on-load, and contiguous-seq hold identically over file bytes and over a transactional store.
## Metadata types
## Metadata and location types
Re-exported from `dsh-session`: `SessionHeader` (immutable session metadata: `version`, `id`, `createdAt`, `cwd?`, `parentSession?`, `seedLength?`).
Re-exported from `dsh-session`: `SessionHeader` (immutable session metadata: `version`, `id`, `createdAt`, `cwd?`, `parentSession?`, `seedLength?`). `SessionLocation` is `{ readonly kind: string; readonly path: string }`; its path is an absolute backend target, not proof that the artifact exists or contains an unflushed turn.
## Model Experience

View File

@@ -21,6 +21,18 @@ declare module 'cordis' {
}
}
/**
* A backend-resolved, per-session local artifact location. The path is an
* absolute target path and can name an artifact that has not materialized yet.
* Consumers must treat it as a location hint, never as an authorization token.
*/
export interface SessionLocation {
/** Backend-specific artifact kind, for example `jsonl`. */
readonly kind: string
/** Absolute path to this session's backend-owned artifact. */
readonly path: string
}
/**
* Durable append-only session storage. Implementations preserve contiguous,
* losslessly JSON-serializable events; {@link append} resolves only after
@@ -32,6 +44,15 @@ export abstract class SessionPersistence extends Service {
super(ctx, 'sessionPersistence')
}
/**
* Resolve this backend's independent local artifact for a session without
* reading, creating, flushing, or otherwise materializing it. Backends such
* as SQLite that do not own one artifact per session return `undefined`.
* @param meta - the immutable session header whose artifact is requested.
* @returns the backend-specific absolute location, when one exists.
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a

View File

@@ -41,6 +41,10 @@ class MemoryPersistence extends SessionPersistence implements PersistenceBackend
// --- service surface (delegated to the coordinator) ---
locate(_meta: SessionHeader): undefined {
return undefined
}
create(m: SessionHeader): Promise<void> {
return this.coordinator.create(m)
}