@deepseek-ai/dsh-session-projection-cache 0.1.2-alpha.4 → 0.1.2-alpha.5
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 +3 -0
- package/README.zh.md +3 -0
- package/lib/index.js +34 -5
- package/lib/types/spec.d.ts +30 -7
- 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-projection-cache/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 9fd9766d75ab3f9a460b2802ac59810839f21ae4
|
|
6
|
+
README.zh.md: 98725d9ce76ab44821adf3d43e3807932cf6b675
|
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ Three mandatory points always write: session creation persists the seed-derived
|
|
|
64
64
|
|
|
65
65
|
The log leads and the cache follows: a live checkpoint flushes the session's buffered events durably before the cache row lands, so a crash can leave the cache behind the log but never ahead of it. Reads and writes share the storage domain's coherent in-memory state; the per-unit write chain mutates memory only after durability. Each version-stamped record must match the live unit schema and complete lifecycle identity (`createdAt`, `cwd`, `isSeeded`, and `inheritedEventCount`), so a row initialized under one fork cut cannot seed another. The JSON backend stores each record at `<root>/session_projcache/sessions/<id>.json` in an owner-only directory tree.
|
|
66
66
|
|
|
67
|
+
Upgrades never cost the boot or the listing: records stamped with a version in the spec's `compatibleVersions` stay readable (their absent lineage fields decode as the unseeded lineage — exact for unseeded sessions, while a seeded caller fails the identity match and refolds cold), and a stored record that still fails schema validation is moved aside as `<id>.json.bak.<stamp>` under the domain's `invalidRecords: 'backup-and-skip'` policy, logged with its cause, and rebuilt by the next checkpoint.
|
|
68
|
+
|
|
67
69
|
-----
|
|
68
70
|
|
|
69
71
|
<a id="understand-the-implementation"></a>
|
|
@@ -126,6 +128,7 @@ These limits define where the cache needs operational care. They are current pac
|
|
|
126
128
|
- **No eviction or retention surface** — records accumulate per session; pruning stored checkpoints is out-of-band maintenance, same stance as session persistence itself.
|
|
127
129
|
- **Interval throttle is per-session coarse** — the timer arms at the first dirty event after a clean write; a steady sub-threshold trickle writes once per interval, not a sliding window.
|
|
128
130
|
- **No cache-side cold refold** — the cache serves and refreshes its rows but never reads the session log (it does not depend on the persistence layer); a consumer that needs a guaranteed cold snapshot refolds from the log itself.
|
|
131
|
+
- **Every schema or domain-version change must prove its upgrade story** — a change to the stored record schema or the domain version lands in the same PR with an archived fixture of the previously shipped on-disk format under `tests/fixtures/` and test cases in `tests/fixtures.spec.ts` proving the chosen disposition: read-compat recovery (`compatibleVersions`), current-version rewrite, or backup-and-skip salvage. A bump whose old records are simply discarded still proves that the discard neither fails the boot nor poisons the tree.
|
|
129
132
|
|
|
130
133
|
<a id="dev-note"></a>
|
|
131
134
|
### Dev Note
|
package/README.zh.md
CHANGED
|
@@ -64,6 +64,8 @@ kind: "package-reference"
|
|
|
64
64
|
|
|
65
65
|
日志领先,缓存跟随:实时检查点先把会话的缓冲事件持久化,然后才保存缓存记录。因此崩溃可能让缓存落后于日志,但绝不会让缓存领先。读取和写入共享存储域内一致的内存状态;逐单元写入链只在持久化成功后修改内存。每个带版本戳的记录必须匹配实时单元 schema 与完整生命周期身份(`createdAt`、`cwd`、`isSeeded` 和 `inheritedEventCount`),因此在一个 fork 切点下初始化的行不能播种另一个切点。JSON 后端把每条记录存于仅所有者可访问的 `<root>/session_projcache/sessions/<id>.json` 目录树中。
|
|
66
66
|
|
|
67
|
+
升级绝不拖垮启动或列表:版本戳落在 spec `compatibleVersions` 集合内的记录保持可读(缺失的 lineage 字段解码为 unseeded lineage——对非 fork 会话精确无误,seeded 调用方则通不过身份比对、回落冷折叠),而仍然通不过 schema 校验的存量记录会按域的 `invalidRecords: 'backup-and-skip'` 策略移出为 `<id>.json.bak.<时间戳>`、连同原因写入日志,并由下一次检查点重建。
|
|
68
|
+
|
|
67
69
|
-----
|
|
68
70
|
|
|
69
71
|
<a id="understand-the-implementation"></a>
|
|
@@ -126,6 +128,7 @@ kind: "package-reference"
|
|
|
126
128
|
- **无淘汰或保留接口**——记录按会话持续累积;清理已存储检查点属于带外维护,与会话持久化采用相同策略。
|
|
127
129
|
- **间隔节流采用按会话的粗粒度控制**——一次无脏数据的写入完成后,计时器在首个脏事件到达时启动;持续但低于条数阈值的事件流每间隔写入一次,而非滑动窗口。
|
|
128
130
|
- **缓存侧不做冷重折叠**——缓存只服务并刷新自己的记录,从不读取会话日志,因为它不依赖持久化层;需要保证冷快照的消费方自行从日志重新折叠。
|
|
131
|
+
- **每次 schema 或域版本变更都必须论证升级路径**——改动存储记录 schema 或域版本时,同一 PR 必须在 `tests/fixtures/` 下归档此前已发布的磁盘格式样本,并在 `tests/fixtures.spec.ts` 中用测试论证所选的处置方式:读兼容恢复(`compatibleVersions`)、当前版本重写,或 backup-and-skip 抢救。即便选择直接丢弃旧记录的 bump,也要证明丢弃既不炸启动、也不污染缓存树。
|
|
129
132
|
|
|
130
133
|
<a id="dev-note"></a>
|
|
131
134
|
### 开发备注
|
package/lib/index.js
CHANGED
|
@@ -37,12 +37,19 @@ const checkpointRow = z$1.object({
|
|
|
37
37
|
* old record pass every watermark check and seed state folded from an
|
|
38
38
|
* unrelated log. Reads validate this against the live header (listing) or
|
|
39
39
|
* the stored header (cold read) before accepting any record.
|
|
40
|
+
*
|
|
41
|
+
* The lineage fields are optional because records admitted through
|
|
42
|
+
* `compatibleVersions` predate them. The reader (`identityMatches`)
|
|
43
|
+
* interprets their absence as the unseeded lineage — exact for an unseeded
|
|
44
|
+
* session, while a seeded expectation fails the match and the record is
|
|
45
|
+
* discarded to a cold rebuild. Current-version writes always store both
|
|
46
|
+
* fields.
|
|
40
47
|
*/
|
|
41
48
|
const checkpointIdentity = z$1.object({
|
|
42
49
|
createdAt: z$1.number().int().nonnegative(),
|
|
43
50
|
cwd: z$1.string().optional(),
|
|
44
|
-
isSeeded: z$1.boolean(),
|
|
45
|
-
inheritedEventCount: z$1.number().int().nonnegative().transform(SessionLogOffset)
|
|
51
|
+
isSeeded: z$1.boolean().optional(),
|
|
52
|
+
inheritedEventCount: z$1.number().int().nonnegative().transform(SessionLogOffset).optional()
|
|
46
53
|
});
|
|
47
54
|
/**
|
|
48
55
|
* One session's stored record: the log identity it was folded from plus its
|
|
@@ -59,11 +66,27 @@ const checkpointRecord = z$1.object({
|
|
|
59
66
|
* bumps per session: after a bump, a stale session document is discarded on
|
|
60
67
|
* open (cache semantics — a stale or unreadable cache costs a longer tail
|
|
61
68
|
* replay, never a wrong value) while the rest of the domain stays usable,
|
|
62
|
-
* instead of rejecting the whole medium.
|
|
69
|
+
* instead of rejecting the whole medium. The `compatibleVersions` entries
|
|
70
|
+
* are declared because those records differ from the current version only
|
|
71
|
+
* by the absent optional lineage fields, so upgraded homes keep serving
|
|
72
|
+
* their cached listing projections instead of dropping every title until
|
|
73
|
+
* each session is reopened; the per-record version map lives in the
|
|
74
|
+
* read-compat Agent Note
|
|
75
|
+
* (.agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.md).
|
|
76
|
+
* The per-row `ver` guard and the identity match still discard anything the
|
|
77
|
+
* current fold semantics cannot vouch for.
|
|
78
|
+
*
|
|
79
|
+
* `invalidRecords: 'backup-and-skip'`: a stored record that fails the schema
|
|
80
|
+
* anyway is disposable derived data, so it must never cost the boot — the
|
|
81
|
+
* domain layer moves the document aside as `<key>.json.bak.<stamp>`, logs
|
|
82
|
+
* the concrete validation failure, and serves the session as uncached (a
|
|
83
|
+
* cold read rebuilds and rewrites it).
|
|
63
84
|
*/
|
|
64
85
|
const projectionCacheDomainSpec = defineDomain({
|
|
65
86
|
name: "session_projcache",
|
|
66
87
|
version: 5,
|
|
88
|
+
compatibleVersions: [3, 4],
|
|
89
|
+
invalidRecords: "backup-and-skip",
|
|
67
90
|
layout: "per-record",
|
|
68
91
|
tables: { sessions: domainTable(checkpointRecord) }
|
|
69
92
|
});
|
|
@@ -305,9 +328,15 @@ function identityOf(header, inheritedEventCount) {
|
|
|
305
328
|
inheritedEventCount: cut
|
|
306
329
|
};
|
|
307
330
|
}
|
|
308
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* Whether a stored record's bound identity names the caller's lifecycle.
|
|
333
|
+
* Absent lineage fields (records admitted via `compatibleVersions` predate
|
|
334
|
+
* them) read as the unseeded lineage: exact for an unseeded caller, and a
|
|
335
|
+
* seeded caller's expectation then fails the match, discarding the record to
|
|
336
|
+
* a cold rebuild.
|
|
337
|
+
*/
|
|
309
338
|
function identityMatches(stored, expected) {
|
|
310
|
-
return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && stored.isSeeded === expected.isSeeded && stored.inheritedEventCount === expected.inheritedEventCount;
|
|
339
|
+
return stored.createdAt === expected.createdAt && stored.cwd === expected.cwd && (stored.isSeeded ?? false) === expected.isSeeded && (stored.inheritedEventCount ?? 0) === expected.inheritedEventCount;
|
|
311
340
|
}
|
|
312
341
|
//#endregion
|
|
313
342
|
export { Config, SessionProjectionCache, SessionProjectionCache as default, checkpointIdentity, checkpointRecord, checkpointRow, projectionCacheDomainSpec };
|
package/lib/types/spec.d.ts
CHANGED
|
@@ -33,12 +33,19 @@ export declare const checkpointRow: z.ZodObject<{
|
|
|
33
33
|
* old record pass every watermark check and seed state folded from an
|
|
34
34
|
* unrelated log. Reads validate this against the live header (listing) or
|
|
35
35
|
* the stored header (cold read) before accepting any record.
|
|
36
|
+
*
|
|
37
|
+
* The lineage fields are optional because records admitted through
|
|
38
|
+
* `compatibleVersions` predate them. The reader (`identityMatches`)
|
|
39
|
+
* interprets their absence as the unseeded lineage — exact for an unseeded
|
|
40
|
+
* session, while a seeded expectation fails the match and the record is
|
|
41
|
+
* discarded to a cold rebuild. Current-version writes always store both
|
|
42
|
+
* fields.
|
|
36
43
|
*/
|
|
37
44
|
export declare const checkpointIdentity: z.ZodObject<{
|
|
38
45
|
createdAt: z.ZodNumber;
|
|
39
46
|
cwd: z.ZodOptional<z.ZodString>;
|
|
40
|
-
isSeeded: z.ZodBoolean
|
|
41
|
-
inheritedEventCount: z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionLogOffset, number
|
|
47
|
+
isSeeded: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
inheritedEventCount: z.ZodOptional<z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionLogOffset, number>>>;
|
|
42
49
|
}, z.core.$strip>;
|
|
43
50
|
/** The identity fields a record is bound to, inferred from {@link checkpointIdentity}. */
|
|
44
51
|
export type CheckpointIdentity = z.infer<typeof checkpointIdentity>;
|
|
@@ -52,8 +59,8 @@ export declare const checkpointRecord: z.ZodObject<{
|
|
|
52
59
|
identity: z.ZodObject<{
|
|
53
60
|
createdAt: z.ZodNumber;
|
|
54
61
|
cwd: z.ZodOptional<z.ZodString>;
|
|
55
|
-
isSeeded: z.ZodBoolean
|
|
56
|
-
inheritedEventCount: z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionLogOffset, number
|
|
62
|
+
isSeeded: z.ZodOptional<z.ZodBoolean>;
|
|
63
|
+
inheritedEventCount: z.ZodOptional<z.ZodPipe<z.ZodNumber, z.ZodTransform<SessionLogOffset, number>>>;
|
|
57
64
|
}, z.core.$strip>;
|
|
58
65
|
rows: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
59
66
|
ver: z.ZodNumber;
|
|
@@ -68,19 +75,35 @@ export type CheckpointRecord = z.infer<typeof checkpointRecord>;
|
|
|
68
75
|
* bumps per session: after a bump, a stale session document is discarded on
|
|
69
76
|
* open (cache semantics — a stale or unreadable cache costs a longer tail
|
|
70
77
|
* replay, never a wrong value) while the rest of the domain stays usable,
|
|
71
|
-
* instead of rejecting the whole medium.
|
|
78
|
+
* instead of rejecting the whole medium. The `compatibleVersions` entries
|
|
79
|
+
* are declared because those records differ from the current version only
|
|
80
|
+
* by the absent optional lineage fields, so upgraded homes keep serving
|
|
81
|
+
* their cached listing projections instead of dropping every title until
|
|
82
|
+
* each session is reopened; the per-record version map lives in the
|
|
83
|
+
* read-compat Agent Note
|
|
84
|
+
* (.agents/notes/implemented/architecture/2026-09-02-projcache-cross-version-read-compat.md).
|
|
85
|
+
* The per-row `ver` guard and the identity match still discard anything the
|
|
86
|
+
* current fold semantics cannot vouch for.
|
|
87
|
+
*
|
|
88
|
+
* `invalidRecords: 'backup-and-skip'`: a stored record that fails the schema
|
|
89
|
+
* anyway is disposable derived data, so it must never cost the boot — the
|
|
90
|
+
* domain layer moves the document aside as `<key>.json.bak.<stamp>`, logs
|
|
91
|
+
* the concrete validation failure, and serves the session as uncached (a
|
|
92
|
+
* cold read rebuilds and rewrites it).
|
|
72
93
|
*/
|
|
73
94
|
export declare const projectionCacheDomainSpec: {
|
|
74
95
|
name: string;
|
|
75
96
|
version: number;
|
|
97
|
+
compatibleVersions: number[];
|
|
98
|
+
invalidRecords: "backup-and-skip";
|
|
76
99
|
layout: "per-record";
|
|
77
100
|
tables: {
|
|
78
101
|
sessions: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<SessionId, {
|
|
79
102
|
identity: {
|
|
80
103
|
createdAt: number;
|
|
81
|
-
isSeeded: boolean;
|
|
82
|
-
inheritedEventCount: SessionLogOffset;
|
|
83
104
|
cwd?: string | undefined;
|
|
105
|
+
isSeeded?: boolean | undefined;
|
|
106
|
+
inheritedEventCount?: SessionLogOffset | undefined;
|
|
84
107
|
};
|
|
85
108
|
rows: Record<string, {
|
|
86
109
|
ver: number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session-projection-cache",
|
|
3
3
|
"description": "Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read",
|
|
4
|
-
"version": "0.1.2-alpha.
|
|
4
|
+
"version": "0.1.2-alpha.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -28,21 +28,21 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
|
-
"@deepseek-ai/dsh-util-values": "^0.1.2-alpha.
|
|
31
|
+
"@deepseek-ai/dsh-util-values": "^0.1.2-alpha.5",
|
|
32
32
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
36
|
-
"@deepseek-ai/dsh-session": "^0.1.2-alpha.
|
|
37
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.
|
|
38
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.
|
|
36
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
|
|
37
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.5",
|
|
38
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
42
|
-
"@deepseek-ai/dsh-session": "^0.1.2-alpha.
|
|
43
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.
|
|
44
|
-
"@deepseek-ai/dsh-storage": "^0.1.2-alpha.
|
|
45
|
-
"@deepseek-ai/dsh-storage-
|
|
46
|
-
"@deepseek-ai/dsh-storage-
|
|
42
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.5",
|
|
43
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.5",
|
|
44
|
+
"@deepseek-ai/dsh-storage": "^0.1.2-alpha.5",
|
|
45
|
+
"@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.5",
|
|
46
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.5"
|
|
47
47
|
}
|
|
48
48
|
}
|