dsh-session-cloud 0.1.2 → 0.1.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## 0.1.4(未发布)
4
+
5
+ 适配 dsh 0.1.3-alpha.1(源码版)的两处兼容修复:
6
+
7
+ ### 修复
8
+
9
+ - **恢复会话弹窗所有会话被误标"版本不兼容"、checkbox 无法勾选**
10
+ 根因:`LOCAL_FORMAT_VERSION` 写死为 `0`,而 dsh 的 `SESSION_FORMAT_VERSION`
11
+ 实际为 `2`(rc.1 与 0.1.3-alpha.1 均如此),导致云端目录里每一行都满足
12
+ `formatVersion > 0` 而被禁用。
13
+ 修复:常量更正为 `2`;同时恢复目录的兼容基准改为动态取本机会话 header
14
+ 的 `version`(即本机运行时的 `SESSION_FORMAT_VERSION`),上游未来升级
15
+ 格式版本后随本地新会话自适应,仅本地无任何会话时才落到常量兜底。
16
+
17
+ - **"立即全部同步"成功 0:v1 宿主日志文件名漏了格式代(`vN`)段**
18
+ 根因:dsh 0.1.3-alpha.1 的 jsonl 后端日志文件按格式代命名
19
+ (`session.v2.jsonl.zstd`),而 persistence-port 仍按 rc.1 旧形态推导
20
+ `session.jsonl.zstd`,`stat` 必然 ENOENT;该 ENOENT 又被"未物化会话"
21
+ 的静默跳过逻辑吞掉——会话从未上传,引擎却显示全部已同步。
22
+ 修复:
23
+ - `locateDerived` 按会话 `header.version` 生成当前代文件名
24
+ (`v0 → session.jsonl`、`vN>0 → session.vN.jsonl`,与 dsh
25
+ `session-format/filename.ts` 一致),并按"当前代 zstd → v0 旧形态
26
+ zstd → 当前代明文 → v0 明文"优先级探测;
27
+ - 引擎 ENOENT 分支不再无条件静默:会话目录中存在 canonical 日志
28
+ (`session[.vN].jsonl[.zstd]`)而推导路径缺失时按布局失配报错
29
+ (计入"失败"并展示错误),只有目录中无日志文件(真未物化:v1 后端
30
+ lock 先建目录、首个 append 才物化日志)才静默记录 revision 跳过。
31
+
32
+ ## 0.1.3
33
+
34
+ 适配 dsh 0.1.3-alpha.1 的 `SessionPersistence` API 收敛:
35
+
36
+ - 新增 `persistence-port` 双版本兼容层:运行时探测宿主形态,v0(rc.1)
37
+ 透传 `listSnapshots/list/locate`,v1 以 `list()` 作快照枚举并按 jsonl
38
+ 磁盘布局推导落位路径(复刻 `projectKey`/`encodeSegment` 编码);
39
+ - 引擎对 v1 "已创建未物化" 会话(首个 append 前无日志文件)的 ENOENT
40
+ 静默跳过,不计失败、不进退避。
41
+
42
+ ## 0.1.2
43
+
44
+ - npm 发布链路与 `dsh plugin add` 安装修复。
45
+
46
+ ## 0.1.1 / 0.1.0
47
+
48
+ - 初始版本:端到端加密上传、增量同步、云端恢复对话框、路径映射、
49
+ 启动检查等(M1–M4)。
@@ -6,6 +6,7 @@
6
6
  * → meta 覆写 → 推进本地 state。与 cordis 解耦,纯类便于单测。
7
7
  */
8
8
  import fsp from 'node:fs/promises';
9
+ import path from 'node:path';
9
10
  import { LogEncryptor, encryptMeta, verifyTailSegment } from '../crypto/envelope.js';
10
11
  import { detectEncoding, emptyFold, foldFrame, scanZstdFrames } from './frames.js';
11
12
  import { ProtocolError } from './http-client.js';
@@ -248,7 +249,31 @@ export class SyncEngine {
248
249
  await store.putSession(id, state);
249
250
  return;
250
251
  }
251
- const stat = await fsp.stat(location.path);
252
+ let stat;
253
+ try {
254
+ stat = await fsp.stat(location.path);
255
+ }
256
+ catch (error) {
257
+ if (error.code !== 'ENOENT')
258
+ throw error;
259
+ // 区分两种 ENOENT:目录里有 canonical 日志(session[.vN].jsonl[.zstd])而
260
+ // locate 指的路径不存在 = 布局推导失配(上游改名/改布局),静默跳过会把
261
+ // 失配伪装成「已同步」(曾表现为「成功 0」无任何报错),必须抛错暴露;
262
+ // 目录不存在或只有 session.lock(v1 后端 lock 先建目录、首个 append 才
263
+ // 物化日志)= 真未物化,记 revision 静默跳过,物化后 revision 变化自然重调度。
264
+ const dirEntries = await fsp.readdir(path.dirname(location.path)).catch((dirError) => {
265
+ if (dirError.code === 'ENOENT')
266
+ return [];
267
+ throw dirError;
268
+ });
269
+ const hasCanonicalLog = dirEntries.some((name) => /^session(\.v\d+)?\.jsonl(\.zstd)?$/.test(name));
270
+ if (hasCanonicalLog) {
271
+ throw new Error(`session log artifact not found at derived path (layout mismatch?): ${location.path}`);
272
+ }
273
+ state.localRevision = revision;
274
+ await store.putSession(id, state);
275
+ return;
276
+ }
252
277
  if (stat.size < state.uploadedBytes) {
253
278
  // 文件变短(防御:正常不可达)——回退全量重传
254
279
  Object.assign(state, freshSessionState());
@@ -8,8 +8,9 @@
8
8
  * 消费的 SessionPersistenceLike 端口:
9
9
  * - v0(rc.1 及以前):listSnapshots/list/locate 原样透传;
10
10
  * - v1(0.1.3-alpha.1 起):list() 即快照枚举;落位路径无法再问宿主,
11
- * 按 jsonl 后端的磁盘布局推导(<root>/<projectKey>/<id>/session.jsonl[.zstd],
12
- * 复刻 dsh format.ts 的 projectKey/encodeSegment 编码)。
11
+ * 按 jsonl 后端的磁盘布局推导(<root>/<projectKey>/<id>/session[.vN].jsonl[.zstd],
12
+ * vN 为会话格式代(v0 无标签),复刻 dsh format.ts 的 projectKey/encodeSegment
13
+ * 与 session-format filename.ts 的代命名)。
13
14
  */
14
15
  import { existsSync } from 'node:fs';
15
16
  import { join } from 'node:path';
@@ -75,13 +76,31 @@ export function sessionDirOf(root, cwd, id) {
75
76
  const project = cwd === undefined ? join(root, '_no-cwd') : join(root, projectKeyOf(cwd));
76
77
  return join(project, encodeSegmentOf(id));
77
78
  }
78
- /** 推导会话 artifact 位置:zstd 存在优先;仅明文存在则指明文(引擎会标记 skipped-plain)。 */
79
+ /**
80
+ * 推导会话 artifact 位置:候选按优先级探测——当前代(header.version)zstd →
81
+ * v0 旧形态 zstd(rc.1 数据 / 代升级残留)→ 当前代明文 → v0 明文;都不存在则
82
+ * 推导当前代 zstd 路径(调用方按 ENOENT 语义处理未物化会话)。
83
+ */
79
84
  function locateDerived(root, meta) {
80
85
  const dir = sessionDirOf(root, meta.cwd, meta.id);
81
- const zstd = join(dir, 'session.jsonl.zstd');
82
- if (existsSync(zstd) || !existsSync(join(dir, 'session.jsonl')))
83
- return { kind: 'jsonl', path: zstd };
84
- return { kind: 'jsonl', path: join(dir, 'session.jsonl') };
86
+ const candidates = [
87
+ join(dir, `${logBasenameOf(meta.version)}.zstd`),
88
+ join(dir, 'session.jsonl.zstd'),
89
+ join(dir, logBasenameOf(meta.version)),
90
+ join(dir, 'session.jsonl'),
91
+ ];
92
+ for (const candidate of candidates) {
93
+ if (existsSync(candidate))
94
+ return { kind: 'jsonl', path: candidate };
95
+ }
96
+ return { kind: 'jsonl', path: candidates[0] };
97
+ }
98
+ /**
99
+ * 当前代日志 basename(复刻 dsh session-format filename.ts:14):
100
+ * version 0 保持 `session.jsonl`,之后每代带小写 `.vN` 段。
101
+ */
102
+ function logBasenameOf(version) {
103
+ return version === 0 ? 'session.jsonl' : `session.v${version}.jsonl`;
85
104
  }
86
105
  /**
87
106
  * 把宿主 sessionPersistence 归一为引擎/恢复消费的端口。
@@ -1,8 +1,12 @@
1
1
  import type { SessionHeaderLike, SessionPersistenceLike } from '../dsh-types.js';
2
2
  import type { SyncClient } from './http-client.js';
3
3
  import type { StateStore } from './state.js';
4
- /** 本机支持的会话格式版本(dsh SESSION_FORMAT_VERSION,pre-release 恒为 0)。 */
5
- export declare const LOCAL_FORMAT_VERSION = 0;
4
+ /**
5
+ * 本机支持的会话格式版本兜底值(dsh core/session SESSION_FORMAT_VERSION,
6
+ * rc.1 与 0.1.3-alpha.1 均为 2)。运行时基准优先取本机会话 header 的 version
7
+ * (见 fetchCatalog),本地无任何会话时才落到此常量。
8
+ */
9
+ export declare const LOCAL_FORMAT_VERSION = 2;
6
10
  /** 恢复目录的一行(Host 计算好解析结果,Client 只负责呈现与改选)。 */
7
11
  export interface CatalogEntry {
8
12
  sessionId: string;
@@ -16,8 +16,12 @@ import path from 'node:path';
16
16
  import { decryptLog, decryptMeta } from '../crypto/envelope.js';
17
17
  import { logKey } from './engine.js';
18
18
  import { scanZstdFrames, zstdCompressAsync, zstdDecompressAsync, ZSTD_CHECKSUM_OPTIONS } from './frames.js';
19
- /** 本机支持的会话格式版本(dsh SESSION_FORMAT_VERSION,pre-release 恒为 0)。 */
20
- export const LOCAL_FORMAT_VERSION = 0;
19
+ /**
20
+ * 本机支持的会话格式版本兜底值(dsh core/session 的 SESSION_FORMAT_VERSION,
21
+ * rc.1 与 0.1.3-alpha.1 均为 2)。运行时基准优先取本机会话 header 的 version
22
+ * (见 fetchCatalog),本地无任何会话时才落到此常量。
23
+ */
24
+ export const LOCAL_FORMAT_VERSION = 2;
21
25
  // ---- JSON 载荷的编解码(两端各自拼写,不跨半 import;Host 侧编码 + 防御性解码) ----
22
26
  export function encodeCatalog(catalog) {
23
27
  return JSON.stringify(catalog);
@@ -144,7 +148,11 @@ const META_SUFFIX = '.meta.enc';
144
148
  export async function fetchCatalog(deps) {
145
149
  const objects = await deps.client.list('sessions/');
146
150
  const metaKeys = objects.filter((object) => object.key.endsWith(META_SUFFIX));
147
- const localIds = new Set((await deps.persistence.list()).map((header) => header.id));
151
+ const localHeaders = await deps.persistence.list();
152
+ const localIds = new Set(localHeaders.map((header) => header.id));
153
+ // 兼容基准优先取本机会话 header 的 version(即本机运行时的 SESSION_FORMAT_VERSION,
154
+ // 上游 bump 后随本地新会话自适应);本地一个会话都没有才落到常量兜底
155
+ const localFormatVersion = localHeaders.reduce((max, header) => Math.max(max, header.version), LOCAL_FORMAT_VERSION);
148
156
  const mappings = deps.state.getGlobal().pathMappings;
149
157
  const entries = [];
150
158
  for (const object of metaKeys) {
@@ -173,7 +181,7 @@ export async function fetchCatalog(deps) {
173
181
  cwd: meta.cwd,
174
182
  formatVersion: meta.formatVersion,
175
183
  existsLocal,
176
- versionIncompatible: meta.formatVersion > LOCAL_FORMAT_VERSION,
184
+ versionIncompatible: meta.formatVersion > localFormatVersion,
177
185
  resolvedCwd: resolution.cwd,
178
186
  resolution: resolution.kind,
179
187
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-cloud",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "E2E-encrypted cloud mirror plugin for deepseek-harness sessions",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "lib",
11
- "cordis.patch.yml"
11
+ "cordis.patch.yml",
12
+ "CHANGELOG.md"
12
13
  ],
13
14
  "exports": {
14
15
  ".": {