@sema-agent/server 7.103.2 → 7.105.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 +378 -2
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +320 -16
- 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 +6 -1
- 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 +28 -30
- package/dist/config.js +95 -77
- 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/file-history-disclosure.d.ts +7 -31
- package/dist/file-history-disclosure.js +12 -0
- package/dist/fleet/fleet-bus.d.ts +5 -1
- package/dist/http/admission.d.ts +12 -5
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.d.ts +18 -0
- package/dist/http/dispatch.js +19 -6
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.d.ts +1 -1
- package/dist/http/resume-legs.js +9 -5
- package/dist/http/route-table.d.ts +15 -4
- package/dist/http/route-table.js +6 -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/fleet.d.ts +3 -2
- package/dist/http/routes/images.js +0 -1
- 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 +29 -10
- package/dist/http/routes/session-sync.js +14 -15
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +49 -25
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/side-query.js +8 -1
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.d.ts +2 -0
- package/dist/http/routes/trace-usage.js +8 -13
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +30 -8
- package/dist/http/server.js +11 -5
- package/dist/http/sse-lifecycle.d.ts +1 -1
- package/dist/http/sse-log.d.ts +12 -9
- package/dist/http/sse-log.js +1 -8
- 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 +17 -1
- package/dist/observability/fail-open.js +17 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal.d.ts +60 -10
- package/dist/observability/run-terminal.js +119 -9
- 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/image-bake-store-sql.d.ts +0 -2
- package/dist/plugins/image-bake-store-sql.js +0 -5
- 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/retention-store-sql.js +1 -0
- package/dist/plugins/run-store-sql.d.ts +15 -6
- package/dist/plugins/run-store-sql.js +15 -13
- package/dist/plugins/schema-indexes.js +1 -0
- 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/session-policy-store-sql.js +18 -5
- package/dist/plugins/store-backend.d.ts +13 -7
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +12 -5
- 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/read-face-posture.d.ts +21 -0
- package/dist/read-face-posture.js +9 -0
- 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/rules-consent.js +0 -11
- 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/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-policy-wire.d.ts +98 -12
- package/dist/session-policy-wire.js +71 -8
- 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 +11 -6
- package/dist/trace/project.js +12 -3
- package/dist/trace/redact.d.ts +7 -0
- package/dist/trace/redact.js +153 -61
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +2 -2
- package/dist/plugins/memory-session-policy-store.d.ts +0 -46
- package/dist/plugins/memory-session-policy-store.js +0 -52
|
@@ -55,10 +55,12 @@ export declare class MemoryRunStore {
|
|
|
55
55
|
/** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
|
|
56
56
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
57
57
|
setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
|
|
58
|
-
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
|
|
59
|
+
* S-173:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
|
|
60
|
+
setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
61
|
+
/** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review.
|
|
62
|
+
* S-173:同 {@link setSuspended} 落封口脱敏后的 `result`。 */
|
|
63
|
+
setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
62
64
|
/** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
|
|
63
65
|
* [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
|
|
64
66
|
markResuming(taskId: string, instanceId: string): Promise<boolean>;
|
|
@@ -124,7 +126,7 @@ export declare class MemoryRunStore {
|
|
|
124
126
|
/** Fail stale `running` rows (crashed-run backstop) + release task_active for any now-terminal row. */
|
|
125
127
|
reapStale(olderThanMs: number): Promise<number>;
|
|
126
128
|
/** [ref] shared tail of both suspended-run reapers: flip one parked row → failed('approval.expired'), the
|
|
127
|
-
* in-memory twin of the SQL UPDATE (`error`/`error_code` set directly — no result blob to derive from). */
|
|
129
|
+
* in-memory twin of the SQL UPDATE (`error`/`error_code` set directly — no result blob to derive from; S-173: the park-time `result` is cleared, same as the SQL `result = NULL`). */
|
|
128
130
|
private failExpiredPark;
|
|
129
131
|
/** Release the claim for TERMINAL rows only — suspended/needs_review keep it (the `DELETE ta … WHERE tr.status
|
|
130
132
|
* NOT IN ('running','suspended','needs_review')` twin, shared by reapStale + the [ref] suspended reapers). */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactErrorMessage, redactLedgerEventData,
|
|
1
|
+
import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../observability/run-terminal.js";
|
|
2
2
|
import { outwardPlane } from "../terminal.js";
|
|
3
3
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
4
4
|
import { notifyRunTerminal } from "../observability/run-terminal.js";
|
|
@@ -105,7 +105,7 @@ export class MemoryRunStore {
|
|
|
105
105
|
.map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
|
|
106
106
|
}
|
|
107
107
|
async setTerminal(taskId, status, result, error) {
|
|
108
|
-
result =
|
|
108
|
+
result = sealRunRowResult(result);
|
|
109
109
|
error = error === null ? null : (redactErrorMessage(error) ?? null);
|
|
110
110
|
const r = this.runs.get(taskId);
|
|
111
111
|
if (!r) {
|
|
@@ -140,17 +140,21 @@ export class MemoryRunStore {
|
|
|
140
140
|
if (pending)
|
|
141
141
|
notifyRunTerminal(pending);
|
|
142
142
|
}
|
|
143
|
-
async setSuspended(taskId) {
|
|
143
|
+
async setSuspended(taskId, result) {
|
|
144
|
+
const sealed = sealRunRowResult(result);
|
|
144
145
|
const r = this.runs.get(taskId);
|
|
145
146
|
if (r && (r.status === "running" || r.status === "suspended")) {
|
|
146
147
|
r.status = "suspended";
|
|
148
|
+
r.result = sealed;
|
|
147
149
|
r.updatedAt = new Date();
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
|
-
async setNeedsReview(taskId) {
|
|
152
|
+
async setNeedsReview(taskId, result) {
|
|
153
|
+
const sealed = sealRunRowResult(result);
|
|
151
154
|
const r = this.runs.get(taskId);
|
|
152
155
|
if (r && (r.status === "running" || r.status === "needs_review")) {
|
|
153
156
|
r.status = "needs_review";
|
|
157
|
+
r.result = sealed;
|
|
154
158
|
r.updatedAt = new Date();
|
|
155
159
|
}
|
|
156
160
|
}
|
|
@@ -159,6 +163,7 @@ export class MemoryRunStore {
|
|
|
159
163
|
if (!r || (r.status !== "suspended" && r.status !== "needs_review"))
|
|
160
164
|
return false;
|
|
161
165
|
r.status = "running";
|
|
166
|
+
r.result = null;
|
|
162
167
|
r.instanceId = instanceId;
|
|
163
168
|
r.cancelRequested = false;
|
|
164
169
|
r.preemptRequested = false;
|
|
@@ -292,6 +297,7 @@ export class MemoryRunStore {
|
|
|
292
297
|
for (const r of this.runs.values()) {
|
|
293
298
|
if (r.status === "running" && r.updatedAt.getTime() < cutoff) {
|
|
294
299
|
r.status = "failed";
|
|
300
|
+
r.result = null;
|
|
295
301
|
r.error = "run stalled (instance lost?)";
|
|
296
302
|
r.updatedAt = new Date();
|
|
297
303
|
reaped += 1;
|
|
@@ -302,6 +308,7 @@ export class MemoryRunStore {
|
|
|
302
308
|
}
|
|
303
309
|
failExpiredPark(r) {
|
|
304
310
|
r.status = "failed";
|
|
311
|
+
r.result = null;
|
|
305
312
|
r.error = "approval expired before decision";
|
|
306
313
|
r.errorCode = "approval.expired";
|
|
307
314
|
r.updatedAt = new Date();
|
|
@@ -16,8 +16,45 @@ export interface OneTimeMigrationReport {
|
|
|
16
16
|
/** M4:`device_audit.detail` 里 JSON 文本 `null` 被改成列 NULL 的行数(表不在 ⇒ 恒 0)。
|
|
17
17
|
* 非零 = 这次迁移真改写了存量字节,必须让运维看见(与 M1 的 `retiredSnapshotRows` 同一条纪律)。 */
|
|
18
18
|
readonly auditDetailJsonNullRows: number;
|
|
19
|
+
/** M5:`session_meta.incarnation` 列 —— `added` = 本次 `ADD COLUMN`;`present` = 早已在(新库 / 第二跑);
|
|
20
|
+
* `no-table` = 这只库上没有 `session_meta`(中央 DDL 之后不可达,只为不编一个读数);`lazy-on-read` = local 文件车道
|
|
21
|
+
* (没有这张表,存量会话首读补铸)。 */
|
|
22
|
+
readonly sessionIncarnationColumn: "added" | "present" | "no-table" | "lazy-on-read";
|
|
23
|
+
/** M5:本次给空化身铸了令牌的行数。非零 = 这次迁移真写了存量行,必须让运维看见(与 M4 同一条纪律)。 */
|
|
24
|
+
readonly sessionIncarnationsBackfilled: number;
|
|
19
25
|
}
|
|
20
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
|
|
28
|
+
* 规则(窗长 30 天、窗关后的第一个 minor 删码、门怎么判)在本文件头注「窗」一节,只写那一次。
|
|
29
|
+
*/
|
|
30
|
+
export interface OneTimeMigrationWindow {
|
|
31
|
+
/** 本文件里的条目名(头注「四条迁移」的 M1…M4)。 */
|
|
32
|
+
readonly id: string;
|
|
33
|
+
/** 引入这条迁移的版本 = 第一只含其代码的发布 tag(取证:`git log -S '<该条的常量名>' -- src/plugins/one-time-migrations.ts`
|
|
34
|
+
* 找引入提交 → `git tag --contains <提交> --sort=creatordate` 取最早)。 */
|
|
35
|
+
readonly introducedIn: string;
|
|
36
|
+
/** 该版本在 npm 上的发布日,UTC 日历日 YYYY-MM-DD(取证:`npm view @sema-agent/server time`)。`null` = **未发布形**:引入它的
|
|
37
|
+
* 那一版还没发布,由抬 `package.json` 到那一版的发布准备提交回填(规则见文件头「窗」一节;键必须显式写,缺席 ⇒ 到期门红)。 */
|
|
38
|
+
readonly releasedOn: string | null;
|
|
39
|
+
/** = `releasedOn` + 30 天(UTC 日历日;到期门逐条核这条等式)。今天晚于它 ⇒ 到期门红,删码排进其后第一个 minor。
|
|
40
|
+
* 与 `releasedOn` 同为 `null`(未发布形,不计到期)或同为日期。 */
|
|
41
|
+
readonly removableAfter: string | null;
|
|
42
|
+
/** 删码时删什么(到期门的红句逐字引用)。 */
|
|
43
|
+
readonly removal: string;
|
|
44
|
+
/** 删码之后,存量库若还带着迁移前的形,由哪一条**既有**响亮臂接住;没有就如实写没有(不为它新造兼容读)。 */
|
|
45
|
+
readonly afterRemoval: string;
|
|
46
|
+
/** 本条在 {@link OneTimeMigrationReport} 里的读数键 —— 报告的每个键恰属一条(到期门核:新迁移不登记 ⇒ 红)。 */
|
|
47
|
+
readonly reportKeys: readonly (keyof OneTimeMigrationReport)[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 🔴 S-590 —— 一次性迁移登记表,本文件各条的窗的**唯一属主**。别处(接口 JSDoc、调用点、DDL 注、部署文档)只写「窗口见
|
|
51
|
+
* `plugins/one-time-migrations.ts` 登记表」;起动日志行 `one_time_migrations_applied` 的 `removableAfter` 读数由本表派生。
|
|
52
|
+
*
|
|
53
|
+
* M1–M3 同一次删:M3 的「本版在这只库上跑过了」标记就是 M1 / M2 的退役 schema 在不在(头注「幂等」一节),先删 M1 / M2
|
|
54
|
+
* 而留着 M3 会让 M3 永远判「跑过了」。三条同版引入、同一天到期,本来就同一次删。
|
|
55
|
+
*/
|
|
56
|
+
export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
|
|
57
|
+
/** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数:那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
|
|
21
58
|
export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated">;
|
|
22
59
|
/** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */
|
|
23
60
|
export interface MigrationLogSink {
|
|
@@ -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). */
|