@sema-agent/server 7.104.0 → 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 +32 -0
- package/USAGE.md +59 -10
- package/dist/boot/runner-deps.js +2 -1
- package/dist/config.d.ts +15 -2
- package/dist/config.js +42 -7
- 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 +1 -1
- package/dist/http/dispatch.d.ts +18 -0
- package/dist/http/dispatch.js +17 -4
- package/dist/http/resume-legs.d.ts +1 -1
- package/dist/http/route-table.d.ts +15 -4
- package/dist/http/route-table.js +6 -5
- package/dist/http/routes/fleet.d.ts +3 -2
- package/dist/http/routes/images.js +0 -1
- package/dist/http/routes/runs.js +2 -8
- package/dist/http/routes/session-sync.js +6 -1
- package/dist/http/routes/sessions.js +5 -2
- package/dist/http/routes/side-query.js +8 -1
- package/dist/http/routes/trace-usage.d.ts +2 -0
- package/dist/http/routes/trace-usage.js +4 -9
- package/dist/http/server.d.ts +9 -4
- package/dist/http/server.js +4 -1
- 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/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal.d.ts +45 -9
- package/dist/observability/run-terminal.js +115 -9
- package/dist/plugins/image-bake-store-sql.d.ts +0 -2
- package/dist/plugins/image-bake-store-sql.js +0 -5
- package/dist/plugins/retention-store-sql.js +1 -0
- package/dist/plugins/run-store-sql.d.ts +3 -2
- package/dist/plugins/schema-indexes.js +1 -0
- package/dist/plugins/session-policy-store-sql.js +18 -5
- package/dist/plugins/store-backend.d.ts +6 -4
- package/dist/plugins/tidb-pool.js +4 -3
- package/dist/read-face-posture.d.ts +21 -0
- package/dist/read-face-posture.js +9 -0
- package/dist/rules-consent.js +0 -11
- package/dist/session-policy-wire.d.ts +98 -12
- package/dist/session-policy-wire.js +71 -8
- package/dist/trace/project.d.ts +10 -5
- package/dist/trace/project.js +3 -3
- package/dist/trace/redact.d.ts +7 -0
- package/dist/trace/redact.js +153 -61
- package/package.json +1 -1
- package/dist/plugins/memory-session-policy-store.d.ts +0 -46
- package/dist/plugins/memory-session-policy-store.js +0 -52
|
@@ -420,7 +420,6 @@ async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
|
|
|
420
420
|
return bake ? { status: bake.status, updatedAt: bake.updatedAt } : undefined;
|
|
421
421
|
},
|
|
422
422
|
getEvents: (id, after) => bakes.getEvents(id, after),
|
|
423
|
-
retainedFrom: (id) => bakes.retainedFrom(id),
|
|
424
423
|
formatEvent: (ev) => ({
|
|
425
424
|
id: ev.seq,
|
|
426
425
|
event: ev.kind,
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { persistedPlaneOf } from "../../terminal.js";
|
|
2
|
-
import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead,
|
|
3
|
-
import { isModelTextLedgerEventType } from "../../trace/ledger-events.js";
|
|
2
|
+
import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS } from "@sema-agent/core";
|
|
6
5
|
import { isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
|
|
@@ -126,14 +125,9 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
|
|
|
126
125
|
return run ? { status: run.status, updatedAt: run.updatedAt } : undefined;
|
|
127
126
|
},
|
|
128
127
|
getEvents: (id, after) => runStore.getEvents(id, after),
|
|
129
|
-
retainedFrom: (id) => runStore.retainedFrom(id),
|
|
130
128
|
formatEvent: (ev) => {
|
|
131
129
|
const type = normalizeRunEventType(ev.type);
|
|
132
|
-
const redacted = type
|
|
133
|
-
? { ...ev.data, result: memoRedactTerminalResult("ev.done.result", ev.data.result) }
|
|
134
|
-
: type === "failed" ? memoRedactedRead("ev.failed", ev.data, (d) => redactLedgerEventData(type, d))
|
|
135
|
-
: isModelTextLedgerEventType(type) ? memoRedactedRead("ev.model-text", ev.data, (d) => redactLedgerEventData(type, d))
|
|
136
|
-
: redactLedgerEventData(type, ev.data);
|
|
130
|
+
const redacted = redactLedgerEventForRead(type, ev.data);
|
|
137
131
|
const data = screenedStoredEventData(type, redacted);
|
|
138
132
|
return { id: ev.seq, event: type, data: { type, ...(data ?? {}) } };
|
|
139
133
|
},
|
|
@@ -13,7 +13,7 @@ const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
13
13
|
const SYNC_IMPORT_BATCH = 500;
|
|
14
14
|
import { SHA256_HEX_RE } from "../../digest-form.js";
|
|
15
15
|
import { assertNever } from "../route-ctx.js";
|
|
16
|
-
import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
|
|
16
|
+
import { readSessionRulesRecords, describeSessionRulesRecordProblem, legacyToolNameProblemOf } from "../../session-policy-wire.js";
|
|
17
17
|
export function createSessionSyncLocal() {
|
|
18
18
|
const pendingStagings = new Map();
|
|
19
19
|
const importLeases = new Map();
|
|
@@ -235,6 +235,11 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
235
235
|
}
|
|
236
236
|
const policyRead = readSessionRulesRecords(body.policy);
|
|
237
237
|
if (policyRead.problem !== undefined) {
|
|
238
|
+
const legacy = legacyToolNameProblemOf(policyRead.problem);
|
|
239
|
+
if (legacy !== undefined) {
|
|
240
|
+
sendError(res, 400, "rules.legacy_tool_name", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`, { field: legacy.field, name: legacy.name });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
238
243
|
sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
|
|
239
244
|
return;
|
|
240
245
|
}
|
|
@@ -6,7 +6,7 @@ import { redactSecrets } from "../../trace/redact.js";
|
|
|
6
6
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
7
7
|
import { targetOutsideStoreOf } from "../../memory-store-refusal.js";
|
|
8
8
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
9
|
-
import {
|
|
9
|
+
import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
10
10
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
11
11
|
import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
12
12
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -777,7 +777,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
777
777
|
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
778
778
|
return;
|
|
779
779
|
}
|
|
780
|
-
const read =
|
|
780
|
+
const read = readWritableSessionPermissionRules(body.rules);
|
|
781
781
|
if (read.problem !== undefined) {
|
|
782
782
|
switch (read.problem.kind) {
|
|
783
783
|
case "not-an-object":
|
|
@@ -786,6 +786,9 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
786
786
|
case "field-not-string-array":
|
|
787
787
|
sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
|
|
788
788
|
return;
|
|
789
|
+
case "legacy-tool-name":
|
|
790
|
+
sendError(res, 400, "rules.legacy_tool_name", describeSessionPermissionRulesProblem(read.problem), { field: read.problem.field, name: read.problem.name });
|
|
791
|
+
return;
|
|
789
792
|
default: {
|
|
790
793
|
const unhandled = read.problem;
|
|
791
794
|
throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
|
|
@@ -3,6 +3,7 @@ import { HttpError } from "../../security.js";
|
|
|
3
3
|
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
|
|
4
4
|
import { modelUnavailableMessage } from "../../model-entry-refusal.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
|
+
import { admitProviderEcho } from "../../observability/run-terminal.js";
|
|
6
7
|
import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
|
|
7
8
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
8
9
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -28,6 +29,12 @@ function isModelRole(value) {
|
|
|
28
29
|
function passthroughModelRole(value) {
|
|
29
30
|
return value;
|
|
30
31
|
}
|
|
32
|
+
function shapeSideQueryEcho(result) {
|
|
33
|
+
if (result.responseModel === undefined || admitProviderEcho(result.responseModel, "side-query.responseModel"))
|
|
34
|
+
return result;
|
|
35
|
+
const { responseModel: _outOfShape, ...admitted } = result;
|
|
36
|
+
return admitted;
|
|
37
|
+
}
|
|
31
38
|
export async function handleSideQuery(req, res, match, ctx) {
|
|
32
39
|
const { deps } = ctx;
|
|
33
40
|
const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
|
|
@@ -100,7 +107,7 @@ export async function handleSideQuery(req, res, match, ctx) {
|
|
|
100
107
|
deps.sideQueryAccounting?.(principal, { model: modelId, usage: rr.usage });
|
|
101
108
|
deps.metrics?.inc?.("side_query_total", { result: "ok" });
|
|
102
109
|
if (!res.writableEnded)
|
|
103
|
-
sendJson(res, 200, result);
|
|
110
|
+
sendJson(res, 200, shapeSideQueryEcho(result));
|
|
104
111
|
}
|
|
105
112
|
catch (e) {
|
|
106
113
|
deps.metrics?.inc?.("side_query_total", { result: "error" });
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* ⚠️ 三块都跑在全局 service-credential 门**之前**(与 /metrics 同款待遇)——只读 traceToken 单凭据的调用方
|
|
8
8
|
* (registry 中继的姿势)在全局门会被 401,挂门后=门内 traceToken 支持成死代码(1.148 a2 实机抓获)。
|
|
9
|
+
* S-672(7.105.0):门前不等于不设防 —— 本域坐在读路谓词段,先过 `dispatch.ts` 的 `readCredentialOk`
|
|
10
|
+
* (部署配了 service 凭据时要「service token / traceToken / 已验 principal」之一),块内这几道门在它之后。
|
|
9
11
|
* 因此本域在 `handle()` 里的位置就是契约的一部分:必须留在鉴权门上方,顺序不可与其他域对调。本域零可变状态。
|
|
10
12
|
*/
|
|
11
13
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { projectEvents, taskSummary, mapTraceEvent } from "../../trace/project.js";
|
|
2
|
+
import { redactLedgerEventForRead } from "../../observability/run-terminal.js";
|
|
2
3
|
import { projectArtifacts } from "../../trace/artifacts.js";
|
|
3
4
|
import { usageSummary, usageSeries, usageBreakdown } from "../../usage-analytics.js";
|
|
4
5
|
import { sleep } from "../sse-log.js";
|
|
@@ -220,6 +221,7 @@ async function handleToolResult(res, store, runStore, taskId, ownedRun, ref, que
|
|
|
220
221
|
return notFound();
|
|
221
222
|
sendJson(res, 200, { content: slice.content, offset: slice.offset, totalChars: slice.totalChars });
|
|
222
223
|
}
|
|
224
|
+
const readFaceEvent = (ev) => ({ ...ev, data: redactLedgerEventForRead(ev.type, ev.data) });
|
|
223
225
|
async function handleTaskTurns(res, runStore, taskId, query) {
|
|
224
226
|
const run = await runStore.getRun(taskId);
|
|
225
227
|
if (!run) {
|
|
@@ -229,7 +231,7 @@ async function handleTaskTurns(res, runStore, taskId, query) {
|
|
|
229
231
|
const limit = Math.min(100, Math.max(1, Number(query.get("limit") ?? 20) || 20));
|
|
230
232
|
const cursorSeq = Number(query.get("cursor") ?? 0) || 0;
|
|
231
233
|
const [events, retainedFrom] = await Promise.all([runStore.getEvents(taskId, 0), runStore.retainedFrom(taskId)]);
|
|
232
|
-
const all = projectEvents(events);
|
|
234
|
+
const all = projectEvents(events.map(readFaceEvent));
|
|
233
235
|
const eligible = cursorSeq > 0 ? all.filter((t) => t.seq < cursorSeq) : all;
|
|
234
236
|
const turns = eligible.slice(Math.max(0, eligible.length - limit));
|
|
235
237
|
const oldest = turns[0];
|
|
@@ -244,13 +246,6 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs, search) {
|
|
|
244
246
|
}
|
|
245
247
|
const lastId = Number(req.headers["last-event-id"] ?? new URLSearchParams(search).get("from") ?? 0);
|
|
246
248
|
let from = Number.isFinite(lastId) ? lastId : 0;
|
|
247
|
-
if (from > 0) {
|
|
248
|
-
const retainedFrom = await runStore.retainedFrom(taskId);
|
|
249
|
-
if (retainedFrom > from + 1) {
|
|
250
|
-
sendError(res, 416, "limit.retention_evicted", "resume point evicted past retention", { retainedFrom });
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
249
|
sseHeaders(res);
|
|
255
250
|
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "delta", resumeFrom: from })}\n\n`);
|
|
256
251
|
let closed = false;
|
|
@@ -261,7 +256,7 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs, search) {
|
|
|
261
256
|
let lastBeat = Date.now();
|
|
262
257
|
const write = (events) => {
|
|
263
258
|
for (const ev of events) {
|
|
264
|
-
const out = mapTraceEvent(ev.type, ev.seq, (ev.data ?? {}));
|
|
259
|
+
const out = mapTraceEvent(ev.type, ev.seq, (readFaceEvent(ev).data ?? {}));
|
|
265
260
|
if (out)
|
|
266
261
|
res.write(`id: ${ev.seq}\nevent: ${out.event}\ndata: ${JSON.stringify(out.data)}\n\n`);
|
|
267
262
|
from = ev.seq;
|
package/dist/http/server.d.ts
CHANGED
|
@@ -112,10 +112,15 @@ export interface ServiceCoreDeps {
|
|
|
112
112
|
}) => TaskSpec | Promise<TaskSpec>;
|
|
113
113
|
/**
|
|
114
114
|
* [ref] 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
|
|
115
|
-
* 装配点(
|
|
116
|
-
*
|
|
115
|
+
* 装配点(`boot/stage-10-http-server.ts`)传的是 `createHandsRunnerSelector` —— 选路规则只有那一份实现,
|
|
116
|
+
* 本层不解释场景,只取用。
|
|
117
|
+
* 🔴 **必填**(S-175,7.105.0):修前这一席可选,缺席 ⇒ 静默落 {@link ServiceDeps.runner} —— 最宽的那只
|
|
118
|
+
* (带全量手的主 Runner)。而同一轴上「车道**字段**缺席」早已是拒(S-149,`handsLaneOf` 抛
|
|
119
|
+
* `HandsLaneUnstampedError`),席缺席回落最宽 = 同一件事两条律。现在缺席是**编译错**;绕过类型
|
|
120
|
+
* (JS 嵌入方 / `as` 硬转)造的 deps 在 {@link createHttpServer} 入口**拒启**。单 Runner 的嵌入方显式传
|
|
121
|
+
* `runnerFor: () => runner` —— 那是一句表态,不是一个回落。
|
|
117
122
|
*/
|
|
118
|
-
runnerFor
|
|
123
|
+
runnerFor: (spec: TaskSpec) => Runner;
|
|
119
124
|
}
|
|
120
125
|
/** **durable 持久面**:每个键 present ⇔ 对应路由/能力位在场,absent ⇒ 501(诚实缺席),
|
|
121
126
|
* 绝不静默降级。装配点 = main.ts 的 store backend 开箱段。 */
|
|
@@ -856,7 +861,7 @@ export declare function createHttpServer(rawDeps: ServiceDeps): http.Server & {
|
|
|
856
861
|
/** SSE: replay durable events after Last-Event-ID, then tail until the run is terminal (or stale). */
|
|
857
862
|
/**
|
|
858
863
|
* The differences a concrete log (task_run | image_bake) feeds the ONE resumable SSE reader (P2.8). Everything
|
|
859
|
-
* the reader does — Last-Event-ID/`?from=` resume
|
|
864
|
+
* the reader does — Last-Event-ID/`?from=` resume (every row with seq > from; no eviction answer, S-211 ①), the per-poll concurrent
|
|
860
865
|
* status+events read, the terminal re-fetch (the terminal event lands in the gap before the status flips), the
|
|
861
866
|
* stale fallback, the 15-min cap, the 15s idle heartbeat — is provider-agnostic and lives in `streamSseLog`.
|
|
862
867
|
* 🔴 The task_run provider MUST keep the existing wire bytes EXACTLY (center's relay + 730+ tests depend on it).
|
package/dist/http/server.js
CHANGED
|
@@ -38,6 +38,9 @@ export function flattenServiceDeps(deps) {
|
|
|
38
38
|
}
|
|
39
39
|
export function createHttpServer(rawDeps) {
|
|
40
40
|
const deps = flattenServiceDeps(rawDeps);
|
|
41
|
+
if (typeof deps.runnerFor !== "function") {
|
|
42
|
+
throw new Error("createHttpServer: the runnerFor seat is required (it picks each task's Runner from its execution lane) — pass the hands-lane selector, or `runnerFor: () => runner` for a single-Runner embedding; refusing to start rather than run every task on the widest Runner");
|
|
43
|
+
}
|
|
41
44
|
const idemCache = new IdempotencyCache();
|
|
42
45
|
const inflightRuns = new Map();
|
|
43
46
|
const cancelledViaVerb = new Set();
|
|
@@ -118,7 +121,7 @@ export function createHttpServer(rawDeps) {
|
|
|
118
121
|
});
|
|
119
122
|
deps.deviceHub?.attach(server);
|
|
120
123
|
function runnerFor(spec) {
|
|
121
|
-
return deps.runnerFor
|
|
124
|
+
return deps.runnerFor(spec);
|
|
122
125
|
}
|
|
123
126
|
function recordTaskResult(result) {
|
|
124
127
|
const plane = outwardPlane(result.terminal);
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* ## 为什么注册完还要回看一眼状态
|
|
34
34
|
*
|
|
35
35
|
* 光「订阅未来事件」不够:`'close'` **只发一次**,而四条腿的接线全都排在若干次 await 之后
|
|
36
|
-
* (trace 是 `getRun
|
|
36
|
+
* (trace 是 `getRun`,runs 是 subagent probe;sse-log 自 S-211 ① 起写头前已无 await,此前是 `retainedFrom`)。客户端
|
|
37
37
|
* 在那段慢查询里断开 ⇒ 两个 close 都在监听器注册**之前**烧完(实测:后装的监听器不会被补发)⇒
|
|
38
38
|
* 清理整条不发生,泵对着死连接轮询到 15 分钟帽。这不是假想:`sse-log.ts` 的 preamble 注里记着的
|
|
39
39
|
* 同一个坑,当时只把 preamble 挪到了接线之后,而接线本身前面还有别的 await。
|
package/dist/http/sse-log.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [ref] A9 seam —— 通用可恢复 SSE 泵(P2.8),从 `http/server.ts` 整段搬出的原文。
|
|
3
3
|
*
|
|
4
|
-
* task_run 与 image_bake 两条日志共用同一套 liveness/heartbeat
|
|
4
|
+
* task_run 与 image_bake 两条日志共用同一套 liveness/heartbeat/终态复取/15 分钟帽逻辑;域模块
|
|
5
5
|
* (routes/images.ts)与 server.ts 都要用它,所以它必须住在 http/ 叶子里(routes/* 值 import server.ts
|
|
6
6
|
* 会闭合运行时装载环)。
|
|
7
7
|
*/
|
|
@@ -17,8 +17,6 @@ export interface SseLogProvider<E extends {
|
|
|
17
17
|
} | null | undefined>;
|
|
18
18
|
/** Events with seq strictly greater than `after`, ascending. */
|
|
19
19
|
getEvents(id: string, after: number): Promise<E[]>;
|
|
20
|
-
/** Earliest retained seq → the 416 resume boundary. */
|
|
21
|
-
retainedFrom(id: string): Promise<number>;
|
|
22
20
|
/** Map ONE event to its SSE frame (the `id:`/`event:`/`data:` lines differ per log). */
|
|
23
21
|
formatEvent(ev: E): {
|
|
24
22
|
id: number;
|
|
@@ -31,13 +29,12 @@ export interface SseLogProvider<E extends {
|
|
|
31
29
|
data: unknown;
|
|
32
30
|
};
|
|
33
31
|
/**
|
|
34
|
-
* [ref] 车3 §5:**开流 preamble** —— 在
|
|
32
|
+
* [ref] 车3 §5:**开流 preamble** —— 在 `sseHeaders` **之后**、日志 tail **之前**写的
|
|
35
33
|
* 一批**非账本**帧(今天唯一的用户 = 审批未决卡的全量对账基准)。
|
|
36
34
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* headers-already-sent),而调用方自己先调 `sseHeaders` 又会被本 helper 重复写头。
|
|
35
|
+
* 为什么是这里的内部 hook,而不是调用方在 `streamSseLog` **之前**自己写:写头是本 helper 的事,调用方
|
|
36
|
+
* 先调 `sseHeaders` 会被这里重复写头。(S-211 ① 之前本 helper 在写头之前还有一道 416 判定,那是这条
|
|
37
|
+
* 理由的另一半 —— 判定随「账本不存在局部头部裁剪」一起删了,见 {@link streamSseLog} 头注。)
|
|
41
38
|
*
|
|
42
39
|
* 返回帧**不带 `id:`** —— 它不是账本行,不参与 `Last-Event-ID` 游标(壳的规则:preamble 给的是
|
|
43
40
|
* 权威全量卡集,账本重放里的同 id 帧只用于时间线渲染)。
|
|
@@ -83,7 +80,13 @@ export declare function collectSsePreamble(preamble: (signal: AbortSignal) => Pr
|
|
|
83
80
|
data: unknown;
|
|
84
81
|
}>>;
|
|
85
82
|
/** The shared resumable SSE pump (P2.8) — provider supplies the log-specific differences; the liveness/
|
|
86
|
-
* heartbeat/
|
|
83
|
+
* heartbeat/terminal-re-fetch/15-min-cap logic is identical for task_run + image_bake.
|
|
84
|
+
*
|
|
85
|
+
* 🔴 S-211 ①:**没有「续读点已被淘汰」这一答**。两条日志都**没有局部头部裁剪腿** —— `task_event` 唯一的删除者
|
|
86
|
+
* 是整棵会话树连删(会话删除 / 托管留存,`task_run` 行同批消失 ⇒ 调用方开流前 404),`image_bake_event` 库里一条
|
|
87
|
+
* 删除语句都没有。此前这里按 `retainedFrom > from + 1` 答 416 `limit.retention_evicted`:真淘汰永远走不到它,
|
|
88
|
+
* 唯一走得到的形是「写失败耗掉了号」留下的**头部缺行**(ledger-sink 账本留洞)—— 那一形里什么都没被淘汰,416
|
|
89
|
+
* 是一句谎。续读语义因此只有一句:投 `seq > from` 的每一行(缺的行就是缺的,不补、不猜)。 */
|
|
87
90
|
export declare function streamSseLog<E extends {
|
|
88
91
|
seq: number;
|
|
89
92
|
}>(req: IncomingMessage, res: ServerResponse, provider: SseLogProvider<E>, id: string, staleMs: number): Promise<void>;
|
package/dist/http/sse-log.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bindSseLifecycle } from "./sse-lifecycle.js";
|
|
2
|
-
import {
|
|
2
|
+
import { sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "./send.js";
|
|
3
3
|
export const SSE_PREAMBLE_TIMEOUT_MS = 2_000;
|
|
4
4
|
export async function collectSsePreamble(preamble, onFailure, timeoutMs = SSE_PREAMBLE_TIMEOUT_MS) {
|
|
5
5
|
const timedOut = Symbol("preamble-timeout");
|
|
@@ -34,13 +34,6 @@ export async function collectSsePreamble(preamble, onFailure, timeoutMs = SSE_PR
|
|
|
34
34
|
export async function streamSseLog(req, res, provider, id, staleMs) {
|
|
35
35
|
const lastId = Number(req.headers["last-event-id"] ?? new URL(req.url ?? "", "http://x").searchParams.get("from") ?? 0);
|
|
36
36
|
let from = Number.isFinite(lastId) ? lastId : 0;
|
|
37
|
-
if (from > 0) {
|
|
38
|
-
const retainedFrom = await provider.retainedFrom(id);
|
|
39
|
-
if (retainedFrom > from + 1) {
|
|
40
|
-
sendError(res, 416, "limit.retention_evicted", "resume point evicted past retention", { retainedFrom });
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
37
|
sseHeaders(res);
|
|
45
38
|
let closed = false;
|
|
46
39
|
bindSseLifecycle(req, res, () => {
|
|
@@ -259,7 +259,11 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
259
259
|
};
|
|
260
260
|
readonly "server.run-terminal.redact-threw": {
|
|
261
261
|
readonly cls: "F";
|
|
262
|
-
readonly note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError
|
|
262
|
+
readonly note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形由 S-219 的 atLeast 铸点闭掉;JSON 秘密字段臂的转义对循环与 PEM 栅栏臂的标签交替循环由 S-265 换成手写线性扫描器闭掉 —— 已知形全部闭掉,本臂留作结构性兜底,计数恒零是证据、不归零即新形)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
|
|
263
|
+
};
|
|
264
|
+
readonly "server.run-terminal.provider-echo-shaped": {
|
|
265
|
+
readonly cls: "F";
|
|
266
|
+
readonly note: "S-283(7.105.0,`observability/run-terminal.ts` 的 `admitProviderEcho`,唯一判决口;S-889 起动作与 core #1204 / 7.34.1 源头闸同一条):模型服务商回声、引擎逐字转交的不透明 id(`apiFailure.requestId` / `responseModel`,以及账本孪生 `turn_end.responseModel`、子代通告 `task_notification.apiFailure.requestId` 与 `POST /v1/side-query` 200 体的 `responseModel`;铸点 + 账本汇点两口)越出形状(字符串、长度 1–128、每个码元是可见 ASCII 0x21–0x7E;空串与非串值都在外)⇒ **缺席**(键删掉 / 不铸)。方向是 **fail-closed**(越界字节不上 wire / 不落库 / 不进日志),不截断、不过脱敏器(不透明 id,脱敏会改坏它 —— 与 S-270 structuredOutput 同方向)。放行的最坏后果 = 这枚 id / 模型名在 wire 上缺席(`apiFailure` 留 `{}` = 「provider 故障、无可补充标签」,core 契约原句),运维拿不到它去服务商那边对单;detail 只带座位 / 原长或类型,**不带值**。计数非零 = 这台部署接的端点(或中间网关)在回显越界字节,运维按座位去查那一跳;读面不改写存量行,存量越界行每次回放各计一次。";
|
|
263
267
|
};
|
|
264
268
|
readonly "server.run-terminal.redact-capped": {
|
|
265
269
|
readonly cls: "F";
|
|
@@ -250,7 +250,11 @@ export const FAIL_OPEN_TAGS = {
|
|
|
250
250
|
},
|
|
251
251
|
"server.run-terminal.redact-threw": {
|
|
252
252
|
cls: "F",
|
|
253
|
-
note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError
|
|
253
|
+
note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形由 S-219 的 atLeast 铸点闭掉;JSON 秘密字段臂的转义对循环与 PEM 栅栏臂的标签交替循环由 S-265 换成手写线性扫描器闭掉 —— 已知形全部闭掉,本臂留作结构性兜底,计数恒零是证据、不归零即新形)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
|
|
254
|
+
},
|
|
255
|
+
"server.run-terminal.provider-echo-shaped": {
|
|
256
|
+
cls: "F",
|
|
257
|
+
note: "S-283(7.105.0,`observability/run-terminal.ts` 的 `admitProviderEcho`,唯一判决口;S-889 起动作与 core #1204 / 7.34.1 源头闸同一条):模型服务商回声、引擎逐字转交的不透明 id(`apiFailure.requestId` / `responseModel`,以及账本孪生 `turn_end.responseModel`、子代通告 `task_notification.apiFailure.requestId` 与 `POST /v1/side-query` 200 体的 `responseModel`;铸点 + 账本汇点两口)越出形状(字符串、长度 1–128、每个码元是可见 ASCII 0x21–0x7E;空串与非串值都在外)⇒ **缺席**(键删掉 / 不铸)。方向是 **fail-closed**(越界字节不上 wire / 不落库 / 不进日志),不截断、不过脱敏器(不透明 id,脱敏会改坏它 —— 与 S-270 structuredOutput 同方向)。放行的最坏后果 = 这枚 id / 模型名在 wire 上缺席(`apiFailure` 留 `{}` = 「provider 故障、无可补充标签」,core 契约原句),运维拿不到它去服务商那边对单;detail 只带座位 / 原长或类型,**不带值**。计数非零 = 这台部署接的端点(或中间网关)在回显越界字节,运维按座位去查那一跳;读面不改写存量行,存量越界行每次回放各计一次。",
|
|
254
258
|
},
|
|
255
259
|
"server.run-terminal.redact-capped": { cls: "F", note: "S-107:终态自由文本(errorMessage/blockedReason/remoteEnvFailures[].message/error 列/done·failed 行)长度 > REDACT_INPUT_CAP(64KB)⇒ 整段换 «redacted:oversized»(fail-closed 取证丢弃,不截断不扫);计数=多少条终态文案被整段丢弃" },
|
|
256
260
|
"server.run-terminal.artifact-capped": { cls: "F", note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃" },
|
|
@@ -241,7 +241,7 @@ export function createMetrics() {
|
|
|
241
241
|
m.counter("task_model_not_allowlisted_resume_total", "Resumed legs whose model/compactionModel is outside the @-mention allowlist (allowed to keep running by design — the allowlist gates fresh picks only)");
|
|
242
242
|
m.counter("task_model_unavailable_fallback_total", "Resumed legs whose explicit model pick names a catalog entry that is unavailable on this deployment (no brain mounted for its provider / entry fails its schema) — the leg runs on the default model; fresh submits get 400 request.model_unavailable");
|
|
243
243
|
m.counter("prompt_constitution_total", "Prompt assembly constitution mode per task (core 1.243 提示词主权): steady state = core; provider-assembled/replaced/legacy warrant eyes");
|
|
244
|
-
m.counter("rewind_snapshot_failed_total", "Per-turn rewind file snapshots that failed, by the engine's FileHistoryResult.error.code (snapshot_failed/aborted/…; `unknown` = the
|
|
244
|
+
m.counter("rewind_snapshot_failed_total", "Per-turn rewind file snapshots that failed, by the engine's FileHistoryResult.error.code (snapshot_failed/aborted/…; `unknown` = the text carried no word of that closed set, e.g. the boundary-timeout arm). COARSE on purpose — whether a failure is permanent (the same file will fail every turn until the deployment changes) is counted by file_history_permanent_failure_total instead, from the typed error object rather than from prose");
|
|
245
245
|
m.counter("verifications_total", "Tasks run through the verification gate (1.44), by final verdict");
|
|
246
246
|
m.counter("cascade_total", "Tasks run through the model cascade (1.45), by outcome (passed/exhausted)");
|
|
247
247
|
m.counter("degenerate_total", "Degenerate-output failures (1.59), by salvage class (a/b/unknown)");
|
|
@@ -78,10 +78,12 @@ export declare function notifyRunTerminal(rec: RunTerminalRecord): void;
|
|
|
78
78
|
/** S-105(test [ref] 旁生):终态**持久化**的 `errorMessage` 与日志副本同一脱敏口——provider 错误体(403 体带回显请求头/
|
|
79
79
|
* 订阅链接)里的密钥形不得原文落 run 行再由 `GET /v1/runs/:id` 直出。三 run store 的 `setTerminal` 写入前同调此函数;
|
|
80
80
|
* `undefined` 透传(缺席仍缺席),不截断(日志上限是日志的事,行上保留全文供取证)。 */
|
|
81
|
-
/** r4 最终轮 [medium]:`redactSecrets`
|
|
82
|
-
*
|
|
81
|
+
/** r4 最终轮 [medium]:`redactSecrets` 曾会抛 `RangeError`(关键字/前缀臂那一形由 S-219 的 `atLeast` 铸点闭掉;JSON 秘密字段臂的
|
|
82
|
+
* 转义对循环与 PEM 栅栏臂的标签交替循环由 S-265 换成手写线性扫描器闭掉 —— 已知形全部闭掉);S-105 把脱敏
|
|
83
83
|
* 挪到 `setTerminal` **入口**后,这个抛会先于 CAS/claim 释放 ⇒ run 卡 `running`、会话 claim 悬挂到 reaper。这里 **fail-closed
|
|
84
|
-
* 不抛**:脱敏失败 ⇒ 落固定占位 `«redacted:oversized»`(绝不落原文,也绝不阻断终态翻转),并走既有 fail-open 遥测 tag。
|
|
84
|
+
* 不抛**:脱敏失败 ⇒ 落固定占位 `«redacted:oversized»`(绝不落原文,也绝不阻断终态翻转),并走既有 fail-open 遥测 tag。
|
|
85
|
+
* 🔴 S-265 之后这条 throw 臂**照留**(结构性兜底,不依赖「脱敏器今天抛不抛」):它守的是「终态翻转不被一次脱敏异常拦下」,
|
|
86
|
+
* 而脱敏器是一张会长新臂的表 —— 计数 `server.run-terminal.redact-threw` 恒零是证据,不归零就是新形,运维按 detail 回溯。 */
|
|
85
87
|
export declare const OVERSIZED_REDACTION_PLACEHOLDER = "\u00ABredacted:oversized\u00BB";
|
|
86
88
|
/** S-107 窄范围重扫 [medium]:`redactSecrets` 对 PEM 起始栅栏密集的多 MB 文案**不抛但慢**(实测 1MB 1.4s / 8MB 7.9s 同步阻塞
|
|
87
89
|
* 事件循环),而终态座每次写要扫两遍(appendEvent + setTerminal 入口)、每次读再扫一遍。超过本上限的自由文本**整段**换占位
|
|
@@ -111,20 +113,26 @@ export declare function redactErrorMessage(raw: string | undefined): string | un
|
|
|
111
113
|
* ⚠️ **命中那一次仍然给剥后文本**(S-103 的「先扫描后渲染」不动摇):把零宽字符塞进密钥中间的那条文本,
|
|
112
114
|
* 渲染的必须是剥后那份,否则消费端把残片拼回原钥。
|
|
113
115
|
* ② **扫描器抛 ⇒ 登记过的占位(fail-closed),不把整条 run 拖下水**(codex r1 [high],亲核实测后修)。
|
|
114
|
-
* `redactSecrets` 的 JSON
|
|
116
|
+
* 立案时 `redactSecrets` 的 JSON 秘密字段臂含多字符循环体(`(?:\\.[^"\\]*)*`),实测
|
|
115
117
|
* `{"password":"<400 万个 \\n 转义对>"}`(8MB)抛 `RangeError`。`result` 接进来之前这条路不存在;接进来之后,
|
|
116
118
|
* 裸调用会让异常穿过 `redactTerminalResult` → 三孪生 `setTerminal`,**一条跑成功的 run 落成 failed + result:null**
|
|
117
119
|
* (实测读数)。⇒ 与本文件既有的 {@link redactErrorMessage} / `redactDiagnosticText` / `redactArtifactText`
|
|
118
120
|
* 三座**同一条**处置:抛就换占位、计数、留痕([ref] 的 loud,不是静默 fail-open)。丢的是这一条病态文本,
|
|
119
|
-
* 不是这条 run。根因(那条正则)
|
|
121
|
+
* 不是这条 run。根因(那条正则)由 S-265(7.105.0)换成手写线性扫描器闭掉(同批闭掉同类第二例 PEM 栅栏标签循环);
|
|
122
|
+
* 本处置**照留** —— 理由同 {@link OVERSIZED_REDACTION_PLACEHOLDER} 顶注末句。
|
|
120
123
|
* ③ **不设体量上限**(裁定):`result` 住在 `result` JSON/JSONB 列(亲核 `plugins/tidb-pool.ts` 与
|
|
121
124
|
* `plugins/pg-pool.ts` 的 DDL),数 MB 终稿是正常形 —— {@link redactErrorMessage} 的两道上限是为
|
|
122
125
|
* `task_run.error` 那一列 `TEXT(65 535 字节)` 定的,套到这里等于用静默的数据损坏换一次脱敏。
|
|
123
126
|
*/
|
|
124
127
|
export declare function redactModelText(raw: string): string;
|
|
125
|
-
/**
|
|
126
|
-
* `
|
|
127
|
-
export declare
|
|
128
|
+
/** 回声座上的值留不留:形内 ⇒ `true`(原样留);`undefined` ⇒ `false`(缺席本身,不计数);其余越界 ⇒ `false` + 计数。
|
|
129
|
+
* 调用方按 `false` 删键 / 不铸键 —— 本函数是全部回声座(结果面 / 账本孪生 / 子代通告 / side-query 200 体)的**唯一**判决口。 */
|
|
130
|
+
export declare function admitProviderEcho(value: unknown, seat: string): value is string;
|
|
131
|
+
/** {@link redactTerminalResult} 会碰的全部顶层键 = 现行形里**不是** `"verbatim"` 的行(`"redacted"` ∪ S-283 的
|
|
132
|
+
* `"provider-echo"`)∪ 旧形平面键;`memoRedactTerminalResult` 的记忆键由它派生(两处同源,见 `RESULT_KEY_TREATMENT` 顶注)。
|
|
133
|
+
* 🔴 S-283 起改名(原 `REDACTABLE_RESULT_LEAVES`):回声键不脱敏但**会被改**(越界 ⇒ 删键),漏进记忆键集 = 读面记忆腿对存量行
|
|
134
|
+
* O(1) 原样直返,读面纵深对那几片恒不生效([ref] 立案时 `editedFiles` 那一格的同一个病)。 */
|
|
135
|
+
export declare const TREATED_RESULT_LEAVES: readonly string[];
|
|
128
136
|
export declare function redactTerminalResult<T extends {
|
|
129
137
|
terminal?: unknown;
|
|
130
138
|
/** 旧形(≤7.63.0)的三个顶层座;形上取 `unknown` —— 读盘面对盘上字节不作假设。 */
|
|
@@ -144,6 +152,10 @@ export declare function redactTerminalResult<T extends {
|
|
|
144
152
|
editedFiles?: unknown;
|
|
145
153
|
/** S-394:形上同样 `unknown` —— 本函数是读盘面,`stats` 的树形逐层现判(见 {@link redactStats})。 */
|
|
146
154
|
stats?: unknown;
|
|
155
|
+
/** S-283:provider 回声三键(形上 `unknown`,读盘面纪律同上;处置见 {@link PROVIDER_ECHO_SHAPERS})。 */
|
|
156
|
+
apiFailure?: unknown;
|
|
157
|
+
strandedHumanAnswers?: unknown;
|
|
158
|
+
responseModel?: unknown;
|
|
147
159
|
} | null | undefined>(result: T): T;
|
|
148
160
|
/**
|
|
149
161
|
* 🔴 **S-173 —— run 行 `result` 列的唯一入口**:先过出口封口({@link sealResultForExit}:摘恢复凭据 + 折域外码),
|
|
@@ -202,7 +214,7 @@ export declare function redactLeaderResultWithFlag(result: unknown): {
|
|
|
202
214
|
changed: boolean;
|
|
203
215
|
};
|
|
204
216
|
export declare function memoRedactedRead<T>(seat: string, raw: T, redact: (v: T) => T): T;
|
|
205
|
-
/** run 终态 result
|
|
217
|
+
/** run 终态 result 的读面记忆只按**本函数会改字节的那几叶**({@link TREATED_RESULT_LEAVES}:脱敏叶 ∪ S-283 回声叶 ∪ 旧形平面键,属主是
|
|
206
218
|
* `RESULT_KEY_TREATMENT`)做键与留体——
|
|
207
219
|
* 第十六轮重扫:按整块 result 摘要每次轮询要 stringify 一份可能数 MB 的 result,且大 result 的脏行永远存不进去。叶全缺席
|
|
208
220
|
* ⇒ O(1) 直返(`redactTerminalResult` 本就同引用)。
|
|
@@ -215,6 +227,30 @@ export declare function memoRedactedRead<T>(seat: string, raw: T, redact: (v: T)
|
|
|
215
227
|
* `editedFiles` 那一格的病)。读数:8MB result 的一次摘要 ~10ms 量级,远小于一次全扫;干净行仍只摘要
|
|
216
228
|
* 一次即进 clean 集,脏行超过 {@link READ_MEMO_DIRTY_MAX_ENTRY_BYTES} 时退化为每次重扫(既有形)。 */
|
|
217
229
|
export declare function memoRedactTerminalResult<T extends Parameters<typeof redactTerminalResult>[0]>(seat: string, result: T): T;
|
|
230
|
+
/**
|
|
231
|
+
* 🔴 **账本行的读面口 —— 全部耐久读腿共用这一只**(S-269,7.105.0)。
|
|
232
|
+
*
|
|
233
|
+
* 脱敏的**属主**仍是 {@link redactLedgerEventData}(写读共用的唯一汇点,规则一张:哪些行型有自由文本座、怎么脱);
|
|
234
|
+
* 本函数**不是第二只脱敏器**,只在汇点外面加一层**按型记忆**(读面每次轮询 / 每个观看者 / 每次重连都重放同一批
|
|
235
|
+
* 不可变的行):
|
|
236
|
+
* · `done` 行 —— 只按 `result` 的可脱敏叶记忆({@link memoRedactTerminalResult};整块 result 可能数 MB);
|
|
237
|
+
* · `failed` 行与模型自由文本两行(`text` / `reasoning`,词表属主 `trace/ledger-events.ts`)—— 按内容摘要记忆({@link memoRedactedRead});
|
|
238
|
+
* · 其余行型 —— 直调汇点(park 行只有一个小 gate,扫一次的代价远低于摘要 + 记忆表位;其余型汇点逐字透传)。
|
|
239
|
+
*
|
|
240
|
+
* **三条耐久读腿同调**:`GET /v1/runs/:id/events`(账本裸回放)、`GET /v1/tasks/:id/turns`(`projectEvents`)、
|
|
241
|
+
* trace SSE `GET /v1/tasks/:id/stream`(`mapTraceEvent`)。此前只有第一条在 `formatEvent` 里手写了这段分派,后两条
|
|
242
|
+
* **直吃原行** —— 新行在三孪生 `appendEvent` 落库前已脱,所以只打在**存量行**上:7.75.1(B-106)之前 `text` 行的写口
|
|
243
|
+
* 不脱,那批行在两条 trace 腿上原字节回放(S-269,车EC U-2)。分派收进本函数、三条腿同调 ⇒ 读面纵深一条规则。
|
|
244
|
+
*
|
|
245
|
+
* **性能取舍(读实现后定)**:汇点对已脱行在**输出**上幂等(`redactModelText` 认不出凭据形就回同一引用),但**代价不是
|
|
246
|
+
* 空过** —— 每次都整段重扫(`redactSecrets` 全池一遍)。所以读面走记忆:同一行第二次起只付一次 sha256 摘要 + 集合查找
|
|
247
|
+
* (车QO 本机实测,8.6MB 散文 min-of-5:整段重扫 112ms vs 摘要〔含 `JSON.stringify`〕31ms,约 3.6×);记忆表有界(干净 8192 条 / 脏 512 条且总字节封顶,见 {@link memoRedactedRead})。
|
|
248
|
+
* **没**选「行上打已脱标记 ⇒ 跳过」:那要在三孪生写口加一个持久字段,而需要读面纵深的恰恰是**没有**标记的存量行 ——
|
|
249
|
+
* 标记只能省新行的扫描;记忆对新旧行一视同仁,且不新开一张持久面。
|
|
250
|
+
* trace 两腿对 `done` 行只投 `seq`(不投 `result`),它的摘要是**白付**的那一份;**刻意**不按「这条腿投哪些键」裁掉 ——
|
|
251
|
+
* 那等于在读腿里手抄一份汇点的行型表([ref]),汇点将来加一种带自由文本的行型时读腿静默漏。
|
|
252
|
+
*/
|
|
253
|
+
export declare function redactLedgerEventForRead(type: string, data: unknown): unknown;
|
|
218
254
|
export declare function diffScanShadowCountForTest(): number;
|
|
219
255
|
export declare function resetDiffScanShadowCountForTest(): void;
|
|
220
256
|
export declare function resetReadRedactionMemoForTest(): void;
|