@morlay/session-rdb 0.1.4 → 0.1.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.
- package/README.md +4 -7
- package/dist/artifact.d.mts +2 -1
- package/dist/{src-BmogreEz.mjs → deletion-ODZFcbZm.mjs} +134 -90
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/export.d.mts +1 -1
- package/dist/gc.d.mts +1 -1
- package/dist/import.d.mts +2 -2
- package/dist/import.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/log-C0q5svm7.d.mts +594 -0
- package/dist/{sqlite-YCr2-Fov.mjs → sqlite-DjOnMRh2.mjs} +73 -2
- package/dist/storage.d.mts +2 -2
- package/dist/storage.mjs +1 -2
- package/dist/testing.d.mts +2 -2
- package/dist/testing.mjs +95 -14669
- package/dist/usage.d.mts +1 -1
- package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +11 -0
- package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +14 -0
- package/package.json +60 -42
- package/src/branch.ts +1 -1
- package/src/entities/v3/index.ts +4 -0
- package/src/entities/v3/schedule-tasks.ts +16 -0
- package/src/index.ts +2 -1
- package/src/schema.ts +2 -0
- package/src/sqlite.ts +5 -2
- package/src/storage-takeover/repository.ts +61 -0
- package/src/storage-takeover/storage-backend.ts +120 -42
- package/src/storage-takeover/types.ts +9 -0
- package/dist/dist-DHer1cql.mjs +0 -1531
- package/dist/invariant.d.mts +0 -6
- package/dist/invariant.mjs +0 -8
- package/dist/log-DYyxRh7j.d.mts +0 -6078
- package/dist/rolldown-runtime-EAsCxrDo.mjs +0 -14
- package/src/invariant.ts +0 -13
- package/dist/{branch-aiXPmnKC.mjs → branch-DbFW3rJX.mjs} +1 -1
package/dist/usage.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { B as UsageTotals, F as UsageAggregate, G as emptyTotals, H as addActivityTotals, I as UsageBucket,
|
|
1
|
+
import { B as UsageTotals, F as UsageAggregate, G as emptyTotals, H as addActivityTotals, I as UsageBucket, J as resolveUsageSince, K as localDayKey, L as UsageRangeKey, Lt as SESSION_USAGE_PATH, M as COUNTED_EVENT_TYPES, N as SessionUsageReport, P as UsageActivityTotals, R as UsageSessionRow, U as addTokenTotals, V as addActivityCount, W as addTotals, q as registerSessionUsage, z as UsageTokenTotals } from "./log-C0q5svm7.mjs";
|
|
2
2
|
export { COUNTED_EVENT_TYPES, SESSION_USAGE_PATH, SessionUsageReport, UsageActivityTotals, UsageAggregate, UsageBucket, UsageRangeKey, UsageSessionRow, UsageTokenTotals, UsageTotals, addActivityCount, addActivityTotals, addTokenTotals, addTotals, emptyTotals, localDayKey, registerSessionUsage, resolveUsageSince };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- 见 SQLite 侧同名迁移的说明:官方 `schedule` 域的 `tasks` 表落进本库。
|
|
2
|
+
CREATE TABLE "t_schedule_tasks" (
|
|
3
|
+
"f_id" text PRIMARY KEY,
|
|
4
|
+
"f_session_id" text NOT NULL,
|
|
5
|
+
"f_status" text NOT NULL,
|
|
6
|
+
"f_record" text NOT NULL,
|
|
7
|
+
"f_last_delivery" text,
|
|
8
|
+
"f_delivery_history" text
|
|
9
|
+
);
|
|
10
|
+
--> statement-breakpoint
|
|
11
|
+
CREATE INDEX "idx_schedule_tasks_session_id" ON "t_schedule_tasks" USING btree ("f_session_id");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- 官方 `schedule` 域(`dsh-schedule` 的 `tasks` 表)落进本库:web-app 在每份 profile 里挂那一行,
|
|
2
|
+
-- 而 `storage-domain` 的 backend 路由指向 rdb——一个提醒一行,归属会话与状态是列,记录与投递回执存 JSON。
|
|
3
|
+
-- 手写迁移(生成器产不出可用 diff,见 `.agents/debts/20260921-drizzle生成器与实体定义对不上.md`):
|
|
4
|
+
-- 本文件只含这次的真实改动,没有需要说明的略过段。
|
|
5
|
+
CREATE TABLE `t_schedule_tasks` (
|
|
6
|
+
`f_id` text PRIMARY KEY,
|
|
7
|
+
`f_session_id` text NOT NULL,
|
|
8
|
+
`f_status` text NOT NULL,
|
|
9
|
+
`f_record` text NOT NULL,
|
|
10
|
+
`f_last_delivery` text,
|
|
11
|
+
`f_delivery_history` text
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
CREATE INDEX `idx_schedule_tasks_session_id` ON `t_schedule_tasks` (`f_session_id`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-rdb",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "RDB durable session backend for DeepSeek Harness: implements both session-persistence and session-branch (rewind / retry / fork) providers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"branch",
|
|
@@ -25,60 +25,78 @@
|
|
|
25
25
|
],
|
|
26
26
|
"type": "module",
|
|
27
27
|
"exports": {
|
|
28
|
-
".":
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"./
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"./
|
|
37
|
-
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"default": "./dist/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./artifact": {
|
|
33
|
+
"types": "./dist/artifact.d.mts",
|
|
34
|
+
"default": "./dist/artifact.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./deletion": {
|
|
37
|
+
"types": "./dist/deletion.d.mts",
|
|
38
|
+
"default": "./dist/deletion.mjs"
|
|
39
|
+
},
|
|
40
|
+
"./export": {
|
|
41
|
+
"types": "./dist/export.d.mts",
|
|
42
|
+
"default": "./dist/export.mjs"
|
|
43
|
+
},
|
|
44
|
+
"./gc": {
|
|
45
|
+
"types": "./dist/gc.d.mts",
|
|
46
|
+
"default": "./dist/gc.mjs"
|
|
47
|
+
},
|
|
48
|
+
"./import": {
|
|
49
|
+
"types": "./dist/import.d.mts",
|
|
50
|
+
"default": "./dist/import.mjs"
|
|
51
|
+
},
|
|
52
|
+
"./storage": {
|
|
53
|
+
"types": "./dist/storage.d.mts",
|
|
54
|
+
"default": "./dist/storage.mjs"
|
|
55
|
+
},
|
|
56
|
+
"./testing": {
|
|
57
|
+
"types": "./dist/testing.d.mts",
|
|
58
|
+
"default": "./dist/testing.mjs"
|
|
59
|
+
},
|
|
60
|
+
"./usage": {
|
|
61
|
+
"types": "./dist/usage.d.mts",
|
|
62
|
+
"default": "./dist/usage.mjs"
|
|
63
|
+
},
|
|
38
64
|
"./package.json": "./package.json",
|
|
39
65
|
"./locale/*.json": "./locale/*.json"
|
|
40
66
|
},
|
|
41
67
|
"dependencies": {
|
|
42
|
-
"@morlay/session-branch": "0.1.
|
|
68
|
+
"@morlay/session-branch": "0.1.6",
|
|
43
69
|
"drizzle-orm": "^1.0.0-rc.5-ab785fc",
|
|
44
70
|
"fflate": "^0.8.2",
|
|
45
71
|
"pg": "^8.23.0"
|
|
46
72
|
},
|
|
47
73
|
"devDependencies": {
|
|
48
|
-
"@deepseek-ai/cosmokit": "1.8.
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-session-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-storage": "0.2.
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-token-meter": "0.2.0-rc.2",
|
|
56
|
-
"@deepseek-ai/dsh-workspace": "0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/cosmokit": "1.8.6-alpha.2",
|
|
75
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "0.2.1-alpha.5",
|
|
76
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-alpha.5",
|
|
77
|
+
"@deepseek-ai/dsh-session-turn-outline": "0.2.1-alpha.5",
|
|
78
|
+
"@deepseek-ai/dsh-storage": "0.2.1-alpha.5",
|
|
79
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.1-alpha.5",
|
|
80
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-alpha.5",
|
|
57
81
|
"@types/pg": "^8.23.1",
|
|
58
82
|
"drizzle-kit": "1.0.0-rc.5-ab785fc"
|
|
59
83
|
},
|
|
60
84
|
"peerDependencies": {
|
|
61
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
62
|
-
"@deepseek-ai/cosmokit": "^1.8.
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-scope": "^0.2.
|
|
66
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
67
|
-
"@deepseek-ai/dsh-session-format": "^0.2.
|
|
68
|
-
"@deepseek-ai/dsh-session-format-catalog": "^0.2.
|
|
69
|
-
"@deepseek-ai/dsh-session-persistence": "^0.2.
|
|
70
|
-
"@deepseek-ai/dsh-session-
|
|
71
|
-
"@deepseek-ai/dsh-
|
|
72
|
-
"@deepseek-ai/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"@
|
|
76
|
-
"chai": "6.2.2",
|
|
77
|
-
"expect-type": "1.4.0",
|
|
78
|
-
"magic-string": "1.4.1",
|
|
79
|
-
"tinybench": "6.1.4",
|
|
80
|
-
"vitest": "5.0.0",
|
|
81
|
-
"zod": "4.6.5"
|
|
85
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.2",
|
|
86
|
+
"@deepseek-ai/cosmokit": "^1.8.6-alpha.2",
|
|
87
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-alpha.5",
|
|
88
|
+
"@deepseek-ai/dsh-schedule": "0.2.1-alpha.5",
|
|
89
|
+
"@deepseek-ai/dsh-scope": "^0.2.1-alpha.5",
|
|
90
|
+
"@deepseek-ai/dsh-session": "^0.2.1-alpha.5",
|
|
91
|
+
"@deepseek-ai/dsh-session-format": "^0.2.1-alpha.5",
|
|
92
|
+
"@deepseek-ai/dsh-session-format-catalog": "^0.2.1-alpha.5",
|
|
93
|
+
"@deepseek-ai/dsh-session-persistence": "^0.2.1-alpha.5",
|
|
94
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.5",
|
|
95
|
+
"@deepseek-ai/dsh-session-projection-cache": "0.2.1-alpha.5",
|
|
96
|
+
"@deepseek-ai/dsh-session-query": "^0.2.1-alpha.5",
|
|
97
|
+
"@deepseek-ai/dsh-storage": "^0.2.1-alpha.5",
|
|
98
|
+
"@deepseek-ai/dsh-workspace": "0.2.1-alpha.5",
|
|
99
|
+
"@deepseek-ai/schemastery": "^3.18.5-alpha.2"
|
|
82
100
|
},
|
|
83
101
|
"scripts": {
|
|
84
102
|
"build": "pnpm exec tsdown",
|
package/src/branch.ts
CHANGED
package/src/entities/v3/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { storageUnits } from "./storage-units.ts";
|
|
|
7
7
|
import { workspaces } from "./workspaces.ts";
|
|
8
8
|
import { workspaceSessions } from "./workspace-sessions.ts";
|
|
9
9
|
import { workspaceState } from "./workspace-state.ts";
|
|
10
|
+
import { scheduleTasks } from "./schedule-tasks.ts";
|
|
10
11
|
import { sessionProjcacheRows } from "./session-projcache-rows.ts";
|
|
11
12
|
import { eventUsage } from "./event-usage.ts";
|
|
12
13
|
import { sessionUsage } from "./session-usage.ts";
|
|
@@ -21,6 +22,7 @@ export { storageUnits };
|
|
|
21
22
|
export { workspaces };
|
|
22
23
|
export { workspaceSessions };
|
|
23
24
|
export { workspaceState };
|
|
25
|
+
export { scheduleTasks };
|
|
24
26
|
export { sessionProjcacheRows };
|
|
25
27
|
export { eventUsage };
|
|
26
28
|
export { sessionUsage };
|
|
@@ -36,6 +38,7 @@ export const sqliteTableDefs = [
|
|
|
36
38
|
workspaces,
|
|
37
39
|
workspaceSessions,
|
|
38
40
|
workspaceState,
|
|
41
|
+
scheduleTasks,
|
|
39
42
|
sessionProjcacheRows,
|
|
40
43
|
eventUsage,
|
|
41
44
|
sessionUsage,
|
|
@@ -52,6 +55,7 @@ export const postgresTableDefs = [
|
|
|
52
55
|
workspaces,
|
|
53
56
|
workspaceSessions,
|
|
54
57
|
workspaceState,
|
|
58
|
+
scheduleTasks,
|
|
55
59
|
sessionProjcacheRows,
|
|
56
60
|
eventUsage,
|
|
57
61
|
sessionUsage,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TableDef } from "../../adapters/types.ts";
|
|
2
|
+
|
|
3
|
+
// 官方 `schedule` 域的 `tasks` 表:一个提醒一行(key = `ScheduleId`)。归属会话与状态是可查的列,
|
|
4
|
+
// 记录本体与投递回执按上游契约原样存 JSON(它们只有上游 `dsh-schedule` 自己消费,拆列没有第二个读法)。
|
|
5
|
+
export const scheduleTasks: TableDef = {
|
|
6
|
+
name: "t_schedule_tasks",
|
|
7
|
+
columns: {
|
|
8
|
+
f_id: { type: "text", primaryKey: true },
|
|
9
|
+
f_session_id: { type: "text", notNull: true },
|
|
10
|
+
f_status: { type: "text", notNull: true },
|
|
11
|
+
f_record: { type: "text", notNull: true },
|
|
12
|
+
f_last_delivery: { type: "text" },
|
|
13
|
+
f_delivery_history: { type: "text" },
|
|
14
|
+
},
|
|
15
|
+
indexes: { idx_schedule_tasks_session_id: { columns: ["f_session_id"] } },
|
|
16
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1099,7 +1099,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1099
1099
|
}
|
|
1100
1100
|
}
|
|
1101
1101
|
|
|
1102
|
-
//
|
|
1102
|
+
// 重算一个会话的两张汇总表(旧日志重写后;best-effort,同 `recordSessionStats`)——
|
|
1103
|
+
// fork 与 rewind 都不动统计。
|
|
1103
1104
|
async rebuildSessionStats(id: SessionId): Promise<void> {
|
|
1104
1105
|
try {
|
|
1105
1106
|
await this.backend.rebuildSessionStats(id);
|
package/src/schema.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const tWorkspaceSessions = sqliteTables["t_workspace_sessions"]!;
|
|
|
28
28
|
|
|
29
29
|
export const tWorkspaceState = sqliteTables["t_workspace_state"]!;
|
|
30
30
|
|
|
31
|
+
export const tScheduleTasks = sqliteTables["t_schedule_tasks"]!;
|
|
32
|
+
|
|
31
33
|
export const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"]!;
|
|
32
34
|
|
|
33
35
|
export const tEventUsage = sqliteTables["t_event_usage"]!;
|
package/src/sqlite.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
tEventUsage,
|
|
39
39
|
tEvents,
|
|
40
40
|
tPersistenceState,
|
|
41
|
+
tScheduleTasks,
|
|
41
42
|
tSessionEvents,
|
|
42
43
|
tSessionProjcacheRows,
|
|
43
44
|
tSessions,
|
|
@@ -430,6 +431,7 @@ export class SqliteBackend implements Backend {
|
|
|
430
431
|
t_workspaces: tWorkspaces,
|
|
431
432
|
t_workspace_sessions: tWorkspaceSessions,
|
|
432
433
|
t_workspace_state: tWorkspaceState,
|
|
434
|
+
t_schedule_tasks: tScheduleTasks,
|
|
433
435
|
t_session_projcache_row: tSessionProjcacheRows,
|
|
434
436
|
},
|
|
435
437
|
});
|
|
@@ -824,7 +826,8 @@ export class SqliteBackend implements Backend {
|
|
|
824
826
|
}
|
|
825
827
|
|
|
826
828
|
// 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,`t_session_usage` /
|
|
827
|
-
// `t_session_counts`
|
|
829
|
+
// `t_session_counts` 给按会话的行(**含 fork 继承前缀**,见 ADR-fork不继承统计);归属、本地日与「是否被引用」
|
|
830
|
+
// 都是物化列,不再现算、也不
|
|
828
831
|
// 回连事件表。事件级按毫秒过滤、汇总表按本地日过滤,`sinceMs` 对齐本地零点时两者等价。
|
|
829
832
|
async usageReport(sinceMs?: number): Promise<UsageAggregate> {
|
|
830
833
|
const usageSince = sinceMs === undefined ? "" : " AND f_created_at >= ?";
|
|
@@ -999,7 +1002,7 @@ export class SqliteBackend implements Backend {
|
|
|
999
1002
|
}
|
|
1000
1003
|
}
|
|
1001
1004
|
|
|
1002
|
-
//
|
|
1005
|
+
// 按会话重算两张汇总表(旧日志重写后):先删该会话的行,再从事件表重算——rewind 与 fork 不走这里。
|
|
1003
1006
|
async rebuildSessionStats(id: SessionId): Promise<void> {
|
|
1004
1007
|
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
1005
1008
|
this.db.$client
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
CheckpointIdentity,
|
|
13
13
|
ProjectionCheckpoint,
|
|
14
14
|
ProjectionCheckpointRow,
|
|
15
|
+
ScheduleTask,
|
|
15
16
|
StorageRepository,
|
|
16
17
|
StoredProjcacheEntry,
|
|
17
18
|
WorkspaceDomainState,
|
|
@@ -89,6 +90,39 @@ function pendingMutationOf(row: {
|
|
|
89
90
|
return { operation, workspaceId: (row.fPendingWorkspaceId ?? "") as WorkspaceId };
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
interface ScheduleTaskRowRecord {
|
|
94
|
+
fId: string;
|
|
95
|
+
fSessionId: string;
|
|
96
|
+
fStatus: string;
|
|
97
|
+
fRecord: string;
|
|
98
|
+
fLastDelivery: string | null;
|
|
99
|
+
fDeliveryHistory: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 行的读写映射:形状校验在 KV 那一层做(`storage-backend.ts` 的 `scheduleTaskOf`),这里只按列拆装——
|
|
103
|
+
// 归属会话与状态是列,其余三段保持上游契约的 JSON 原样(读回时 `dsh-schedule` 的域 schema 还会再校验)。
|
|
104
|
+
function taskFromRow(row: ScheduleTaskRowRecord): ScheduleTask {
|
|
105
|
+
return {
|
|
106
|
+
sessionId: row.fSessionId as SessionId,
|
|
107
|
+
record: JSON.parse(row.fRecord),
|
|
108
|
+
status: row.fStatus === "inactive" ? "inactive" : "active",
|
|
109
|
+
...(row.fLastDelivery === null ? {} : { lastDelivery: JSON.parse(row.fLastDelivery) }),
|
|
110
|
+
...(row.fDeliveryHistory === null ? {} : { deliveryHistory: JSON.parse(row.fDeliveryHistory) }),
|
|
111
|
+
} as ScheduleTask;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function rowFromTask(id: string, task: ScheduleTask): ScheduleTaskRowRecord {
|
|
115
|
+
return {
|
|
116
|
+
fId: id,
|
|
117
|
+
fSessionId: task.sessionId,
|
|
118
|
+
fStatus: task.status === "inactive" ? "inactive" : "active",
|
|
119
|
+
fRecord: JSON.stringify(task.record),
|
|
120
|
+
fLastDelivery: task.lastDelivery === undefined ? null : JSON.stringify(task.lastDelivery),
|
|
121
|
+
fDeliveryHistory:
|
|
122
|
+
task.deliveryHistory === undefined ? null : JSON.stringify(task.deliveryHistory),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
92
126
|
export function createStorageRepository(host: StorageRepositoryHost): StorageRepository {
|
|
93
127
|
const tables = host.tables as Record<string, any>;
|
|
94
128
|
const tUnits = tables["t_storage_units"]!;
|
|
@@ -96,6 +130,7 @@ export function createStorageRepository(host: StorageRepositoryHost): StorageRep
|
|
|
96
130
|
const tWorkspaces = tables["t_workspaces"]!;
|
|
97
131
|
const tWorkspaceSessions = tables["t_workspace_sessions"]!;
|
|
98
132
|
const tWorkspaceState = tables["t_workspace_state"]!;
|
|
133
|
+
const tScheduleTasks = tables["t_schedule_tasks"]!;
|
|
99
134
|
const tProjcacheRows = tables["t_session_projcache_row"]!;
|
|
100
135
|
|
|
101
136
|
const sessionIdentity = {
|
|
@@ -338,6 +373,32 @@ export function createStorageRepository(host: StorageRepositoryHost): StorageRep
|
|
|
338
373
|
});
|
|
339
374
|
},
|
|
340
375
|
|
|
376
|
+
async listScheduleTasks(): Promise<Array<{ id: string; task: ScheduleTask }>> {
|
|
377
|
+
const db = await dbx();
|
|
378
|
+
const rows = await allRows<ScheduleTaskRowRecord>(db.select().from(tScheduleTasks));
|
|
379
|
+
return rows.map((row) => ({ id: row.fId, task: taskFromRow(row) }));
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
async putScheduleTask(id: string, task: ScheduleTask): Promise<void> {
|
|
383
|
+
await host.writeAtomically(async () => {
|
|
384
|
+
const db = await dbx();
|
|
385
|
+
const values = rowFromTask(id, task);
|
|
386
|
+
await runQuery(
|
|
387
|
+
db
|
|
388
|
+
.insert(tScheduleTasks)
|
|
389
|
+
.values(values)
|
|
390
|
+
.onConflictDoUpdate({ target: tScheduleTasks.fId, set: values }),
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
async deleteScheduleTask(id: string): Promise<void> {
|
|
396
|
+
await host.writeAtomically(async () => {
|
|
397
|
+
const db = await dbx();
|
|
398
|
+
await runQuery(db.delete(tScheduleTasks).where(eq(tScheduleTasks.fId, id)));
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
|
|
341
402
|
async loadProjcache(): Promise<StoredProjcacheEntry[]> {
|
|
342
403
|
const db = await dbx();
|
|
343
404
|
const rows = await allRows<ProjcacheRowRecord>(db.select().from(tProjcacheRows));
|
|
@@ -1,36 +1,35 @@
|
|
|
1
1
|
import { StorageError } from "@deepseek-ai/dsh-storage";
|
|
2
2
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import type { ScheduleTask } from "@deepseek-ai/dsh-schedule";
|
|
3
4
|
import type { WorkspaceId } from "@deepseek-ai/dsh-workspace";
|
|
4
5
|
import type { KvFacet, KvUnit, KvUnitDescriptor, StorageBackend } from "@deepseek-ai/dsh-storage";
|
|
5
6
|
import type { StorageRepository, WorkspaceRecord, WorkspaceDomainState } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
export const RDB_STORAGE_BACKEND = "rdb";
|
|
8
9
|
|
|
10
|
+
// 本后端服务哪些域:一个域一个显式映射(表结构显式维护,未知域名 fail loud——见
|
|
11
|
+
// `.agents/adrs/20260917-接管storages到rdb语义表.md`)。官方那几行谁读哪个域是上游的事,
|
|
12
|
+
// 这里只回答"这个域的数据落在哪张表"。
|
|
9
13
|
const WORKSPACE_UNIT = "workspace";
|
|
10
14
|
const WORKSPACE_TABLE = "workspaces";
|
|
15
|
+
const SCHEDULE_UNIT = "schedule";
|
|
16
|
+
const SCHEDULE_TABLE = "tasks";
|
|
17
|
+
|
|
18
|
+
// 本后端对 unit 的内部约定:关闭时把域名让回后端(上游 `KvUnit` 没有这个入口,它归"谁能再开一次"这件事)。
|
|
19
|
+
interface ReleasableKvUnit extends KvUnit {
|
|
20
|
+
onClose(release: () => void): void;
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
export class RdbStorageBackend implements StorageBackend {
|
|
13
24
|
readonly kv: KvFacet = { open: (descriptor) => this.openUnit(descriptor) };
|
|
14
25
|
|
|
15
|
-
private readonly open = new Map<string,
|
|
26
|
+
private readonly open = new Map<string, ReleasableKvUnit>();
|
|
16
27
|
private closed = false;
|
|
17
28
|
|
|
18
29
|
constructor(private readonly repository: StorageRepository) {}
|
|
19
30
|
|
|
20
31
|
private async openUnit(descriptor: KvUnitDescriptor): Promise<KvUnit> {
|
|
21
32
|
if (this.closed) throw new StorageError("closed", "rdb storage backend is closed");
|
|
22
|
-
if (descriptor.name !== WORKSPACE_UNIT) {
|
|
23
|
-
throw new Error(
|
|
24
|
-
`rdb storage backend serves only the '${WORKSPACE_UNIT}' domain (requested '${descriptor.name}')`,
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
if (!descriptor.tables.includes(WORKSPACE_TABLE) || descriptor.hasGlobal !== true) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`rdb storage backend expects the '${WORKSPACE_UNIT}' domain shape ` +
|
|
30
|
-
`(table '${WORKSPACE_TABLE}' plus a global slot)`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
33
|
if (descriptor.layout !== undefined && descriptor.layout !== "single") {
|
|
35
34
|
throw new Error(
|
|
36
35
|
`rdb storage backend serves only the 'single' layout (domain '${descriptor.name}' ` +
|
|
@@ -41,7 +40,7 @@ export class RdbStorageBackend implements StorageBackend {
|
|
|
41
40
|
throw new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
const unit =
|
|
43
|
+
const unit = this.unitFor(descriptor);
|
|
45
44
|
this.open.set(descriptor.name, unit);
|
|
46
45
|
try {
|
|
47
46
|
const stored = await this.repository.readUnitVersion(descriptor.name);
|
|
@@ -64,6 +63,31 @@ export class RdbStorageBackend implements StorageBackend {
|
|
|
64
63
|
return unit;
|
|
65
64
|
}
|
|
66
65
|
|
|
66
|
+
private unitFor(descriptor: KvUnitDescriptor): ReleasableKvUnit {
|
|
67
|
+
if (descriptor.name === WORKSPACE_UNIT) {
|
|
68
|
+
if (!descriptor.tables.includes(WORKSPACE_TABLE) || descriptor.hasGlobal !== true) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`rdb storage backend expects the '${WORKSPACE_UNIT}' domain shape ` +
|
|
71
|
+
`(table '${WORKSPACE_TABLE}' plus a global slot)`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return new WorkspaceKvUnit(this.repository, descriptor);
|
|
75
|
+
}
|
|
76
|
+
if (descriptor.name === SCHEDULE_UNIT) {
|
|
77
|
+
if (!descriptor.tables.includes(SCHEDULE_TABLE) || descriptor.hasGlobal !== false) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`rdb storage backend expects the '${SCHEDULE_UNIT}' domain shape ` +
|
|
80
|
+
`(table '${SCHEDULE_TABLE}' without a global slot)`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return new ScheduleKvUnit(this.repository, descriptor);
|
|
84
|
+
}
|
|
85
|
+
throw new Error(
|
|
86
|
+
`rdb storage backend serves only the '${WORKSPACE_UNIT}' and '${SCHEDULE_UNIT}' domains ` +
|
|
87
|
+
`(requested '${descriptor.name}')`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
async close(): Promise<void> {
|
|
68
92
|
this.closed = true;
|
|
69
93
|
const units = [...this.open.values()];
|
|
@@ -72,21 +96,65 @@ export class RdbStorageBackend implements StorageBackend {
|
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
|
|
75
|
-
|
|
99
|
+
// 一个 KV unit 的公共形态:关闭后拒写、关闭前把已接受的写排干、关闭回调把名字让回给后端。
|
|
100
|
+
// 域自己的读写在子类里(`unitFor` 的返回类型就地校验它们合得上 `KvUnit`)。
|
|
101
|
+
abstract class TrackedKvUnit {
|
|
76
102
|
private closed = false;
|
|
77
103
|
|
|
78
104
|
private readonly inflight = new Set<Promise<void>>();
|
|
105
|
+
|
|
79
106
|
private onClosed: (() => void) | undefined;
|
|
80
107
|
|
|
81
108
|
constructor(
|
|
82
|
-
|
|
83
|
-
|
|
109
|
+
protected readonly repository: StorageRepository,
|
|
110
|
+
protected readonly descriptor: KvUnitDescriptor,
|
|
84
111
|
) {}
|
|
85
112
|
|
|
113
|
+
// 这个 unit 认的表名(由各自的域决定)。
|
|
114
|
+
protected abstract readonly tables: readonly string[];
|
|
115
|
+
|
|
86
116
|
onClose(release: () => void): void {
|
|
87
117
|
this.onClosed = release;
|
|
88
118
|
}
|
|
89
119
|
|
|
120
|
+
async close(): Promise<void> {
|
|
121
|
+
if (this.closed) return;
|
|
122
|
+
this.closed = true;
|
|
123
|
+
while (this.inflight.size > 0) {
|
|
124
|
+
await Promise.allSettled(this.inflight);
|
|
125
|
+
}
|
|
126
|
+
this.onClosed?.();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected async track(operation: Promise<unknown>): Promise<void> {
|
|
130
|
+
const settled = operation.then(
|
|
131
|
+
() => undefined,
|
|
132
|
+
() => undefined,
|
|
133
|
+
);
|
|
134
|
+
this.inflight.add(settled);
|
|
135
|
+
try {
|
|
136
|
+
await operation;
|
|
137
|
+
} finally {
|
|
138
|
+
this.inflight.delete(settled);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
protected assertOpen(): void {
|
|
143
|
+
if (this.closed) {
|
|
144
|
+
throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
protected assertTable(table: string): void {
|
|
149
|
+
if (!this.tables.includes(table)) {
|
|
150
|
+
throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
class WorkspaceKvUnit extends TrackedKvUnit {
|
|
156
|
+
protected readonly tables = [WORKSPACE_TABLE];
|
|
157
|
+
|
|
90
158
|
async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }> {
|
|
91
159
|
this.assertOpen();
|
|
92
160
|
const records: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
@@ -113,39 +181,35 @@ class WorkspaceKvUnit implements KvUnit {
|
|
|
113
181
|
this.assertOpen();
|
|
114
182
|
await this.track(this.repository.writeWorkspaceState(workspaceStateOf(value)));
|
|
115
183
|
}
|
|
184
|
+
}
|
|
116
185
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
186
|
+
class ScheduleKvUnit extends TrackedKvUnit {
|
|
187
|
+
protected readonly tables = [SCHEDULE_TABLE];
|
|
188
|
+
|
|
189
|
+
async loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }> {
|
|
190
|
+
this.assertOpen();
|
|
191
|
+
const records: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
192
|
+
for (const { id, task } of await this.repository.listScheduleTasks()) {
|
|
193
|
+
records[id] = task;
|
|
122
194
|
}
|
|
123
|
-
|
|
195
|
+
// 这个域没有 global 槽(`scheduleDomain` 不声明):`null` = "从未写过",域层不会读它。
|
|
196
|
+
return { tables: { [SCHEDULE_TABLE]: records }, global: null };
|
|
124
197
|
}
|
|
125
198
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
131
|
-
this.inflight.add(settled);
|
|
132
|
-
try {
|
|
133
|
-
await operation;
|
|
134
|
-
} finally {
|
|
135
|
-
this.inflight.delete(settled);
|
|
136
|
-
}
|
|
199
|
+
async putRecord(table: string, key: string, value: unknown): Promise<void> {
|
|
200
|
+
this.assertOpen();
|
|
201
|
+
this.assertTable(table);
|
|
202
|
+
await this.track(this.repository.putScheduleTask(key, scheduleTaskOf(value)));
|
|
137
203
|
}
|
|
138
204
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
205
|
+
async deleteRecord(table: string, key: string): Promise<void> {
|
|
206
|
+
this.assertOpen();
|
|
207
|
+
this.assertTable(table);
|
|
208
|
+
await this.track(this.repository.deleteScheduleTask(key));
|
|
143
209
|
}
|
|
144
210
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
throw new Error(`unit '${this.descriptor.name}' does not declare table '${table}'`);
|
|
148
|
-
}
|
|
211
|
+
setGlobal(_value: unknown): Promise<void> {
|
|
212
|
+
throw new Error(`unit '${this.descriptor.name}' declares no global slot`);
|
|
149
213
|
}
|
|
150
214
|
}
|
|
151
215
|
|
|
@@ -191,3 +255,17 @@ function workspaceStateOf(value: unknown): WorkspaceDomainState {
|
|
|
191
255
|
...(state.pendingMutation === undefined ? {} : { pendingMutation: state.pendingMutation }),
|
|
192
256
|
};
|
|
193
257
|
}
|
|
258
|
+
|
|
259
|
+
function scheduleTaskOf(value: unknown): ScheduleTask {
|
|
260
|
+
const task = value as Partial<ScheduleTask> | null;
|
|
261
|
+
if (
|
|
262
|
+
typeof task !== "object" ||
|
|
263
|
+
task === null ||
|
|
264
|
+
typeof task.sessionId !== "string" ||
|
|
265
|
+
typeof task.record !== "object" ||
|
|
266
|
+
task.record === null
|
|
267
|
+
) {
|
|
268
|
+
throw new TypeError("schedule task does not match the stored shape");
|
|
269
|
+
}
|
|
270
|
+
return task as ScheduleTask;
|
|
271
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import type { ScheduleTask } from "@deepseek-ai/dsh-schedule";
|
|
2
3
|
import type { CheckpointIdentity } from "@deepseek-ai/dsh-session-projection-cache";
|
|
3
4
|
import type {
|
|
4
5
|
ProjectionCheckpoint,
|
|
@@ -10,6 +11,7 @@ export type {
|
|
|
10
11
|
CheckpointIdentity,
|
|
11
12
|
ProjectionCheckpoint,
|
|
12
13
|
ProjectionCheckpointRow,
|
|
14
|
+
ScheduleTask,
|
|
13
15
|
WorkspaceDomainState,
|
|
14
16
|
WorkspaceRecord,
|
|
15
17
|
};
|
|
@@ -35,6 +37,13 @@ export interface StorageRepository {
|
|
|
35
37
|
|
|
36
38
|
writeWorkspaceState(state: WorkspaceDomainState): Promise<void>;
|
|
37
39
|
|
|
40
|
+
// 官方 `schedule` 域的 `tasks` 表:一个提醒一行(key 是 `ScheduleId`)。
|
|
41
|
+
listScheduleTasks(): Promise<Array<{ id: string; task: ScheduleTask }>>;
|
|
42
|
+
|
|
43
|
+
putScheduleTask(id: string, task: ScheduleTask): Promise<void>;
|
|
44
|
+
|
|
45
|
+
deleteScheduleTask(id: string): Promise<void>;
|
|
46
|
+
|
|
38
47
|
loadProjcache(): Promise<StoredProjcacheEntry[]>;
|
|
39
48
|
|
|
40
49
|
// 不经 await、直接读的路径(供同步的 `cachedSnapshot` 消费);宿主提供不了则缺省。
|