@sema-agent/server 7.103.1 → 7.104.0
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/MIGRATION.md +362 -3
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +279 -18
- package/deploy/sema-up/README.md +57 -7
- package/deploy/sema-up/cluster-up.sh +18 -4
- package/deploy/sema-up/kube-up.sh +16 -0
- package/deploy/sema-up/lib/dataroot.sh +142 -0
- package/deploy/sema-up/lib/flagshape.sh +69 -0
- package/deploy/sema-up/lib/lanes.py +14 -4
- package/deploy/sema-up/lib/shell-access.sh +41 -0
- package/deploy/sema-up/preflight.sh +14 -1
- package/deploy/sema-up/sema-up.sh +134 -37
- package/deploy/sema-up/smoke.sh +5 -0
- package/dist/auto-mode-face.js +3 -4
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/config-center.d.ts +6 -2
- package/dist/boot/config-center.js +5 -59
- package/dist/boot/execution-env.d.ts +11 -0
- package/dist/boot/execution-env.js +14 -0
- package/dist/boot/execution-lane.d.ts +25 -0
- package/dist/boot/execution-lane.js +95 -0
- package/dist/boot/memory-consolidation.d.ts +185 -9
- package/dist/boot/memory-consolidation.js +192 -32
- package/dist/boot/resolve-spec.d.ts +2 -0
- package/dist/boot/resolve-spec.js +28 -24
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +4 -0
- package/dist/boot/session-faces.d.ts +5 -0
- package/dist/boot/session-faces.js +18 -2
- package/dist/boot/session-incarnation-seat.d.ts +41 -0
- package/dist/boot/session-incarnation-seat.js +69 -0
- package/dist/boot/stage-01-config.js +2 -2
- package/dist/boot/stage-02-registry-pull.js +3 -1
- package/dist/boot/stage-04-stores.d.ts +2 -0
- package/dist/boot/stage-04-stores.js +3 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -0
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +4 -1
- package/dist/boot/stage-06-runners.js +7 -14
- package/dist/boot/stage-07-capability-layer.d.ts +4 -1
- package/dist/boot/stage-08-reapers.d.ts +4 -1
- package/dist/boot/stage-09-leader.d.ts +6 -1
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +6 -1
- package/dist/boot/stage-10-http-server.js +4 -1
- package/dist/brain.d.ts +1 -1
- package/dist/config-catalog.d.ts +16 -1
- package/dist/config-catalog.js +9 -9
- package/dist/config-center/restart-signal.d.ts +4 -4
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +37 -6
- package/dist/config-provider.d.ts +42 -10
- package/dist/config-provider.js +18 -16
- package/dist/config-types.d.ts +37 -16
- package/dist/config-types.js +1 -0
- package/dist/config.d.ts +13 -28
- package/dist/config.js +53 -70
- package/dist/env-facts.d.ts +13 -3
- package/dist/env-facts.js +15 -3
- package/dist/execution-lane-caps.d.ts +15 -0
- package/dist/execution-lane-caps.js +8 -0
- package/dist/file-history-capture.d.ts +42 -35
- package/dist/file-history-capture.js +15 -10
- package/dist/host-lsp-manager.d.ts +1 -1
- package/dist/http/admission.d.ts +11 -4
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.js +3 -3
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.js +9 -5
- package/dist/http/routes/agents-roster.d.ts +7 -0
- package/dist/http/routes/agents-roster.js +6 -2
- package/dist/http/routes/approvals-assistant.d.ts +48 -19
- package/dist/http/routes/approvals-assistant.js +117 -50
- package/dist/http/routes/capabilities.js +3 -2
- package/dist/http/routes/config-catalog.js +1 -0
- package/dist/http/routes/memory-consolidation.d.ts +3 -2
- package/dist/http/routes/memory-policy.js +3 -3
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +27 -2
- package/dist/http/routes/session-sync.js +8 -14
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +44 -23
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.js +4 -4
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +21 -4
- package/dist/http/server.js +7 -4
- package/dist/http/sync-submit-body.d.ts +43 -0
- package/dist/http/sync-submit-body.js +4 -0
- package/dist/http/wire-types.d.ts +23 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/leader/merge.d.ts +8 -3
- package/dist/memory-incident-sink.js +2 -1
- package/dist/model-select.d.ts +9 -0
- package/dist/model-select.js +5 -1
- package/dist/observability/corrupt-read-seat.d.ts +151 -16
- package/dist/observability/corrupt-read-seat.js +158 -15
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.d.ts +15 -1
- package/dist/observability/run-terminal.js +4 -0
- package/dist/parked-decide.d.ts +8 -3
- package/dist/parked-decide.js +56 -20
- package/dist/parked-revive-surface.d.ts +14 -7
- package/dist/parked-revive-surface.js +5 -1
- package/dist/plugins/approval-ask-store-file.d.ts +4 -1
- package/dist/plugins/approval-ask-store-file.js +9 -12
- package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -1
- package/dist/plugins/approval-exemption-store.d.ts +21 -1
- package/dist/plugins/approval-exemption-store.js +36 -7
- package/dist/plugins/approval-nonce-store.d.ts +16 -2
- package/dist/plugins/approval-nonce-store.js +17 -7
- package/dist/plugins/caching-session-store.d.ts +5 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
- package/dist/plugins/file-resume-anchor-store.js +19 -5
- package/dist/plugins/file-run-store.d.ts +21 -8
- package/dist/plugins/file-run-store.js +60 -38
- package/dist/plugins/local-session-store.d.ts +22 -0
- package/dist/plugins/local-session-store.js +74 -11
- package/dist/plugins/memory-run-store.d.ts +7 -5
- package/dist/plugins/memory-run-store.js +11 -4
- package/dist/plugins/one-time-migrations.d.ts +38 -1
- package/dist/plugins/one-time-migrations.js +143 -15
- package/dist/plugins/permission-rule-store-file.d.ts +9 -2
- package/dist/plugins/permission-rule-store-file.js +37 -51
- package/dist/plugins/pg-session-storage.d.ts +3 -0
- package/dist/plugins/pg-session-storage.js +19 -10
- package/dist/plugins/run-store-sql.d.ts +12 -4
- package/dist/plugins/run-store-sql.js +15 -13
- package/dist/plugins/send-file-ledger.d.ts +5 -1
- package/dist/plugins/send-file-ledger.js +11 -6
- package/dist/plugins/session-event-payload.d.ts +14 -4
- package/dist/plugins/session-event-payload.js +5 -2
- package/dist/plugins/store-backend.d.ts +7 -3
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +8 -2
- package/dist/plugins/tidb-session-store.d.ts +6 -1
- package/dist/plugins/tidb-session-store.js +17 -10
- package/dist/project-identity.d.ts +4 -6
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +72 -3
- package/dist/request-value-closure.js +12 -4
- package/dist/roster-bound-http-status.d.ts +6 -0
- package/dist/roster-bound-http-status.js +6 -0
- package/dist/run-local.js +22 -27
- package/dist/runs.d.ts +22 -2
- package/dist/runs.js +29 -7
- package/dist/sealed-key.d.ts +31 -4
- package/dist/sealed-key.js +9 -0
- package/dist/security.d.ts +5 -1
- package/dist/server-secret-env.d.ts +2 -2
- package/dist/session-children-retire.d.ts +92 -0
- package/dist/session-children-retire.js +85 -0
- package/dist/session-error-wire.d.ts +58 -0
- package/dist/session-error-wire.js +38 -0
- package/dist/session-incarnation.d.ts +61 -0
- package/dist/session-incarnation.js +43 -0
- package/dist/session-records-skipped-notice.d.ts +20 -0
- package/dist/session-records-skipped-notice.js +27 -0
- package/dist/tool-approval.d.ts +65 -0
- package/dist/tool-approval.js +36 -9
- package/dist/trace/core-keyset-guard.d.ts +8 -7
- package/dist/trace/engine-notice-wire.d.ts +3 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.d.ts +1 -1
- package/dist/trace/project.js +9 -0
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +3 -3
|
@@ -1,12 +1,76 @@
|
|
|
1
1
|
import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
|
|
2
2
|
import { acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
|
|
3
3
|
import { jsonColumnTextExpr } from "./sql-json-column.js";
|
|
4
|
+
import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
|
|
5
|
+
export const ONE_TIME_MIGRATIONS = [
|
|
6
|
+
{
|
|
7
|
+
id: "M1",
|
|
8
|
+
introducedIn: "7.74.0",
|
|
9
|
+
releasedOn: "2026-09-13",
|
|
10
|
+
removableAfter: "2026-10-13",
|
|
11
|
+
removal: "删 migrateOn 的 M1 步(`snapshot_manifest` 在场探针、删前计数、`DROP TABLE`)与 `RETIRED_SNAPSHOT_TABLE`;报告、日志行与 " +
|
|
12
|
+
"`NO_SQL_MIGRATIONS` 的 `retiredSnapshotTable` / `retiredSnapshotRows`;与 M2 / M3 同一次删(见本表头注)",
|
|
13
|
+
afterRemoval: "没有响亮臂:存量库里的 `snapshot_manifest` 成一张无人读写的孤儿表 —— 它的读腿(边界探针)已随 core 7.17.0 的 seam 退役删掉," +
|
|
14
|
+
"起服列册探针 `schema-consistency.ts` 只对账代码认识的表。只有从 7.73.x 及更早直接跳到删码版的库会遇到;不影响任何读腿," +
|
|
15
|
+
"运维可手工 `DROP TABLE snapshot_manifest`",
|
|
16
|
+
reportKeys: ["retiredSnapshotTable", "retiredSnapshotRows"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "M2",
|
|
20
|
+
introducedIn: "7.74.0",
|
|
21
|
+
releasedOn: "2026-09-13",
|
|
22
|
+
removableAfter: "2026-10-13",
|
|
23
|
+
removal: "删 migrateOn 的 M2 拒启臂与 `DROP COLUMN` 步、`RETIRED_STEER_SEAT_COLUMN`、`DELIVERABLE_STEER_SEAT_WHERE`、`RetiredSteerSeatError`;" +
|
|
24
|
+
"报告、日志行与 `NO_SQL_MIGRATIONS` 的 `retiredSteerSeatColumn` / `retiredSteerSeatStaleRows`;与 M1 / M3 同一次删",
|
|
25
|
+
afterRemoval: "起服列册探针 `schema-consistency.ts`(S-547)的多列臂:存量库的 `checkpoint.pending_steer` 是代码不认识的列 ⇒ 拒启 " +
|
|
26
|
+
"`config.schema_mismatch`(响亮;⚠️ 那一臂的句子说「库比代码新」,对这一形方向说反了)。运维照拒句手工删列时,列上还送得出去的 " +
|
|
27
|
+
"steering 指令随之丢失 —— 删码之后没有任何东西再替它数一遍送得出去的格",
|
|
28
|
+
reportKeys: ["retiredSteerSeatColumn", "retiredSteerSeatStaleRows"],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "M3",
|
|
32
|
+
introducedIn: "7.74.0",
|
|
33
|
+
releasedOn: "2026-09-13",
|
|
34
|
+
removableAfter: "2026-10-13",
|
|
35
|
+
removal: "删 migrateOn 的 M3 两条整表 `UPDATE` 与它的「退役 schema 还在」跳过标记、`RETIRED_ASK_ORIGIN`、`GATE_BEARING_LEDGER_EVENT_TYPE`、" +
|
|
36
|
+
"`migrateRetiredAskOrigin`,以及 local 腿(`FileRunStore.migrateRetiredAskOriginRows` 与 `LocalBackend.runOneTimeMigrations` 里那一格);" +
|
|
37
|
+
"报告与日志行的 `askOriginRowsMigrated`;与 M1 / M2 同一次删",
|
|
38
|
+
afterRemoval: "读腿既有的 `server.trace.gate-outcome-defective`(`trace/project.ts` 的 `screenedGate`):残留旧拼法的 gate 与任何集外词同判 —— " +
|
|
39
|
+
"整条 withhold + 计数 + 一次 warn(与头注「M3 够不到的一格」同一条臂)",
|
|
40
|
+
reportKeys: ["askOriginRowsMigrated"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "M4",
|
|
44
|
+
introducedIn: "7.93.5",
|
|
45
|
+
releasedOn: "2026-09-21",
|
|
46
|
+
removableAfter: "2026-10-21",
|
|
47
|
+
removal: "删 migrateOn 的 M4 步(`device_audit` 在场探针 + `UPDATE … SET detail = NULL`)与 `AUDIT_TABLE` / `AUDIT_DETAIL_COLUMN`;" +
|
|
48
|
+
"报告、日志行与 `NO_SQL_MIGRATIONS` 的 `auditDetailJsonNullRows`;它若是表里最后一条,按头注「窗」一节整个文件一起删",
|
|
49
|
+
afterRemoval: "`device-store-sql.ts` `listAudit` 的读形拒(S-550:一行读不出形 ⇒ 整页拒,刻意不逐行隔离):含一行 JSON 文本 `null` 的那一页" +
|
|
50
|
+
"读不出来,拒句点名行 id —— 响亮,但那一页会一直读不出来,直到运维手工把那些行的 `detail` 置成 SQL NULL",
|
|
51
|
+
reportKeys: ["auditDetailJsonNullRows"],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "M5",
|
|
55
|
+
introducedIn: "7.104.0",
|
|
56
|
+
releasedOn: "2026-09-28",
|
|
57
|
+
removableAfter: "2026-10-28",
|
|
58
|
+
removal: "删 migrateOn 的 M5 步(`session_meta` 在场探针、`incarnation` 列在场探针 + `ADD COLUMN`、空化身回填循环)与 `backfillSessionIncarnations`;" +
|
|
59
|
+
"报告、日志行与 `NO_SQL_MIGRATIONS` 的 `sessionIncarnationColumn` / `sessionIncarnationsBackfilled`;**列本身留在中央 DDL 里**(它不是迁移的一部分)",
|
|
60
|
+
afterRemoval: "① 列不在的存量库:起服列册探针 `schema-consistency.ts`(S-547)的缺列臂拒启 `config.schema_mismatch`,拒句里的 `ALTER TABLE … ADD COLUMN` " +
|
|
61
|
+
"由同一份 DDL 生成(响亮);② 列在但行上仍是空化身(跳过了带 M5 的版本直接升):会话店读口 `sessionFaceOf` 对那一行响亮拒 " +
|
|
62
|
+
"`session.incarnation_missing`(parked 决定腿因此拒、记忆座退回按 id 绑定并 warn)—— 运维手工给那些行填一只随机令牌即可",
|
|
63
|
+
reportKeys: ["sessionIncarnationColumn", "sessionIncarnationsBackfilled"],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
4
66
|
export const NO_SQL_MIGRATIONS = {
|
|
5
67
|
retiredSnapshotTable: "absent",
|
|
6
68
|
retiredSnapshotRows: 0,
|
|
7
69
|
retiredSteerSeatStaleRows: 0,
|
|
8
70
|
retiredSteerSeatColumn: "absent",
|
|
9
71
|
auditDetailJsonNullRows: 0,
|
|
72
|
+
sessionIncarnationColumn: "lazy-on-read",
|
|
73
|
+
sessionIncarnationsBackfilled: 0,
|
|
10
74
|
};
|
|
11
75
|
const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
|
|
12
76
|
const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
|
|
@@ -37,24 +101,24 @@ export class RetiredSteerSeatError extends Error {
|
|
|
37
101
|
}
|
|
38
102
|
function mysqlDb(conn) {
|
|
39
103
|
return {
|
|
40
|
-
async rows(sql) {
|
|
41
|
-
const [rows] = await conn.query(sql);
|
|
104
|
+
async rows(sql, params) {
|
|
105
|
+
const [rows] = params === undefined ? await conn.query(sql) : await conn.query(sql, [...params]);
|
|
42
106
|
return rows;
|
|
43
107
|
},
|
|
44
|
-
async exec(sql) {
|
|
45
|
-
const [r] = await conn.query(sql);
|
|
108
|
+
async exec(sql, params) {
|
|
109
|
+
const [r] = params === undefined ? await conn.query(sql) : await conn.query(sql, [...params]);
|
|
46
110
|
return Number(r.affectedRows ?? 0);
|
|
47
111
|
},
|
|
48
112
|
};
|
|
49
113
|
}
|
|
50
114
|
function pgDb(client) {
|
|
51
115
|
return {
|
|
52
|
-
async rows(sql) {
|
|
53
|
-
const r = await client.query(sql);
|
|
116
|
+
async rows(sql, params) {
|
|
117
|
+
const r = params === undefined ? await client.query(sql) : await client.query(sql, [...params]);
|
|
54
118
|
return r.rows;
|
|
55
119
|
},
|
|
56
|
-
async exec(sql) {
|
|
57
|
-
const r = await client.query(sql);
|
|
120
|
+
async exec(sql, params) {
|
|
121
|
+
const r = params === undefined ? await client.query(sql) : await client.query(sql, [...params]);
|
|
58
122
|
return r.rowCount ?? 0;
|
|
59
123
|
},
|
|
60
124
|
};
|
|
@@ -139,6 +203,7 @@ async function migrateOn(db, dialect) {
|
|
|
139
203
|
? 0
|
|
140
204
|
: await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
|
|
141
205
|
`WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
|
|
206
|
+
const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
|
|
142
207
|
let retiredSnapshotTable = "absent";
|
|
143
208
|
let retiredSnapshotRows = 0;
|
|
144
209
|
let retiredSteerSeatColumn = "absent";
|
|
@@ -169,7 +234,64 @@ async function migrateOn(db, dialect) {
|
|
|
169
234
|
throw new Error(`one-time migration failed AFTER completing destructive steps — record these before retrying, a retry cannot ` +
|
|
170
235
|
`report them again: ${destroyed.join("; ")}. Underlying failure: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
171
236
|
}
|
|
172
|
-
return {
|
|
237
|
+
return {
|
|
238
|
+
retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows,
|
|
239
|
+
sessionIncarnationColumn, sessionIncarnationsBackfilled,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const INCARNATION_BACKFILL_BATCH = 500;
|
|
243
|
+
async function migrateSessionIncarnations(db, dialect) {
|
|
244
|
+
if (!(await tableExists(db, dialect, "session_meta")))
|
|
245
|
+
return { sessionIncarnationColumn: "no-table", sessionIncarnationsBackfilled: 0 };
|
|
246
|
+
const col = SESSION_META_INCARNATION_COLUMN;
|
|
247
|
+
let sessionIncarnationColumn = "present";
|
|
248
|
+
if (!(await columnExists(db, dialect, "session_meta", col.name))) {
|
|
249
|
+
try {
|
|
250
|
+
await db.exec(`ALTER TABLE session_meta ADD COLUMN ${dialect === "tidb" ? col.tidb : col.pg}`);
|
|
251
|
+
sessionIncarnationColumn = "added";
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
if (!(await columnExists(db, dialect, "session_meta", col.name)))
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { sessionIncarnationColumn, sessionIncarnationsBackfilled: await backfillSessionIncarnations(db, dialect) };
|
|
259
|
+
}
|
|
260
|
+
async function backfillSessionIncarnations(db, dialect) {
|
|
261
|
+
const remaining = async () => Number((await db.rows("SELECT COUNT(*) AS n FROM session_meta WHERE incarnation = ''"))[0]?.n ?? 0);
|
|
262
|
+
const initial = await remaining();
|
|
263
|
+
if (initial === 0)
|
|
264
|
+
return 0;
|
|
265
|
+
const ceiling = Math.ceil(initial / INCARNATION_BACKFILL_BATCH) * 2 + 2;
|
|
266
|
+
let written = 0;
|
|
267
|
+
let after;
|
|
268
|
+
for (let round = 0; round < ceiling; round++) {
|
|
269
|
+
const ids = (after === undefined
|
|
270
|
+
? await db.rows(`SELECT session_id FROM session_meta WHERE incarnation = '' ORDER BY session_id LIMIT ${INCARNATION_BACKFILL_BATCH}`)
|
|
271
|
+
: await db.rows(`SELECT session_id FROM session_meta WHERE incarnation = '' AND session_id > ${dialect === "pg" ? "$1" : "?"} ORDER BY session_id LIMIT ${INCARNATION_BACKFILL_BATCH}`, [after])).map((r) => String(r.session_id));
|
|
272
|
+
if (ids.length === 0) {
|
|
273
|
+
if (after === undefined)
|
|
274
|
+
return written;
|
|
275
|
+
after = undefined;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const params = [];
|
|
279
|
+
const bind = (v) => {
|
|
280
|
+
params.push(v);
|
|
281
|
+
return dialect === "pg" ? `$${params.length}` : "?";
|
|
282
|
+
};
|
|
283
|
+
const cases = ids.map((id) => `WHEN ${bind(id)} THEN ${bind(mintSessionIncarnation())}`).join(" ");
|
|
284
|
+
const inList = ids.map((id) => bind(id)).join(", ");
|
|
285
|
+
written += await db.exec(`UPDATE session_meta SET incarnation = CASE session_id ${cases} END WHERE incarnation = '' AND session_id IN (${inList})`, params);
|
|
286
|
+
after = ids.length < INCARNATION_BACKFILL_BATCH ? undefined : ids[ids.length - 1];
|
|
287
|
+
}
|
|
288
|
+
const left = await remaining();
|
|
289
|
+
if (left > 0) {
|
|
290
|
+
throw new Error(`one-time migration M5: ${left} session_meta row(s) still carry an empty incarnation after ${ceiling} backfill round(s) ` +
|
|
291
|
+
`(${written} written) — refusing to start rather than serve sessions whose occupant cannot be told apart. Retry the boot; ` +
|
|
292
|
+
`if it repeats, something keeps writing rows without an incarnation (an older replica still inserting?).`);
|
|
293
|
+
}
|
|
294
|
+
return written;
|
|
173
295
|
}
|
|
174
296
|
export function migrateRetiredAskOrigin(type, data) {
|
|
175
297
|
if (type !== GATE_BEARING_LEDGER_EVENT_TYPE)
|
|
@@ -186,7 +308,8 @@ export function migrateRetiredAskOrigin(type, data) {
|
|
|
186
308
|
return { ...d, gate: { ...g, origin: RETIRED_ASK_ORIGIN.to } };
|
|
187
309
|
}
|
|
188
310
|
export function logOneTimeMigrations(log, backend, report) {
|
|
189
|
-
const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0
|
|
311
|
+
const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0 ||
|
|
312
|
+
report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0;
|
|
190
313
|
(changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
191
314
|
backend,
|
|
192
315
|
retiredSnapshotTable: report.retiredSnapshotTable,
|
|
@@ -195,11 +318,16 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
195
318
|
retiredSteerSeatStaleRows: report.retiredSteerSeatStaleRows,
|
|
196
319
|
askOriginRowsMigrated: report.askOriginRowsMigrated,
|
|
197
320
|
auditDetailJsonNullRows: report.auditDetailJsonNullRows,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"
|
|
321
|
+
sessionIncarnationColumn: report.sessionIncarnationColumn,
|
|
322
|
+
sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
|
|
323
|
+
removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
|
|
324
|
+
note: "one-time persisted-data migrations (M1: retired whole-tree snapshot index table dropped; M2: retired single-seat " +
|
|
325
|
+
"steering column dropped; M3: retired ask-origin spelling rewritten in place; M4: audit details written as the JSON " +
|
|
326
|
+
"text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
|
|
327
|
+
"session incarnation column added and every session row without one minted its own token). Each step " +
|
|
328
|
+
"runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
|
|
329
|
+
"removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
|
|
330
|
+
"version that still carries a step at least once before upgrading past its deletion.",
|
|
203
331
|
});
|
|
204
332
|
}
|
|
205
333
|
//# sourceMappingURL=one-time-migrations.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FileDurableRulePartitionProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type StaleRuleApprovalRecord } from "@sema-agent/core";
|
|
2
2
|
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
|
|
3
3
|
import type { PermissionRuleStoreBundle } from "../rules-consent.js";
|
|
4
|
+
import type { Logger } from "../observability/logger.js";
|
|
4
5
|
import { type RuleCandidate } from "@sema-agent/core";
|
|
5
6
|
/**
|
|
6
7
|
* `RuleApprovalRecordStore` 的 File 形。
|
|
@@ -11,8 +12,10 @@ import { type RuleCandidate } from "@sema-agent/core";
|
|
|
11
12
|
export declare class FileRuleApprovalRecordStore implements RuleApprovalRecordStore {
|
|
12
13
|
private readonly rows;
|
|
13
14
|
private readonly log;
|
|
15
|
+
/** 本面的损坏闸(S-582 起由重放属主交回;置位后每个方法第一行响亮拒)。 */
|
|
14
16
|
private readonly gate;
|
|
15
|
-
|
|
17
|
+
/** @param warn 腐读披露座(S-582;缺席 ⇒ 属主退回 `console.warn`)。 */
|
|
18
|
+
constructor(dir: string, warn?: Logger["warn"]);
|
|
16
19
|
/** 取证/自证用:本面是否因内部损坏而 fail-closed。 */
|
|
17
20
|
get corrupt(): boolean;
|
|
18
21
|
/** 归还本面 eager 持有的日志描述符(束的 `dispose()` 唯一调用点)。`AppendLog` 上没有 finalizer,
|
|
@@ -39,10 +42,12 @@ export declare class FileRuleApprovalRecordStore implements RuleApprovalRecordSt
|
|
|
39
42
|
export declare class FileRuleImportTicketStore {
|
|
40
43
|
private readonly rows;
|
|
41
44
|
private readonly log;
|
|
45
|
+
/** 本面的损坏闸(S-582 起由重放属主交回)。 */
|
|
42
46
|
private readonly gate;
|
|
43
47
|
/** 单调钟(判定用)。与 {@link now}(墙钟,记账用)是两只不同的钟,别混。 */
|
|
44
48
|
private readonly monotonic;
|
|
45
|
-
|
|
49
|
+
/** @param warn 腐读披露座(S-582;缺席 ⇒ 属主退回 `console.warn`)。 */
|
|
50
|
+
constructor(dir: string, now?: () => number, warn?: Logger["warn"], monotonic?: () => number);
|
|
46
51
|
private readonly now;
|
|
47
52
|
/** 取证/自证用:本面是否因内部损坏而 fail-closed。 */
|
|
48
53
|
get corrupt(): boolean;
|
|
@@ -100,9 +105,11 @@ export interface FilePermissionRuleStores extends PermissionRuleStoreBundle {
|
|
|
100
105
|
* `onError` 接的是 core 规则文件的**披露面**(读不出来 / 校验和不符 / 撞上符号链接时它答零规则并
|
|
101
106
|
* 说出来)。接住它打一条 warn 是**必须**的:那条路径上「零规则」与「真的没有规则」在读面上同形,
|
|
102
107
|
* 不留痕就变成一次静默的 fail-closed(用户会突然被反复询问,却没有任何线索)。
|
|
108
|
+
* `warn`(S-582)接的是本仓审批记录 / 导入票两份日志的**腐读座**(逐行披露 + 计数;缺席 ⇒ `console.warn`)。
|
|
103
109
|
*/
|
|
104
110
|
export declare function createFilePermissionRuleStores(root: string, opts?: {
|
|
105
111
|
onError?: (message: string) => void;
|
|
112
|
+
warn?: Logger["warn"];
|
|
106
113
|
now?: () => number;
|
|
107
114
|
monotonic?: () => number;
|
|
108
115
|
}): FilePermissionRuleStores;
|
|
@@ -2,10 +2,10 @@ import { join } from "node:path";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { readdirSync } from "node:fs";
|
|
4
4
|
import { performance } from "node:perf_hooks";
|
|
5
|
-
import { FileDurableRulePartitionProvider, AppendLog, ensureDir,
|
|
5
|
+
import { FileDurableRulePartitionProvider, AppendLog, ensureDir, } from "@sema-agent/core";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
8
|
-
import {
|
|
8
|
+
import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
|
|
9
9
|
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
10
10
|
import { RULE_BEHAVIORS } from "@sema-agent/core";
|
|
11
11
|
const RuleScopeSchema = z.union([
|
|
@@ -109,53 +109,44 @@ const TicketLineSchema = z.union([
|
|
|
109
109
|
z.object({ op: z.literal("settle"), ticketId: z.string().min(1), outcome: z.unknown() }).strict(),
|
|
110
110
|
]);
|
|
111
111
|
const LEGACY_TICKET_PURPOSE = "cc-import";
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return;
|
|
132
|
-
throw new Error(`refusing to use a corrupt ${this.face} (${this.path}): ${this.reason} — ` +
|
|
133
|
-
"the permission-rule lane is fail-closed until a person inspects the file (moving it aside restarts the lane with an empty log)");
|
|
134
|
-
}
|
|
135
|
-
}
|
|
112
|
+
const APPROVAL_LOG_SHAPE = {
|
|
113
|
+
what: "permission-rule approval line that is neither a current-form put/discard nor a pre-form put",
|
|
114
|
+
read(record) {
|
|
115
|
+
const parsed = ApprovalLineSchema.safeParse(record);
|
|
116
|
+
if (parsed.success)
|
|
117
|
+
return { kind: "line", line: parsed.data };
|
|
118
|
+
const stale = StaleApprovalLineSchema.safeParse(record);
|
|
119
|
+
if (stale.success && stale.data.record.schema !== RULE_APPROVAL_RECORD_SCHEMA)
|
|
120
|
+
return { kind: "stale", envelope: toStaleEnvelope(stale.data.record) };
|
|
121
|
+
return undefined;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const TICKET_LOG_SHAPE = {
|
|
125
|
+
what: "permission-rule import-ticket line outside the mint/consume/release/settle shapes",
|
|
126
|
+
read(record) {
|
|
127
|
+
const parsed = TicketLineSchema.safeParse(record);
|
|
128
|
+
return parsed.success ? parsed.data : undefined;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
136
131
|
export class FileRuleApprovalRecordStore {
|
|
137
132
|
rows = new Map();
|
|
138
133
|
log;
|
|
139
134
|
gate;
|
|
140
|
-
constructor(dir,
|
|
135
|
+
constructor(dir, warn) {
|
|
141
136
|
ensureDir(dir);
|
|
142
137
|
const path = join(dir, "approvals.jsonl");
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
if (stale.success && stale.data.record.schema !== RULE_APPROVAL_RECORD_SCHEMA) {
|
|
149
|
-
this.rows.set(stale.data.record.id, toStaleEnvelope(stale.data.record));
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
this.gate.trip(`a replayed record does not carry a readable shape: ${parsed.error.message}`);
|
|
138
|
+
const { records, gate } = replayJsonlGuarded(path, "permission-rule-approvals", APPROVAL_LOG_SHAPE, warn);
|
|
139
|
+
this.gate = gate;
|
|
140
|
+
for (const entry of records) {
|
|
141
|
+
if (entry.kind === "stale") {
|
|
142
|
+
this.rows.set(entry.envelope.id, entry.envelope);
|
|
153
143
|
continue;
|
|
154
144
|
}
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
const line = entry.line;
|
|
146
|
+
if (line.op === "discard")
|
|
147
|
+
this.rows.delete(line.id);
|
|
157
148
|
else
|
|
158
|
-
this.rows.set(
|
|
149
|
+
this.rows.set(line.record.id, toApprovalRecord(line.record));
|
|
159
150
|
}
|
|
160
151
|
this.log = new AppendLog(path);
|
|
161
152
|
}
|
|
@@ -204,19 +195,14 @@ export class FileRuleImportTicketStore {
|
|
|
204
195
|
log;
|
|
205
196
|
gate;
|
|
206
197
|
monotonic;
|
|
207
|
-
constructor(dir, now,
|
|
198
|
+
constructor(dir, now, warn, monotonic) {
|
|
208
199
|
this.now = now ?? Date.now;
|
|
209
200
|
this.monotonic = monotonic ?? (() => performance.now());
|
|
210
201
|
ensureDir(dir);
|
|
211
202
|
const path = join(dir, "import-tickets.jsonl");
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!parsed.success) {
|
|
216
|
-
this.gate.trip(`a replayed record does not carry a readable shape: ${parsed.error.message}`);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
const line = parsed.data;
|
|
203
|
+
const { records, gate } = replayJsonlGuarded(path, "permission-rule-tickets", TICKET_LOG_SHAPE, warn);
|
|
204
|
+
this.gate = gate;
|
|
205
|
+
for (const line of records) {
|
|
220
206
|
if (line.op === "mint") {
|
|
221
207
|
this.rows.set(line.ticketId, {
|
|
222
208
|
ownerKey: line.ownerKey,
|
|
@@ -344,8 +330,8 @@ export function createFilePermissionRuleStores(root, opts) {
|
|
|
344
330
|
const dir = join(root, "permission-rules");
|
|
345
331
|
ensureDir(dir);
|
|
346
332
|
const durable = opts?.onError ? new FileDurableRulePartitionProvider(dir, opts.onError) : new FileDurableRulePartitionProvider(dir);
|
|
347
|
-
const approvals = new FileRuleApprovalRecordStore(dir, opts?.
|
|
348
|
-
const tickets = new FileRuleImportTicketStore(dir, opts?.now, opts?.
|
|
333
|
+
const approvals = new FileRuleApprovalRecordStore(dir, opts?.warn);
|
|
334
|
+
const tickets = new FileRuleImportTicketStore(dir, opts?.now, opts?.warn, opts?.monotonic);
|
|
349
335
|
return {
|
|
350
336
|
durable,
|
|
351
337
|
approvals,
|
|
@@ -4,6 +4,7 @@ import type { SessionSummary } from "./store-contracts.js";
|
|
|
4
4
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
5
5
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
6
6
|
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
7
|
+
import { type SessionFace } from "../session-incarnation.js";
|
|
7
8
|
import { type IndexSpec } from "./ensure-index.js";
|
|
8
9
|
/**
|
|
9
10
|
* PG-dialect DDL for the L1 session log, translated from tidb-pool.ts SCHEMA_STATEMENTS:
|
|
@@ -80,6 +81,8 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
80
81
|
}): Promise<PlacedSessionRow[]>;
|
|
81
82
|
/** Owner principal of a session, `null` if registered ownerless, `undefined` if no such session. */
|
|
82
83
|
ownerOf(sessionId: string): Promise<string | null | undefined>;
|
|
84
|
+
/** S-790:属主 + 化身一次读(TiDB 孪生 {@link TiDBSessionStore.sessionFaceOf} 同义;空化身 ⇒ 响亮拒)。 */
|
|
85
|
+
sessionFaceOf(sessionId: string): Promise<SessionFace | undefined>;
|
|
83
86
|
touch(sessionId: string): Promise<void>;
|
|
84
87
|
/** E18 — the session's current leaf SessionTreeEntry.id (session_meta.leaf_id). @see TiDBSessionStore.getLeafId
|
|
85
88
|
* (single SELECT, no acquire/lock, cache-bypassing — safe to read mid-run for the resume-at turn-capture). */
|
|
@@ -11,6 +11,7 @@ import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING
|
|
|
11
11
|
import { isPgUniqueViolation } from "./sql-errors.js";
|
|
12
12
|
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
13
13
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
14
|
+
import { SESSION_META_INCARNATION_COLUMN, carriedIncarnation, mintSessionIncarnation, sessionFaceFromRow } from "../session-incarnation.js";
|
|
14
15
|
import { ensureIndexes } from "./ensure-index.js";
|
|
15
16
|
const isDupKey = isPgUniqueViolation;
|
|
16
17
|
function baselineQuery(text, values) {
|
|
@@ -40,6 +41,8 @@ export const PG_SESSION_SCHEMA = [
|
|
|
40
41
|
placement_root_session_id VARCHAR(190) COLLATE "C",
|
|
41
42
|
placement_handle VARCHAR(190) COLLATE "C",
|
|
42
43
|
placed_at TIMESTAMPTZ(3),
|
|
44
|
+
-- S-790 / S-506:会话化身(TiDB 孪生同名同义;列定义唯一属主 = src/session-incarnation.ts 的 SESSION_META_INCARNATION_COLUMN)。
|
|
45
|
+
${SESSION_META_INCARNATION_COLUMN.pg},
|
|
43
46
|
PRIMARY KEY (session_id)
|
|
44
47
|
)`,
|
|
45
48
|
`CREATE TABLE IF NOT EXISTS session_event (
|
|
@@ -217,9 +220,9 @@ export class PgSessionStore {
|
|
|
217
220
|
}
|
|
218
221
|
async register(sessionId, owner, placement) {
|
|
219
222
|
const now = new Date();
|
|
220
|
-
await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, " +
|
|
223
|
+
await this.pool.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation, " +
|
|
221
224
|
"placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at) " +
|
|
222
|
-
"VALUES ($1,$2,$3,$4,NULL,0,$5,$6,$7,$8,$9,$10) ON CONFLICT (session_id) DO NOTHING", [sessionId, owner, now, now, ...placementInsertValues(placement, now)]);
|
|
225
|
+
"VALUES ($1,$2,$3,$4,NULL,0,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (session_id) DO NOTHING", [sessionId, owner, now, now, mintSessionIncarnation(), ...placementInsertValues(placement, now)]);
|
|
223
226
|
}
|
|
224
227
|
async placementOf(sessionId) {
|
|
225
228
|
const res = await this.pool.query(`SELECT ${PLACEMENT_SELECT_COLUMNS} FROM session_meta WHERE session_id = $1`, [sessionId]);
|
|
@@ -246,6 +249,12 @@ export class PgSessionStore {
|
|
|
246
249
|
return undefined;
|
|
247
250
|
return res.rows[0].owner ?? null;
|
|
248
251
|
}
|
|
252
|
+
async sessionFaceOf(sessionId) {
|
|
253
|
+
const res = await this.pool.query("SELECT owner, incarnation FROM session_meta WHERE session_id = $1", [sessionId]);
|
|
254
|
+
if (res.rows.length === 0)
|
|
255
|
+
return undefined;
|
|
256
|
+
return sessionFaceFromRow(sessionId, res.rows[0].owner ?? null, res.rows[0].incarnation);
|
|
257
|
+
}
|
|
249
258
|
async touch(sessionId) {
|
|
250
259
|
await this.pool.query("UPDATE session_meta SET updated_at = $1 WHERE session_id = $2", [
|
|
251
260
|
new Date(),
|
|
@@ -384,7 +393,7 @@ export class PgSessionStore {
|
|
|
384
393
|
const payload = String(r.payload);
|
|
385
394
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
|
|
386
395
|
}
|
|
387
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6)", [newId, owner, now, now, leafId, rows.length]);
|
|
396
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,$5,$6,$7)", [newId, owner, now, now, leafId, rows.length, mintSessionIncarnation()]);
|
|
388
397
|
await conn.query("COMMIT");
|
|
389
398
|
return newId;
|
|
390
399
|
}
|
|
@@ -438,7 +447,7 @@ export class PgSessionStore {
|
|
|
438
447
|
const conn = await this.pool.connect();
|
|
439
448
|
try {
|
|
440
449
|
await conn.query("BEGIN");
|
|
441
|
-
await this.insertValidatedLog(conn, sessionId, owner, validated);
|
|
450
|
+
await this.insertValidatedLog(conn, sessionId, owner, validated, mintSessionIncarnation());
|
|
442
451
|
await conn.query("COMMIT");
|
|
443
452
|
}
|
|
444
453
|
catch (err) {
|
|
@@ -461,14 +470,14 @@ export class PgSessionStore {
|
|
|
461
470
|
const conn = await this.pool.connect();
|
|
462
471
|
try {
|
|
463
472
|
await conn.query("BEGIN");
|
|
464
|
-
const placedRes = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1 FOR UPDATE", [sessionId]);
|
|
473
|
+
const placedRes = await conn.query("SELECT placement_kind, owner, incarnation FROM session_meta WHERE session_id = $1 FOR UPDATE", [sessionId]);
|
|
465
474
|
if (placedRes.rows[0]?.placement_kind != null) {
|
|
466
475
|
await conn.query("ROLLBACK");
|
|
467
476
|
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placedRes.rows[0].placement_kind} placement partition — a session-sync replace cannot overwrite a delegation transcript`);
|
|
468
477
|
}
|
|
469
478
|
await conn.query("DELETE FROM session_event WHERE session_id = $1", [sessionId]);
|
|
470
479
|
await conn.query("DELETE FROM session_meta WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2 AND placement_kind IS NULL", [sessionId, owner]);
|
|
471
|
-
await this.insertValidatedLog(conn, sessionId, owner, validated);
|
|
480
|
+
await this.insertValidatedLog(conn, sessionId, owner, validated, carriedIncarnation(placedRes.rows[0], owner));
|
|
472
481
|
await conn.query("COMMIT");
|
|
473
482
|
}
|
|
474
483
|
catch (err) {
|
|
@@ -485,7 +494,7 @@ export class PgSessionStore {
|
|
|
485
494
|
conn.release();
|
|
486
495
|
}
|
|
487
496
|
}
|
|
488
|
-
async insertValidatedLog(conn, sessionId, owner, validated) {
|
|
497
|
+
async insertValidatedLog(conn, sessionId, owner, validated, incarnation) {
|
|
489
498
|
let leafId = null;
|
|
490
499
|
for (const e of validated)
|
|
491
500
|
leafId = leafIdAfterEntry(e);
|
|
@@ -494,7 +503,7 @@ export class PgSessionStore {
|
|
|
494
503
|
const e = validated[seq];
|
|
495
504
|
await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [sessionId, seq, e.id, e.parentId ?? null, e.type, pgSafeJsonStringify(e), new Date(e.timestamp)]);
|
|
496
505
|
}
|
|
497
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6)", [sessionId, owner, now, now, leafId, validated.length]);
|
|
506
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,$5,$6,$7)", [sessionId, owner, now, now, leafId, validated.length, incarnation]);
|
|
498
507
|
}
|
|
499
508
|
beginImportStaging(realSessionId, token) {
|
|
500
509
|
return new PgStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
|
|
@@ -614,8 +623,8 @@ class PgStagingHandle {
|
|
|
614
623
|
if (lastRes.rows[0])
|
|
615
624
|
leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRes.rows[0].payload, this.realSessionId, "last staged row"));
|
|
616
625
|
const now = new Date();
|
|
617
|
-
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6) " +
|
|
618
|
-
"ON CONFLICT (session_id) DO UPDATE SET owner = EXCLUDED.owner, updated_at = EXCLUDED.updated_at, leaf_id = EXCLUDED.leaf_id, leaf_seq = EXCLUDED.leaf_seq", [this.realSessionId, owner, now, now, leafId, srcIds.length]);
|
|
626
|
+
await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq, incarnation) VALUES ($1,$2,$3,$4,$5,$6,$7) " +
|
|
627
|
+
"ON CONFLICT (session_id) DO UPDATE SET owner = EXCLUDED.owner, updated_at = EXCLUDED.updated_at, leaf_id = EXCLUDED.leaf_id, leaf_seq = EXCLUDED.leaf_seq", [this.realSessionId, owner, now, now, leafId, srcIds.length, mintSessionIncarnation()]);
|
|
619
628
|
const afterMeta = await conn.query("SELECT placement_kind FROM session_meta WHERE session_id = $1", [this.realSessionId]);
|
|
620
629
|
if (afterMeta.rows[0]?.placement_kind != null) {
|
|
621
630
|
await conn.query("ROLLBACK");
|
|
@@ -83,10 +83,14 @@ export declare class SqlRunStore {
|
|
|
83
83
|
/**
|
|
84
84
|
* Durable F4 ([ref]): a task suspended on a policy `ask`. Sets the run NON-terminal `suspended` and
|
|
85
85
|
* KEEPS the `task_active` claim — the session stays locked (no new run) until an operator resumes it to a
|
|
86
|
-
* terminal state or the reaper expires it.
|
|
87
|
-
*
|
|
86
|
+
* terminal state or the reaper expires it.
|
|
87
|
+
*
|
|
88
|
+
* 🔴 S-173:park 也落 `result` 列 —— 存的是**摘掉恢复凭据、脱过敏的投影**(`sealRunRowResult`,与 `setTerminal`
|
|
89
|
+
* 同一只入口、同列同序列化),不是引擎交出的原件:`paused.token` / `failed.nestedPause.token` 两座在落列前摘掉,
|
|
90
|
+
* 所以 poll 这一行的调用方(`GET /v1/runs/:id → result`)拿到的是「停在哪扇门上」而拿不到 resume 能力。
|
|
91
|
+
* `result = null` = 这条腿没观察到 park 结果(断连竞态 / 重开 re-park),列写空 —— 与修前行形同。
|
|
88
92
|
*/
|
|
89
|
-
setSuspended(taskId: string): Promise<void>;
|
|
93
|
+
setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
90
94
|
/**
|
|
91
95
|
* [ref] D-B: PARK a run that paused on a REVIEW gate — a `needs_review` result (a dry-run diff review OR a
|
|
92
96
|
* pre-action `plan_review`; core assemble-result slot 8.6). Like {@link setSuspended} this is a NON-terminal park
|
|
@@ -104,7 +108,7 @@ export declare class SqlRunStore {
|
|
|
104
108
|
* mocked run-store unit tests stayed green. A mock cannot observe a unique-key claim race, a rolled-back loser,
|
|
105
109
|
* or a multi-table reaper DELETE.
|
|
106
110
|
*/
|
|
107
|
-
setNeedsReview(taskId: string): Promise<void>;
|
|
111
|
+
setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
108
112
|
/**
|
|
109
113
|
* Durable F4 resume: a suspended run is being actively resumed. Flip it back to `running` (CAS — only if
|
|
110
114
|
* still suspended) and refresh liveness, so the in-flight resume is protected by the running-run heartbeat
|
|
@@ -288,6 +292,10 @@ export declare class SqlRunStore {
|
|
|
288
292
|
* a NEW pending checkpoint — the `NOT EXISTS pending` clause excludes it. Idempotent across replicas. Returns count.
|
|
289
293
|
*
|
|
290
294
|
* Same alias-qualification divergence as {@link reapSuspended} — see its header note.
|
|
295
|
+
*
|
|
296
|
+
* S-173:两条收割腿都把 `result` 列写回 NULL —— park 行上那份 park 结果(`terminal.kind:"paused"`)说的是
|
|
297
|
+
* 「停在门上」,而这一刻行被打成 `failed`/`approval.expired`;留着它 = 一行两句互相矛盾的话(读面还会从它
|
|
298
|
+
* 投出 `review.pending` 之类的 park 码)。收割后的行形因此与 S-173 之前逐字同(结果空,码在 error_code 列)。
|
|
291
299
|
*/
|
|
292
300
|
failSuspendedWithExpiredCheckpoint(): Promise<number>;
|
|
293
301
|
}
|