@deepseek-ai/dsh-session-persistence 0.0.1-rc.1 → 0.0.1-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +5 -3
- package/README.zh.md +4 -2
- package/lib/index.js +87 -5
- package/lib/types/coordinator.d.ts +54 -1
- package/lib/types/index.d.ts +31 -1
- package/package.json +10 -10
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/session/session-persistence/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 6e1898f8a49e54f8fe90ff27cf8571c5959f27e9
|
|
6
|
+
README.zh.md: 901c41b6894d86bdc4ffb345314a3dd506e4a770
|
package/README.md
CHANGED
|
@@ -11,12 +11,14 @@ The persisted unit IS the existing `SessionEvent` (event-sourced model — the l
|
|
|
11
11
|
| Method | Contract |
|
|
12
12
|
|---|---|
|
|
13
13
|
| `locate(meta): SessionLocation \| undefined` | Resolve an absolute per-session artifact target without I/O or materialization. Backends without an independent local artifact return `undefined`. |
|
|
14
|
+
| `supportsRawArtifacts: boolean` | State explicitly whether this backend exposes one verbatim artifact per session. Consumers check this capability before calling `readRaw`; `false` is not session absence. |
|
|
15
|
+
| `readRaw(id, signal?): Promise<SessionRawArtifact \| undefined>` | Read a supported backend's own artifact text verbatim, decoded from its physical encoding but never reconstructed from events. `undefined` means only that the requested artifact is absent; an unsupported backend rejects. |
|
|
14
16
|
| `create(meta): Promise<void>` | Register a new session's metadata. MAY defer the physical write until the first `append` (lazy materialization). |
|
|
15
17
|
| `append(id, events): Promise<void>` | Durably persist a batch. Append-only; first event `seq` == stored next-seq after any repair; rejects non-JSON-serializable data naming the offending type. |
|
|
16
18
|
| `prepare(id, signal?): Promise<SessionPreparation>` | Reserve the exact unpublished Session used by resume. A coordinator reuses an earlier inspection when available, commits pending recovery, and releases an unpublished reservation back to its bounded cache on disposal. |
|
|
17
|
-
| `load(id): Promise<{ meta; events }>` | Return an immutable balanced logical log after converting supported older records from the same format version and committing cold recovery. A live load first flushes its snapshot and rejects while its turn is open; a cold load preserves an interrupted final turn and durably closes it with synthetic `tool/result`/`step/end?`/`turn/end {interrupted}` events. Only a torn tail fragment is dropped; committed corruption
|
|
19
|
+
| `load(id): Promise<{ meta; events }>` | Return an immutable balanced logical log after converting supported older records from the same format version and committing cold recovery. A live load first flushes its snapshot and rejects while its turn is open; a cold load preserves an interrupted final turn and durably closes it with synthetic `tool/result`/`step/end?`/`turn/end {interrupted}` events. Only a torn tail fragment is dropped; committed corruption and malformed records reject as `SessionPersistenceCorruptionError`, while an unsupported format `version` or an event type unknown to this build (without the envelope's `ignorable` marker) refuses as `SessionFormatUnsupportedError`, naming the refusal direction and the raw log path when the backend keeps one artifact per session. |
|
|
18
20
|
| `inspect(id, signal?): Promise<{ meta; events }>` | Return an upgraded, validated, deeply frozen logical view without committing recovery or publishing a Session. A cold view receives in-memory synthetic recovery closers while its physical torn tail remains untouched; an already-live view is its current immutable snapshot and may contain an open turn. Coordinator-backed implementations retain the exact cold unpublished Session in a bounded LRU for later `prepare`, but discard and reload it when the stored revision changes. Same-id inspections share an in-flight read. |
|
|
19
|
-
| `readFrom(id, fromSeq, signal?): Promise<{ meta; events }>` | Return valid stored events with `seq >= fromSeq` without preparation caching, truncation, closers, or coordinator state. A `fromSeq` at or past the stored end returns an empty event list; a negative or non-safe-integer `fromSeq` rejects. Seek-capable backends (SQLite) read only the suffix unless converting a supported older record requires earlier records; sequential backends (JSONL) parse the whole artifact and skip forward. Intended for checkpoint consumers that apply only events after a stored sequence number. |
|
|
21
|
+
| `readFrom(id, fromSeq, signal?): Promise<{ meta; events }>` | Return valid stored events with `seq >= fromSeq` without preparation caching, truncation, closers, or coordinator state. A `fromSeq` at or past the stored end returns an empty event list; a negative or non-safe-integer `fromSeq` rejects. Seek-capable backends (SQLite) read only the suffix unless converting a supported older record requires earlier records; sequential backends (JSONL) parse the whole artifact and skip forward. Unknown-type refusal follows that access pattern: a seek read checks only the returned suffix, while the sequential fallback also refuses on an unknown required event below the window. Intended for checkpoint consumers that apply only events after a stored sequence number. |
|
|
20
22
|
| `list(signal?): Promise<SessionHeader[]>` | Lightweight listing from metadata, no full-log parse. The optional signal cancels backend listing work. A zero-event lazily-materialized session is absent from `list`. |
|
|
21
23
|
| `listSnapshots(signal?): Promise<SessionPersistenceSnapshot[]>` | Lightweight metadata plus an opaque branded per-log revision, without loading event logs. A revision stays equal while that log and its backing store are unchanged, changes after append or mutating load repair, and cannot collide solely because two stores use the same local counter. The optional signal requests cancellation of backend discovery work; first-party backends settle any started listing work before rejecting so an awaited call is quiescent. |
|
|
22
24
|
|
|
@@ -78,6 +80,6 @@ Persistence does not mutate live request prefixes. A resumed loop can reuse prov
|
|
|
78
80
|
|
|
79
81
|
## Known Limitations and Deferred Work
|
|
80
82
|
|
|
81
|
-
- **No deletion or retention
|
|
83
|
+
- **No deletion or retention API** — pruning stored sessions is out-of-band backend maintenance.
|
|
82
84
|
- **`list()` is unpaginated and unfiltered** — it returns every stored session's header; fine for local stores, unindexed at scale.
|
|
83
85
|
- **Repair-time synthetic closers are the only crash story** — a backend must synthesize `tool/result`/`step/end`/`turn/end` closers on load; there is no partial-turn resume that continues an interrupted turn instead of closing it.
|
package/README.zh.md
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
| 方法 | 约定 |
|
|
12
12
|
|---|---|
|
|
13
13
|
| `locate(meta): SessionLocation \| undefined` | 在不执行 I/O 或实体化的情况下解析绝对的每会话产物目标。没有独立本地产物的后端返回 `undefined`。 |
|
|
14
|
+
| `supportsRawArtifacts: boolean` | 明确说明该后端是否为每个会话暴露一份逐字工件。Consumer 在调用 `readRaw` 前检查此能力;`false` 并不表示会话缺失。 |
|
|
15
|
+
| `readRaw(id, signal?): Promise<SessionRawArtifact \| undefined>` | 读取受支持后端自身的逐字工件文本;只解码物理编码,绝不从事件重建。`undefined` 仅表示所请求工件缺失;不支持的后端会拒绝。 |
|
|
14
16
|
| `create(meta): Promise<void>` | 注册新会话元数据。可以将物理写入延迟到第一次 `append`(延迟实体化)。 |
|
|
15
17
|
| `append(id, events): Promise<void>` | 持久保存一个批次。仅追加;任何修复后,第一个事件 `seq` == 已存储 next-seq;非 JSON 可序列化数据会被拒绝,并命名违规类型。 |
|
|
16
18
|
| `prepare(id, signal?): Promise<SessionPreparation>` | 预留恢复所使用的那个未发布 Session。协调器会尽可能复用之前的检查结果、提交待处理恢复,并在 dispose 时将未发布 reservation 释放回有界缓存。 |
|
|
17
|
-
| `load(id): Promise<{ meta; events }>` | 转换同一格式版本中受支持的旧记录后,返回不可变、平衡的逻辑日志,并提交冷恢复。实时 load 先 flush 其快照,并在轮次开放时拒绝;冷 load 保留中断的最终轮次,并用合成 `tool/result`/`step/end?`/`turn/end {interrupted}`
|
|
19
|
+
| `load(id): Promise<{ meta; events }>` | 转换同一格式版本中受支持的旧记录后,返回不可变、平衡的逻辑日志,并提交冷恢复。实时 load 先 flush 其快照,并在轮次开放时拒绝;冷 load 保留中断的最终轮次,并用合成 `tool/result`/`step/end?`/`turn/end {interrupted}` 事件持久关闭它。只丢弃撕裂尾部碎片;已提交损坏和格式错误的记录以 `SessionPersistenceCorruptionError` 拒绝,不支持的格式 `version` 或本构建不认识且信封未带 `ignorable` 标记的事件类型以 `SessionFormatUnsupportedError` 拒绝,消息说明拒绝方向,并在后端为每个会话保留独立文件时给出原始日志路径。 |
|
|
18
20
|
| `inspect(id, signal?): Promise<{ meta; events }>` | 返回已经升级、验证和深度冻结的逻辑视图,但不提交恢复或发布 Session。冷视图会获得仅存在于内存的合成恢复 closer,物理撕裂尾部保持不变;实时状态下的视图则是当前不可变快照,可能包含开放的轮次。基于协调器的实现会在有界 LRU 中保留该冷状态下未发布的 Session 本身,供后续 `prepare` 使用,但已存储修订值变化后会丢弃并重新读取。同 id 检查共享进行中的读取。 |
|
|
19
|
-
| `readFrom(id, fromSeq, signal?): Promise<{ meta; events }>` | 返回 `seq >= fromSeq` 的有效已存储事件,不进入 preparation 缓存、不截断、不合成 closer,也不发布协调器状态。`fromSeq` 达到或超过已存储末尾时返回空事件列表;负数或非安全整数 `fromSeq` 会被拒绝。可寻址后端(SQLite)只读后缀,除非转换受支持的旧记录需要读取更早的记录;顺序后端(JSONL
|
|
21
|
+
| `readFrom(id, fromSeq, signal?): Promise<{ meta; events }>` | 返回 `seq >= fromSeq` 的有效已存储事件,不进入 preparation 缓存、不截断、不合成 closer,也不发布协调器状态。`fromSeq` 达到或超过已存储末尾时返回空事件列表;负数或非安全整数 `fromSeq` 会被拒绝。可寻址后端(SQLite)只读后缀,除非转换受支持的旧记录需要读取更早的记录;顺序后端(JSONL)解析整个产物并向前跳过。未知类型拒绝遵循同一读取方式:寻址读取只检查返回的后缀,顺序回退路径还会拒绝窗口以下的未知必需事件。供 checkpoint 消费方只应用已存序号之后的事件。 |
|
|
20
22
|
| `list(signal?): Promise<SessionHeader[]>` | 从元数据轻量列出,不解析完整日志。可选信号取消后端列表工作。零事件延迟实体化会话不在 `list` 中。 |
|
|
21
23
|
| `listSnapshots(signal?): Promise<SessionPersistenceSnapshot[]>` | 返回轻量元数据和每份日志一个不透明、带品牌类型的修订值,不加载事件日志。日志及其后端存储不变时,修订保持相等;append 或变更性 load 修复后会改变;不会仅因两个存储使用相同本地计数器而冲突。可选信号请求取消后端发现工作;第一方后端会先等待所有已启动的列出工作结束,再予以拒绝,因此调用返回拒绝时,相关工作已完全停稳。 |
|
|
22
24
|
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
-
import { SESSION_FORMAT_VERSION, SessionPreparation, adoptSessionEvent, interruptedTurnClosers, snapshotJsonValue, snapshotSessionEvent } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, SessionPreparation, adoptSessionEvent, interruptedTurnClosers, snapshotJsonValue, snapshotSessionEvent } from "@deepseek-ai/dsh-session";
|
|
3
3
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
4
4
|
//#region lib/types/revision.js
|
|
5
5
|
/** Opaque revision identity for lightweight persistence observations. */
|
|
@@ -447,6 +447,40 @@ var SessionPersistenceCorruptionError = class extends Error {
|
|
|
447
447
|
this.name = "SessionPersistenceCorruptionError";
|
|
448
448
|
}
|
|
449
449
|
};
|
|
450
|
+
/**
|
|
451
|
+
* The stored log is intact but this runtime cannot faithfully interpret it:
|
|
452
|
+
* the header carries an unsupported format version, or an event's type is
|
|
453
|
+
* unknown to this build and the event is not marked ignorable. Distinct from
|
|
454
|
+
* {@link SessionPersistenceCorruptionError} — nothing is damaged; the raw log
|
|
455
|
+
* remains readable at {@link location} when the backend keeps one artifact
|
|
456
|
+
* per session.
|
|
457
|
+
*/
|
|
458
|
+
var SessionFormatUnsupportedError = class extends Error {
|
|
459
|
+
location;
|
|
460
|
+
/**
|
|
461
|
+
* @param message - stable reason the log cannot be interpreted, already
|
|
462
|
+
* including the raw-log path when one exists.
|
|
463
|
+
* @param location - the backend's artifact location, when one exists.
|
|
464
|
+
*/
|
|
465
|
+
constructor(message, location) {
|
|
466
|
+
super(message);
|
|
467
|
+
this.location = location;
|
|
468
|
+
this.name = "SessionFormatUnsupportedError";
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
/**
|
|
472
|
+
* Direction-aware refusal text for a stored session whose format version this
|
|
473
|
+
* build does not read. Shared by the coordinator's load-time check and by
|
|
474
|
+
* backends that must refuse BEFORE decoding version-dependent structure (a
|
|
475
|
+
* future format may not satisfy today's structural checks at all, and the
|
|
476
|
+
* user must see "upgrade the harness", never "corrupt").
|
|
477
|
+
* @param id - the stored session id, for message context.
|
|
478
|
+
* @param version - the stored format version.
|
|
479
|
+
* @returns the stable refusal text, without a raw-log path suffix.
|
|
480
|
+
*/
|
|
481
|
+
function sessionFormatVersionRefusal(id, version) {
|
|
482
|
+
return version > SESSION_FORMAT_VERSION ? `session "${id}" uses log format v${version}, but this harness reads only v${SESSION_FORMAT_VERSION}: the log was written by a newer harness — upgrade the harness to open it` : `session "${id}" uses log format v${version}, older than the supported v${SESSION_FORMAT_VERSION}, and this build ships no upgrade path for it`;
|
|
483
|
+
}
|
|
450
484
|
/** Collect the rejection reasons from a set of promises (none-throwing). */
|
|
451
485
|
async function settledErrors(promises) {
|
|
452
486
|
const settled = await Promise.allSettled([...promises]);
|
|
@@ -920,9 +954,11 @@ var PersistenceCoordinator = class {
|
|
|
920
954
|
events: whole.events.filter((event) => event.seq >= fromSeq)
|
|
921
955
|
};
|
|
922
956
|
}
|
|
957
|
+
const events = snapshotStoredEvents(suffix.events, id);
|
|
958
|
+
this.assertEventsSupported(suffix.meta, events);
|
|
923
959
|
return {
|
|
924
960
|
meta: structuredClone(suffix.meta),
|
|
925
|
-
events
|
|
961
|
+
events
|
|
926
962
|
};
|
|
927
963
|
}
|
|
928
964
|
const whole = await this.readStoredPrefix(id, signal);
|
|
@@ -939,9 +975,11 @@ var PersistenceCoordinator = class {
|
|
|
939
975
|
if (stored === void 0) throw new Error(`session "${id}" not found`);
|
|
940
976
|
this.assertStoredId(id, stored.meta);
|
|
941
977
|
this.assertVersion(stored.meta);
|
|
978
|
+
const events = snapshotStoredEvents(stored.events, id);
|
|
979
|
+
this.assertEventsSupported(stored.meta, events);
|
|
942
980
|
return {
|
|
943
981
|
meta: structuredClone(stored.meta),
|
|
944
|
-
events
|
|
982
|
+
events
|
|
945
983
|
};
|
|
946
984
|
}
|
|
947
985
|
/** Read, repair in memory, validate, and freeze one cold source once. */
|
|
@@ -953,6 +991,7 @@ var PersistenceCoordinator = class {
|
|
|
953
991
|
this.assertStoredId(id, meta);
|
|
954
992
|
this.assertVersion(meta);
|
|
955
993
|
const storedEvents = adoptStoredEvents(events, id);
|
|
994
|
+
this.assertEventsSupported(meta, storedEvents);
|
|
956
995
|
const closers = interruptedTurnClosers(storedEvents).map(adoptSessionEvent);
|
|
957
996
|
const balanced = [...storedEvents, ...closers];
|
|
958
997
|
const session = this.ctx.sessions.prepare(id, {
|
|
@@ -972,6 +1011,7 @@ var PersistenceCoordinator = class {
|
|
|
972
1011
|
closers
|
|
973
1012
|
};
|
|
974
1013
|
} catch (error) {
|
|
1014
|
+
if (error instanceof SessionFormatUnsupportedError) throw error;
|
|
975
1015
|
throw new SessionPersistenceCorruptionError(`stored session "${id}" failed validation: ${String(error)}`, { cause: error });
|
|
976
1016
|
}
|
|
977
1017
|
}
|
|
@@ -1061,7 +1101,29 @@ var PersistenceCoordinator = class {
|
|
|
1061
1101
|
}
|
|
1062
1102
|
}
|
|
1063
1103
|
assertVersion(meta) {
|
|
1064
|
-
if (meta.version
|
|
1104
|
+
if (meta.version === SESSION_FORMAT_VERSION) return;
|
|
1105
|
+
throw this.unsupported(meta, sessionFormatVersionRefusal(meta.id, meta.version));
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Refuse a log containing an event type this build does not know, unless the
|
|
1109
|
+
* writer marked the event ignorable: an unrecognized required event may
|
|
1110
|
+
* change how the rest of the log must be interpreted, so silently skipping
|
|
1111
|
+
* it would reconstruct a wrong session (the envelope contract on
|
|
1112
|
+
* `SessionEvent.ignorable`). Runs on NORMALIZED events — after
|
|
1113
|
+
* `snapshotStoredEvents`/`adoptStoredEvents` has upgraded the legacy shapes
|
|
1114
|
+
* this build still reads and rejected the ones it does not, so those keep
|
|
1115
|
+
* their specific diagnostics.
|
|
1116
|
+
*/
|
|
1117
|
+
assertEventsSupported(meta, events) {
|
|
1118
|
+
for (const event of events) {
|
|
1119
|
+
if (KNOWN_SESSION_EVENT_TYPES.has(event.type) || event.ignorable === true) continue;
|
|
1120
|
+
throw this.unsupported(meta, `session "${meta.id}" contains event type "${event.type}" (seq ${event.seq}) unknown to this harness and not marked ignorable; refusing to interpret the log — it was likely written by a newer harness`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
/** Build a format refusal that points at the raw artifact when the backend has one. */
|
|
1124
|
+
unsupported(meta, reason) {
|
|
1125
|
+
const location = this.backend.locate?.(meta);
|
|
1126
|
+
return new SessionFormatUnsupportedError(location === void 0 ? reason : `${reason} (raw log: ${location.path})`, location);
|
|
1065
1127
|
}
|
|
1066
1128
|
/** Reject backend metadata that is not bound to the requested session id. */
|
|
1067
1129
|
assertStoredId(id, meta) {
|
|
@@ -1226,6 +1288,7 @@ var PersistenceCoordinator = class {
|
|
|
1226
1288
|
if (meta.cwd !== session.header.cwd) throw new Error(`session "${session.header.id}" is already persisted at a different cwd (persisted: ${String(meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`);
|
|
1227
1289
|
this.assertVersion(meta);
|
|
1228
1290
|
const storedEvents = snapshotStoredEvents(events, session.header.id);
|
|
1291
|
+
this.assertEventsSupported(meta, storedEvents);
|
|
1229
1292
|
if (!seedCoversPrefix(seed, storedEvents)) throw new Error(`session "${session.header.id}" already has a persisted log on disk that does not match this live session (id collision)`);
|
|
1230
1293
|
if (tornMarker !== void 0) await this.backend.commitRepair(meta, tornMarker, []);
|
|
1231
1294
|
this.states.set(session.header.id, {
|
|
@@ -1288,6 +1351,25 @@ var SessionPersistence = class extends Service {
|
|
|
1288
1351
|
super(ctx, "sessionPersistence");
|
|
1289
1352
|
}
|
|
1290
1353
|
/**
|
|
1354
|
+
* Read a session's backend-owned artifact text verbatim — the exact durable
|
|
1355
|
+
* bytes the backend wrote (decoded from its physical encoding, e.g. a
|
|
1356
|
+
* decompressed JSONL). The returned `content` is the raw text, not a
|
|
1357
|
+
* reconstruction from parsed events, so it preserves backend-specific
|
|
1358
|
+
* serialization (chunk packing, key order, line breaks). Callers first test
|
|
1359
|
+
* {@link supportsRawArtifacts}; `undefined` then means only that the requested
|
|
1360
|
+
* session has no materialized artifact.
|
|
1361
|
+
* @param _id - the persisted session to read (unused by the default: no
|
|
1362
|
+
* per-session artifact).
|
|
1363
|
+
* @param signal - optional cancellation for backend read work.
|
|
1364
|
+
* @returns the raw artifact plus its parsed header, or `undefined` when the
|
|
1365
|
+
* session is absent.
|
|
1366
|
+
* @throws when this backend does not expose per-session raw artifacts.
|
|
1367
|
+
*/
|
|
1368
|
+
readRaw(_id, signal) {
|
|
1369
|
+
if (signal?.aborted === true) return Promise.reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
|
|
1370
|
+
return Promise.reject(/* @__PURE__ */ new Error("this session persistence backend does not expose raw artifacts"));
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1291
1373
|
* Prepare the exact unpublished Session used by resume. Implementations may
|
|
1292
1374
|
* reuse object graphs retained by an earlier {@link inspect} after confirming
|
|
1293
1375
|
* their durable revision is still current; disposal releases an unpublished
|
|
@@ -1311,4 +1393,4 @@ var SessionPersistence = class extends Service {
|
|
|
1311
1393
|
}
|
|
1312
1394
|
};
|
|
1313
1395
|
//#endregion
|
|
1314
|
-
export { DEFAULT_PREPARED_SESSION_CACHE_SIZE, DEFAULT_WRITE_BATCH_MAX_DELAY_MS, MAX_WRITE_BATCH_DELAY_MS, PersistenceCoordinator, SessionPersistence, SessionPersistence as default, SessionPersistenceCorruptionError, SessionPersistenceRevision };
|
|
1396
|
+
export { DEFAULT_PREPARED_SESSION_CACHE_SIZE, DEFAULT_WRITE_BATCH_MAX_DELAY_MS, MAX_WRITE_BATCH_DELAY_MS, PersistenceCoordinator, SessionFormatUnsupportedError, SessionPersistence, SessionPersistence as default, SessionPersistenceCorruptionError, SessionPersistenceRevision, sessionFormatVersionRefusal };
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import { SessionPreparation } from '@deepseek-ai/dsh-session';
|
|
9
9
|
import type { SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session';
|
|
10
|
-
import type { SessionInspection } from './index.ts';
|
|
10
|
+
import type { SessionInspection, SessionLocation } from './index.ts';
|
|
11
11
|
import type { SessionPersistenceRevision } from './revision.ts';
|
|
12
12
|
/** Default number of detached session preparations retained by a coordinator. */
|
|
13
13
|
export declare const DEFAULT_PREPARED_SESSION_CACHE_SIZE = 5;
|
|
@@ -23,6 +23,34 @@ export declare class SessionPersistenceCorruptionError extends Error {
|
|
|
23
23
|
*/
|
|
24
24
|
constructor(message: string, options: ErrorOptions);
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The stored log is intact but this runtime cannot faithfully interpret it:
|
|
28
|
+
* the header carries an unsupported format version, or an event's type is
|
|
29
|
+
* unknown to this build and the event is not marked ignorable. Distinct from
|
|
30
|
+
* {@link SessionPersistenceCorruptionError} — nothing is damaged; the raw log
|
|
31
|
+
* remains readable at {@link location} when the backend keeps one artifact
|
|
32
|
+
* per session.
|
|
33
|
+
*/
|
|
34
|
+
export declare class SessionFormatUnsupportedError extends Error {
|
|
35
|
+
readonly location?: SessionLocation | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* @param message - stable reason the log cannot be interpreted, already
|
|
38
|
+
* including the raw-log path when one exists.
|
|
39
|
+
* @param location - the backend's artifact location, when one exists.
|
|
40
|
+
*/
|
|
41
|
+
constructor(message: string, location?: SessionLocation | undefined);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Direction-aware refusal text for a stored session whose format version this
|
|
45
|
+
* build does not read. Shared by the coordinator's load-time check and by
|
|
46
|
+
* backends that must refuse BEFORE decoding version-dependent structure (a
|
|
47
|
+
* future format may not satisfy today's structural checks at all, and the
|
|
48
|
+
* user must see "upgrade the harness", never "corrupt").
|
|
49
|
+
* @param id - the stored session id, for message context.
|
|
50
|
+
* @param version - the stored format version.
|
|
51
|
+
* @returns the stable refusal text, without a raw-log path suffix.
|
|
52
|
+
*/
|
|
53
|
+
export declare function sessionFormatVersionRefusal(id: string, version: number): string;
|
|
26
54
|
/** Coordinator policy supplied by a concrete persistence backend. */
|
|
27
55
|
export interface PersistenceCoordinatorOptions {
|
|
28
56
|
/** Maximum completed unpublished preparations retained for reuse. */
|
|
@@ -100,6 +128,11 @@ export interface PersistenceBackend<TornMarker = unknown> {
|
|
|
100
128
|
* contains a supported legacy shape whose normalization needs earlier
|
|
101
129
|
* message-identity facts, in which case the coordinator falls back
|
|
102
130
|
* to the complete stored prefix.
|
|
131
|
+
* Unknown-type refusal follows the same suffix scope: a seek-capable
|
|
132
|
+
* backend's `readFrom` checks only the returned suffix, while the
|
|
133
|
+
* sequential fallback parses the whole artifact and refuses on an unknown
|
|
134
|
+
* required event anywhere in it — over-refusal on the sequential side is
|
|
135
|
+
* accepted rather than widening the seek read.
|
|
103
136
|
* @param id - persisted session id to resolve.
|
|
104
137
|
* @param fromSeq - first event seq to include (non-negative safe integer,
|
|
105
138
|
* validated by the coordinator before this hook runs).
|
|
@@ -126,6 +159,13 @@ export interface PersistenceBackend<TornMarker = unknown> {
|
|
|
126
159
|
* @param signal - optional cancellation for backend listing work.
|
|
127
160
|
*/
|
|
128
161
|
list(signal?: AbortSignal): Promise<SessionHeader[]>;
|
|
162
|
+
/**
|
|
163
|
+
* Optional side-effect-free artifact locator, used to point refusal
|
|
164
|
+
* diagnostics ({@link SessionFormatUnsupportedError}) at the raw log.
|
|
165
|
+
* Backends without one artifact per session omit it or return `undefined`.
|
|
166
|
+
* @param meta - the header whose artifact is requested.
|
|
167
|
+
*/
|
|
168
|
+
locate?(meta: SessionHeader): SessionLocation | undefined;
|
|
129
169
|
/**
|
|
130
170
|
* Optional lifecycle teardown (e.g. close a database handle). Awaited by the
|
|
131
171
|
* coordinator's dispose effect AFTER the quiescence drain. A stateless file
|
|
@@ -248,6 +288,19 @@ export declare class PersistenceCoordinator<TornMarker = unknown> {
|
|
|
248
288
|
/** Build a state for a session discovered in storage but not yet in memory. */
|
|
249
289
|
private adopt;
|
|
250
290
|
private assertVersion;
|
|
291
|
+
/**
|
|
292
|
+
* Refuse a log containing an event type this build does not know, unless the
|
|
293
|
+
* writer marked the event ignorable: an unrecognized required event may
|
|
294
|
+
* change how the rest of the log must be interpreted, so silently skipping
|
|
295
|
+
* it would reconstruct a wrong session (the envelope contract on
|
|
296
|
+
* `SessionEvent.ignorable`). Runs on NORMALIZED events — after
|
|
297
|
+
* `snapshotStoredEvents`/`adoptStoredEvents` has upgraded the legacy shapes
|
|
298
|
+
* this build still reads and rejected the ones it does not, so those keep
|
|
299
|
+
* their specific diagnostics.
|
|
300
|
+
*/
|
|
301
|
+
private assertEventsSupported;
|
|
302
|
+
/** Build a format refusal that points at the raw artifact when the backend has one. */
|
|
303
|
+
private unsupported;
|
|
251
304
|
/** Reject backend metadata that is not bound to the requested session id. */
|
|
252
305
|
private assertStoredId;
|
|
253
306
|
private installWritePath;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -24,7 +24,16 @@ export interface SessionInspection {
|
|
|
24
24
|
/** Validated contiguous logical event log. */
|
|
25
25
|
readonly events: readonly SessionEvent[];
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/** A backend's own raw artifact text for one session, verbatim. */
|
|
28
|
+
export interface SessionRawArtifact {
|
|
29
|
+
/** The session header parsed from the artifact's own first line. */
|
|
30
|
+
readonly meta: SessionHeader;
|
|
31
|
+
/** The artifact's base filename on disk, without any physical encoding suffix. */
|
|
32
|
+
readonly filename: string;
|
|
33
|
+
/** The artifact's full text content, decoded from the backend's physical encoding. */
|
|
34
|
+
readonly content: string;
|
|
35
|
+
}
|
|
36
|
+
export { DEFAULT_PREPARED_SESSION_CACHE_SIZE, DEFAULT_WRITE_BATCH_MAX_DELAY_MS, MAX_WRITE_BATCH_DELAY_MS, PersistenceCoordinator, SessionFormatUnsupportedError, SessionPersistenceCorruptionError, sessionFormatVersionRefusal, } from './coordinator.ts';
|
|
28
37
|
export type { PersistenceBackend, PersistenceCoordinatorOptions, StoredPrefix, StoredSuffix, } from './coordinator.ts';
|
|
29
38
|
declare module '@deepseek-ai/cordis' {
|
|
30
39
|
interface Context {
|
|
@@ -58,6 +67,27 @@ export declare abstract class SessionPersistence extends Service {
|
|
|
58
67
|
* @returns the backend-specific absolute location, when one exists.
|
|
59
68
|
*/
|
|
60
69
|
abstract locate(meta: SessionHeader): SessionLocation | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Whether this backend exposes one verbatim raw artifact per session.
|
|
72
|
+
* A backend that declares `true` must override {@link readRaw}.
|
|
73
|
+
*/
|
|
74
|
+
abstract readonly supportsRawArtifacts: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Read a session's backend-owned artifact text verbatim — the exact durable
|
|
77
|
+
* bytes the backend wrote (decoded from its physical encoding, e.g. a
|
|
78
|
+
* decompressed JSONL). The returned `content` is the raw text, not a
|
|
79
|
+
* reconstruction from parsed events, so it preserves backend-specific
|
|
80
|
+
* serialization (chunk packing, key order, line breaks). Callers first test
|
|
81
|
+
* {@link supportsRawArtifacts}; `undefined` then means only that the requested
|
|
82
|
+
* session has no materialized artifact.
|
|
83
|
+
* @param _id - the persisted session to read (unused by the default: no
|
|
84
|
+
* per-session artifact).
|
|
85
|
+
* @param signal - optional cancellation for backend read work.
|
|
86
|
+
* @returns the raw artifact plus its parsed header, or `undefined` when the
|
|
87
|
+
* session is absent.
|
|
88
|
+
* @throws when this backend does not expose per-session raw artifacts.
|
|
89
|
+
*/
|
|
90
|
+
readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | undefined>;
|
|
61
91
|
/**
|
|
62
92
|
* Register a new session's metadata. A backend MAY defer the physical write
|
|
63
93
|
* until the first {@link append} (lazy materialization), in which case a
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session-persistence",
|
|
3
3
|
"description": "Abstract durable session persistence seam (ctx.sessionPersistence) for the DeepSeek Harness",
|
|
4
|
-
"version": "0.0.1-rc.
|
|
4
|
+
"version": "0.0.1-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "restricted"
|
|
7
7
|
},
|
|
@@ -32,18 +32,18 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "BSD-3-Clause",
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/dsh-brand": "^0.0.1-rc.
|
|
36
|
-
"@deepseek-ai/dsh-
|
|
37
|
-
"@deepseek-ai/dsh-
|
|
38
|
-
"@deepseek-ai/dsh-
|
|
35
|
+
"@deepseek-ai/dsh-brand": "^0.0.1-rc.2",
|
|
36
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.2",
|
|
37
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.2",
|
|
38
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@deepseek-ai/dsh-brand": "^0.0.1-rc.
|
|
43
|
-
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.
|
|
44
|
-
"@deepseek-ai/dsh-scope": "^0.0.1-rc.
|
|
45
|
-
"@deepseek-ai/dsh-session": "^0.0.1-rc.
|
|
46
|
-
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.
|
|
42
|
+
"@deepseek-ai/dsh-brand": "^0.0.1-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-scope": "^0.0.1-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.2",
|
|
47
47
|
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
48
48
|
}
|
|
49
49
|
}
|