@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
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
* 给那个会话的 run 组 `spec.tools` 的同一只工厂、同一个会话分区(任务清单店 `taskListStoreFor(root)` 与领队那一张是同一张;
|
|
12
12
|
* 多租户的 WebFetch 过滤同样生效 ⇒ 子代同样拿不到)。座 = `(t) => tools.includes(t)`(身份判:同名副本不算)。
|
|
13
13
|
* · **禁**用 boot 期单例填座:那只 bundle 没有会话,它的任务清单是一张私有表 —— 填了座,每个会话 / 租户的复活队友都会共用它。
|
|
14
|
-
* ·
|
|
15
|
-
* 根会话删了(
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* · **根会话现在还是这张卡的那一位住客**才填座(codex r1 [high] + S-790 加严):任务清单店只按会话 id 分区,而会话 id 由调用方自选、
|
|
15
|
+
* 删后可被别人(或同一个人)重领 —— 根会话删了(或删了重建)之后赎回一张旧卡,按 id 重解出来的是**下一位住客**的清单。判据两条都要:
|
|
16
|
+
* ① 根会话的属主与卡的权威 principal 逐字相等;② 赎回腿 claim 前 / 后两次读到的是**同一位住客**(化身逐字相等,
|
|
17
|
+
* `session-incarnation.ts` 的 `sameSessionOccupant`)—— 只比属主比不出「同一个人删了重建」。任一条不成立 / 读数缺席 ⇒ 不交座
|
|
18
|
+
* (fail-closed:子代不继承,继承工具上的 allow 回 422、审批待决,deny 照结)。赎回腿自己在两次读不一致时整条拒(`409 decide.not_parked`),
|
|
19
|
+
* 这里的第 ② 条是同一条事实在交座这一处的判据(规则只写在这里,腿只负责读两次)。根删除时把子孙的 parked 卡一并作废 = S-790 件 B
|
|
20
|
+
* (`boot/session-faces.ts` 的删除协调器)。
|
|
18
21
|
* · 只对**一层**行填座(`rootSessionId` 在场且 = `parentSessionId`:派它的 run 就是根会话的 run,列表正是那个 run 的挂载)。更深的行
|
|
19
22
|
* (孙代:派它的是一个子代,它的挂载是继承裁剪后的子集,行上读不出来)不填 —— 用 boot 期那只裸 Agent、无座 = core 的 fail-closed 臂;
|
|
20
23
|
* 行上带继承组成是 core #1112(7.32.x)的半场,到货再改读行。
|
|
@@ -23,6 +26,7 @@
|
|
|
23
26
|
*/
|
|
24
27
|
import { type BackgroundAgentRecord, type ToolSpec } from "@sema-agent/core";
|
|
25
28
|
import type { Scenario } from "./capabilities/scenarios.js";
|
|
29
|
+
import { type SessionFace } from "./session-incarnation.js";
|
|
26
30
|
/** 一次赎回用的面:驱动的 Agent 工具 + (一层行时)与它同出一张列表的继承座。 */
|
|
27
31
|
export interface ParkedReviveSurface {
|
|
28
32
|
readonly reviveTool: ToolSpec;
|
|
@@ -30,12 +34,15 @@ export interface ParkedReviveSurface {
|
|
|
30
34
|
readonly mountsCallerTool?: (tool: ToolSpec) => boolean;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
|
-
* 按行解析赎回面的入参身份:`principal` = 权威 principal(取 checkpoint 的 scope 列,不取行上有损的 `row.scope`);`
|
|
34
|
-
*
|
|
37
|
+
* 按行解析赎回面的入参身份:`principal` = 权威 principal(取 checkpoint 的 scope 列,不取行上有损的 `row.scope`);`root` = 赎回腿对行的
|
|
38
|
+
* 根会话读到的两次会话面(claim 之前 / 之后;属主 `null` = 匿名会话)。缺席 = 没有读数 ⇒ 不交座。
|
|
35
39
|
*/
|
|
36
40
|
export interface ParkedReviveSurfaceIdentity {
|
|
37
41
|
readonly principal: string | undefined;
|
|
38
|
-
readonly
|
|
42
|
+
readonly root?: {
|
|
43
|
+
readonly before: SessionFace;
|
|
44
|
+
readonly after: SessionFace;
|
|
45
|
+
};
|
|
39
46
|
}
|
|
40
47
|
/** 按行解析赎回面。 */
|
|
41
48
|
export type ParkedReviveSurfaceFactory = (row: BackgroundAgentRecord, identity: ParkedReviveSurfaceIdentity) => ParkedReviveSurface;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
|
|
2
|
+
import { sameSessionOccupant } from "./session-incarnation.js";
|
|
2
3
|
const agentOf = (tools) => tools.find((t) => t.name === DEFAULT_SUBAGENT_TOOL_NAME);
|
|
3
4
|
export function createParkedReviveSurface(defaultScenario) {
|
|
4
5
|
const bootTool = agentOf(defaultScenario({}).tools);
|
|
@@ -8,7 +9,10 @@ export function createParkedReviveSurface(defaultScenario) {
|
|
|
8
9
|
const root = row.rootSessionId;
|
|
9
10
|
if (root === undefined || row.parentSessionId !== root)
|
|
10
11
|
return { reviveTool: bootTool };
|
|
11
|
-
|
|
12
|
+
const face = identity.root;
|
|
13
|
+
if (face === undefined || !sameSessionOccupant(face.before, face.after))
|
|
14
|
+
return { reviveTool: bootTool };
|
|
15
|
+
if ((face.before.owner ?? undefined) !== identity.principal || (face.after.owner ?? undefined) !== identity.principal)
|
|
12
16
|
return { reviveTool: bootTool };
|
|
13
17
|
const tools = defaultScenario({}, identity.principal, { sessionId: root }).tools;
|
|
14
18
|
const reviveTool = agentOf(tools);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Logger } from "../observability/logger.js";
|
|
1
2
|
import type { AskState } from "../approval-ask-machine.js";
|
|
2
3
|
import type { AskRow, AskTerminalClaimIntent, AskTerminalClaimOutcome, AskTransitionPatch, ApprovalAskStore, BatchRow, BindGateInput, BindResult, DecideAskInput, DecideResult, EnsureAskResult, ExpireResult, NewAskRow } from "./approval-ask-store-sql.js";
|
|
3
4
|
/** 数据根下的目录名。**按店命名**,与兄弟 File 店同规(`approval-nonces` / `approval-ask-audit` /
|
|
@@ -120,7 +121,9 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
|
|
|
120
121
|
*
|
|
121
122
|
* @param root local 数据根(= `LocalBackend` 的 `FileStorageBackend.root`,零新 env)。
|
|
122
123
|
*/
|
|
123
|
-
constructor(root: string
|
|
124
|
+
constructor(root: string, opts?: {
|
|
125
|
+
warn?: Logger["warn"];
|
|
126
|
+
});
|
|
124
127
|
/**
|
|
125
128
|
* 内核的钟。窗口外读 ⇒ **抛**(不静默回落 `Date.now()`):窗口外的读数按定义不在账本上,重放会得出
|
|
126
129
|
* 另一个时间戳 —— 那是一条**静默**的重放分岔,而本店的全部价值就是重放等价。今天内核的五个读钟点
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { closeSync, copyFileSync, openSync, readSync, statSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { AppendLog, atomicWriteFile, ensureDir
|
|
4
|
-
import {
|
|
3
|
+
import { AppendLog, atomicWriteFile, ensureDir } from "@sema-agent/core";
|
|
4
|
+
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
5
5
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
6
6
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
7
7
|
import { InMemoryApprovalAskStore, encodeDecideUpdatedInput, encodeStoreJsonPayload } from "./approval-ask-store-memory.js";
|
|
@@ -26,6 +26,10 @@ const JOURNAL_OP_WORDS = new Set(JOURNAL_OPS);
|
|
|
26
26
|
export const JOURNAL_OP_TABLE_GATE = true;
|
|
27
27
|
const JOURNAL_V1_CLAIM_INTENTS = ["expire", "cancel"];
|
|
28
28
|
const JOURNAL_V1_CLAIM_INTENT_WORDS = new Set(JOURNAL_V1_CLAIM_INTENTS);
|
|
29
|
+
const ASK_JOURNAL_SHAPE = {
|
|
30
|
+
what: "approval-ask journal record",
|
|
31
|
+
read: (record) => record,
|
|
32
|
+
};
|
|
29
33
|
function endsAtRecordBoundary(path) {
|
|
30
34
|
const size = statSync(path).size;
|
|
31
35
|
if (size === 0)
|
|
@@ -50,7 +54,7 @@ export class FileApprovalAskStore {
|
|
|
50
54
|
replayRejectedCount = 0;
|
|
51
55
|
corruptLineCount = 0;
|
|
52
56
|
corruptQuarantinePath;
|
|
53
|
-
constructor(root) {
|
|
57
|
+
constructor(root, opts) {
|
|
54
58
|
const dir = join(root, APPROVAL_ASK_DIR);
|
|
55
59
|
ensureDir(dir);
|
|
56
60
|
this.tmpDir = join(dir, ".tmp");
|
|
@@ -58,10 +62,8 @@ export class FileApprovalAskStore {
|
|
|
58
62
|
this.path = join(dir, APPROVAL_ASK_JOURNAL_FILE);
|
|
59
63
|
this.core = new InMemoryApprovalAskStore({ now: () => this.clockRead() });
|
|
60
64
|
const tornTail = existsSync(this.path) && !endsAtRecordBoundary(this.path);
|
|
61
|
-
const records =
|
|
62
|
-
|
|
63
|
-
recordFailOpen("server.approval-ask-journal.corrupt-record-skipped", `path=${info.path} reason=${info.reason}`);
|
|
64
|
-
}));
|
|
65
|
+
const { records, skipped } = replayJsonlSkipping(this.path, "approval-ask-journal", ASK_JOURNAL_SHAPE, opts?.warn);
|
|
66
|
+
this.corruptLineCount = skipped;
|
|
65
67
|
for (const rec of records)
|
|
66
68
|
this.replayRecord(rec);
|
|
67
69
|
this.records = records.length;
|
|
@@ -127,11 +129,6 @@ export class FileApprovalAskStore {
|
|
|
127
129
|
this.records = lines.length;
|
|
128
130
|
}
|
|
129
131
|
replayRecord(raw) {
|
|
130
|
-
if (raw === null || typeof raw !== "object") {
|
|
131
|
-
this.corruptLineCount += 1;
|
|
132
|
-
recordFailOpen("server.approval-ask-journal.corrupt-record-skipped", `path=${this.path} reason=record is not an object`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
132
|
const probe = raw;
|
|
136
133
|
const refuse = (why) => {
|
|
137
134
|
throw new StoreBootRefusalError(`FileApprovalAskStore: approval ask journal ${this.path} carries a record this build cannot replay — ${why} ` +
|
|
@@ -270,6 +270,10 @@ export declare function assertIntegralEpochMs(field: string, value: number): voi
|
|
|
270
270
|
export declare function assertIdempotencyKeyShape(key: string): void;
|
|
271
271
|
/** `settled_by` 列宽 = 这一格的唯一尺(两方言 DDL 与下面的形门读同一个常量,不手抄第二份)。 */
|
|
272
272
|
export declare const MAX_SETTLED_BY_CHARS = 16;
|
|
273
|
+
/** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 形门 ① 的**谓词本身**,店门口
|
|
274
|
+
* ({@link assertDecideSettledBy})与耐久 ask 回决口的 CAS 前体门(S-843:确定的体形错误拒成 400,不落进提交歧义臂)
|
|
275
|
+
* 读同一只,尺不抄第二份。 */
|
|
276
|
+
export declare function settledByFitsColumn(v: string): boolean;
|
|
273
277
|
/**
|
|
274
278
|
* S-558 的**入店卫生门**(三 twin 同判,与 {@link assertIdempotencyKeyShape} / {@link assertIntegralEpochMs} 同族)。
|
|
275
279
|
* 判的是**形**,不是**词** —— 词表属主恒是 core(`resolveAsk` 对自报词表外的值是响亮 fail-closed deny),
|
|
@@ -14,11 +14,14 @@ export function assertIdempotencyKeyShape(key) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
export const MAX_SETTLED_BY_CHARS = 16;
|
|
17
|
+
export function settledByFitsColumn(v) {
|
|
18
|
+
return v.length > 0 && v.length <= MAX_SETTLED_BY_CHARS;
|
|
19
|
+
}
|
|
17
20
|
export function assertDecideSettledBy(decision) {
|
|
18
21
|
const v = decision.settledBy;
|
|
19
22
|
if (v === undefined)
|
|
20
23
|
return;
|
|
21
|
-
if (v
|
|
24
|
+
if (!settledByFitsColumn(v)) {
|
|
22
25
|
throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
|
|
23
26
|
}
|
|
24
27
|
if (decision.decision !== "deny") {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
+
import type { Logger } from "../observability/logger.js";
|
|
3
4
|
export interface ApprovalExemptionRow {
|
|
4
5
|
toolName: string;
|
|
5
6
|
grantedBy: string | null;
|
|
@@ -43,11 +44,30 @@ export declare class MemoryApprovalExemptionStore implements ApprovalExemptionSt
|
|
|
43
44
|
revoke(sessionId: string, toolName: string): Promise<boolean>;
|
|
44
45
|
deleteBySession(sessionId: string): Promise<number>;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* 🔴 S-582 —— **fail-closed per session file.** A record in the MIDDLE of a session's log that cannot be read (bit rot,
|
|
49
|
+
* a hand edit, a line `42`, an object missing `toolName`) used to be skipped silently. When that line was a REVOKE
|
|
50
|
+
* tombstone, the grant before it survived the replay: a "don't ask again" the operator had revoked came back and the
|
|
51
|
+
* tool ran WITHOUT asking — the fail-open direction on the approval axis. We cannot tell which fact the line held, so
|
|
52
|
+
* the replay owner (`observability/corrupt-read-seat.ts`, seat `approval-exemption`) discloses it and closes that
|
|
53
|
+
* file's gate: `has` / `list` / `grant` / `revoke` for THAT session reject with `JsonlStoreCorruptError`, and every
|
|
54
|
+
* consumer already treats a rejected `has` as "not exempt" (ask anyway). Other sessions are untouched (one file = one
|
|
55
|
+
* session's facts). `deleteBySession` is NOT gated: right-to-delete must not be blocked by a bad byte, and deleting
|
|
56
|
+
* the file is exactly the remedy — it clears the gate with the file.
|
|
57
|
+
*/
|
|
46
58
|
export declare class FileApprovalExemptionStore implements ApprovalExemptionStore {
|
|
47
59
|
private readonly dir;
|
|
48
60
|
private readonly bySession;
|
|
49
61
|
private readonly logs;
|
|
50
|
-
|
|
62
|
+
/** S-582: sanitized session key → that session log's gate (only files that replayed a bad record are listed). */
|
|
63
|
+
private readonly corruptGates;
|
|
64
|
+
private readonly warn;
|
|
65
|
+
/** @param opts.warn corrupt-read disclosure seat (`LocalBackend` wires `discloseWarn`; absent ⇒ the owner falls back to `console.warn`, never to silence). */
|
|
66
|
+
constructor(root: string, opts?: {
|
|
67
|
+
warn?: Logger["warn"];
|
|
68
|
+
});
|
|
69
|
+
/** S-582: first line of every judging / appending method (see the class note). */
|
|
70
|
+
private assertUsable;
|
|
51
71
|
private pathFor;
|
|
52
72
|
/** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. */
|
|
53
73
|
private hydrate;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
3
|
-
import { AppendLog, ensureDir,
|
|
3
|
+
import { AppendLog, ensureDir, sanitizePathComponent } from "@sema-agent/core";
|
|
4
|
+
import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
|
|
4
5
|
export class TiDBApprovalExemptionStore {
|
|
5
6
|
pool;
|
|
6
7
|
constructor(pool) {
|
|
@@ -98,15 +99,36 @@ export class MemoryApprovalExemptionStore {
|
|
|
98
99
|
return n;
|
|
99
100
|
}
|
|
100
101
|
}
|
|
102
|
+
const EXEMPTION_RECORD_SHAPE = {
|
|
103
|
+
what: "approval-exemption record without a string toolName (or with a mistyped revoked / grantedBy / ts)",
|
|
104
|
+
read(record) {
|
|
105
|
+
const { toolName, revoked, grantedBy, ts } = record;
|
|
106
|
+
if (typeof toolName !== "string")
|
|
107
|
+
return undefined;
|
|
108
|
+
if (!(revoked === undefined || typeof revoked === "boolean"))
|
|
109
|
+
return undefined;
|
|
110
|
+
if (!(grantedBy === undefined || grantedBy === null || typeof grantedBy === "string"))
|
|
111
|
+
return undefined;
|
|
112
|
+
if (!(ts === undefined || typeof ts === "string"))
|
|
113
|
+
return undefined;
|
|
114
|
+
return { toolName, grantedBy: grantedBy ?? null, ts: ts ?? "", ...(revoked !== undefined ? { revoked } : {}) };
|
|
115
|
+
},
|
|
116
|
+
};
|
|
101
117
|
export class FileApprovalExemptionStore {
|
|
102
118
|
dir;
|
|
103
119
|
bySession = new Map();
|
|
104
120
|
logs = new Map();
|
|
105
|
-
|
|
121
|
+
corruptGates = new Map();
|
|
122
|
+
warn;
|
|
123
|
+
constructor(root, opts) {
|
|
106
124
|
this.dir = join(root, "approval-exemptions");
|
|
125
|
+
this.warn = opts?.warn;
|
|
107
126
|
ensureDir(this.dir);
|
|
108
127
|
this.hydrate();
|
|
109
128
|
}
|
|
129
|
+
assertUsable(key) {
|
|
130
|
+
this.corruptGates.get(key)?.assertUsable();
|
|
131
|
+
}
|
|
110
132
|
pathFor(sessionId) {
|
|
111
133
|
return join(this.dir, `${sanitizePathComponent(sessionId)}.jsonl`);
|
|
112
134
|
}
|
|
@@ -122,13 +144,13 @@ export class FileApprovalExemptionStore {
|
|
|
122
144
|
if (!name.endsWith(".jsonl"))
|
|
123
145
|
continue;
|
|
124
146
|
const key = name.slice(0, -".jsonl".length);
|
|
125
|
-
const records =
|
|
147
|
+
const { records, gate } = replayJsonlGuarded(join(this.dir, name), "approval-exemption", EXEMPTION_RECORD_SHAPE, this.warn);
|
|
148
|
+
if (gate.tripped)
|
|
149
|
+
this.corruptGates.set(key, gate);
|
|
126
150
|
if (!records.length)
|
|
127
151
|
continue;
|
|
128
152
|
const m = this.bySession.get(key) ?? new Map();
|
|
129
153
|
for (const r of records) {
|
|
130
|
-
if (!r || typeof r.toolName !== "string")
|
|
131
|
-
continue;
|
|
132
154
|
if (r.revoked) {
|
|
133
155
|
m.delete(r.toolName);
|
|
134
156
|
}
|
|
@@ -154,6 +176,7 @@ export class FileApprovalExemptionStore {
|
|
|
154
176
|
}
|
|
155
177
|
async grant(sessionId, toolName, grantedBy) {
|
|
156
178
|
const key = sanitizePathComponent(sessionId);
|
|
179
|
+
this.assertUsable(key);
|
|
157
180
|
this.logFor(sessionId).append({ toolName, grantedBy: grantedBy ?? null, ts: new Date().toISOString() }, true);
|
|
158
181
|
let m = this.bySession.get(key);
|
|
159
182
|
if (!m) {
|
|
@@ -164,10 +187,14 @@ export class FileApprovalExemptionStore {
|
|
|
164
187
|
m.set(toolName, { grantedBy, createdAt: prior?.createdAt ?? Date.now() });
|
|
165
188
|
}
|
|
166
189
|
async has(sessionId, toolName) {
|
|
167
|
-
|
|
190
|
+
const key = sanitizePathComponent(sessionId);
|
|
191
|
+
this.assertUsable(key);
|
|
192
|
+
return this.bySession.get(key)?.has(toolName) ?? false;
|
|
168
193
|
}
|
|
169
194
|
async list(sessionId) {
|
|
170
|
-
const
|
|
195
|
+
const key = sanitizePathComponent(sessionId);
|
|
196
|
+
this.assertUsable(key);
|
|
197
|
+
const m = this.bySession.get(key);
|
|
171
198
|
if (!m)
|
|
172
199
|
return [];
|
|
173
200
|
return [...m.entries()]
|
|
@@ -176,6 +203,7 @@ export class FileApprovalExemptionStore {
|
|
|
176
203
|
}
|
|
177
204
|
async revoke(sessionId, toolName) {
|
|
178
205
|
const key = sanitizePathComponent(sessionId);
|
|
206
|
+
this.assertUsable(key);
|
|
179
207
|
const m = this.bySession.get(key);
|
|
180
208
|
if (!m?.has(toolName))
|
|
181
209
|
return false;
|
|
@@ -200,6 +228,7 @@ export class FileApprovalExemptionStore {
|
|
|
200
228
|
throw new Error(`approval-exemption purge failed for ${sessionId}: ${e instanceof Error ? e.message : String(e)}`);
|
|
201
229
|
}
|
|
202
230
|
this.bySession.delete(key);
|
|
231
|
+
this.corruptGates.delete(key);
|
|
203
232
|
return removed;
|
|
204
233
|
}
|
|
205
234
|
dispose() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
3
|
import { type SqlDriver } from "./sql-driver.js";
|
|
4
|
+
import type { Logger } from "../observability/logger.js";
|
|
4
5
|
/** 表名(单数形;`test/schema-naming-invariants.test.ts` 的闭集词表钉着)。 */
|
|
5
6
|
export declare const APPROVAL_NONCE_TABLE = "approval_nonce";
|
|
6
7
|
/**
|
|
@@ -71,7 +72,15 @@ export declare class MemoryApprovalNonceStore implements ApprovalNonceStore {
|
|
|
71
72
|
*
|
|
72
73
|
* 崩溃语义:`append` 走 fsync。丢一条记录 = 那个 nonce 在它剩下的 TTL 里可被重放一次(与「整台副本重启」
|
|
73
74
|
* 的既有暴露同级,且 SQL 车道不受此影响);**绝不**能出现的是反向 —— 记录还在却被读成不在,那要靠
|
|
74
|
-
*
|
|
75
|
+
* core JSONL 读法的断尾安全(半行丢弃)兜住。
|
|
76
|
+
*
|
|
77
|
+
* 🔴 **S-582:中间一行读不出 ⇒ 整本拒用(fail-closed)**。撕尾是崩溃的正常形(core 丢弃且不报);而**中间**
|
|
78
|
+
* 那一行读不出(位腐 / 手改 / 一行 `42` / 缺键的对象)时,我们不知道它是哪一只 nonce —— 修前静默跳过它,等于
|
|
79
|
+
* 让那只已消费的证明再被受理一次(S-280 要关的重放洞换了个入口)。现在由属主披露(`approval_nonce_corrupt_read`)
|
|
80
|
+
* 并合上 {@link gate}:`consume` / `reapExpired` 第一行即带码拒(`JsonlStoreCorruptError`,外层答 500);`nowMs` 不读账、
|
|
81
|
+
* 不经闸。清扫同样要拒:压实会把幸存者原样写回、把坏行连同证据一起抹掉,下一次重启读到的就是一本「干净」的账 ——
|
|
82
|
+
* 闸自己打开,而那只 nonce 仍然没被记住。出路在拒绝句里(移开文件的代价与安全时机)。
|
|
83
|
+
* 代价如实:这台 local 机器上直连门的 plan_review 在有人处置之前全部 500(`/decide` 不经本店,不受影响)。
|
|
75
84
|
*/
|
|
76
85
|
export declare class FileApprovalNonceStore implements ApprovalNonceStore {
|
|
77
86
|
private readonly path;
|
|
@@ -80,7 +89,12 @@ export declare class FileApprovalNonceStore implements ApprovalNonceStore {
|
|
|
80
89
|
* 从复合键里**解析**出来是一条没必要的反向铸形 —— 记录留着就不必解析。 */
|
|
81
90
|
private readonly rows;
|
|
82
91
|
private log;
|
|
83
|
-
|
|
92
|
+
/** S-582:本账的损坏闸(见类注)。 */
|
|
93
|
+
private readonly gate;
|
|
94
|
+
/** @param opts.warn 腐读披露座(`LocalBackend` 接 `discloseWarn`;缺席 ⇒ 属主退回 `console.warn`,不退回沉默)。 */
|
|
95
|
+
constructor(root: string, opts?: {
|
|
96
|
+
warn?: Logger["warn"];
|
|
97
|
+
});
|
|
84
98
|
private logFor;
|
|
85
99
|
/** local 车道:店 = 这台机器上的这个数据根,所以店钟就是本进程的墙钟(理由同 Memory 腿)。 */
|
|
86
100
|
nowMs(): Promise<number>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { atomicWriteFile, ensureDir,
|
|
2
|
+
import { atomicWriteFile, ensureDir, AppendLog } from "@sema-agent/core";
|
|
3
3
|
import { mysqlDriver, pgDriver, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-driver.js";
|
|
4
4
|
import { isDupKeyError } from "./sql-errors.js";
|
|
5
|
+
import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
|
|
5
6
|
export const APPROVAL_NONCE_TABLE = "approval_nonce";
|
|
6
7
|
export const MAX_APPROVAL_NONCE_CHARS = 190;
|
|
7
8
|
export class SqlApprovalNonceStore {
|
|
@@ -66,22 +67,29 @@ export class MemoryApprovalNonceStore {
|
|
|
66
67
|
return n;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
const NONCE_RECORD_SHAPE = {
|
|
71
|
+
what: "approval-nonce record without a string s / string n / numeric e",
|
|
72
|
+
read(record) {
|
|
73
|
+
const { s, n, e } = record;
|
|
74
|
+
return typeof s === "string" && typeof n === "string" && typeof e === "number" ? { s, n, e } : undefined;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
69
77
|
export class FileApprovalNonceStore {
|
|
70
78
|
path;
|
|
71
79
|
tmpDir;
|
|
72
80
|
rows = new Map();
|
|
73
81
|
log;
|
|
74
|
-
|
|
82
|
+
gate;
|
|
83
|
+
constructor(root, opts) {
|
|
75
84
|
const dir = join(root, "approval-nonces");
|
|
76
85
|
ensureDir(dir);
|
|
77
86
|
this.tmpDir = join(dir, ".tmp");
|
|
78
87
|
ensureDir(this.tmpDir);
|
|
79
88
|
this.path = join(dir, "consumed.jsonl");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.rows.set(keyOf(r.s, r.n),
|
|
84
|
-
}
|
|
89
|
+
const { records, gate } = replayJsonlGuarded(this.path, "approval-nonce", NONCE_RECORD_SHAPE, opts?.warn);
|
|
90
|
+
this.gate = gate;
|
|
91
|
+
for (const r of records)
|
|
92
|
+
this.rows.set(keyOf(r.s, r.n), r);
|
|
85
93
|
}
|
|
86
94
|
logFor() {
|
|
87
95
|
return (this.log ??= new AppendLog(this.path));
|
|
@@ -90,6 +98,7 @@ export class FileApprovalNonceStore {
|
|
|
90
98
|
return Date.now();
|
|
91
99
|
}
|
|
92
100
|
async consume(sessionId, nonce, expiresAtMs) {
|
|
101
|
+
this.gate.assertUsable();
|
|
93
102
|
const k = keyOf(sessionId, nonce);
|
|
94
103
|
if (this.rows.has(k))
|
|
95
104
|
return false;
|
|
@@ -99,6 +108,7 @@ export class FileApprovalNonceStore {
|
|
|
99
108
|
return true;
|
|
100
109
|
}
|
|
101
110
|
async reapExpired() {
|
|
111
|
+
this.gate.assertUsable();
|
|
102
112
|
const nowMs = await this.nowMs();
|
|
103
113
|
const survivors = [];
|
|
104
114
|
let removed = 0;
|
|
@@ -3,6 +3,7 @@ import type { SessionListItem } from "../security.js";
|
|
|
3
3
|
import type { StagingHandle } from "../session-sync-kernel.js";
|
|
4
4
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
5
5
|
import type { InstructionSourceStore } from "../instruction-source-notice.js";
|
|
6
|
+
import type { SessionFaceReader } from "../session-incarnation.js";
|
|
6
7
|
export interface CachingOptions {
|
|
7
8
|
/** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */
|
|
8
9
|
ttlSec: number;
|
|
@@ -37,6 +38,10 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
37
38
|
private readonly timer?;
|
|
38
39
|
/** Delegated tenant-ownership ops, present only if the inner store exposes them (the TiDB store does). */
|
|
39
40
|
readonly ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
|
|
41
|
+
/** S-790:属主 + 化身一次读,纯转发(不经暖缓存 —— 化身判的就是「此刻坐在座位上的是谁」,缓存的旧答案正是要消灭的那一形)。
|
|
42
|
+
* 🔴 必须转发(与 retention / placements / instruction-source 同一条教训):生产默认把真店包进本类,不转发 = parked 决定腿
|
|
43
|
+
* 与记忆座在默认部署上读不到化身。 */
|
|
44
|
+
readonly sessionFaceOf?: SessionFaceReader;
|
|
40
45
|
readonly register?: (sessionId: string, owner: string | null) => Promise<void>;
|
|
41
46
|
/** Delegated SessionRepo seam (§0.5) — list/fork/delete operate on the DURABLE tables directly (they bypass the
|
|
42
47
|
* warm cache by design; a fork/delete must read/write durable state, not a possibly-stale cached view). */
|
|
@@ -6,6 +6,7 @@ export class CachingSessionStore {
|
|
|
6
6
|
ttlMs;
|
|
7
7
|
timer;
|
|
8
8
|
ownerOf;
|
|
9
|
+
sessionFaceOf;
|
|
9
10
|
register;
|
|
10
11
|
listSessions;
|
|
11
12
|
fork;
|
|
@@ -45,6 +46,8 @@ export class CachingSessionStore {
|
|
|
45
46
|
const ownerAware = inner;
|
|
46
47
|
if (ownerAware.ownerOf)
|
|
47
48
|
this.ownerOf = ownerAware.ownerOf.bind(inner);
|
|
49
|
+
if (ownerAware.sessionFaceOf)
|
|
50
|
+
this.sessionFaceOf = ownerAware.sessionFaceOf.bind(inner);
|
|
48
51
|
if (ownerAware.register)
|
|
49
52
|
this.register = ownerAware.register.bind(inner);
|
|
50
53
|
if (ownerAware.listSessions)
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import type { Logger } from "../observability/logger.js";
|
|
1
2
|
export declare class FileResumeAnchorStore {
|
|
2
3
|
private readonly dir;
|
|
3
4
|
/** sessionId → (eventId → {entryId, owner}) — the same in-memory shape MemoryResumeAnchorStore holds, hydrated on ctor. */
|
|
4
5
|
private readonly bySession;
|
|
5
6
|
/** sessionId → the open append log (one O_APPEND fd per session, lazily opened on first put, held for lifetime). */
|
|
6
7
|
private readonly logs;
|
|
7
|
-
|
|
8
|
+
private readonly warn;
|
|
9
|
+
/** @param opts.warn corrupt-read disclosure seat (S-582; absent ⇒ the replay owner falls back to `console.warn`). */
|
|
10
|
+
constructor(root: string, opts?: {
|
|
11
|
+
warn?: Logger["warn"];
|
|
12
|
+
});
|
|
8
13
|
/** `<dir>/<sanitizePathComponent(sessionId)>.jsonl` — the per-session log path (sessionId is uuidv7, already safe). */
|
|
9
14
|
private pathFor;
|
|
10
|
-
/** Replay every session log into the in-memory index on boot (torn-tail-safe via
|
|
15
|
+
/** Replay every session log into the in-memory index on boot (torn-tail-safe via the replay owner; last-wins upsert
|
|
11
16
|
* so a re-stamped eventId reflects the latest appended record). The in-memory index is keyed by the SANITIZED
|
|
12
17
|
* sessionId throughout (put/resolve/list/delete all sanitize), and each on-disk file's base name IS
|
|
13
18
|
* `sanitizePathComponent(sessionId)` — so the index key is just the file base, with no de-sanitize needed. */
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
3
|
-
import { AppendLog, ensureDir,
|
|
3
|
+
import { AppendLog, ensureDir, sanitizePathComponent } from "@sema-agent/core";
|
|
4
|
+
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
4
5
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
6
|
+
const ANCHOR_RECORD_SHAPE = {
|
|
7
|
+
what: "resume-anchor record without a string eventId / entryId",
|
|
8
|
+
read(record) {
|
|
9
|
+
const { eventId, entryId, owner } = record;
|
|
10
|
+
if (typeof eventId !== "string" || typeof entryId !== "string")
|
|
11
|
+
return undefined;
|
|
12
|
+
return { eventId, entryId, owner: typeof owner === "string" ? owner : null, ownerReadable: owner === undefined || owner === null || typeof owner === "string" };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
5
15
|
export class FileResumeAnchorStore {
|
|
6
16
|
dir;
|
|
7
17
|
bySession = new Map();
|
|
8
18
|
logs = new Map();
|
|
9
|
-
|
|
19
|
+
warn;
|
|
20
|
+
constructor(root, opts) {
|
|
10
21
|
this.dir = join(root, "resume-anchors");
|
|
22
|
+
this.warn = opts?.warn;
|
|
11
23
|
ensureDir(this.dir);
|
|
12
24
|
this.hydrate();
|
|
13
25
|
}
|
|
@@ -26,14 +38,16 @@ export class FileResumeAnchorStore {
|
|
|
26
38
|
if (!name.endsWith(".jsonl"))
|
|
27
39
|
continue;
|
|
28
40
|
const key = name.slice(0, -".jsonl".length);
|
|
29
|
-
const records =
|
|
41
|
+
const { records } = replayJsonlSkipping(join(this.dir, name), "resume-anchor", ANCHOR_RECORD_SHAPE, this.warn);
|
|
30
42
|
if (!records.length)
|
|
31
43
|
continue;
|
|
32
44
|
const m = this.bySession.get(key) ?? new Map();
|
|
33
45
|
for (const r of records) {
|
|
34
|
-
if (!r
|
|
46
|
+
if (!r.ownerReadable) {
|
|
47
|
+
m.delete(r.eventId);
|
|
35
48
|
continue;
|
|
36
|
-
|
|
49
|
+
}
|
|
50
|
+
m.set(r.eventId, { entryId: r.entryId, owner: r.owner });
|
|
37
51
|
}
|
|
38
52
|
this.bySession.set(key, m);
|
|
39
53
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type UsageRow } from "../usage-analytics.js";
|
|
2
2
|
import type { TaskStatus } from "@sema-agent/core";
|
|
3
|
+
import type { Logger } from "../observability/logger.js";
|
|
3
4
|
import { StoreBootRefusalError, type RunRecord, type SessionSummary, type RunEvent, type PersistedTaskResult, type LatestRunRef } from "./store-contracts.js";
|
|
4
5
|
import type { RunStoreCheckpointProbe } from "./memory-run-store.js";
|
|
5
6
|
import type { LedgerEventType } from "../trace/ledger-events.js";
|
|
@@ -45,9 +46,12 @@ export declare class FileRunStore {
|
|
|
45
46
|
/** [ref] 件B(census 第 29 行终局):`RUN_STORE_STRICT_HYDRATE` 的**部署级**表态。
|
|
46
47
|
* `true` ⇒ hydrate 的 readdir 失败拒启;缺席/`false` ⇒ 件6 的记债腿,行为字节不变。 */
|
|
47
48
|
private readonly strictHydrate;
|
|
49
|
+
/** S-582:三个盘面坏行的腐读披露座(`LocalBackend` 接 `discloseWarn`;缺席 ⇒ 重放属主退回 `console.warn`)。 */
|
|
50
|
+
private readonly corruptReadWarn;
|
|
48
51
|
constructor(root: string, opts?: {
|
|
49
52
|
error?(msg: string, fields?: Record<string, unknown>): void;
|
|
50
53
|
strictHydrate?: boolean;
|
|
54
|
+
warn?: Logger["warn"];
|
|
51
55
|
});
|
|
52
56
|
private runDir;
|
|
53
57
|
private runJsonPath;
|
|
@@ -58,8 +62,9 @@ export declare class FileRunStore {
|
|
|
58
62
|
private toRecord;
|
|
59
63
|
/** Replay the on-disk registry/claims/event-logs into the in-memory index on boot. Each plane is torn-tail-/
|
|
60
64
|
* partial-write-safe (run.json via atomicWriteFile can never be half-written; the active file via writeThenLink is
|
|
61
|
-
* all-or-nothing; events.jsonl via
|
|
62
|
-
* skipped — that run is lost, not a poison that fails the boot
|
|
65
|
+
* all-or-nothing; events.jsonl via the JSONL replay drops a crash tail). A corrupt run.json (failed parse) is
|
|
66
|
+
* skipped — that run is lost, not a poison that fails the boot — and since S-582 the skip is disclosed and counted
|
|
67
|
+
* (`server.run-store.corrupt-record-skipped`), never silent. */
|
|
63
68
|
/**
|
|
64
69
|
* hydrate 期**逐文件**读盘的统一口([ref] 件B / codex 对抗复审 R4-[high])。
|
|
65
70
|
*
|
|
@@ -73,6 +78,12 @@ export declare class FileRunStore {
|
|
|
73
78
|
* 吞、也不改形),既有的「坏行跳过、不毒 boot」宽容契约与既有的抛法一起原封保留。
|
|
74
79
|
*/
|
|
75
80
|
private replay;
|
|
81
|
+
/**
|
|
82
|
+
* S-582 —— 本店**唯一**的盘上记录读口(hydrate 经 {@link replay} 包一层 strict 档,运行期读点直调它):
|
|
83
|
+
* 交形判据给重放属主,坏行按登记名 `run-store` 跳过 + 披露 + 计数。strict 档的射程是「文件读不出」,
|
|
84
|
+
* 不含「文件里有坏行」—— 后者在两档下都走这里的 F 臂(普查表第 96 行)。
|
|
85
|
+
*/
|
|
86
|
+
private readRecords;
|
|
76
87
|
/**
|
|
77
88
|
* strict 档下的**存在性判别**(codex R4-[high] 的前半):`existsSync` 对 ENOENT 与 EACCES 同答 false,
|
|
78
89
|
* 而这两句话在本门下必须分开 —— 前者是常态(`runs/` 下的非目录杂物、还没落盘的目录),后者是「我读
|
|
@@ -110,10 +121,12 @@ export declare class FileRunStore {
|
|
|
110
121
|
/** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
|
|
111
122
|
* [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
|
|
112
123
|
setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
|
|
113
|
-
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard).
|
|
125
|
+
* S-173:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。 */
|
|
126
|
+
setSuspended(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
127
|
+
/** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review.
|
|
128
|
+
* S-173:同 {@link setSuspended} 落封口脱敏后的 `result`。 */
|
|
129
|
+
setNeedsReview(taskId: string, result: PersistedTaskResult | null): Promise<void>;
|
|
117
130
|
/** Durable F4 resume: flip suspended/needs_review → running (CAS) + reset cancel/preempt flags.
|
|
118
131
|
* [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改。 */
|
|
119
132
|
markResuming(taskId: string, instanceId: string): Promise<boolean>;
|
|
@@ -192,7 +205,7 @@ export declare class FileRunStore {
|
|
|
192
205
|
/** [ref] wire the checkpoint probe (LocalBackend.checkpoint(), where checkpoint + run store share one root). */
|
|
193
206
|
setCheckpointProbe(probe: RunStoreCheckpointProbe): void;
|
|
194
207
|
/** [ref] shared tail of both suspended-run reapers: flip one parked row → failed('approval.expired') and
|
|
195
|
-
* PERSIST it — the file twin of the SQL UPDATE (`error`/`error_code` set directly, no result blob). */
|
|
208
|
+
* PERSIST it — the file twin of the SQL UPDATE (`error`/`error_code` set directly, no result blob; S-173: the park-time `result` is cleared, same as the SQL `result = NULL`). */
|
|
196
209
|
private failExpiredPark;
|
|
197
210
|
/**
|
|
198
211
|
* [ref] Durable F4 expiry — the file twin of TiDBRunStore.reapSuspended, driven by the injected checkpoint
|
|
@@ -294,7 +307,7 @@ export declare class FileRunStore {
|
|
|
294
307
|
/** Drop a run entirely (registry + event log + open fd + on-disk dir) — used by deleteBySession. */
|
|
295
308
|
private dropRun;
|
|
296
309
|
/**
|
|
297
|
-
* 🔴 **一次性迁移
|
|
310
|
+
* 🔴 **一次性迁移 M3 的 local 腿(窗口见 `one-time-migrations.ts` 登记表,随 M3 一起删)** —— 退役词旧拼法的账本改写,
|
|
298
311
|
* SQL 两孪生那条整表 UPDATE 的 local 形。逐条走**同一只**纯改写函数,所以三条车道改出来的字节一致。
|
|
299
312
|
*
|
|
300
313
|
* 为什么是 store 上的一只动词、而不是外面一个文件走查:本店的账本有**两份**在场 —— 盘上的 jsonl
|