@morlay/session-rdb 0.0.21-alpha.5 → 0.0.21-alpha.6

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.
Files changed (48) hide show
  1. package/README.md +53 -27
  2. package/dist/artifact.d.mts +27 -2
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{backend-a0xDrUFG.d.mts → backend-E0bmTXpz.d.mts} +233 -27
  5. package/dist/{branch-Dlmbkfjj.mjs → branch-BjUR6wfe.mjs} +31 -15
  6. package/dist/deletion.d.mts +1 -1
  7. package/dist/deletion.mjs +1 -1
  8. package/dist/import.d.mts +1 -1
  9. package/dist/import.mjs +2 -3
  10. package/dist/index.d.mts +2 -2
  11. package/dist/index.mjs +4 -4
  12. package/dist/{log-C-rbdFwK.mjs → log-qCNkDjh5.mjs} +93 -22
  13. package/dist/{sqlite-C3LHLzYW.mjs → sqlite-zcXVyMxr.mjs} +85 -10
  14. package/dist/{src-Bn0038Sp.mjs → src-B9X8BKyh.mjs} +210 -38
  15. package/dist/storage.d.mts +2 -1
  16. package/dist/storage.mjs +1 -1
  17. package/dist/testing.d.mts +2 -11
  18. package/dist/testing.mjs +2 -19
  19. package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +1 -0
  20. package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +2 -0
  21. package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +10 -0
  22. package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +7 -0
  23. package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +1041 -0
  24. package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +4 -0
  25. package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +13 -0
  26. package/package.json +21 -21
  27. package/src/backend.ts +46 -1
  28. package/src/branch.ts +28 -11
  29. package/src/drizzle/postgres-v3.ts +1 -0
  30. package/src/drizzle/sqlite-v3.ts +1 -0
  31. package/src/entities/v3/event-usage.ts +5 -5
  32. package/src/entities/v3/sessions.ts +5 -0
  33. package/src/import.ts +3 -3
  34. package/src/index.ts +56 -22
  35. package/src/legacy.ts +14 -3
  36. package/src/log.ts +107 -22
  37. package/src/postgres.ts +65 -5
  38. package/src/rows.ts +125 -0
  39. package/src/schema.ts +3 -2
  40. package/src/session-query.ts +27 -1
  41. package/src/sqlite.ts +71 -3
  42. package/src/storage-takeover/projection-cache.ts +40 -15
  43. package/src/storage-takeover/repository.ts +34 -0
  44. package/src/storage-takeover/storage-backend.ts +3 -1
  45. package/src/testing/coordinator-contract.ts +2 -1
  46. package/src/testing.ts +0 -1
  47. package/src/write-guard.ts +5 -0
  48. package/src/testing/helpers.ts +0 -20
package/README.md CHANGED
@@ -7,31 +7,26 @@ SQLite 或 PostgreSQL 后端。表结构、原样存储与各条流程的设计
7
7
 
8
8
  ## 配置
9
9
 
10
- 配置写在 `${DSH_HOME}/settings.yaml`,settings namespace 为插件短名
11
- `session-rdb`(与 cordis 插件 `name` 一致):
10
+ 配置**就是这一行的 config**(`cordis.patch.yml` 的 `session-rdb` 行,或 profile patch /
11
+ 设置页改的那份 entry config):
12
12
 
13
13
  ```yaml
14
- session-rdb:
15
- type: sqlite
16
- # path 省略时回落 cordis.patch.yml 的默认($DSH_HOME/sessions/sessions.sqlite,
17
- # 由 bundle patch 的 !!js 表达式求值);自定义路径请用绝对路径字符串。
18
- path: /absolute/path/to/sessions.sqlite
19
- journalMode: wal
20
- busyTimeout: 5000
14
+ - id: session-rdb
15
+ name: "@morlay/session-rdb"
16
+ config:
17
+ type: sqlite
18
+ # path 省略时回落 bundle patch 的默认($DSH_HOME/sessions/sessions.sqlite,
19
+ # 由 patch 的 !!js 表达式求值);自定义路径请用绝对路径字符串。
20
+ path: /absolute/path/to/sessions.sqlite
21
+ journalMode: wal
22
+ busyTimeout: 5000
21
23
  ```
22
24
 
23
- > settings.yaml 是纯 YAML(settings-local 用 `yaml` 库解析),**不支持 `!!js`
24
- > JS 表达式**——`!!js dshHomePath(...)` 会被当作字面字符串。`!!js` 只在
25
- > `cordis.patch.yml`(bundle patch 层,loader 求值)有效。
25
+ PostgreSQL 同理:`type: postgres` + `connectionString`。
26
26
 
27
- 字段即 Config 判别联合(见下);未写出的字段回落到 bundle patch / cordis.yml 的
28
- config 默认值。PostgreSQL:
29
-
30
- ```yaml
31
- session-rdb:
32
- type: postgres
33
- connectionString: postgres://user:pass@localhost:5432/sessions
34
- ```
27
+ > 上游 0.1.7 取消了「settings namespace 覆盖插件 config」那套机制(settings 现在只做
28
+ > volatile 字段的表单编辑,持久化落回 profile patch)。老库的 `settings.yaml` 由上游在
29
+ > 启动时**一次性导入**到同 id 的行 config,内容不丢;此后要改就改行 config 或设置页。
35
30
 
36
31
  Config 类型:
37
32
 
@@ -45,6 +40,8 @@ type Config =
45
40
  journalMode?: "wal" | "delete" | "truncate" | "persist";
46
41
  /** 写锁竞争等待毫秒数(默认 5000)。 */
47
42
  busyTimeout?: number;
43
+ /** 投影 checkpoint 的写入节奏(`writeEveryEvents` / `writeIntervalMs`)。 */
44
+ projectionCache?: ProjectionCacheOptions;
48
45
  }
49
46
  | {
50
47
  type: "postgres";
@@ -52,6 +49,8 @@ type Config =
52
49
  connectionString: string;
53
50
  /** 目标 schema(默认 public,必须已存在)。 */
54
51
  schema?: string;
52
+ /** 投影 checkpoint 的写入节奏(`writeEveryEvents` / `writeIntervalMs`)。 */
53
+ projectionCache?: ProjectionCacheOptions;
55
54
  };
56
55
  ```
57
56
 
@@ -79,6 +78,24 @@ web 模式经 `POST /api/session.delete`(body `{ sessionId }`)暴露,状
79
78
  [ADR-会话删除仅限已归档且硬删](.agents/adrs/20260917-会话删除仅限已归档且硬删.md) 与
80
79
  [ADR-删除不再清孤儿与vacuum独立成gc通道](.agents/adrs/20260918-删除不再清孤儿与vacuum独立成gc通道.md)。
81
80
 
81
+ ## 会话列表(两条路)
82
+
83
+ | 用途 | 接缝 | 内容 |
84
+ | --------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
85
+ | 官方 UI(上游 `ui-*`)| `session/list` → `ctx.sessionQuery.listSessions`(本包接管,**默认排除已归档**) | 未归档的可见语料,标题走投影(`projections.title`) |
86
+ | 我们自己的管理面 | `POST /api/session.rows`(本包路由) | **完整语料(含归档)** + 标题 + 最后活动时间 + 工作区归属,**搜索 / 子代理过滤 / 分页都在后端** |
87
+
88
+ 排除归档是本部署的策略:归档是「收起来」的会话,不该混进日常列表与搜索。管理动作(取消归档 / 删除 /
89
+ 导入)需要完整集合,所以另给一条自有路由,两条路不混。决策与取舍见
90
+ [ADR-会话列表两条路](.agents/adrs/20260922-会话列表两条路.md)。
91
+
92
+ `session.rows` 的请求体是 `{ query?, includeSubagents?, page?, pageSize? }`(`pageSize` 默认 20、上限 200),
93
+ 响应是 `{ items, total, page, pageSize }`——**搜索与分页都在后端**:前端分页等于每次拉全量,数据一多就崩。
94
+
95
+ 排序键是会话行的 **`f_last_event_at`**(由写路径维护、老数据由迁移一次性回填、没有事件的会话回落
96
+ `f_created_at`)。早先这里对每行算一次「该会话最后事件时间」的相关子查询:真实库 243 行要 **4.2s**,
97
+ 加 `LIMIT` 也救不了(排序要求全表先算出来);物化之后同一条分页查询是 **0.2ms**。
98
+
82
99
  ## 导出
83
100
 
84
101
  `POST /api/session.export`(body `{ sessionId }`)直接把会话日志打成 zip **响应体**:
@@ -109,6 +126,13 @@ subagent 会话**(`origin = 'subagent'`、父不在表里;有 open handle /
109
126
  孤儿行不计),GC 顺带回收不再有事件行的用量行。口径与取舍见
110
127
  [ADR-用量统计走专用用量日志表](.agents/adrs/20260918-用量统计走专用用量日志表.md)。
111
128
 
129
+ ## 查询接管(`ctx.sessionQuery`)
130
+
131
+ 官方 `session-query-sqlite` 由 `@morlay/better-session` 的 patch 禁用,本包注册 `ctx.sessionQuery`:
132
+ 精确读 / 过滤 / 血缘复用上游 `SessionQueryEngine` 基类,**全文检索恒 disabled**
133
+ (抛 `SESSION_QUERY_SEARCH_DISABLED`,不引入派生索引库)。决策见
134
+ [ADR-接管会话查询服务所有权](.agents/adrs/20260917-接管会话查询服务所有权.md)。
135
+
112
136
  ## storages 接管(workspace 与投影缓存)
113
137
 
114
138
  `$DSH_HOME/storages` 不再产生文件:官方 `storage-json` 与
@@ -118,15 +142,17 @@ subagent 会话**(`origin = 'subagent'`、父不在表里;有 open handle /
118
142
  [表结构](.agents/designs/20260917-表结构.md),决策见
119
143
  [ADR-接管storages到rdb语义表](.agents/adrs/20260917-接管storages到rdb语义表.md)。
120
144
 
121
- 写节流参数(默认 200 / 5000,与官方 base 装配一致)可经 settings 覆盖:
145
+ 写节流参数(默认 200 / 5000,与官方 base 装配一致)同样写在行 config 里:
122
146
 
123
147
  ```yaml
124
- session-rdb:
125
- type: sqlite
126
- path: /absolute/path/to/sessions.sqlite
127
- projectionCache:
128
- writeEveryEvents: 200
129
- writeIntervalMs: 5000
148
+ - id: session-rdb
149
+ name: "@morlay/session-rdb"
150
+ config:
151
+ type: sqlite
152
+ path: /absolute/path/to/sessions.sqlite
153
+ projectionCache:
154
+ writeEveryEvents: 200
155
+ writeIntervalMs: 5000
130
156
  ```
131
157
 
132
158
  旧 `storages` JSON 的导入是**包内 API**(先停掉 dsh,旧文件保留不删)——没有 CLI
@@ -1,4 +1,4 @@
1
- import { a as SessionRow, i as EventRow } from "./backend-a0xDrUFG.mjs";
1
+ import { i as EventRow, s as SessionRow } from "./backend-E0bmTXpz.mjs";
2
2
  import { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport } from "./import.mjs";
3
3
  import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
4
4
  import { SessionEvent, SessionHeader } from "@deepseek-ai/dsh-session";
@@ -15,6 +15,7 @@ declare function sessionInsertRow(storage: SessionStorageMetadata, incarnation:
15
15
  fSeedLength: number | null;
16
16
  fOrigin: string | null;
17
17
  fDelegationDepth: number | null;
18
+ fAgentPreset: string | null;
18
19
  fIncarnation: string;
19
20
  fRevision: number;
20
21
  };
@@ -26,6 +27,7 @@ declare function sessionConflictRow(storage: SessionStorageMetadata): {
26
27
  fSeedLength: number | null;
27
28
  fOrigin: string | null;
28
29
  fDelegationDepth: number | null;
30
+ fAgentPreset: string | null;
29
31
  };
30
32
  declare function rowToEvent(row: EventRow): SessionEvent;
31
33
  declare function recomputeReplaceProvenance(events: SessionEvent[]): void;
@@ -40,6 +42,29 @@ declare function repairAssistantSettlement(events: SessionEvent[]): void;
40
42
  declare function repairRequestHeaders(events: SessionEvent[]): void;
41
43
  declare function syncMeteringRanges(events: SessionEvent[]): void;
42
44
  declare function renameLegacyPtcEvents(events: SessionEvent[]): void;
45
+ /**
46
+ * 把**旧代(v3 及更早)的消息形状**归一到当前格式(v4):迁移链(严格)拒绝那些形状时,读路径仍要
47
+ * 能把它们读出来——只动字段与标记,不改坐标、不改内容。
48
+ *
49
+ * 三件事:
50
+ * - `system/message` 的 source:`{ kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }` → `{ kind: 'system-prompt' }`
51
+ * (v4 起 system 消息只认这个 kind);
52
+ * - `tool/result` 的消息:v3 是 user 角色、结果块包在 `content[0]`;v4 是 `role: 'tool'` + 顶层
53
+ * `toolCallId` + `content` 就是结果块;
54
+ * - 本仓库自己写过、上游已删除的事件类型(见 `LEGACY_OWN_EVENT_TYPES`)标 `ignorable: true`——v4 的
55
+ * 校验对未知类型只接受可忽略信封。
56
+ *
57
+ * 只认这张白名单:**其它未知类型保持 fail loud**(那是「数据来自更新的 harness」的信号,不能吞掉)。
58
+ */
59
+ declare function normalizeToCurrentShape(events: SessionEvent[]): void;
60
+ /**
61
+ * 本仓库写过、后来连同机制一起删除的事件类型:读的时候标 `ignorable: true` 让校验跳过它们。
62
+ * 新增历史类型时登记到这里——**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件)。
63
+ */
64
+ declare const LEGACY_OWN_EVENT_TYPES: ReadonlySet<string>;
65
+ /** 行里是否还带着旧代消息形状(`session-branch/version` 这类自造事件也算)——写路径曾把回退视图的结果
66
+ * 以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
67
+ declare function hasLegacyShape(events: readonly SessionEvent[]): boolean;
43
68
  declare function repairReadView(events: SessionEvent[]): void;
44
69
  declare function orphanInboxSpliceSeqs(events: readonly SessionEvent[]): Set<number>;
45
70
  declare function repairOrphanInboxSplices(events: SessionEvent[]): void;
@@ -74,4 +99,4 @@ declare function usageRowOf(event: {
74
99
  fData: string;
75
100
  }): EventUsageRow | undefined;
76
101
  //#endregion
77
- export { EventUsageRow, SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
102
+ export { EventUsageRow, LEGACY_OWN_EVENT_TYPES, SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, hasLegacyShape, normalizeToCurrentShape, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
package/dist/artifact.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { _ as toJsonlArtifact, a as repairAssistantSettlement, c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as titleOfEventData, h as syncMeteringRanges, i as renameLegacyPtcEvents, l as repairSurfaceOps, m as sessionInsertRow, n as orphanInboxSpliceSeqs, o as repairOrphanInboxSplices, p as sessionConflictRow, r as recomputeReplaceProvenance, s as repairReadView, t as findSurfaceRepairs, u as rowToEvent, v as usageRowOf } from "./log-C-rbdFwK.mjs";
1
+ import { _ as sessionInsertRow, a as orphanInboxSpliceSeqs, b as toJsonlArtifact, c as repairAssistantSettlement, d as repairRequestHeaders, f as repairSurfaceOps, g as sessionConflictRow, h as scanRows, i as normalizeToCurrentShape, l as repairOrphanInboxSplices, m as rowToMeta, n as findSurfaceRepairs, o as recomputeReplaceProvenance, p as rowToEvent, r as hasLegacyShape, s as renameLegacyPtcEvents, t as LEGACY_OWN_EVENT_TYPES, u as repairReadView, v as syncMeteringRanges, x as usageRowOf, y as titleOfEventData } from "./log-qCNkDjh5.mjs";
2
2
  import { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport } from "./import.mjs";
3
- export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
3
+ export { LEGACY_OWN_EVENT_TYPES, SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, hasLegacyShape, normalizeToCurrentShape, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
@@ -2917,20 +2917,24 @@ declare class SessionProjectionCache extends Service {
2917
2917
  private recordFor;
2918
2918
  /**
2919
2919
  * The zero-I/O listing read: whole values viewed straight from the stored
2920
- * rows (version-matching keys only), each cut carried with its watermark so
2921
- * a client value store can seed under its higher-seq-wins rule — as stale
2922
- * as the last durable checkpoint but never wrong, and never from an
2923
- * unrelated log (the caller's header is the identity witness). Fresher
2924
- * paths (the history tail baseline) supersede these values whenever a
2925
- * session is actually opened.
2920
+ * rows (version-matching keys only) of the record bound to the caller's
2921
+ * lifecycle. The header is the only identity witness a listing holds, so
2922
+ * this face matches the lifecycle identity (`formatVersion`, `createdAt`,
2923
+ * `cwd`, `isSeeded`) and not the inherited cut: within one format
2924
+ * generation the cut is fixed at fork time, so it distinguishes no
2925
+ * lifecycle the other fields do not, and a viewed value never seeds a fold.
2926
+ * The view is as stale as the last durable checkpoint but never wrong and
2927
+ * never from an unrelated log. Its `asOfSeq` is the lowest watermark among
2928
+ * the served rows: the stored record's own position, which the header
2929
+ * cannot relate to the log the caller later opens. The Session list
2930
+ * therefore labels the block as cached, and the client lets every value the
2931
+ * connected Session produces supersede it whatever this number says.
2926
2932
  * @param meta - the listed session's header (identity witness; no log read).
2927
- * @param inheritedEventCount - exact inherited prefix length that completes
2928
- * the checkpoint identity.
2929
2933
  * @param keys - optional projection keys required by the caller's audience.
2930
- * @returns the cut (`asOfSeq` = lowest served-row watermark), or
2931
- * `undefined` when no usable row exists for this lifecycle.
2934
+ * @returns the viewed block, or `undefined` when no usable row exists for
2935
+ * this lifecycle at the current Session format.
2932
2936
  */
2933
- cachedSnapshot(meta: SessionHeader, inheritedEventCount: SessionLogOffset, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
2937
+ cachedSnapshot(meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[]): ProjectionSnapshot | undefined;
2934
2938
  /**
2935
2939
  * Read only a predecessor checkpoint's title as a zero-I/O listing hint.
2936
2940
  *
@@ -2940,16 +2944,20 @@ declare class SessionProjectionCache extends Service {
2940
2944
  * fact from this Session. The registry still requires the current title
2941
2945
  * projection's row version and schema. No other predecessor projection is
2942
2946
  * exposed: format normalization can change their current meaning, and the
2943
- * strict {@link cachedSnapshot} / hydration paths continue to reject them.
2947
+ * {@link cachedSnapshot} / hydration paths continue to reject them.
2944
2948
  * @param meta - authoritative listed Session header.
2945
- * @param inheritedEventCount - exact inherited cut completing the lifecycle identity.
2946
- * @returns a title-only checkpoint view with `asOfSeq: -1`, or `undefined`
2947
- * when the record is current, newer, unrelated, missing, or incompatible
2948
- * with the title unit. The sentinel avoids reusing a sequence that a
2949
- * cardinality-changing Session migration may have remapped.
2950
- */
2951
- cachedPredecessorTitle(meta: SessionHeader, inheritedEventCount: SessionLogOffset): ProjectionSnapshot | undefined;
2952
- /** View selected wire rows and bind them to their lowest served watermark. */
2949
+ * @returns a title-only block at the stored title row's watermark, or
2950
+ * `undefined` when the record is current, newer, unrelated, missing, or
2951
+ * incompatible with the title unit.
2952
+ */
2953
+ cachedPredecessorTitle(meta: SessionHeader): ProjectionSnapshot | undefined;
2954
+ /**
2955
+ * View selected wire rows as one block bound to the lowest served
2956
+ * watermark: the seq every served value has folded through at least. The
2957
+ * number is the record's own; whether a consumer may compare it with a
2958
+ * live Session's seqs is decided by the face that serves the block, not
2959
+ * here.
2960
+ */
2953
2961
  private viewRecord;
2954
2962
  /**
2955
2963
  * Hydrate projection cells for an already-prepared Session without another
@@ -3033,6 +3041,34 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
3033
3041
  };
3034
3042
  }
3035
3043
  }
3044
+ /**
3045
+ * Activity families a `workspace/session-activity` listener may report. This
3046
+ * package declares none: each provider merges its own key from a module both
3047
+ * its Host and Client faces import, so a consumer that renders the families
3048
+ * sees exactly the keys its program compiled and falls through to a generic
3049
+ * description for any other. The shipped providers merge `turn` (the Agent
3050
+ * registry), `job` (the job registry seam), `subagent` (the Subagent
3051
+ * runtime), and `schedule` (the Schedule plugin).
3052
+ */
3053
+ interface SessionActivityKindMap {}
3054
+ /** One activity family key. */
3055
+ type SessionActivityKind = keyof SessionActivityKindMap;
3056
+ /** One active item of a family that has per-item identity. */
3057
+ interface SessionActivityItem {
3058
+ /** Family-specific identity: a session id, a job id, or a schedule id. */
3059
+ readonly id: string;
3060
+ /** Display label when the family carries one (a job label, a subagent label). */
3061
+ readonly label?: string;
3062
+ }
3063
+ /**
3064
+ * One reason a session counts as active for archive admission. Families with
3065
+ * per-item identity list their items so a caller can name what must stop.
3066
+ */
3067
+ interface SessionActivity {
3068
+ readonly kind: SessionActivityKind;
3069
+ /** Active items of the family; absent for a family without per-item identity (`turn`). */
3070
+ readonly items?: readonly SessionActivityItem[];
3071
+ }
3036
3072
  /**
3037
3073
  * One workspace: a stable id over an existing directory, a display title, and
3038
3074
  * an ordered candidate account of sessions. Membership requires both an id in
@@ -3134,12 +3170,17 @@ type WorkspaceRecord = output<typeof workspaceRecord>;
3134
3170
  * the registry-global archive set layered over workspace accounting: an
3135
3171
  * archived session keeps its `sessionIds` slot (unarchiving must restore the
3136
3172
  * position), so the set never participates in the one-owner accounting
3137
- * invariant. Defaulted so records written before the field parse unchanged.
3173
+ * invariant. `pinnedSessionIds` is the registry-global pin set in pin order
3174
+ * (most recently pinned first); pinning and archival are mutually
3175
+ * exclusive, so archiving drops the session's pin. Both session sets are
3176
+ * defaulted so records written before the fields parse unchanged.
3138
3177
  */
3139
3178
  declare const workspaceDomainState: ZodObject<{
3140
3179
  initialized: ZodBoolean;
3180
+ defaultWorkspaceId: ZodOptional<ZodPipe<ZodString, ZodTransform<WorkspaceId$1, string>>>;
3141
3181
  workspaceIds: ZodArray<ZodPipe<ZodString, ZodTransform<WorkspaceId$1, string>>>;
3142
3182
  archivedSessionIds: ZodDefault<ZodArray<ZodPipe<ZodString, ZodTransform<SessionId, string>>>>;
3183
+ pinnedSessionIds: ZodDefault<ZodArray<ZodPipe<ZodString, ZodTransform<SessionId, string>>>>;
3143
3184
  pendingMutation: ZodOptional<ZodDiscriminatedUnion<[ZodObject<{
3144
3185
  operation: ZodLiteral<"create">;
3145
3186
  workspaceId: ZodPipe<ZodString, ZodTransform<WorkspaceId$1, string>>;
@@ -3160,10 +3201,52 @@ type WorkspaceId = WorkspaceId$1;
3160
3201
  * @returns the same string, branded at compile time.
3161
3202
  */
3162
3203
  declare function WorkspaceId(id: string): WorkspaceId;
3204
+ /** The session an archive request is about to write into the archive set. */
3205
+ interface SessionActivityRequest {
3206
+ readonly sessionId: SessionId;
3207
+ }
3208
+ /** Caller choices for {@link WorkspaceRegistry.archiveSession}. */
3209
+ interface ArchiveSessionOptions {
3210
+ /**
3211
+ * Ask the composed providers to stop the session's running work instead of
3212
+ * refusing the archive because of it. The archive is written first, then
3213
+ * the stops are requested; running work is never awaited to settlement, and
3214
+ * a provider failure is logged without undoing the archive.
3215
+ */
3216
+ readonly stopActivity?: boolean;
3217
+ }
3163
3218
  declare module '@deepseek-ai/cordis' {
3164
3219
  interface Context {
3165
3220
  workspaceRegistry: WorkspaceRegistry;
3166
3221
  }
3222
+ interface Events {
3223
+ /**
3224
+ * Ask the composed providers what still runs for a session before it is
3225
+ * archived. A listener prepends its own {@link SessionActivity} entries to
3226
+ * the result of `next()`; the registry's innermost callback returns an
3227
+ * empty list, so a composition without providers archives freely. Any
3228
+ * non-empty result refuses the archive without a write.
3229
+ * @param request - the session about to be archived.
3230
+ * @param next - delegate to the remaining providers.
3231
+ * @mode waterfall
3232
+ */
3233
+ 'workspace/session-activity'(request: SessionActivityRequest, next: () => Promise<readonly SessionActivity[]>): Promise<readonly SessionActivity[]>;
3234
+ /**
3235
+ * Stop a session's running work because the caller archived it with
3236
+ * `stopActivity`; the archive set is durable when this dispatches. Each
3237
+ * provider stops its own families — cancelling a turn, its subagent
3238
+ * descendants, owned jobs, or active schedules — through the same cancel
3239
+ * paths the user's own stop actions use, so the session log ends every
3240
+ * open turn regularly and a later unarchive can continue the
3241
+ * conversation. Listeners issue their stop requests without waiting for
3242
+ * running work to settle; a listener may await its own durability
3243
+ * barrier. A rejection is logged by the registry and does not undo the
3244
+ * archive.
3245
+ * @param request - the session being archived.
3246
+ * @mode parallel
3247
+ */
3248
+ 'workspace/session-stop'(request: SessionActivityRequest): Promise<void> | void;
3249
+ }
3167
3250
  }
3168
3251
  /**
3169
3252
  * Durable workspace registry. Startup waits for `sessionPersistence`, builds
@@ -3198,6 +3281,20 @@ declare class WorkspaceRegistry extends Service {
3198
3281
  * @returns the existing or newly durable workspace.
3199
3282
  */
3200
3283
  create(path: string, title?: string): Promise<Workspace>;
3284
+ /**
3285
+ * Initialize the default Workspace only while both the registry and Session
3286
+ * history are empty. Repeated requests reuse its durable identity; deleting
3287
+ * that registration permanently disables automatic creation.
3288
+ * @param resolveDirectory - resolve the absolute directory and initial title;
3289
+ * called only for eligible creation, inside the registry mutation queue.
3290
+ * Missing directories are created recursively before registration.
3291
+ * After resolution, caller cancellation does not roll back creation or registration.
3292
+ * @returns the initialized Workspace, or undefined when automatic creation is ineligible.
3293
+ */
3294
+ initializeDefault(resolveDirectory: () => Promise<{
3295
+ path: string;
3296
+ title: string;
3297
+ }>): Promise<Workspace | undefined>;
3201
3298
  /**
3202
3299
  * Look up a workspace by id.
3203
3300
  * @param id - Workspace id.
@@ -3238,11 +3335,21 @@ declare class WorkspaceRegistry extends Service {
3238
3335
  /**
3239
3336
  * Archive one session durably. The session must exist (live or in session
3240
3337
  * persistence); its workspace accounting — or lack of one — is irrelevant.
3241
- * An already archived id resolves without writing.
3338
+ * Without `stopActivity` the session must also be inactive: the
3339
+ * `workspace/session-activity` waterfall is asked once, and any reported
3340
+ * activity rejects with {@link WorkspaceActiveSessionError} before anything
3341
+ * is written. With `stopActivity` the archive is written without an
3342
+ * activity check, and the `workspace/session-stop` providers are then asked
3343
+ * to stop the session's work: the durable archive set is what a provider's
3344
+ * `agent/pre-step` gate reads, so every wake the stops induce is already
3345
+ * blocked. Archiving drops the session's pin in the same durable write
3346
+ * (pinning and archival are mutually exclusive). An already archived id
3347
+ * resolves without writing, asking, or stopping.
3242
3348
  * @param sessionId - The session to archive.
3243
- * @returns resolution after durability.
3349
+ * @param options - Whether running work is stopped instead of refusing.
3350
+ * @returns resolution after durability and, with `stopActivity`, after every stop request was issued.
3244
3351
  */
3245
- archiveSession(sessionId: SessionId): Promise<void>;
3352
+ archiveSession(sessionId: SessionId, options?: ArchiveSessionOptions): Promise<void>;
3246
3353
  /**
3247
3354
  * Unarchive one session durably by dropping it from the registry-global
3248
3355
  * archive set; the accounting slot was never touched, so the session
@@ -3254,6 +3361,29 @@ declare class WorkspaceRegistry extends Service {
3254
3361
  * @returns resolution after durability.
3255
3362
  */
3256
3363
  unarchiveSession(sessionId: SessionId): Promise<void>;
3364
+ /**
3365
+ * The registry-global pin set: sessions surfaced ahead of every unpinned
3366
+ * session on grouping surfaces. Pinning never touches workspace accounting.
3367
+ * @returns Session ids in pin order (most recently pinned first).
3368
+ */
3369
+ get pinnedSessionIds(): readonly SessionId[];
3370
+ /**
3371
+ * Pin one session durably, prepending it to the registry-global pin set.
3372
+ * The session must exist (live or in session persistence) and must not be
3373
+ * archived. An already pinned id resolves without writing or reordering.
3374
+ * @param sessionId - The session to pin.
3375
+ * @returns resolution after durability.
3376
+ */
3377
+ pinSession(sessionId: SessionId): Promise<void>;
3378
+ /**
3379
+ * Unpin one session durably by dropping it from the registry-global pin
3380
+ * set. Unpinning runs no session-existence check because removing an id
3381
+ * cannot introduce an unknown one, so an entry whose session is gone still
3382
+ * resolves. An id that is not pinned resolves without writing.
3383
+ * @param sessionId - The session to unpin.
3384
+ * @returns resolution after durability.
3385
+ */
3386
+ unpinSession(sessionId: SessionId): Promise<void>;
3257
3387
  /**
3258
3388
  * Whether a session is live, header-indexed, or present in a fresh
3259
3389
  * persistence listing. Only a definite miss returns false — a failing
@@ -3261,6 +3391,8 @@ declare class WorkspaceRegistry extends Service {
3261
3391
  * masquerade as an unknown session.
3262
3392
  */
3263
3393
  private sessionKnown;
3394
+ /** Request every provider's stop; a failing provider is logged, never a reason to keep the session visible. */
3395
+ private stopSessionActivity;
3264
3396
  /**
3265
3397
  * Resolve by canonical directory path without creating or mutating a
3266
3398
  * workspace. A missing path rejects during `realpath`; an existing unowned
@@ -3326,6 +3458,8 @@ interface StorageRepository {
3326
3458
  declare class WriteGuard {
3327
3459
  private readonly headSeqs;
3328
3460
  confirmHead(id: SessionId, head: number): void;
3461
+ /** 本实例有没有这条会话的 head 记录(迁移重写用它决定是"登记基准"还是"严格比较")。 */
3462
+ has(id: SessionId): boolean;
3329
3463
  assertNoConcurrentWriter(id: SessionId, storedHead: number): void;
3330
3464
  }
3331
3465
  //#endregion
@@ -3359,6 +3493,36 @@ declare function eventDimensions(event: SessionEvent): {
3359
3493
  actionId: string;
3360
3494
  };
3361
3495
  //#endregion
3496
+ //#region src/rows.d.ts
3497
+ /**
3498
+ * 管理面的会话列表:**我们自己的**一条路由(完整语料,含归档),与官方 `session/list` 分开。
3499
+ *
3500
+ * 官方那条(`session/list` → `ctx.sessionQuery.listSessions`)按部署策略默认排除已归档——那是给上游
3501
+ * UI 用的。归档集的管理动作要有完整列表,所以这里单独给一条:标题直接从会话列取,最后活动时间用
3502
+ * 该会话最后一个事件的时间。
3503
+ */
3504
+ declare const SESSION_ROWS_PATH = "/api/session.rows";
3505
+ interface SessionRowsItem {
3506
+ sessionId: string;
3507
+ title: string | null;
3508
+ /** 会话 header 的 origin(`subagent` 表示子代理会话)。 */
3509
+ origin: string | null;
3510
+ cwd: string | null;
3511
+ createdAt: number;
3512
+ /** 最后一个事件的时间(无事件时等于 `createdAt`)。 */
3513
+ updatedAt: number;
3514
+ archived: boolean;
3515
+ /** 所属工作区标题;不属于任何工作区时为 null。 */
3516
+ workspace: string | null;
3517
+ }
3518
+ interface SessionRowsValue {
3519
+ items: SessionRowsItem[];
3520
+ /** 过滤后的总数(分页前),前端据此算页数。 */
3521
+ total: number;
3522
+ page: number;
3523
+ pageSize: number;
3524
+ }
3525
+ //#endregion
3362
3526
  //#region src/branch.d.ts
3363
3527
  declare function locateTurnEnd(events: readonly SessionEvent[], atSeq?: number, mode?: BranchAnchorMode): number;
3364
3528
  interface LiveSessionHooks {
@@ -3434,6 +3598,8 @@ interface SessionPersistenceRdbInternals {
3434
3598
  listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
3435
3599
  readStoredRevision(id: SessionId, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceRevision | undefined>;
3436
3600
  registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
3601
+ /** fork 失败时丢弃还没被消费的映射。 */
3602
+ dropReuseEventIds(childId: SessionId): void;
3437
3603
  }
3438
3604
  interface ProjectionCacheOptions {
3439
3605
  writeEveryEvents?: number;
@@ -3527,7 +3693,6 @@ declare class SessionPersistenceRdb extends SessionPersistence {
3527
3693
  config: Config$1;
3528
3694
  static inject: string[];
3529
3695
  static Config: z<Config$1>;
3530
- static readonly settingsNs = "session-rdb";
3531
3696
  readonly name = "session-rdb";
3532
3697
  readonly tracker: RdbBackendTracker;
3533
3698
  private readonly backend;
@@ -3598,6 +3763,8 @@ declare class SessionPersistenceRdb extends SessionPersistence {
3598
3763
  }>;
3599
3764
  close(): Promise<void>;
3600
3765
  registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
3766
+ /** 丢弃一个还没被消费的复用映射(fork 失败时):留着会让同一个 childId 之后的落写复用父会话的事件行。 */
3767
+ dropReuseEventIds(childId: SessionId): void;
3601
3768
  internals(): SessionPersistenceRdbInternals;
3602
3769
  private rowRevision;
3603
3770
  private readonly liveFailures;
@@ -3666,9 +3833,12 @@ interface SessionRow {
3666
3833
  fSeedLength: number | null;
3667
3834
  fOrigin: string | null;
3668
3835
  fDelegationDepth: number | null;
3836
+ fAgentPreset: string | null;
3669
3837
  fIncarnation: string;
3670
3838
  fRevision: number;
3671
3839
  fArchivedAt: number | null;
3840
+ fPinnedSeq: number | null;
3841
+ fLastEventAt: number | null;
3672
3842
  }
3673
3843
  interface EventInsert {
3674
3844
  fEventId: string;
@@ -3708,7 +3878,8 @@ interface BackendTx {
3708
3878
  fSurfaceOp: string | null;
3709
3879
  }>): Promise<void>;
3710
3880
  updateHead(id: SessionId, headEventId: string, headSequence: number): Promise<void>;
3711
- bumpRevision(id: SessionId): Promise<void>;
3881
+ /** 写批次提交时递增 revision;给了 `lastEventAt` 就一并把「最后活动时间」往前推(只增不减)。 */
3882
+ bumpRevision(id: SessionId, lastEventAt?: number): Promise<void>;
3712
3883
  deleteBridgeTail(id: SessionId, fromSequence: number): Promise<void>;
3713
3884
  getPrevBridge(id: SessionId, sequence: number): Promise<{
3714
3885
  fEventId: string;
@@ -3718,6 +3889,39 @@ interface BackendTx {
3718
3889
  /** 批量删除整条会话:桥接行、workspace 归属行、投影行与会话行。 */
3719
3890
  deleteSessions(ids: SessionId[]): Promise<number>;
3720
3891
  }
3892
+ /**
3893
+ * 「会话行」列表项:管理面自己的列表用(完整语料,含归档)+ 标题 + 最后活动时间。
3894
+ *
3895
+ * 与官方列表(`session/list` → `ctx.sessionQuery.listSessions`)的区别:那条按部署策略**默认排除归档**,
3896
+ * 这里给的是完整集合,标题也直接带出来(官方那条的标题走投影缓存)。
3897
+ */
3898
+ interface SessionListRowRecord {
3899
+ sessionId: string;
3900
+ title: string | null;
3901
+ origin: string | null;
3902
+ cwd: string | null;
3903
+ createdAt: number;
3904
+ /** 该会话最后一个事件的时间(没有事件时回落 `createdAt`)。 */
3905
+ updatedAt: number;
3906
+ archived: boolean;
3907
+ subagent: boolean;
3908
+ /** 所属工作区标题;不属于任何工作区时为 null。 */
3909
+ workspace: string | null;
3910
+ }
3911
+ /** 会话行列表的查询条件:搜索、子代理过滤与分页都在后端做(前端分页等于每次拉全量)。 */
3912
+ interface SessionListRowsQuery {
3913
+ /** 匹配标题或所属工作区标题(大小写不敏感);空串即不过滤。 */
3914
+ query?: string;
3915
+ /** 是否连子代理派生会话一起返回(默认不含)。 */
3916
+ includeSubagents?: boolean;
3917
+ limit?: number;
3918
+ offset?: number;
3919
+ }
3920
+ interface SessionListRowsPage {
3921
+ items: SessionListRowRecord[];
3922
+ /** 过滤后的总数(分页前的),前端据此算页数。 */
3923
+ total: number;
3924
+ }
3721
3925
  /** 活动计数的一个桶:本地日 + 事件类型 + 计数。 */
3722
3926
  interface EventCountBucket {
3723
3927
  day: string;
@@ -3745,6 +3949,8 @@ interface Backend {
3745
3949
  * @param sinceMs - 只算该时刻(含)之后的事件行;省略即全量。
3746
3950
  */
3747
3951
  usageReport(sinceMs?: number): Promise<UsageAggregate>;
3952
+ /** 管理面的会话行列表:完整语料(含归档)+ 标题 + 最后活动时间,按活动倒序分页。 */
3953
+ listSessionRows(query?: SessionListRowsQuery): Promise<SessionListRowsPage>;
3748
3954
  /**
3749
3955
  * 活动计数**旁路累加**(派生表 `t_event_counts`,不参与写事务、失败可丢——表可销毁重建):
3750
3956
  * 每个 (会话, 本地日, 事件类型) 记一行计数。
@@ -3764,4 +3970,4 @@ interface Backend {
3764
3970
  close(): Promise<void>;
3765
3971
  }
3766
3972
  //#endregion
3767
- export { tSessionEvents as A, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as C, tEvents as D, tEventUsage as E, tWorkspaceState as F, tWorkspaces as I, WriteGuard as L, tSessions as M, tStorageUnits as N, tPersistenceState as O, tWorkspaceSessions as P, StorageRepository as R, SCHEMA_VERSION as S, eventKind as T, DEFAULT_BUSY_TIMEOUT_MS as _, SessionRow as a, EventRole as b, ProjectionCacheOptions as c, SessionPersistenceRdb as d, SessionPersistenceRdbInternals as f, truncateLiveSession as g, locateTurnEnd as h, EventRow as i, tSessionProjcacheRows as j, tSchemaMeta as k, SessionDeletionError as l, SessionBranchRdbProvider as m, BackendTx as n, UsageAggregate as o, SessionBranchRdb as p, EventCountBucket as r, Config$1 as s, Backend as t, SessionDeletionErrorCode as u, EVENT_ENCODING as v, eventDimensions as w, JournalMode as x, EventKind as y };
3973
+ export { eventKind as A, tWorkspaceState as B, EVENT_ENCODING as C, SCHEMA_VERSION as D, JournalMode as E, tSessionEvents as F, WriteGuard as H, tSessionProjcacheRows as I, tSessions as L, tEvents as M, tPersistenceState as N, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as O, tSchemaMeta as P, tStorageUnits as R, DEFAULT_BUSY_TIMEOUT_MS as S, EventRole as T, StorageRepository as U, tWorkspaces as V, locateTurnEnd as _, SessionListRowsPage as a, SessionRowsItem as b, UsageAggregate as c, SessionDeletionError as d, SessionDeletionErrorCode as f, SessionBranchRdbProvider as g, SessionBranchRdb as h, EventRow as i, tEventUsage as j, eventDimensions as k, Config$1 as l, SessionPersistenceRdbInternals as m, BackendTx as n, SessionListRowsQuery as o, SessionPersistenceRdb as p, EventCountBucket as r, SessionRow as s, Backend as t, ProjectionCacheOptions as u, truncateLiveSession as v, EventKind as w, SessionRowsValue as x, SESSION_ROWS_PATH as y, tWorkspaceSessions as z };