@sema-agent/server 7.112.0 → 7.114.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 +95 -0
- package/USAGE.md +4 -4
- package/dist/approval-ask-audit-store.d.ts +3 -4
- package/dist/approval-ask-audit-store.js +3 -3
- package/dist/approval-card.d.ts +4 -4
- package/dist/boot/coordinators.js +1 -1
- package/dist/boot/memory-boundary.d.ts +2 -1
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/resolve-spec.js +2 -2
- package/dist/boot/stores.js +4 -6
- package/dist/boot/workflow-orchestration.js +4 -4
- package/dist/config-catalog.js +6 -5
- package/dist/config-invariants.d.ts +27 -0
- package/dist/config-invariants.js +54 -39
- package/dist/config-types.d.ts +5 -11
- package/dist/config-types.js +0 -1
- package/dist/config.d.ts +0 -16
- package/dist/config.js +44 -69
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +5 -1
- package/dist/execution-lane-caps.d.ts +54 -30
- package/dist/execution-lane-caps.js +48 -20
- package/dist/hooks/hook-runner.d.ts +87 -7
- package/dist/hooks/hook-runner.js +113 -20
- package/dist/host-decision.d.ts +5 -3
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -4
- package/dist/http/request-guards.js +2 -2
- package/dist/http/resume-legs.d.ts +9 -10
- package/dist/http/resume-legs.js +64 -18
- package/dist/http/route-ctx.d.ts +38 -8
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/routes/background.js +2 -2
- package/dist/http/routes/capabilities.js +2 -2
- package/dist/http/routes/mcp-reconnect.js +2 -2
- package/dist/http/routes/notify-wake.js +3 -3
- package/dist/http/routes/runs.js +19 -13
- package/dist/http/routes/session-sync.js +6 -6
- package/dist/http/routes/sessions.js +20 -20
- package/dist/http/routes/tasks.js +1 -0
- package/dist/http/send.d.ts +1 -1
- package/dist/http/send.js +1 -1
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/local-data-root.d.ts +14 -0
- package/dist/local-data-root.js +5 -0
- package/dist/observability/corrupt-read-seat.d.ts +46 -66
- package/dist/observability/corrupt-read-seat.js +79 -63
- package/dist/observability/fail-open.d.ts +28 -0
- package/dist/observability/fail-open.js +28 -0
- package/dist/observability/run-terminal.js +39 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +1 -5
- package/dist/orchestration/workflow-agent-session-index.js +2 -2
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -2
- package/dist/plugins/approval-ask-store-file.d.ts +1 -4
- package/dist/plugins/approval-ask-store-file.js +2 -2
- package/dist/plugins/file-resume-anchor-store.d.ts +1 -6
- package/dist/plugins/file-resume-anchor-store.js +2 -4
- package/dist/plugins/file-run-store.d.ts +0 -4
- package/dist/plugins/file-run-store.js +1 -3
- package/dist/plugins/remote-env-host.d.ts +0 -35
- package/dist/plugins/remote-env-host.js +0 -8
- package/dist/plugins/remote-env-local-docker.d.ts +5 -13
- package/dist/plugins/remote-env-local-docker.js +2 -4
- package/dist/plugins/send-file-ledger.d.ts +1 -5
- package/dist/plugins/send-file-ledger.js +2 -2
- package/dist/plugins/store-backend.js +8 -9
- package/dist/plugins/user-input-ledger-file.d.ts +1 -5
- package/dist/plugins/user-input-ledger-file.js +2 -2
- package/dist/run-local.d.ts +1 -1
- package/dist/run-local.js +2 -2
- package/dist/runs.js +1 -0
- package/dist/spec-fields.js +0 -1
- package/dist/store-posture.d.ts +7 -7
- package/dist/store-posture.js +0 -1
- package/dist/task-settings.d.ts +13 -0
- package/dist/task-settings.js +3 -0
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.js +2 -3
- package/dist/trace/redact.d.ts +18 -0
- package/dist/trace/redact.js +4 -0
- package/dist/unattended-permission-mode.d.ts +9 -2
- package/dist/unattended-permission-mode.js +5 -1
- package/dist/user-input-ledger.d.ts +24 -5
- package/dist/user-input-ledger.js +46 -11
- package/package.json +4 -3
- package/scripts/retired-mode-seed-drain-local.mjs +317 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **本机数据根的唯一解析**(`DB_BACKEND=local` 的 file 店把全部落盘件放在这个目录下;`config.localDataRoot` 就是它的值)。
|
|
3
|
+
*
|
|
4
|
+
* 链(第一个非空者胜,空串视作缺席):`LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `<home>/.ai-agent`。
|
|
5
|
+
*
|
|
6
|
+
* 为什么是一只**纯函数、env 与 home 都由调用方递入**:本函数有两个读者 —— 服务自己的 `loadConfig`(递 `process.env` / `os.homedir()`)与随包发的运维脚本
|
|
7
|
+
* `scripts/retired-mode-seed-drain-local.mjs`(经 `dist/local-data-root.js` 取同一只函数,缺省数据根不再自抄一份链:抄本只对了一半,
|
|
8
|
+
* 设了 `AGENT_DATA_DIR` 的部署不带参数跑脚本会读到另一个目录并报「干净」)。纯叶、零反向依赖 ⇒ 脚本引它不会把整个服务装载起来。
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ 与**配置根**(`configLocalRoot`:`CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`,不吃 `LOCAL_DATA_ROOT`)不是同一条链 —— 两根分家的那一形由
|
|
11
|
+
* boot 期的 `local_roots_split` 告警说出来,本函数不改那条解析。
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveLocalDataRoot(env: Readonly<Record<string, string | undefined>>, home: string): string;
|
|
14
|
+
//# sourceMappingURL=local-data-root.d.ts.map
|
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 🔴 **腐读披露座的唯一属主**(S-569,core 7.28.1 提货批;#1013 F4 的 server 半场)。
|
|
3
|
-
*
|
|
4
|
-
* ## core 那边发生了什么
|
|
5
|
-
* core 对**读不出来的**持久文件是 documented fail-open —— 当作「缺席」继续跑(抛会让整条任务死,也会
|
|
6
|
-
* 自锁修复写)。7.27.0(#1013 F4)把这条 fail-open 的披露面收成**一只共用 guard**
|
|
7
|
-
* (`stores/file/corrupt-read-sink.js`:`CorruptReadInfo` + `guardCorruptSink`),并给每一只容错 File 店
|
|
8
|
-
* 都开了座 —— core 自己的原话是那只 guard「hand-written four times」,其中三份包住了 async sink 的
|
|
9
|
-
* rejection、mailbox 那份只包住同步 throw。**同一个病,server 这侧也有**:座的载荷投影当时在本仓被
|
|
10
|
-
* 手抄了四遍(roster / mailbox / run-local 与 store-backend 的两只 FileStorageBackend)。
|
|
11
|
-
*
|
|
12
|
-
* ## 为什么是一个属主,而不是第五、六、七、八份手抄([ref])
|
|
13
|
-
* 7.27.0 之后 server 真构造的店里又多出**四只空座**(`FileCheckpointStore` / `FileWorkflowRunStore` /
|
|
14
|
-
* `FileBackgroundAgentStore` / `FileWorkflowJournalStore`)。按老形接线 = 八处各写一遍
|
|
15
|
-
* `{ path, reason, ...可选成员 }` 的转发 —— 而这八处**本来就该是同一句话**:
|
|
16
|
-
*
|
|
17
|
-
* 「一次持久读因为字节读不出来被当成了缺席。ENOENT 不触发(那是真缺席,不是腐读)。」
|
|
18
|
-
*
|
|
19
|
-
* 载荷形不是本仓铸的:`FileStorageCorruptReadInfo` 是 **core 的导出**,并且是 core 亲口说的**最宽**那一只
|
|
20
|
-
* ——"the widest shape any store this backend constructs can deliver, so ONE sink reaches all of them…
|
|
21
|
-
* the two-field store seats are satisfied by it"。于是本属主就按那只最宽形收参 —— 窄座的载荷都可赋给它
|
|
22
|
-
* ⇒ 吃最宽形的函数可赋给窄座。**本仓因此零镜像类型**。
|
|
23
|
-
* ⚠️ core 7.30.0(#1041 三态,S-616 提货)起 `CorruptReadInfo` 不再是 `{path, reason}` 两键形,而是按 `kind`
|
|
24
|
-
* 分三级的 union(`record` / `replay` / `file`,`path` + `reason` 每级都在);逐级读法见 {@link corruptReadLevelFields}。
|
|
25
|
-
*
|
|
26
|
-
* ## 刻意不做的两件事
|
|
27
|
-
* · **不在这里下统一结论**:后果按脸各不相同(少了会话规则这一层 / 列表静默少行 / rewind 边界读成缺席 /
|
|
28
|
-
* durable park 丢一行 / resume 读到「无事可重放」于是把活重跑),把其中一种写成「整个任务无约束」会误导
|
|
29
|
-
* 事故定级,而且对另外几张脸根本不成立(codex R2-medium 的原案)。所以 `note` 是**按脸传进来的一句话**,
|
|
30
|
-
* 不是本属主里的一张词表 —— 属主里放一张「事件名 → 后果句」的表,等于把调用点的知识搬进属主再抄回去。
|
|
31
|
-
* · **不接 `recordFailOpen`**:那条通道是给 F 类(装饰/缓存)fail-open 的计数与探针用的;这里的 fail-open
|
|
32
|
-
* 是 core 的裁定、本仓不改它,server 的职责只有一条 —— **不许无声**([ref] 安全轴纪律)。响亮 warn 就是
|
|
33
|
-
* 那条职责的兑现;要不要再进 fail-open 台账是另一件事、另一台车的设计。
|
|
34
|
-
* (⚠️ 这一条只说 core 自带 File 店的座。**本仓自有**的 JSONL 店是另一回事 —— 失败方向归本仓裁,见文件
|
|
35
|
-
* 后半的 S-582 重放属主:fail-open 的座**必须**进 F 类台账,fail-closed 的座不进台账而是拒用。)
|
|
36
|
-
*
|
|
37
|
-
* ⚠️ core 那只 guard 的纪律同样适用:sink 是**无绑定**调用的(`this` 是 undefined)。本属主返回的是箭头
|
|
38
|
-
* 闭包,天然满足;但**不要**把某个对象的裸方法引用直接塞进座 —— 读 `this` 会抛,而那个抛会被 core 的
|
|
39
|
-
* guard 吞掉,于是披露**静默丢失**(比没接座更难查)。
|
|
40
|
-
*/
|
|
41
1
|
import { type FileStorageCorruptReadInfo } from "@sema-agent/core";
|
|
42
2
|
import { type FailOpenTag } from "./fail-open.js";
|
|
43
3
|
import type { Logger } from "./logger.js";
|
|
@@ -57,11 +17,43 @@ import type { Logger } from "./logger.js";
|
|
|
57
17
|
*/
|
|
58
18
|
export declare function createCorruptReadSeat(warn: Logger["warn"], event: string, note?: string, leg?: (info: FileStorageCorruptReadInfo) => void): (info: FileStorageCorruptReadInfo) => void;
|
|
59
19
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
20
|
+
* 铸一只 **F 类**腐读座(S-990):同一份 core 披露载荷,按 F 类座纪律处置 —— 每命中 `recordFailOpen(tag, 明细)`
|
|
21
|
+
* (计数逐次 + 探针行逐次 + warn 每进程每 tag 一次;三件套的节流粒度见 `fail-open.ts` 模块头)。
|
|
22
|
+
*
|
|
23
|
+
* ## 什么算一次命中
|
|
24
|
+
* 三级 union 按级处置(穷举 switch:core 加第四级 ⇒ 编译红,由人处置):
|
|
25
|
+
* · `record` —— 一条被跳过的记录:**一次命中**,明细进探针行(会话 / 行号 / 坏体形 / 原因);
|
|
26
|
+
* · `file` —— 整个文件 / 目录读不出:**一次命中**(同一 tag,明细里 `kind=file`);
|
|
27
|
+
* · `replay` —— 同一批 `record` 的**汇总**(跳过 ≥1 条的每次读恰一发):**不计、不 warn** —— 计它就是把一条坏行记两次
|
|
28
|
+
* (S-616 的教训)。汇总的两个实数给了用户面(`leg` 腿 → `session.records_skipped` 帧),运维面逐条明细都在探针里。
|
|
29
|
+
*
|
|
30
|
+
* ## 为什么 warn 里不再逐条
|
|
31
|
+
* 修前每命中一行 warn(本座的 `warn` 参数形):一份有 113 行读不出的存档,一次列表 + 一次查看 = 342 行,存档没修好每读再发 ——
|
|
32
|
+
* 把运维日志淹成「看不见哪一行是新的」。现在 warn 是 `fail_open` 一条(`note` 指路探针,`detail` = 首条明细),计数进
|
|
33
|
+
* `fail_open_total{tag}`,逐条明细进探针。
|
|
34
|
+
*
|
|
35
|
+
* @param tag 登记过的 F 类 tag(`fail-open.ts` 闭集词表;`docs/FAIL-OPEN-CENSUS.md` §1 有登记行)。
|
|
36
|
+
* @param leg 与披露**同拍**的附加腿,语义同 {@link createCorruptReadSeat} 的 `leg`:**先于**记账交给它(记账故障不许吃掉用户面那一记),
|
|
37
|
+
* 必须不抛。**收全部三级**(包括不计数的 `replay`)—— 腿要的恰是那条汇总。
|
|
38
|
+
*/
|
|
39
|
+
export declare function createCorruptReadFailOpenSeat(tag: FailOpenTag, leg?: (info: FileStorageCorruptReadInfo) => void): (info: FileStorageCorruptReadInfo) => void;
|
|
40
|
+
/**
|
|
41
|
+
* 铸 `FileStorageBackend` 的**多脸**腐读座 `file_store_corrupt_read`(HTTP 服务的 local 腿与 run-local 共用 —— 修前两处铸点各手抄一遍 `createCorruptReadSeat(…)`
|
|
42
|
+
* + 后果句,S-999 收成这一个)。
|
|
43
|
+
*
|
|
44
|
+
* ## 逐命中响亮不降级
|
|
45
|
+
* 这一座里有安全轴的脸(session-policy),所以每命中一行 warn 保留(降成「每进程一次」是降级,S-990 的裁定);**在此之上**,认出的脸另记账:
|
|
46
|
+
* · session-policy 脸 ⇒ `P-DEBT` tag(`server.session-policy.corrupt-read-fail-open`)—— 计数逐次 + 探针行 + 一次性 warn。core 7.39.2 起按键读拒,
|
|
47
|
+
* 但枚举 / 别的会话抹除 / 数据根重绑三张脸仍按「无规则」(会话规则只许收紧 ⇒ 丢的恰是收紧);座上分不出读法 ⇒ 这一脸每一发都计(含已拒的按键读)。
|
|
48
|
+
* 别的会话抹除的常见形 core **不披露** ⇒ 这一形座上数不到,只能在 CENSUS / CHANGELOG 披露。放行是 core 的裁定、本仓不包层(源头修复纪律),所以**只登债、不改放行**;
|
|
49
|
+
* · 其余脸 ⇒ 不计(见 {@link DurableAbsenceFace})。
|
|
50
|
+
* 记账**先于** warn:记账是总函数,warn 可能抛(core 的 guard 会吞掉那个抛),一次日志故障不许吃掉计数。
|
|
51
|
+
*
|
|
52
|
+
* @param warn 日志座(同 {@link createCorruptReadSeat})。
|
|
53
|
+
* @param root 本座所服务的数据根(同一个值交给 `FileStorageBackend`;用来认 session-policy 脸的目录,见 {@link durableAbsenceFaceOf})。
|
|
54
|
+
* @param thirdFace 后果句里第三张脸的名字(见 {@link buildDurableAbsenceNote})。
|
|
63
55
|
*/
|
|
64
|
-
export declare function
|
|
56
|
+
export declare function createDurableAbsenceSeat(warn: Logger["warn"], root: string, thirdFace: string): (info: FileStorageCorruptReadInfo) => void;
|
|
65
57
|
/**
|
|
66
58
|
* 一只店对自己记录的**形判据 + 投影**。`read` 认得 ⇒ 返回店的记录;认不得 ⇒ `undefined`。
|
|
67
59
|
*
|
|
@@ -86,10 +78,11 @@ type JsonlReplaySeatEntry = {
|
|
|
86
78
|
readonly remedy: string;
|
|
87
79
|
} | {
|
|
88
80
|
readonly direction: "fail-open";
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
/**
|
|
82
|
+
* 逐条计数的 F 类 tag(`fail-open.ts` 闭集词表;普查表 §1 有登记行)。S-1000:fail-open 座**没有 warn 出口** —— 披露 = `recordFailOpen` 三件套
|
|
83
|
+
* (计数逐次 + 探针行逐次 + warn 每进程每 tag 一次),后果句住 tag 的 `note`;所以这里不再有 `event` / `note`(修前是逐条 warn 的事件名与后果句)。
|
|
84
|
+
*/
|
|
91
85
|
readonly tag: FailOpenTag;
|
|
92
|
-
readonly note: string;
|
|
93
86
|
};
|
|
94
87
|
/**
|
|
95
88
|
* JSONL 座的**登记表**(闭集:未登记的座名传不进属主 ⇒ 编译红)。每一座在 `docs/FAIL-OPEN-CENSUS.md` §1 有登记行。
|
|
@@ -128,51 +121,35 @@ export declare const JSONL_REPLAY_SEATS: {
|
|
|
128
121
|
};
|
|
129
122
|
readonly "approval-ask-journal": {
|
|
130
123
|
readonly direction: "fail-open";
|
|
131
|
-
readonly event: "approval_ask_journal_corrupt_read";
|
|
132
124
|
readonly tag: "server.approval-ask-journal.corrupt-record-skipped";
|
|
133
|
-
readonly note: "F: a skipped ask-journal record loses an ask transition's PROGRESS, never a gate (every write is CAS-guarded); the original is quarantined next to the journal.";
|
|
134
125
|
};
|
|
135
126
|
readonly "run-store": {
|
|
136
127
|
readonly direction: "fail-open";
|
|
137
|
-
readonly event: "run_store_corrupt_read";
|
|
138
128
|
readonly tag: "server.run-store.corrupt-record-skipped";
|
|
139
|
-
readonly note: string;
|
|
140
129
|
};
|
|
141
130
|
readonly "resume-anchor": {
|
|
142
131
|
readonly direction: "fail-open";
|
|
143
|
-
readonly event: "resume_anchor_corrupt_read";
|
|
144
132
|
readonly tag: "server.resume-anchor.corrupt-record-skipped";
|
|
145
|
-
readonly note: "F: a skipped anchor means that eventId no longer resolves (resumeAt / rewindFilesTo answer 404 unknown_event for it; the next put re-records it).";
|
|
146
133
|
};
|
|
147
134
|
readonly "send-file-ledger": {
|
|
148
135
|
readonly direction: "fail-open";
|
|
149
|
-
readonly event: "send_file_ledger_corrupt_read";
|
|
150
136
|
readonly tag: "server.send-file-ledger.corrupt-record-skipped";
|
|
151
|
-
readonly note: "F: a skipped ledger row means that issued link is missing from its tenant's listing (the stored object itself is untouched).";
|
|
152
137
|
};
|
|
153
138
|
readonly "approval-ask-audit": {
|
|
154
139
|
readonly direction: "fail-open";
|
|
155
|
-
readonly event: "approval_ask_audit_corrupt_read";
|
|
156
140
|
readonly tag: "server.approval-ask-audit.corrupt-record-skipped";
|
|
157
|
-
readonly note: "F: a skipped ask-audit record is one audit row missing from the boot crash-convergence read (an ask / settle / leg-close fact gone ⇒ that orphan is listed on a thinner basis or not at all, never a gate — the audit store gates nothing and the approval path never reads it back).";
|
|
158
141
|
};
|
|
159
142
|
readonly "workflow-notify-journal": {
|
|
160
143
|
readonly direction: "fail-open";
|
|
161
|
-
readonly event: "workflow_notify_journal_corrupt_read";
|
|
162
144
|
readonly tag: "server.workflow-notify-journal.corrupt-record-skipped";
|
|
163
|
-
readonly note: "F: a skipped notify-journal record is one run's pending entry (or its ack) missing from the boot replay. The recovery sweep walks the journal, so a lost pending line means that run's completion notify is not re-derived after a crash (the live in-process notify and the WorkflowStatus poll floor are untouched); a lost ack line means one extra idempotent re-delivery (the receiver dedups on runId). It gates no permission and changes no run.";
|
|
164
145
|
};
|
|
165
146
|
readonly "workflow-agent-session-index": {
|
|
166
147
|
readonly direction: "fail-open";
|
|
167
|
-
readonly event: "workflow_agent_session_ledger_line_rejected";
|
|
168
148
|
readonly tag: "server.workflow-agent-session-index.corrupt-record-skipped";
|
|
169
|
-
readonly note: "F: a skipped index line leaves that run on its previous known-good snapshot; a /decide for a park the line should have indexed finds no row and falls back to the legacy lane (an honest miss, never a mis-route — the redeem key is compared verbatim on the run record, not read from this index).";
|
|
170
149
|
};
|
|
171
150
|
readonly "user-input-ledger": {
|
|
172
151
|
readonly direction: "fail-open";
|
|
173
|
-
readonly event: "user_input_ledger_corrupt_read";
|
|
174
152
|
readonly tag: "server.user-input-ledger.corrupt-record-skipped";
|
|
175
|
-
readonly note: "F: a skipped user-input ledger record can only bend the crash-loss ANNOUNCEMENT (a lost settle announces one steer that was handled; a lost accept leaves a real loss unannounced; a lost announce repeats it once) — it gates nothing and changes no run.";
|
|
176
153
|
};
|
|
177
154
|
};
|
|
178
155
|
/** 登记名(闭集)。 */
|
|
@@ -221,10 +198,13 @@ export declare function replayJsonlGuarded<T>(path: string, seat: FailClosedJson
|
|
|
221
198
|
gate: JsonlStoreGate;
|
|
222
199
|
};
|
|
223
200
|
/**
|
|
224
|
-
* **fail-open** 座的重放:坏行 ⇒ 跳过 +
|
|
225
|
-
* (撕尾不计 —— core 按设计不报它)。读错(非 ENOENT)照 core 原样抛。
|
|
201
|
+
* **fail-open** 座的重放:坏行 ⇒ 跳过 + 逐条 `recordFailOpen(<登记的 F tag>, 明细)`(明细 = `kind=record path= line= form= reason=`,与 core 披露同形);
|
|
202
|
+
* `skipped` = 跳过的条数(撕尾不计 —— core 按设计不报它)。读错(非 ENOENT)照 core 原样抛。
|
|
203
|
+
*
|
|
204
|
+
* 🔴 **没有 `warn` 形参**(S-1000,硬删、零别名):F 座的披露就是 `recordFailOpen` 三件套(计数逐次 + 探针行逐次 + warn 每进程每 tag 一次);
|
|
205
|
+
* 修前这里还按 `warn` 对每条坏记录再发一行,一份 113 行坏账本 = 114 行 warn + 一条 F 座的 warn,与「F 座 warn 一次」差一项。
|
|
226
206
|
*/
|
|
227
|
-
export declare function replayJsonlSkipping<T>(path: string, seat: FailOpenJsonlSeat, shape: JsonlRecordShape<T
|
|
207
|
+
export declare function replayJsonlSkipping<T>(path: string, seat: FailOpenJsonlSeat, shape: JsonlRecordShape<T>): {
|
|
228
208
|
records: T[];
|
|
229
209
|
skipped: number;
|
|
230
210
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join, sep } from "node:path";
|
|
2
|
+
import { readJsonlRecords, resolveDataRoot } from "@sema-agent/core";
|
|
2
3
|
import { recordFailOpen } from "./fail-open.js";
|
|
3
4
|
export function createCorruptReadSeat(warn, event, note, leg) {
|
|
4
5
|
return (info) => {
|
|
@@ -14,6 +15,37 @@ export function createCorruptReadSeat(warn, event, note, leg) {
|
|
|
14
15
|
});
|
|
15
16
|
};
|
|
16
17
|
}
|
|
18
|
+
export function createCorruptReadFailOpenSeat(tag, leg) {
|
|
19
|
+
return (info) => {
|
|
20
|
+
leg?.(info);
|
|
21
|
+
recordCorruptReadHit(tag, info);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function recordCorruptReadHit(tag, info) {
|
|
25
|
+
switch (info.kind) {
|
|
26
|
+
case "record":
|
|
27
|
+
case "file":
|
|
28
|
+
recordFailOpen(tag, renderCorruptReadDetail(info));
|
|
29
|
+
return;
|
|
30
|
+
case "replay":
|
|
31
|
+
return;
|
|
32
|
+
default: {
|
|
33
|
+
const unreachable = info;
|
|
34
|
+
return unreachable;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function renderCorruptReadDetail(info) {
|
|
39
|
+
const fields = {
|
|
40
|
+
kind: info.kind,
|
|
41
|
+
path: info.path,
|
|
42
|
+
...(info.sessionId !== undefined ? { sessionId: info.sessionId } : {}),
|
|
43
|
+
...(info.principal !== undefined ? { principal: info.principal } : {}),
|
|
44
|
+
...corruptReadLevelFields(info),
|
|
45
|
+
reason: info.reason,
|
|
46
|
+
};
|
|
47
|
+
return Object.entries(fields).map(([k, v]) => `${k}=${String(v)}`).join(" ");
|
|
48
|
+
}
|
|
17
49
|
function corruptReadLevelFields(info) {
|
|
18
50
|
switch (info.kind) {
|
|
19
51
|
case "record":
|
|
@@ -28,11 +60,36 @@ function corruptReadLevelFields(info) {
|
|
|
28
60
|
}
|
|
29
61
|
}
|
|
30
62
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
const SESSION_POLICY_CORRUPT_READ_TAG = "server.session-policy.corrupt-read-fail-open";
|
|
64
|
+
const SESSION_POLICY_DIR = "session-policy";
|
|
65
|
+
function durableAbsenceFaceOf(info, policyDirPrefix) {
|
|
66
|
+
return info.path.startsWith(policyDirPrefix) ? "session-policy" : "unrecognized";
|
|
67
|
+
}
|
|
68
|
+
export function createDurableAbsenceSeat(warn, root, thirdFace) {
|
|
69
|
+
const disclose = createCorruptReadSeat(warn, "file_store_corrupt_read", buildDurableAbsenceNote(thirdFace));
|
|
70
|
+
const policyDirPrefix = join(resolveDataRoot(root), SESSION_POLICY_DIR) + sep;
|
|
71
|
+
return (info) => {
|
|
72
|
+
const face = durableAbsenceFaceOf(info, policyDirPrefix);
|
|
73
|
+
switch (face) {
|
|
74
|
+
case "session-policy":
|
|
75
|
+
recordCorruptReadHit(SESSION_POLICY_CORRUPT_READ_TAG, info);
|
|
76
|
+
break;
|
|
77
|
+
case "unrecognized":
|
|
78
|
+
break;
|
|
79
|
+
default: {
|
|
80
|
+
const unreachable = face;
|
|
81
|
+
return unreachable;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return disclose(info);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function buildDurableAbsenceNote(thirdFace) {
|
|
88
|
+
return ("a durable read hit unreadable bytes (never a plain ENOENT). " +
|
|
89
|
+
"Consequence depends on which store read it: session-policy = a keyed read (a task's session rules, GET/PUT .../policy) was REFUSED " +
|
|
90
|
+
"(the run fails with code \"corrupt\"), but enumeration (session sync export), another session's erase sweep and a data-root rebind still " +
|
|
91
|
+
"read that row as NO RULES — fix or delete the named file (deleting it means no session rules); session repo = a listing silently " +
|
|
92
|
+
`omitted rows; ${thirdFace}. Inspect the named path before deciding.`);
|
|
36
93
|
}
|
|
37
94
|
const RULE_LANE_REMEDY = "the permission-rule lane is fail-closed until a person inspects the file (moving it aside restarts the lane with an empty log)";
|
|
38
95
|
export const JSONL_REPLAY_SEATS = {
|
|
@@ -68,55 +125,14 @@ export const JSONL_REPLAY_SEATS = {
|
|
|
68
125
|
note: "S-582 fail-closed: an unreadable consume/release line would resurrect a claimed import ticket as unclaimed (one human consent redeemed twice). The face refuses every read and write until a person inspects the file.",
|
|
69
126
|
remedy: RULE_LANE_REMEDY,
|
|
70
127
|
},
|
|
71
|
-
"approval-ask-journal": {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
event: "run_store_corrupt_read",
|
|
80
|
-
tag: "server.run-store.corrupt-record-skipped",
|
|
81
|
-
note: "F: a skipped run.json reads that run as ABSENT ('no such run'); a skipped events.jsonl line is one trace event missing from replay; " +
|
|
82
|
-
"a skipped claim record sends that claim down the torn-claim path (adopted by its live row, else quarantined).",
|
|
83
|
-
},
|
|
84
|
-
"resume-anchor": {
|
|
85
|
-
direction: "fail-open",
|
|
86
|
-
event: "resume_anchor_corrupt_read",
|
|
87
|
-
tag: "server.resume-anchor.corrupt-record-skipped",
|
|
88
|
-
note: "F: a skipped anchor means that eventId no longer resolves (resumeAt / rewindFilesTo answer 404 unknown_event for it; the next put re-records it).",
|
|
89
|
-
},
|
|
90
|
-
"send-file-ledger": {
|
|
91
|
-
direction: "fail-open",
|
|
92
|
-
event: "send_file_ledger_corrupt_read",
|
|
93
|
-
tag: "server.send-file-ledger.corrupt-record-skipped",
|
|
94
|
-
note: "F: a skipped ledger row means that issued link is missing from its tenant's listing (the stored object itself is untouched).",
|
|
95
|
-
},
|
|
96
|
-
"approval-ask-audit": {
|
|
97
|
-
direction: "fail-open",
|
|
98
|
-
event: "approval_ask_audit_corrupt_read",
|
|
99
|
-
tag: "server.approval-ask-audit.corrupt-record-skipped",
|
|
100
|
-
note: "F: a skipped ask-audit record is one audit row missing from the boot crash-convergence read (an ask / settle / leg-close fact gone ⇒ that orphan is listed on a thinner basis or not at all, never a gate — the audit store gates nothing and the approval path never reads it back).",
|
|
101
|
-
},
|
|
102
|
-
"workflow-notify-journal": {
|
|
103
|
-
direction: "fail-open",
|
|
104
|
-
event: "workflow_notify_journal_corrupt_read",
|
|
105
|
-
tag: "server.workflow-notify-journal.corrupt-record-skipped",
|
|
106
|
-
note: "F: a skipped notify-journal record is one run's pending entry (or its ack) missing from the boot replay. The recovery sweep walks the journal, so a lost pending line means that run's completion notify is not re-derived after a crash (the live in-process notify and the WorkflowStatus poll floor are untouched); a lost ack line means one extra idempotent re-delivery (the receiver dedups on runId). It gates no permission and changes no run.",
|
|
107
|
-
},
|
|
108
|
-
"workflow-agent-session-index": {
|
|
109
|
-
direction: "fail-open",
|
|
110
|
-
event: "workflow_agent_session_ledger_line_rejected",
|
|
111
|
-
tag: "server.workflow-agent-session-index.corrupt-record-skipped",
|
|
112
|
-
note: "F: a skipped index line leaves that run on its previous known-good snapshot; a /decide for a park the line should have indexed finds no row and falls back to the legacy lane (an honest miss, never a mis-route — the redeem key is compared verbatim on the run record, not read from this index).",
|
|
113
|
-
},
|
|
114
|
-
"user-input-ledger": {
|
|
115
|
-
direction: "fail-open",
|
|
116
|
-
event: "user_input_ledger_corrupt_read",
|
|
117
|
-
tag: "server.user-input-ledger.corrupt-record-skipped",
|
|
118
|
-
note: "F: a skipped user-input ledger record can only bend the crash-loss ANNOUNCEMENT (a lost settle announces one steer that was handled; a lost accept leaves a real loss unannounced; a lost announce repeats it once) — it gates nothing and changes no run.",
|
|
119
|
-
},
|
|
128
|
+
"approval-ask-journal": { direction: "fail-open", tag: "server.approval-ask-journal.corrupt-record-skipped" },
|
|
129
|
+
"run-store": { direction: "fail-open", tag: "server.run-store.corrupt-record-skipped" },
|
|
130
|
+
"resume-anchor": { direction: "fail-open", tag: "server.resume-anchor.corrupt-record-skipped" },
|
|
131
|
+
"send-file-ledger": { direction: "fail-open", tag: "server.send-file-ledger.corrupt-record-skipped" },
|
|
132
|
+
"approval-ask-audit": { direction: "fail-open", tag: "server.approval-ask-audit.corrupt-record-skipped" },
|
|
133
|
+
"workflow-notify-journal": { direction: "fail-open", tag: "server.workflow-notify-journal.corrupt-record-skipped" },
|
|
134
|
+
"workflow-agent-session-index": { direction: "fail-open", tag: "server.workflow-agent-session-index.corrupt-record-skipped" },
|
|
135
|
+
"user-input-ledger": { direction: "fail-open", tag: "server.user-input-ledger.corrupt-record-skipped" },
|
|
120
136
|
};
|
|
121
137
|
function corruptRefusalSentence(seat, path, reason) {
|
|
122
138
|
const entry = JSONL_REPLAY_SEATS[seat];
|
|
@@ -171,9 +187,7 @@ function guardDisclosure(disclose, event) {
|
|
|
171
187
|
out.then(undefined, (err) => reportSinkFailure(info, err));
|
|
172
188
|
};
|
|
173
189
|
}
|
|
174
|
-
function replayJsonl(path, seat, shape,
|
|
175
|
-
const entry = JSONL_REPLAY_SEATS[seat];
|
|
176
|
-
const disclose = guardDisclosure(createCorruptReadSeat(warn ?? warnToConsole, entry.event, entry.note), entry.event);
|
|
190
|
+
function replayJsonl(path, seat, shape, onSkipped, disclose) {
|
|
177
191
|
const judge = { what: shape.what, accepts: (record) => shape.read(record) !== undefined };
|
|
178
192
|
const kept = readJsonlRecords(path, (info) => {
|
|
179
193
|
switch (info.kind) {
|
|
@@ -187,7 +201,7 @@ function replayJsonl(path, seat, shape, warn, onSkipped) {
|
|
|
187
201
|
return unreachable;
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
|
-
disclose(info);
|
|
204
|
+
disclose?.(info);
|
|
191
205
|
}, judge);
|
|
192
206
|
return kept.map((record) => {
|
|
193
207
|
const value = shape.read(record);
|
|
@@ -198,16 +212,18 @@ function replayJsonl(path, seat, shape, warn, onSkipped) {
|
|
|
198
212
|
});
|
|
199
213
|
}
|
|
200
214
|
export function replayJsonlGuarded(path, seat, shape, warn) {
|
|
215
|
+
const entry = JSONL_REPLAY_SEATS[seat];
|
|
201
216
|
const gate = new JsonlStoreGate(seat, path);
|
|
202
|
-
const
|
|
217
|
+
const disclose = guardDisclosure(createCorruptReadSeat(warn ?? warnToConsole, entry.event, entry.note), entry.event);
|
|
218
|
+
const records = replayJsonl(path, seat, shape, (info) => gate.trip(info.reason), disclose);
|
|
203
219
|
return { records, gate };
|
|
204
220
|
}
|
|
205
|
-
export function replayJsonlSkipping(path, seat, shape
|
|
221
|
+
export function replayJsonlSkipping(path, seat, shape) {
|
|
206
222
|
const { tag } = JSONL_REPLAY_SEATS[seat];
|
|
207
223
|
let skipped = 0;
|
|
208
|
-
const records = replayJsonl(path, seat, shape,
|
|
224
|
+
const records = replayJsonl(path, seat, shape, (info) => {
|
|
209
225
|
skipped += 1;
|
|
210
|
-
|
|
226
|
+
recordCorruptReadHit(tag, info);
|
|
211
227
|
});
|
|
212
228
|
return { records, skipped };
|
|
213
229
|
}
|
|
@@ -209,6 +209,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
209
209
|
readonly cls: "F";
|
|
210
210
|
readonly note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。";
|
|
211
211
|
};
|
|
212
|
+
readonly "server.hooks.matcher-unjudged": {
|
|
213
|
+
readonly cls: "F";
|
|
214
|
+
readonly note: "S-995(`hooks/hook-runner.ts` 的 `deadToolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 空 / `*` → 字面读法(两档都先走、不占预算)→ 正则形。三种 reason 同一个 tag —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)/ `over_budget`(regex 档正则形:一次受理共享的 100 ms 墙钟预算用尽,`node:vm` 的 timeout 打断正则回溯)/ `invalid_regex`(regex 档正则形编不过;确定性的坏配置,运行期另记 `hook_matcher_invalid`,拒它是新检测臂、不在本规则)。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。反方向(越界即拒)会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(攻击者喂灾难正则 / 超长 matcher)显形。";
|
|
215
|
+
};
|
|
212
216
|
readonly "server.hooks.notice-sink-threw": {
|
|
213
217
|
readonly cls: "F";
|
|
214
218
|
readonly note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。";
|
|
@@ -345,6 +349,30 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
345
349
|
readonly cls: "F";
|
|
346
350
|
readonly note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。S-969 L6 起 SQL 孪生(`plugins/user-input-ledger-sql.ts` `readRows`)同一只 tag:三只读口取到的行上读不出的列(`input_kind` 词表外 —— 新版本写的输入族 / 手改;收敛行缺 `state_at_ms`)⇒ 逐行跳过、其余照交,`listOpen` 的续翻游标按原始行推进(修前整页抛:收敛断点不前进、同会话通告与读面一并失效);坏行留在表里 ⇒ 每读一次计一次,非零 = 表里有本版本读不懂的行。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 该受理在账上仍 open,下一趟收敛按 run 行判(S-969 L8,两车道同一只判词):本进程写的 / run 行已正常收场、已 park、易手 ⇒ 补结清、不告知;只有 run 行仍 `running` 且持有者是写者(local)、带死亡落款且持有者是写者、或行不在(local:File 店藏了读不出的行)⇒ 当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次;丢 `erased`(S-965,7.111.0 起的会话级墓碑;缺 `sessionId` 的那一行**不**读成「擦全部」)⇒ 被删会话的受理行在本进程的折叠里复现 —— 只有身份 + 时刻(无用户文本),读面 / 通告按 sessionId 取,同一个 id 被重新登记时新住客可见上一位的 `crashed` 告知(= 7.110.x 无级联时的状态);撕尾的 `erased` 不在此列:墓碑写在会话本体删除**之前**,撕掉 ⇒ DELETE 没有成功、会话还在,幂等重试重写。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。";
|
|
347
351
|
};
|
|
352
|
+
readonly "server.session-repo.corrupt-record-skipped": {
|
|
353
|
+
readonly cls: "F";
|
|
354
|
+
readonly note: "S-990(`plugins/store-backend.ts` 的 `FileSessionRepo` 座,local 车道的会话存档):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。";
|
|
355
|
+
};
|
|
356
|
+
readonly "server.workflow-journal.corrupt-record-skipped": {
|
|
357
|
+
readonly cls: "F";
|
|
358
|
+
readonly note: "S-990(`plugins/store-backend.ts` 的 `FileWorkflowJournalStore` 座,local 车道的 workflow 引擎 journal):一条 journal 记录读不出(core `kind:\"record\"` 逐条 / 文件级 `kind:\"file\"`)⇒ 跳过它继续。最坏后果 = resume 读到「无事可重放」于是把那一步**重跑**(S-569 钉的原话);journal 是重放材料,不是授权事实,不裁决任何放行 / 拒绝 ⇒ F。计数单位 = 一条被跳过的记录 = 一次命中;逐条明细(`path` / `line` / `form` / `reason`)进探针行(`SEMA_FAILOPEN_PROBE`),本条 warn 每进程每 tag 一次(`detail` = 首条明细)。修前:每命中一行 warn(事件名 `workflow_journal_corrupt_read`)。";
|
|
359
|
+
};
|
|
360
|
+
readonly "server.workflow-run.corrupt-record-skipped": {
|
|
361
|
+
readonly cls: "F";
|
|
362
|
+
readonly note: "S-990(`boot/workflow-orchestration.ts` 的 `FileWorkflowRunStore` 座,local 车道的 workflow run 账):一条 run 记录读不出 ⇒ 跳过。这本账是 at-least-once 完成通知的重放源:丢一行 ⇒ boot 恢复扫把那次 run 读成「没跑过」⇒ 把活重跑一遍(S-569 原注);run 账不裁决任何放行 / 拒绝 ⇒ F。计数 / 探针 / warn 的单位与 `server.workflow-journal.corrupt-record-skipped` 同。修前:每命中一行 warn(事件名 `workflow_run_corrupt_read`)。";
|
|
363
|
+
};
|
|
364
|
+
readonly "server.roster.corrupt-record-skipped": {
|
|
365
|
+
readonly cls: "F";
|
|
366
|
+
readonly note: "S-990(`boot/stores.ts` 的 `FileRosterStore` 座,local 车道的具名 agent 名册 `roster.json`):文件读不出(core `kind:\"file\"`)⇒ core 按缺席继续、名册读成空。最坏后果 = 具名 agent 的解析全部落空 —— `entryAccessible` 要求 owner + scope 双命中、缺席条目不可达,所以方向是**拒**不是放行(core `agents/roster-store.js` 亲读)⇒ F。单位 / 探针 / warn 同上。修前:每命中一行 warn(事件名 `roster_corrupt_read`)。";
|
|
367
|
+
};
|
|
368
|
+
readonly "server.mailbox.corrupt-record-skipped": {
|
|
369
|
+
readonly cls: "F";
|
|
370
|
+
readonly note: "S-990(`boot/stores.ts` 的 `FileMailboxStore` 座,local 车道的邮箱 `<scope>/<handle>.jsonl`):一条邮箱事件读不出 ⇒ 跳过(撕尾静默是 core 的有意边界,不走本 tag)。最坏后果 = 一条消息读不到 —— 丢的是投递,不是授权,不开任何门 ⇒ F。单位 / 探针 / warn 同上。修前:每命中一行 warn(事件名 `mailbox_corrupt_read`)。";
|
|
371
|
+
};
|
|
372
|
+
readonly "server.session-policy.corrupt-read-fail-open": {
|
|
373
|
+
readonly cls: "P-DEBT";
|
|
374
|
+
readonly note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)坏了。core 7.39.2(#1299,本仓请托)起**按键读**(任务装规则、`GET` / `PUT /v1/sessions/:id/policy`)拒 —— `SessionPolicyError` 码 `corrupt`;但 core 包内 CHANGELOG 自陈另三张脸仍把坏行读成「无规则」:① `listBySession` 跳过坏行(会话同步导出读它 ⇒ 导出里没有这位 principal 的规则,目标端按无规则跑);② 别的会话的 `deleteBySession` 把撕尾行一并扫掉(常见形**不披露**),此后按键读得 ENOENT = 无规则;③ 数据根重绑把撕尾行留在旧键下。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**;SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛(CENSUS 第 81 行 P 终态)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层,只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留。⚠️ **如实全记**:座上分不出是哪一只读法披露的(载荷不带读法),所以这一脸的每一发都计 —— 含已被拒的按键读(那一发本身是 fail-closed);② 的常见形 core 不披露,座上**数不到**。**出路 = core 把三张脸也改成拒,届时撤本 tag**。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。";
|
|
375
|
+
};
|
|
348
376
|
readonly "server.outbound.connect-observer-threw": {
|
|
349
377
|
readonly cls: "F";
|
|
350
378
|
readonly note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。";
|
|
@@ -200,6 +200,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
200
200
|
cls: "F",
|
|
201
201
|
note: "#310:`engine_notice` 分流器把一条白名单通告投给某条 run 腿的登记口时,那只口抛了(live 口写向已断/已撕裂的 SSE socket,或 durable 口的账本写同步抛)。放行的最坏后果 = **这一条通告的这一个终点**缺席:①日志终点在分流之前已经逐字打过(事实一条不丢);②两个终点注册成两只独立 sink ⇒ live 抛不牵连 durable(断连后仍看得见的那半保住);③同会话其余口照投。故 F 类。不放行的代价是把异常回抛给 core 的 `deliverEngineNotice` —— 它会吞掉,于是同一次失败**既没有留痕也没有第二只口**,正是本 tag 要根除的形。留痕是承重的:静默吞掉之后「wire 腿为什么总有几条通告不到」在遥测里与「core 本来就没发」同形。",
|
|
202
202
|
},
|
|
203
|
+
"server.hooks.matcher-unjudged": {
|
|
204
|
+
cls: "F",
|
|
205
|
+
note: "S-995(`hooks/hook-runner.ts` 的 `deadToolMatcherIssue`,fresh 受理面「工具事件 matcher 选中别名而选不中正名 ⇒ 400」判据的**未判**臂;detail = `event=… reason=… matcherChars=…`,不带 matcher 原字节):一只 matcher 没能被判 ⇒ 照旧受理。判据一条顺序、两档共用(7.114.0 修复轮):长度上限 → 空 / `*` → 字面读法(两档都先走、不占预算)→ 正则形。三种 reason 同一个 tag —— `oversize`(两档:超 8 192 字符不读;多租户车道上 hooks 不装配,这一笔只为让超长输入显形)/ `over_budget`(regex 档正则形:一次受理共享的 100 ms 墙钟预算用尽,`node:vm` 的 timeout 打断正则回溯)/ `invalid_regex`(regex 档正则形编不过;确定性的坏配置,运行期另记 `hook_matcher_invalid`,拒它是新检测臂、不在本规则)。**不记**的:literal 档(多租户车道)遇正则形 = 不适用 —— 该车道 hooks 整只不装配,没有被守的对象,不是未判。放行的最坏后果 = **那一只 matcher 没被这条判据守住**:它若真写了别名,照旧永不命中(7.112.0 对正则形一律不判,同一后果);**不是安全闸**(受理面的有效性判 = S-924 的「响亮」,决定不了任何放行 / 拒绝 / 权限;hook 的 deny 语义在运行期 hook-runner)。反方向(越界即拒)会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。拒放集合相对 7.113.0 的一处放宽:超过上限的字面别名 matcher 7.113.0 拒、7.114.0 两档未判受理(已披露)。计数让「受理面一直判不了」(攻击者喂灾难正则 / 超长 matcher)显形。",
|
|
206
|
+
},
|
|
203
207
|
"server.hooks.notice-sink-threw": {
|
|
204
208
|
cls: "F",
|
|
205
209
|
note: "#281:hook 观测回调(`onHookNotice`,部署把它接到 fleet 流)自己抛了。放行的最坏后果 = **这一条观测帧**缺席;补偿是结构性的:①日志终点在投递之前就已逐字落定(`hook_command_failed` / `hook_command_timeout` / `hook_command_spawn_failed` 一族),运维面零回退;②观测帧不参与任何门/裁决(纯 observe,#281 全族判据里「裁决面一个字节不动」是承重条款),故 F 类。不放行的代价严重不对称:异常会掀回 core 的 hook 回调栈,把一次**观察动作**变成一次**工具调用失败** —— 「观察一件事的动作反过来改变了那件事」正是本 tag 要根除的形,而它伤的恰恰是 hook 已经坏掉、用户最需要任务照跑的那一刻。留痕承重:静默吞掉之后「这台机器为什么一条 hook 通知都没有」在遥测里与「hook 从来没坏过」同形。",
|
|
@@ -315,6 +319,30 @@ export const FAIL_OPEN_TAGS = {
|
|
|
315
319
|
cls: "F",
|
|
316
320
|
note: "S-928(`plugins/user-input-ledger-file.ts`,local 车道 File 孪生的 boot 重放;登记名 `user-input-ledger`):账本中间一行读不出形(`42` / 缺键 / 词表外的 `kind`·`input`·`how`·`cause` / 半截 JSON;撕尾不走本 tag)⇒ 跳过它继续。S-969 L6 起 SQL 孪生(`plugins/user-input-ledger-sql.ts` `readRows`)同一只 tag:三只读口取到的行上读不出的列(`input_kind` 词表外 —— 新版本写的输入族 / 手改;收敛行缺 `state_at_ms`)⇒ 逐行跳过、其余照交,`listOpen` 的续翻游标按原始行推进(修前整页抛:收敛断点不前进、同会话通告与读面一并失效);坏行留在表里 ⇒ 每读一次计一次,非零 = 表里有本版本读不懂的行。最坏后果按行种:丢 `accepted` ⇒ 那次受理若真随崩溃丢了,不会被告知(= 本账之前的状态);丢 `settled` ⇒ 该受理在账上仍 open,下一趟收敛按 run 行判(S-969 L8,两车道同一只判词):本进程写的 / run 行已正常收场、已 park、易手 ⇒ 补结清、不告知;只有 run 行仍 `running` 且持有者是写者(local)、带死亡落款且持有者是写者、或行不在(local:File 店藏了读不出的行)⇒ 当成崩溃丢失告知一次;丢 `converged` ⇒ 下一次 boot 重收一次(幂等);丢 `announced` ⇒ 再告知一次;丢 `erased`(S-965,7.111.0 起的会话级墓碑;缺 `sessionId` 的那一行**不**读成「擦全部」)⇒ 被删会话的受理行在本进程的折叠里复现 —— 只有身份 + 时刻(无用户文本),读面 / 通告按 sessionId 取,同一个 id 被重新登记时新住客可见上一位的 `crashed` 告知(= 7.110.x 无级联时的状态);撕尾的 `erased` 不在此列:墓碑写在会话本体删除**之前**,撕掉 ⇒ DELETE 没有成功、会话还在,幂等重试重写。全在告知面,不开门 ⇒ F。修前不存在(本账本 S-928 新立)。",
|
|
317
321
|
},
|
|
322
|
+
"server.session-repo.corrupt-record-skipped": {
|
|
323
|
+
cls: "F",
|
|
324
|
+
note: "S-990(`plugins/store-backend.ts` 的 `FileSessionRepo` 座,local 车道的会话存档):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。",
|
|
325
|
+
},
|
|
326
|
+
"server.workflow-journal.corrupt-record-skipped": {
|
|
327
|
+
cls: "F",
|
|
328
|
+
note: "S-990(`plugins/store-backend.ts` 的 `FileWorkflowJournalStore` 座,local 车道的 workflow 引擎 journal):一条 journal 记录读不出(core `kind:\"record\"` 逐条 / 文件级 `kind:\"file\"`)⇒ 跳过它继续。最坏后果 = resume 读到「无事可重放」于是把那一步**重跑**(S-569 钉的原话);journal 是重放材料,不是授权事实,不裁决任何放行 / 拒绝 ⇒ F。计数单位 = 一条被跳过的记录 = 一次命中;逐条明细(`path` / `line` / `form` / `reason`)进探针行(`SEMA_FAILOPEN_PROBE`),本条 warn 每进程每 tag 一次(`detail` = 首条明细)。修前:每命中一行 warn(事件名 `workflow_journal_corrupt_read`)。",
|
|
329
|
+
},
|
|
330
|
+
"server.workflow-run.corrupt-record-skipped": {
|
|
331
|
+
cls: "F",
|
|
332
|
+
note: "S-990(`boot/workflow-orchestration.ts` 的 `FileWorkflowRunStore` 座,local 车道的 workflow run 账):一条 run 记录读不出 ⇒ 跳过。这本账是 at-least-once 完成通知的重放源:丢一行 ⇒ boot 恢复扫把那次 run 读成「没跑过」⇒ 把活重跑一遍(S-569 原注);run 账不裁决任何放行 / 拒绝 ⇒ F。计数 / 探针 / warn 的单位与 `server.workflow-journal.corrupt-record-skipped` 同。修前:每命中一行 warn(事件名 `workflow_run_corrupt_read`)。",
|
|
333
|
+
},
|
|
334
|
+
"server.roster.corrupt-record-skipped": {
|
|
335
|
+
cls: "F",
|
|
336
|
+
note: "S-990(`boot/stores.ts` 的 `FileRosterStore` 座,local 车道的具名 agent 名册 `roster.json`):文件读不出(core `kind:\"file\"`)⇒ core 按缺席继续、名册读成空。最坏后果 = 具名 agent 的解析全部落空 —— `entryAccessible` 要求 owner + scope 双命中、缺席条目不可达,所以方向是**拒**不是放行(core `agents/roster-store.js` 亲读)⇒ F。单位 / 探针 / warn 同上。修前:每命中一行 warn(事件名 `roster_corrupt_read`)。",
|
|
337
|
+
},
|
|
338
|
+
"server.mailbox.corrupt-record-skipped": {
|
|
339
|
+
cls: "F",
|
|
340
|
+
note: "S-990(`boot/stores.ts` 的 `FileMailboxStore` 座,local 车道的邮箱 `<scope>/<handle>.jsonl`):一条邮箱事件读不出 ⇒ 跳过(撕尾静默是 core 的有意边界,不走本 tag)。最坏后果 = 一条消息读不到 —— 丢的是投递,不是授权,不开任何门 ⇒ F。单位 / 探针 / warn 同上。修前:每命中一行 warn(事件名 `mailbox_corrupt_read`)。",
|
|
341
|
+
},
|
|
342
|
+
"server.session-policy.corrupt-read-fail-open": {
|
|
343
|
+
cls: "P-DEBT",
|
|
344
|
+
note: "S-999(`observability/corrupt-read-seat.ts` 的 `createDurableAbsenceSeat`,`file_store_corrupt_read` 那只多脸座里认出的 **session-policy 脸**;local 车道,HTTP 服务的 local 腿与 run-local 同铸点):会话规则文件(数据根 `session-policy/` 下的行文件)坏了。core 7.39.2(#1299,本仓请托)起**按键读**(任务装规则、`GET` / `PUT /v1/sessions/:id/policy`)拒 —— `SessionPolicyError` 码 `corrupt`;但 core 包内 CHANGELOG 自陈另三张脸仍把坏行读成「无规则」:① `listBySession` 跳过坏行(会话同步导出读它 ⇒ 导出里没有这位 principal 的规则,目标端按无规则跑);② 别的会话的 `deleteBySession` 把撕尾行一并扫掉(常见形**不披露**),此后按键读得 ENOENT = 无规则;③ 数据根重绑把撕尾行留在旧键下。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / hooks / shell 门仍在)。方向 = fail-open,**明知不对**;SQL 孪生(`plugins/session-policy-store-sql.ts`)对同形响亮抛(CENSUS 第 81 行 P 终态)。归 P 类的理由是它决定权限门读到的规则层;归 DEBT 的理由是放行是 **core 的裁定**,源头修复纪律下本仓不包层,只让债在遥测里显形:计数逐次 + 探针行(`kind` / `path` / `sessionId` / `principal` / `reason`)+ 一次性 warn;逐命中 warn(`file_store_corrupt_read`)原样保留。⚠️ **如实全记**:座上分不出是哪一只读法披露的(载荷不带读法),所以这一脸的每一发都计 —— 含已被拒的按键读(那一发本身是 fail-closed);② 的常见形 core 不披露,座上**数不到**。**出路 = core 把三张脸也改成拒,届时撤本 tag**。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。",
|
|
345
|
+
},
|
|
318
346
|
"server.outbound.connect-observer-threw": {
|
|
319
347
|
cls: "F",
|
|
320
348
|
note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。",
|
|
@@ -181,6 +181,38 @@ const TERMINAL_FAILURE_DETAIL_TREATMENT = {
|
|
|
181
181
|
status: "verbatim",
|
|
182
182
|
code: "provider-echo",
|
|
183
183
|
};
|
|
184
|
+
const TERMINAL_BLOCKED_DETAIL_TREATMENT = {
|
|
185
|
+
kind: "verbatim",
|
|
186
|
+
cause: "verbatim",
|
|
187
|
+
hookReason: "free-text",
|
|
188
|
+
};
|
|
189
|
+
function shapeTerminalBlockedDetail(detail) {
|
|
190
|
+
if (detail === null || typeof detail !== "object" || Array.isArray(detail))
|
|
191
|
+
return detail;
|
|
192
|
+
const d = detail;
|
|
193
|
+
let out;
|
|
194
|
+
for (const [key, treatment] of Object.entries(TERMINAL_BLOCKED_DETAIL_TREATMENT)) {
|
|
195
|
+
const v = d[key];
|
|
196
|
+
if (v === undefined)
|
|
197
|
+
continue;
|
|
198
|
+
switch (treatment) {
|
|
199
|
+
case "verbatim":
|
|
200
|
+
break;
|
|
201
|
+
case "free-text":
|
|
202
|
+
if (typeof v === "string") {
|
|
203
|
+
const next = redactErrorMessage(v);
|
|
204
|
+
if (next !== v)
|
|
205
|
+
(out ??= { ...d })[key] = next;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
default: {
|
|
209
|
+
const unhandled = treatment;
|
|
210
|
+
throw new Error(`unhandled blocked-detail treatment: ${String(unhandled)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out ?? detail;
|
|
215
|
+
}
|
|
184
216
|
const PROVIDER_ECHO_SHAPERS = {
|
|
185
217
|
responseModel: (v) => (admitProviderEcho(v, "responseModel") ? v : undefined),
|
|
186
218
|
apiFailure: (v) => shapeEchoMembers(v, API_FAILURE_TREATMENT, "apiFailure"),
|
|
@@ -225,6 +257,13 @@ export function redactTerminalResult(result) {
|
|
|
225
257
|
nt = { ...t, message: redactErrorMessage(t.message) };
|
|
226
258
|
else if (t.kind === "blocked" && typeof t.reason === "string")
|
|
227
259
|
nt = { ...t, reason: redactErrorMessage(t.reason) };
|
|
260
|
+
if (t.kind === "blocked") {
|
|
261
|
+
const cur = nt ?? t;
|
|
262
|
+
const detail = cur.detail;
|
|
263
|
+
const shaped = shapeTerminalBlockedDetail(detail);
|
|
264
|
+
if (shaped !== detail)
|
|
265
|
+
nt = { ...cur, detail: shaped };
|
|
266
|
+
}
|
|
228
267
|
const gateOf = (cause) => {
|
|
229
268
|
if (cause === null || typeof cause !== "object" || !("gate" in cause))
|
|
230
269
|
return undefined;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type WorkflowRun } from "@sema-agent/core";
|
|
2
|
-
import type { Logger } from "../observability/logger.js";
|
|
3
2
|
/** 一条索引行:**子会话 id 是主键**(会话 id 全局唯一 ⇒ 读口天然是单行精确查,无分页假阴性)。 */
|
|
4
3
|
export interface WorkflowAgentSessionRow {
|
|
5
4
|
/** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = 那条 checkpoint 行的 `sessionId`;
|
|
@@ -118,10 +117,7 @@ export declare class LocalWorkflowAgentSessionIndex implements WorkflowAgentSess
|
|
|
118
117
|
private readonly byRun;
|
|
119
118
|
private readonly bySession;
|
|
120
119
|
private log;
|
|
121
|
-
constructor(root?: string, fsyncEnabled?: boolean
|
|
122
|
-
/** 坏账本行的**响亮**出口(可选;缺席 ⇒ 重放属主退回 `console.warn`,不退回沉默)。形与日志座 `Logger["warn"]` 逐字同源,
|
|
123
|
-
* 不另立第二种披露通道。 */
|
|
124
|
-
onWarn?: Logger["warn"]);
|
|
120
|
+
constructor(root?: string, fsyncEnabled?: boolean);
|
|
125
121
|
/** 内存两张表的收敛(重放与写路径共用一份逻辑)。 */
|
|
126
122
|
private apply;
|
|
127
123
|
syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
|
|
@@ -83,7 +83,7 @@ export class LocalWorkflowAgentSessionIndex {
|
|
|
83
83
|
byRun = new Map();
|
|
84
84
|
bySession = new Map();
|
|
85
85
|
log;
|
|
86
|
-
constructor(root, fsyncEnabled = true
|
|
86
|
+
constructor(root, fsyncEnabled = true) {
|
|
87
87
|
this.fsyncEnabled = fsyncEnabled;
|
|
88
88
|
if (root === undefined) {
|
|
89
89
|
this.ledgerPath = undefined;
|
|
@@ -92,7 +92,7 @@ export class LocalWorkflowAgentSessionIndex {
|
|
|
92
92
|
const dir = join(root, "workflow-agent-sessions");
|
|
93
93
|
ensureDir(dir);
|
|
94
94
|
this.ledgerPath = join(dir, "index.jsonl");
|
|
95
|
-
for (const entry of replayJsonlSkipping(this.ledgerPath, "workflow-agent-session-index", LEDGER_LINE_SHAPE
|
|
95
|
+
for (const entry of replayJsonlSkipping(this.ledgerPath, "workflow-agent-session-index", LEDGER_LINE_SHAPE).records)
|
|
96
96
|
this.apply(entry.runId, entry.rows);
|
|
97
97
|
}
|
|
98
98
|
apply(runId, rows) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type WorkflowRun, type WorkflowRunStore, type WorkflowRunStatus, type WorkflowRunSummary } from "@sema-agent/core";
|
|
2
2
|
import { type FleetEventBus } from "../fleet/fleet-bus.js";
|
|
3
3
|
import { type WorkflowAgentSessionIndex } from "./workflow-agent-session-index.js";
|
|
4
|
-
import type { Logger } from "../observability/logger.js";
|
|
5
4
|
/** A workflow completion notification's terminal payload — the bounded, redacted shape core's notifier seam
|
|
6
5
|
* carries, re-derivable from a durable {@link import("@sema-agent/core").WorkflowRun} on the recovery path. */
|
|
7
6
|
export interface WorkflowCompletionPayload {
|
|
@@ -216,8 +215,7 @@ export declare class FileWorkflowNotifyJournalStore implements WorkflowNotifyJou
|
|
|
216
215
|
private readonly ledgerPath;
|
|
217
216
|
private readonly entries;
|
|
218
217
|
private log;
|
|
219
|
-
|
|
220
|
-
constructor(root: string, fsyncEnabled?: boolean, warn?: Logger["warn"]);
|
|
218
|
+
constructor(root: string, fsyncEnabled?: boolean);
|
|
221
219
|
/** Commit one entry: append the JSON line (fsync) THEN flip the in-memory map (crash-safe ordering — the
|
|
222
220
|
* durable record lands before the in-memory state the caller observes). */
|
|
223
221
|
private commit;
|