@sema-agent/server 7.94.0 → 7.95.1
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/USAGE.md +52 -0
- package/dist/boot/resolve-spec.js +14 -1
- package/dist/boot/stage-09-leader.js +1 -1
- package/dist/config-catalog.js +5 -3
- package/dist/config-center/types.d.ts +1 -1
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +17 -0
- package/dist/config-types.d.ts +44 -3
- package/dist/config-types.js +1 -0
- package/dist/config.js +24 -4
- package/dist/hosted-posture.d.ts +176 -0
- package/dist/hosted-posture.js +76 -0
- package/dist/http/resume-ctx-read.d.ts +7 -1
- package/dist/http/resume-ctx-read.js +3 -3
- package/dist/http/resume-legs.js +38 -28
- package/dist/http/routes/capabilities.js +3 -0
- package/dist/http/wire-types.d.ts +13 -4
- package/dist/plugins/approval-ask-store-sql.js +1 -1
- package/dist/plugins/background-agent-store-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.js +1 -1
- package/dist/plugins/device-store-sql.js +1 -1
- package/dist/plugins/image-bake-store-sql.js +1 -1
- package/dist/plugins/image-index-sql.js +1 -1
- package/dist/plugins/leader-run-store-sql.js +1 -1
- package/dist/plugins/local-checkpoint-store.js +10 -2
- package/dist/plugins/mailbox-store-sql.js +5 -3
- package/dist/plugins/memory-engine-pg.js +1 -1
- package/dist/plugins/memory-engine-tidb.js +1 -1
- package/dist/plugins/memory-sync-store-pg.js +1 -1
- package/dist/plugins/permission-rule-store-sql.js +2 -2
- package/dist/plugins/run-store-sql.js +1 -1
- package/dist/plugins/session-event-payload.js +1 -1
- package/dist/plugins/session-policy-store-sql.js +4 -2
- package/dist/plugins/sql-json-column.d.ts +52 -7
- package/dist/plugins/sql-json-column.js +14 -3
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/usage-window-store-sql.js +1 -1
- package/dist/plugins/workflow-journal-store-sql.js +1 -1
- package/dist/plugins/workflow-run-store-sql.js +1 -1
- package/dist/run-local.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
|
|
2
|
+
import { deviceCwdHonored } from "./task-cwd.js";
|
|
3
|
+
export const HOSTED_EVIDENCE_WORDS = ["require-principal", "principal-jwt", "auth-bridge-issuer"];
|
|
4
|
+
export function buildHostedPosture(c) {
|
|
5
|
+
const evidence = [];
|
|
6
|
+
if (c.requirePrincipal === true)
|
|
7
|
+
evidence.push("require-principal");
|
|
8
|
+
if (c.principalJwtPubkeys.length > 0 || c.principalJwtIss !== undefined || c.principalJwtAud !== undefined)
|
|
9
|
+
evidence.push("principal-jwt");
|
|
10
|
+
if (c.authBridgeIssuer !== undefined)
|
|
11
|
+
evidence.push("auth-bridge-issuer");
|
|
12
|
+
return { hosted: evidence.length > 0, evidence };
|
|
13
|
+
}
|
|
14
|
+
export const HOSTED_EVIDENCE_ENV_KEYS = ["PRINCIPAL_JWT_PUBKEYS", "PRINCIPAL_JWT_ISS", "PRINCIPAL_JWT_AUD", "AUTH_BRIDGE_ISSUER"];
|
|
15
|
+
export function clearHostedEvidenceEnv() {
|
|
16
|
+
delete process.env.PRINCIPAL_JWT_PUBKEYS;
|
|
17
|
+
delete process.env.PRINCIPAL_JWT_ISS;
|
|
18
|
+
delete process.env.PRINCIPAL_JWT_AUD;
|
|
19
|
+
delete process.env.AUTH_BRIDGE_ISSUER;
|
|
20
|
+
}
|
|
21
|
+
export function hostedEvidenceNote(e) {
|
|
22
|
+
switch (e) {
|
|
23
|
+
case "require-principal":
|
|
24
|
+
return "REQUIRE_PRINCIPAL=true (this deployment declares itself multi-tenant)";
|
|
25
|
+
case "principal-jwt":
|
|
26
|
+
return "PRINCIPAL_JWT_PUBKEYS/ISS/AUD is configured (principal identities are verified against an external issuer — a single-user turnkey box has no issuer)";
|
|
27
|
+
case "auth-bridge-issuer":
|
|
28
|
+
return "AUTH_BRIDGE_ISSUER is set explicitly (a second external SSO-JWT issuer; the registry-URL fallback is deliberately NOT read here — that fact is fleet management, not tenancy)";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function hostedBecause(facts) {
|
|
32
|
+
return facts.evidence.map(hostedEvidenceNote).join("; ");
|
|
33
|
+
}
|
|
34
|
+
export function hostedPrincipalRefusal(c) {
|
|
35
|
+
const facts = buildHostedPosture(c);
|
|
36
|
+
if (!facts.hosted || c.requirePrincipal === true)
|
|
37
|
+
return undefined;
|
|
38
|
+
return (`this deployment looks HOSTED/multi-tenant (${hostedBecause(facts)}) but REQUIRE_PRINCIPAL is not set — ` +
|
|
39
|
+
`an absent principal takes the single-user "default" amnesty, which would list every ownerless session to every caller ` +
|
|
40
|
+
`and open the single-user capability grants (settings.hooks / settings.env / cwd / additionalDirectories) to all of them. ` +
|
|
41
|
+
`Set REQUIRE_PRINCIPAL=true (and OPERATOR_PRINCIPALS for the operator-only routes), or remove the hosted-deployment configuration if this really is a single-user box.`);
|
|
42
|
+
}
|
|
43
|
+
export const HOST_POSTURE_REFUSED_CODE = "request.host_posture_refused";
|
|
44
|
+
function hostPostureDeclarationsHonored(c) {
|
|
45
|
+
if (deviceCwdHonored(c))
|
|
46
|
+
return true;
|
|
47
|
+
return !buildHostedPosture(c).hosted;
|
|
48
|
+
}
|
|
49
|
+
export function taskApproverPostureHonored(c) {
|
|
50
|
+
return hostPostureDeclarationsHonored(c);
|
|
51
|
+
}
|
|
52
|
+
export function taskWriteFaceRefusal(c) {
|
|
53
|
+
switch (c.taskWriteFaceEscalation) {
|
|
54
|
+
case "allow":
|
|
55
|
+
return undefined;
|
|
56
|
+
case "refuse":
|
|
57
|
+
return (`permissionMode:"bypassPermissions" opens the WRITE containment face (TaskSpec.writeFace:"open") — the run could write outside the task root. ` +
|
|
58
|
+
`Refused because this deployment pins TASK_WRITE_FACE_ESCALATION=refuse (the operator's own declaration — it wins over any request, unconditionally). ` +
|
|
59
|
+
`Omit permissionMode:"bypassPermissions", or ask the operator for TASK_WRITE_FACE_ESCALATION=allow.`);
|
|
60
|
+
case undefined:
|
|
61
|
+
break;
|
|
62
|
+
default: {
|
|
63
|
+
const unhandled = c.taskWriteFaceEscalation;
|
|
64
|
+
return (`permissionMode:"bypassPermissions" opens the WRITE containment face (TaskSpec.writeFace:"open") — the run could write outside the task root. ` +
|
|
65
|
+
`Refused because this deployment carries an UNRECOGNISED TASK_WRITE_FACE_ESCALATION word (${String(unhandled)}) — a knob this build cannot interpret is never read as permission. ` +
|
|
66
|
+
`Set TASK_WRITE_FACE_ESCALATION to one of ${TASK_WRITE_FACE_ESCALATIONS.join(" | ")}, or unset it to let the deployment posture decide.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (hostPostureDeclarationsHonored(c))
|
|
70
|
+
return undefined;
|
|
71
|
+
const facts = buildHostedPosture(c);
|
|
72
|
+
return (`permissionMode:"bypassPermissions" opens the WRITE containment face (TaskSpec.writeFace:"open") — the run could write outside the task root. ` +
|
|
73
|
+
`Refused because this deployment is HOSTED/multi-tenant (${hostedBecause(facts)}), so the task layer may only TIGHTEN the write face, never open it. ` +
|
|
74
|
+
`Set TASK_WRITE_FACE_ESCALATION=allow if the DEPLOYMENT permits the task layer to open it, or omit permissionMode:"bypassPermissions".`);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=hosted-posture.js.map
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
* · `absent` —— 行不在(`reapCtx` 按设计在 run 终态后删掉它),**或**读腿抛了一个**不是坏体**的错
|
|
12
12
|
* (连接 / 超时 / 权限 / 店抖动):证不出它在就当它不在,**这是刻意保守**([ref] 方向锚:
|
|
13
13
|
* 拒绝可重试,搁浅不可)。
|
|
14
|
-
* · `corrupt` ——
|
|
14
|
+
* · `corrupt` —— 店给了它的**拒绝契约**(`StoreCorruptError`,`kind:"corrupt"` + 坐标)。
|
|
15
15
|
* **不折成缺席**:两者的运维动作完全不同(前者去查那一行,后者什么都不用做)。
|
|
16
16
|
* ⚠️ `not-text`(投影漂 = 我方的码病)**刻意不进这一态**,原样上抛、响亮成 500 —— 不许伪装成
|
|
17
17
|
* 「数据坏了」,更不许折成缺席(那会让一次代码漂看起来像「这条会话没有上下文」)。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 **按「店的拒绝形」分类,不按「列错误类」**(S-608,7.95.1:这一句此前反了,整整一版)。修前本口
|
|
20
|
+
* 认的是属主谓词 `isJsonColumnBodyProblem`(= 一只 `SqlJsonColumnError`),而 SQL 两孪生的 `refusing()`
|
|
21
|
+
* 早把列错误**折**成了一只普通 `Error` ⇒ 折后的错落进下面那条「店抖动 ⇒ 当缺席」臂,坏体与缺席在
|
|
22
|
+
* 用户面上仍然同答 409 —— 正是 S-577 第二半要修的那个病,只是它当时只在 local 车道上修成了(那一侧
|
|
23
|
+
* 恰好没折码,把列错误漏了出来 = 巧合式正确)。两条车道现在同一份拒绝契约,本口只认它。
|
|
18
24
|
*/
|
|
19
25
|
import type { CheckpointCtx } from "./route-ctx.js";
|
|
20
26
|
export type ResumeCtxRead = {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SqlJsonColumnError, StoreCorruptError } from "../plugins/sql-json-column.js";
|
|
2
2
|
export async function readResumeCtx(load) {
|
|
3
3
|
try {
|
|
4
4
|
const ctx = await load;
|
|
5
5
|
return ctx == null ? { kind: "absent" } : { kind: "ok", ctx };
|
|
6
6
|
}
|
|
7
7
|
catch (e) {
|
|
8
|
-
if (
|
|
9
|
-
return { kind:
|
|
8
|
+
if (e instanceof StoreCorruptError)
|
|
9
|
+
return { kind: e.kind, where: e.where };
|
|
10
10
|
if (e instanceof SqlJsonColumnError)
|
|
11
11
|
throw e;
|
|
12
12
|
return { kind: "absent" };
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -286,20 +286,10 @@ export function createResumeLegs(ctx) {
|
|
|
286
286
|
if (!ctx)
|
|
287
287
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
288
288
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
catch (e) {
|
|
295
|
-
if (e instanceof HttpError) {
|
|
296
|
-
return {
|
|
297
|
-
status: 409,
|
|
298
|
-
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
throw e;
|
|
302
|
-
}
|
|
289
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
290
|
+
if (!resolved.ok)
|
|
291
|
+
return resolved.answer;
|
|
292
|
+
const { spec, engineNotices } = resolved;
|
|
303
293
|
const pendingTool = cp.pendingAction.toolName;
|
|
304
294
|
if (decision === "approve" && pendingTool === ASK_USER_QUESTION_TOOL_NAME && !answer) {
|
|
305
295
|
return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
|
|
@@ -333,6 +323,24 @@ export function createResumeLegs(ctx) {
|
|
|
333
323
|
const spec = await deps.resolveSpec({ ...body, ...ONE_SHOT_HISTORY_STRIP }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
|
|
334
324
|
return { spec, engineNotices };
|
|
335
325
|
};
|
|
326
|
+
const resolveResumeSpecOrBlocked = async (body, req, auth) => {
|
|
327
|
+
try {
|
|
328
|
+
const { spec, engineNotices } = await resolveResumeSpec(body, req, auth);
|
|
329
|
+
return { ok: true, spec, engineNotices };
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
if (e instanceof HttpError) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
answer: {
|
|
336
|
+
status: 409,
|
|
337
|
+
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
throw e;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
336
344
|
async function driveResumeIntoRunLog(args) {
|
|
337
345
|
if (args.acceptEarly !== true)
|
|
338
346
|
return await driveResumeLeg(args);
|
|
@@ -1041,7 +1049,10 @@ export function createResumeLegs(ctx) {
|
|
|
1041
1049
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1042
1050
|
const ctx = ctxRead.ctx;
|
|
1043
1051
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1044
|
-
const
|
|
1052
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1053
|
+
if (!resolved.ok)
|
|
1054
|
+
return resolved.answer;
|
|
1055
|
+
const { spec, engineNotices } = resolved;
|
|
1045
1056
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1046
1057
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1047
1058
|
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
@@ -1070,17 +1081,10 @@ export function createResumeLegs(ctx) {
|
|
|
1070
1081
|
if (windowDenial)
|
|
1071
1082
|
return { status: windowDenial.status, body: windowDenial.body };
|
|
1072
1083
|
const auth = { sessionId, principal, memoryScope: ctx.memoryScope };
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
catch (e) {
|
|
1079
|
-
if (e instanceof HttpError) {
|
|
1080
|
-
return { status: 409, body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) } };
|
|
1081
|
-
}
|
|
1082
|
-
throw e;
|
|
1083
|
-
}
|
|
1084
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1085
|
+
if (!resolved.ok)
|
|
1086
|
+
return resolved.answer;
|
|
1087
|
+
const { spec, engineNotices } = resolved;
|
|
1084
1088
|
const hostSpec = { ...spec, objective, sessionId };
|
|
1085
1089
|
const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
|
|
1086
1090
|
prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
|
|
@@ -1151,7 +1155,10 @@ export function createResumeLegs(ctx) {
|
|
|
1151
1155
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1152
1156
|
const ctx = ctxRead.ctx;
|
|
1153
1157
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1154
|
-
const
|
|
1158
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1159
|
+
if (!resolved.ok)
|
|
1160
|
+
return resolved.answer;
|
|
1161
|
+
const { spec, engineNotices } = resolved;
|
|
1155
1162
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1156
1163
|
return driveResumeIntoRunLog({
|
|
1157
1164
|
runner: runnerFor(spec),
|
|
@@ -1242,7 +1249,10 @@ export function createResumeLegs(ctx) {
|
|
|
1242
1249
|
}
|
|
1243
1250
|
const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
|
|
1244
1251
|
const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
|
|
1245
|
-
const
|
|
1252
|
+
const resolved = await resolveResumeSpecOrBlocked(resumeBody, req, auth);
|
|
1253
|
+
if (!resolved.ok)
|
|
1254
|
+
return resolved.answer;
|
|
1255
|
+
const { spec, engineNotices } = resolved;
|
|
1246
1256
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1247
1257
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1248
1258
|
const outcome = {
|
|
@@ -3,6 +3,7 @@ import { isModelAllowlisted } from "../../model-select.js";
|
|
|
3
3
|
import { resolveModelApiKey } from "../../key-resolver.js";
|
|
4
4
|
import { isSealedKeyPoison } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
6
|
+
import { taskApproverPostureHonored, taskWriteFaceRefusal } from "../../hosted-posture.js";
|
|
6
7
|
import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
|
|
7
8
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
8
9
|
import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
@@ -135,6 +136,8 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
135
136
|
interactiveTools: true,
|
|
136
137
|
projectContext: cwdHonored(deps.config),
|
|
137
138
|
callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
|
|
139
|
+
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined,
|
|
140
|
+
taskApproverPosture: taskApproverPostureHonored(deps.config),
|
|
138
141
|
executionLane: projectExecutionLaneCapability(deps.config),
|
|
139
142
|
mcpInjection: mcpInjectionHonored(deps.config),
|
|
140
143
|
a2a: true,
|
|
@@ -326,10 +326,19 @@ export interface TaskRequestBody {
|
|
|
326
326
|
* (且只能收紧、deny-wins),前者只说「谁来答卡」。两者在语义上同族,声明它而不开 `bypassPermissions`
|
|
327
327
|
* 只是对模型说了一句与实际不符的话 —— 见契约文档那一段的「谁受伤」。
|
|
328
328
|
*
|
|
329
|
-
* 🔴
|
|
330
|
-
* (`refuseInvalidApproverPosture` 响亮拒一个不认识的词),server
|
|
331
|
-
*
|
|
332
|
-
*
|
|
329
|
+
* 🔴 **值域上本仓零判据**(`boot/resolve-spec.ts` 写点):词的拒绝逻辑属引擎
|
|
330
|
+
* (`refuseInvalidApproverPosture` 响亮拒一个不认识的词),server **不抄词表、不加第二道值域门** ——
|
|
331
|
+
* 在下游再判一次就是同一语义面的第二个判官(源头修复纪律 [ref]),而且本仓抄一份词表的那天 core 加
|
|
332
|
+
* 第二个词就会被静默 400 掉。型也从 core 的 `ApproverPosture` 派生(不抄字面联合)。
|
|
333
|
+
*
|
|
334
|
+
* 🔴 **但「原样过境」自 7.95.0(S-591 腿③)起不再成立 —— 它有一道**准入**闸**:本键是一句**宿主姿态
|
|
335
|
+
* 声明**(「这个 run 的审批卡由宿主自己自动答」),而在**托管 / 多租**部署上提交方**不是**宿主。
|
|
336
|
+
* 所以托管形上它被**忽略 + 留声**(`task_approver_posture_ignored`,`honored:false`),与同族的
|
|
337
|
+
* `settings.hooks` / `cwd` / `settings.env` 三条逐字同姿势;单用户 turnkey 与 device 车道照旧过境
|
|
338
|
+
* (射程判据单点 = `src/hosted-posture.ts` 的 `taskApproverPostureHonored`)。
|
|
339
|
+
* ⚠️ **刻意不是 400/422**:本键零授权,而续跑腿逐字重放盘上的体、调用方删不掉它 ⇒ 拒会把一条 parked
|
|
340
|
+
* 任务永久砖死(clay 裁定① 2026-09-22;本仓成文结论「砖死 resume 比降级糟」)。
|
|
341
|
+
* ⇒ 这一条与「值域零判据」**不矛盾**:值域仍归引擎,本仓判的是**这台部署认不认这句声明**。
|
|
333
342
|
*/
|
|
334
343
|
approverPosture?: ApproverPosture;
|
|
335
344
|
/** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
|
|
@@ -262,7 +262,7 @@ function mapAskRow(r) {
|
|
|
262
262
|
return mapAskRowBody(r, askId);
|
|
263
263
|
}
|
|
264
264
|
catch (e) {
|
|
265
|
-
throw foldJsonColumn(e, (m) =>
|
|
265
|
+
throw foldJsonColumn(e, (m) => `approval-ask-store: ${m} — refusing rather than serve an ask row nobody can read`);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
function mapAskRowBody(r, askId) {
|
|
@@ -15,7 +15,7 @@ function readRecordColumn(v, handle) {
|
|
|
15
15
|
return decodeJsonColumn(v, readAgentRecordBody, `${BACKGROUND_AGENT_TABLE}.record_json for handle ${handle}`);
|
|
16
16
|
}
|
|
17
17
|
catch (e) {
|
|
18
|
-
throw foldJsonColumn(e, (m) =>
|
|
18
|
+
throw foldJsonColumn(e, (m) => `BackgroundAgentStore: ${m} — refusing rather than serve a hollow agent record`);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
function readUsageColumn(v, handle) {
|
|
@@ -23,7 +23,7 @@ function readUsageColumn(v, handle) {
|
|
|
23
23
|
return decodeNullableJsonColumn(v, readUsageBody, `${BACKGROUND_AGENT_TABLE}.usage_json for handle ${handle}`);
|
|
24
24
|
}
|
|
25
25
|
catch (e) {
|
|
26
|
-
throw foldJsonColumn(e, (m) =>
|
|
26
|
+
throw foldJsonColumn(e, (m) => `BackgroundAgentStore: ${m} — refusing rather than report a usage figure nobody can read`);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
const PROJ_COLS = "scope, owner, session_scoped, session_id, parent_session_id, root_session_id, session_anchor, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json, rev, parked_checkpoint_token, park_claim_id, admitted_org_scopes, admitted_org_write_scope, record_json";
|
|
@@ -137,7 +137,7 @@ function refusing(what, read) {
|
|
|
137
137
|
return read();
|
|
138
138
|
}
|
|
139
139
|
catch (e) {
|
|
140
|
-
throw foldJsonColumn(e, (m) =>
|
|
140
|
+
throw foldJsonColumn(e, (m) => `SqlCheckpointStore: ${m} — refusing rather than serve ${what}`);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
function readRuleOffersCell(cell) {
|
|
@@ -314,7 +314,7 @@ function readAuditDetail(raw, id) {
|
|
|
314
314
|
return decodeJsonColumn(raw, readJsonObjectColumn, `${DEVICE_AUDIT_TABLE}.detail for audit row ${id}`);
|
|
315
315
|
}
|
|
316
316
|
catch (e) {
|
|
317
|
-
throw foldJsonColumn(e, (m) =>
|
|
317
|
+
throw foldJsonColumn(e, (m) => `device-store: ${m} — refusing rather than serve an audit row whose detail nobody can read`);
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
function ph(dialect, n) {
|
|
@@ -130,7 +130,7 @@ function refusingBakeRow(read) {
|
|
|
130
130
|
return read();
|
|
131
131
|
}
|
|
132
132
|
catch (e) {
|
|
133
|
-
throw foldJsonColumn(e, (m) =>
|
|
133
|
+
throw foldJsonColumn(e, (m) => `image-bake-store: ${m} — refusing rather than serve a bake row nobody can read`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
export const IMAGE_BAKE_INDEXES = [
|
|
@@ -89,7 +89,7 @@ function refusingImageRow(read) {
|
|
|
89
89
|
return read();
|
|
90
90
|
}
|
|
91
91
|
catch (e) {
|
|
92
|
-
throw foldJsonColumn(e, (m) =>
|
|
92
|
+
throw foldJsonColumn(e, (m) => `image-index: ${m} — refusing rather than serve an image row nobody can read`);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
export const IMAGE_INDEX_INDEXES = [
|
|
@@ -67,7 +67,7 @@ function readResultColumn(text, id) {
|
|
|
67
67
|
return decodeNullableJsonColumn(text, readNonNullJsonValueColumn, `${LEADER_RUN_TABLE}.result for leader run ${id}`);
|
|
68
68
|
}
|
|
69
69
|
catch (e) {
|
|
70
|
-
throw foldJsonColumn(e, (m) =>
|
|
70
|
+
throw foldJsonColumn(e, (m) => `leader_run: ${m} — refusing rather than serve this run as if it had finished empty-handed`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
function statusOf(raw, id) {
|
|
@@ -7,7 +7,7 @@ import { isApprovalGateKind } from "../tool-approval.js";
|
|
|
7
7
|
import { FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
|
|
8
8
|
import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, pendingActionToolCallId, pendingRowPrecedes, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
|
|
9
9
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
10
|
-
import { createZodColumnReader, decodeJsonColumn, readStringArrayColumn } from "./sql-json-column.js";
|
|
10
|
+
import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, readStringArrayColumn } from "./sql-json-column.js";
|
|
11
11
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
12
12
|
import { assertJsonColumnWritable, createObjectColumnReader } from "./sql-json-column.js";
|
|
13
13
|
function terminalAtOf(cp) {
|
|
@@ -36,6 +36,14 @@ function createCtxRecordReader() {
|
|
|
36
36
|
return { value: { ...envelope.value, ctx: payload.value } };
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
function refusingCtx(sessionId, read) {
|
|
40
|
+
try {
|
|
41
|
+
return read();
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
throw foldJsonColumn(e, (m) => `LocalCheckpointStore: ${m} — refusing rather than serve a resume context nobody can read (session ${JSON.stringify(sessionId)})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
39
47
|
export class LocalCheckpointStore {
|
|
40
48
|
retention = UNMANAGED_RETENTION;
|
|
41
49
|
durability = "durable";
|
|
@@ -316,7 +324,7 @@ export class LocalCheckpointStore {
|
|
|
316
324
|
const p = join(this.ctxDir, ctxFileOf(sessionId));
|
|
317
325
|
if (!existsSync(p))
|
|
318
326
|
return null;
|
|
319
|
-
const rec = decodeJsonColumn(readFileSync(p, "utf8"), createCtxRecordReader(), `checkpoint-ctx ${ctxFileOf(sessionId)} for session ${JSON.stringify(sessionId)}`);
|
|
327
|
+
const rec = refusingCtx(sessionId, () => decodeJsonColumn(readFileSync(p, "utf8"), createCtxRecordReader(), `checkpoint-ctx ${ctxFileOf(sessionId)} for session ${JSON.stringify(sessionId)}`));
|
|
320
328
|
return rec.sessionId === sessionId ? rec.ctx : null;
|
|
321
329
|
}
|
|
322
330
|
async reapCtx(cutoff) {
|
|
@@ -2,7 +2,7 @@ import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_TOMBSTONED_RECIPIENT_CODE, Mail
|
|
|
2
2
|
import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
|
|
3
3
|
import { ensureIndexes } from "./ensure-index.js";
|
|
4
4
|
import { escapeLike } from "./sql-escape.js";
|
|
5
|
-
import { decodeNullableJsonColumn, foldJsonColumn, jsonColumnText, readStringArrayColumn } from "./sql-json-column.js";
|
|
5
|
+
import { decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readStringArrayColumn } from "./sql-json-column.js";
|
|
6
6
|
export const MAILBOX_TABLE = "mailbox";
|
|
7
7
|
export const MAILBOX_MSG_TABLE = "mailbox_message";
|
|
8
8
|
export const MAILBOX_TOMBSTONE_TABLE = "mailbox_tombstone";
|
|
@@ -52,7 +52,7 @@ function decodeHopChain(raw, seq) {
|
|
|
52
52
|
return decodeNullableJsonColumn(raw, readStringArrayColumn, `${MAILBOX_MSG_TABLE}.hop_chain for seq ${String(seq)}`);
|
|
53
53
|
}
|
|
54
54
|
catch (e) {
|
|
55
|
-
throw foldJsonColumn(e, (m) =>
|
|
55
|
+
throw foldJsonColumn(e, (m) => `MailboxStore: ${m} — refusing the lease rather than let a message skip peer admission (an absent chain makes core's judgePeerAdmission not run at all)`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const PEER_META_MAX_BYTES = 60_000;
|
|
@@ -73,7 +73,9 @@ function decodePeerMeta(raw, seq) {
|
|
|
73
73
|
catch (e) {
|
|
74
74
|
if (e instanceof MailboxStoreError)
|
|
75
75
|
throw e;
|
|
76
|
-
|
|
76
|
+
if (!isJsonColumnBodyProblem(e))
|
|
77
|
+
throw e;
|
|
78
|
+
throw new MailboxStoreError(MAILBOX_INVALID_PEER_META_CODE, `MailboxStore: ${e.message} — refusing the lease rather than read a corrupt typed record as "no metadata"`);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
const readPeerMetaColumn = (body) => {
|
|
@@ -51,7 +51,7 @@ function readFrontmatter(cell, id) {
|
|
|
51
51
|
return decodeJsonColumn(cell, readFrontmatterColumn, `${PG_MEMORY_ENGINE_TABLES.entry}.frontmatter for entry ${String(id)}`);
|
|
52
52
|
}
|
|
53
53
|
catch (e) {
|
|
54
|
-
throw foldJsonColumn(e, (m) =>
|
|
54
|
+
throw foldJsonColumn(e, (m) => `memory-engine: ${m} — refusing rather than serve a note whose frontmatter (the origin/exposure law carrier) nobody can read`);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
function readEmbedding(cell, id) {
|
|
@@ -48,7 +48,7 @@ function readFrontmatter(cell, id) {
|
|
|
48
48
|
return decodeJsonColumn(cell, readFrontmatterColumn, `${TIDB_MEMORY_ENGINE_TABLES.entry}.frontmatter for entry ${String(id)}`);
|
|
49
49
|
}
|
|
50
50
|
catch (e) {
|
|
51
|
-
throw foldJsonColumn(e, (m) =>
|
|
51
|
+
throw foldJsonColumn(e, (m) => `memory-engine: ${m} — refusing rather than serve a note whose frontmatter (the origin/exposure law carrier) nobody can read`);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
export class TiDBMemoryEngineBackend {
|
|
@@ -34,7 +34,7 @@ export function refusingMemorySyncRow(read) {
|
|
|
34
34
|
return read();
|
|
35
35
|
}
|
|
36
36
|
catch (e) {
|
|
37
|
-
throw foldJsonColumn(e, (m) =>
|
|
37
|
+
throw foldJsonColumn(e, (m) => `memory-sync store: ${m} — refusing rather than serve a sync row nobody can read`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
function toMs(v) {
|
|
@@ -346,7 +346,7 @@ async function refusingUnreadableColumns(what, fn) {
|
|
|
346
346
|
return await fn();
|
|
347
347
|
}
|
|
348
348
|
catch (e) {
|
|
349
|
-
throw foldJsonColumn(e, (m) =>
|
|
349
|
+
throw foldJsonColumn(e, (m) => `${m} — the ${what} does not carry a readable shape; refusing the whole of it rather than report a partial one`);
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
class SqlDurableRulePartition {
|
|
@@ -728,7 +728,7 @@ export class SqlRuleImportTicketStore {
|
|
|
728
728
|
outcome = decodeJsonColumn(row.redeemedOutcome, readJsonObjectColumn, `${PERMISSION_RULE_TICKET_TABLE}.redeemed_outcome for ticket ${ticketId}`);
|
|
729
729
|
}
|
|
730
730
|
catch (e) {
|
|
731
|
-
throw foldJsonColumn(e, (m) =>
|
|
731
|
+
throw foldJsonColumn(e, (m) => `${m} — refusing rather than read a settled ticket as still open (which re-runs a redemption that already ran)`);
|
|
732
732
|
}
|
|
733
733
|
return { state: "settled", outcome };
|
|
734
734
|
}
|
|
@@ -22,7 +22,7 @@ function refusing(what, read) {
|
|
|
22
22
|
return read();
|
|
23
23
|
}
|
|
24
24
|
catch (e) {
|
|
25
|
-
throw foldJsonColumn(e, (m) =>
|
|
25
|
+
throw foldJsonColumn(e, (m) => `SqlRunStore: ${m} — refusing rather than ${what}`);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
function usageResultBody(v) {
|
|
@@ -14,7 +14,7 @@ export function decodeSessionEventPayload(cell, sessionId, rowId) {
|
|
|
14
14
|
return decodeJsonColumn(cell, readSessionEventPayload, `session_event.payload for session ${sessionId} row ${String(rowId)}`);
|
|
15
15
|
}
|
|
16
16
|
catch (e) {
|
|
17
|
-
throw foldJsonColumn(e, (m) =>
|
|
17
|
+
throw foldJsonColumn(e, (m) => `session storage: ${m} — refusing rather than wake a session from a tree with a node nobody can read`);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
//# sourceMappingURL=session-event-payload.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
|
|
3
3
|
import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
|
|
4
|
-
import { decodeJsonColumn,
|
|
4
|
+
import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText } from "./sql-json-column.js";
|
|
5
5
|
import { pgHasUnstorable } from "./pg-safe-json.js";
|
|
6
6
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
7
7
|
import { isDeadlockError, isDupKeyError } from "./sql-errors.js";
|
|
@@ -17,7 +17,9 @@ function readRulesBody(text, where) {
|
|
|
17
17
|
return decodeJsonColumn(text, readSessionRulesBody, where);
|
|
18
18
|
}
|
|
19
19
|
catch (e) {
|
|
20
|
-
|
|
20
|
+
if (!isJsonColumnBodyProblem(e))
|
|
21
|
+
throw e;
|
|
22
|
+
throw new SessionPolicyError("corrupt", `${e.message} — an unreadable rules body served as an EMPTY rule set would silently drop every tightening this session carries`);
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
function rowToStored(r, where) {
|
|
@@ -85,8 +85,17 @@ export declare function jsonColumnTextExpr(dialect: SqlDialect, column: string):
|
|
|
85
85
|
export declare const SQL_JSON_COLUMN_PROBLEMS: readonly ["not-text", "not-json", "malformed"];
|
|
86
86
|
export type SqlJsonColumnProblem = (typeof SQL_JSON_COLUMN_PROBLEMS)[number];
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
88
|
+
* **体**的两形(`not-text` 之外的全部)—— 「数据坏了」这一族。类型从闭集派生,不手抄。
|
|
89
|
+
* 存在的理由:{@link StoreCorruptError} 要在**类型面**说出「我携带的种别恒非 `not-text`」。
|
|
90
|
+
*/
|
|
91
|
+
export type SqlJsonColumnBodyProblem = Exclude<SqlJsonColumnProblem, "not-text">;
|
|
92
|
+
/**
|
|
93
|
+
* 店层码,**不上 wire**:店层折码口 {@link foldJsonColumn} 把它折成 {@link StoreCorruptError}
|
|
94
|
+
* (有自己闭集契约词的店折成自己的码)。终局对外码经 7.92.2 的词法门,永不裸穿。
|
|
95
|
+
*
|
|
96
|
+
* 🔴 **它只许在 `not-text` 那一形上穿过店的边界**(S-608):`not-text` 是读腿自己的缺陷(投影漂),
|
|
97
|
+
* 按 {@link foldJsonColumn} 的规则不折、原样上抛;而体的两形一旦穿出去,上层就得**按列错误类**分类,
|
|
98
|
+
* 那正是 S-608 的病 —— 读口按列错误分类、店按店拒绝形折码,两层各说各话,坏体于是落进「店抖动」臂。
|
|
90
99
|
*/
|
|
91
100
|
export declare class SqlJsonColumnError extends Error {
|
|
92
101
|
readonly problem: SqlJsonColumnProblem;
|
|
@@ -133,13 +142,47 @@ export declare function decodeJsonColumn<T>(text: unknown, read: JsonColumnReade
|
|
|
133
142
|
*/
|
|
134
143
|
export declare function decodeNullableJsonColumn<T>(cell: unknown, read: JsonColumnReader<T>, where: string): T | undefined;
|
|
135
144
|
/**
|
|
136
|
-
*
|
|
145
|
+
* **「这一格耐久载荷读不出来」的店层拒绝**(整仓唯一一只)—— {@link foldJsonColumn} 的**恒定产物**。
|
|
146
|
+
*
|
|
147
|
+
* 🔴 为什么它必须是一只**具名类**而不是 `new Error(…)`(S-608 真案,7.95.1):折码的产物是**店交给
|
|
148
|
+
* 上层的契约**。折成裸 `Error` 之后,「这一行确定性地读不出来」与「店抖了一下」在上层**没有任何判别位**,
|
|
149
|
+
* 而两者的处置完全相反(前者要人去修那一行,后者重试 / 保守当缺席)。S-577 给续跑上下文修的正是这条
|
|
150
|
+
* 分家,可它在 SQL 车道上**结构不可达**整整一版:读口按**列错误类**分类,而店早把列错误折成了裸
|
|
151
|
+
* `Error` ⇒ 坏体一路掉进「店抖动 ⇒ 当缺席」臂,用户拿到的还是那个 409(local 车道答对,只是因为它
|
|
152
|
+
* 根本没折码、把列错误漏了出来 —— 巧合式正确,不是同一份契约)。
|
|
153
|
+
* ⇒ 读口只认**店的拒绝契约**,而店的拒绝契约只有这一只。
|
|
154
|
+
*
|
|
155
|
+
* 🔴 {@link kind} 逐字就是消费点那只联合里的那个词(`http/resume-ctx-read.ts` 的 `ResumeCtxRead`):
|
|
156
|
+
* 店说 `corrupt`,读口原样转述,中间**没有第二张翻译表**。
|
|
157
|
+
*
|
|
158
|
+
* 有**自己的闭集契约词**的店(`session-policy` 的 `SessionPolicyError("corrupt")`、`mailbox` 的
|
|
159
|
+
* `MailboxStoreError`)不经本折码口 —— 它们的消费点按**那个码**判,本类的判别位对它们是多余的第二份。
|
|
160
|
+
* 那两处用属主谓词 {@link isJsonColumnBodyProblem} 自己写那一句(判别仍只有一个写者)。
|
|
161
|
+
*/
|
|
162
|
+
export declare class StoreCorruptError extends Error {
|
|
163
|
+
/** 消费点的判别词(与 `ResumeCtxRead` 的那一态同字)。 */
|
|
164
|
+
readonly kind: "corrupt";
|
|
165
|
+
/** 体的种别(类型面即保证恒非 `not-text` —— 那一形不折,见 {@link foldJsonColumn})。 */
|
|
166
|
+
readonly problem: SqlJsonColumnBodyProblem;
|
|
167
|
+
/** 出事的坐标(`<table>.<column> for <identity>`,由 {@link SqlJsonColumnError.where} 原样带过来)——
|
|
168
|
+
* 进 wire 的 `where` 字段,人按它去找那一行。 */
|
|
169
|
+
readonly where: string;
|
|
170
|
+
constructor(cause: SqlJsonColumnError & {
|
|
171
|
+
readonly problem: SqlJsonColumnBodyProblem;
|
|
172
|
+
}, sentence: string);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 店层折码的**唯一规则**(`catch (e) { throw foldJsonColumn(e, (m) => \`XStore: ${m} — refusing …\`); }`)。
|
|
176
|
+
*
|
|
177
|
+
* 🔴 回调交出来的是**一句话**,不是一只错误(S-608 按类改):产物恒是 {@link StoreCorruptError},于是
|
|
178
|
+
* 「折成一只没有判别位的错」在**构造上**写不出来。此前这一格的签名是 `(message) => Error`,21 个调用点
|
|
179
|
+
* 里 19 个折成了裸 `Error` —— 每一个都是一枚等着下一个消费方来踩的 S-608。
|
|
137
180
|
*
|
|
138
181
|
* 🔴 `not-text` **不折**:它是读腿自己的缺陷(投影漏了),折成「这一行坏了」会把一次代码漂伪装成一次
|
|
139
|
-
* 数据事故 ——
|
|
140
|
-
*
|
|
182
|
+
* 数据事故 —— 这条判别只写在这里一处(与它的容忍侧孪生 {@link isJsonColumnBodyProblem} 共用),
|
|
183
|
+
* 各店不各写一遍。非本模块的错误原样透出(连接 / 超时 / 权限本来就该照原样上抛)。
|
|
141
184
|
*/
|
|
142
|
-
export declare function foldJsonColumn(e: unknown,
|
|
185
|
+
export declare function foldJsonColumn(e: unknown, refusing: (message: string) => string): Error;
|
|
143
186
|
/**
|
|
144
187
|
* 折码的**容忍侧孪生**(与 {@link foldJsonColumn} 同一条判别,只写这一处):这一次失败是不是
|
|
145
188
|
* **数据体**的问题(`not-json` / `malformed`)。
|
|
@@ -149,7 +192,9 @@ export declare function foldJsonColumn(e: unknown, corrupt: (message: string) =>
|
|
|
149
192
|
* 调用点原样上抛:一条读腿漏了投影、或库连不上,绝不许被一条「这一格坏了,跳过它」的容忍臂吞掉 ——
|
|
150
193
|
* 那会让一次代码漂 / 一次故障伪装成「几行数据坏了」,而两者的处置完全不同。
|
|
151
194
|
*/
|
|
152
|
-
export declare function isJsonColumnBodyProblem(e: unknown): e is SqlJsonColumnError
|
|
195
|
+
export declare function isJsonColumnBodyProblem(e: unknown): e is SqlJsonColumnError & {
|
|
196
|
+
readonly problem: SqlJsonColumnBodyProblem;
|
|
197
|
+
};
|
|
153
198
|
/**
|
|
154
199
|
* **写腿的形闸** —— 一个值**序列化之后**还是不是这一列读得回来的形。判据与读腿**共用同一只形读者**,
|
|
155
200
|
* 所以「写得进」与「读得出」在构造上是同一句话。
|
|
@@ -49,9 +49,20 @@ export function decodeJsonColumn(text, read, where) {
|
|
|
49
49
|
export function decodeNullableJsonColumn(cell, read, where) {
|
|
50
50
|
return cell === null ? undefined : decodeJsonColumn(cell, read, where);
|
|
51
51
|
}
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
export class StoreCorruptError extends Error {
|
|
53
|
+
kind = "corrupt";
|
|
54
|
+
problem;
|
|
55
|
+
where;
|
|
56
|
+
constructor(cause, sentence) {
|
|
57
|
+
super(sentence, { cause });
|
|
58
|
+
this.name = "StoreCorruptError";
|
|
59
|
+
this.problem = cause.problem;
|
|
60
|
+
this.where = cause.where;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function foldJsonColumn(e, refusing) {
|
|
64
|
+
if (isJsonColumnBodyProblem(e))
|
|
65
|
+
return new StoreCorruptError(e, refusing(e.message));
|
|
55
66
|
return e instanceof Error ? e : new Error(String(e));
|
|
56
67
|
}
|
|
57
68
|
export function isJsonColumnBodyProblem(e) {
|
|
@@ -27,7 +27,7 @@ function parseItem(v, where) {
|
|
|
27
27
|
return decodeJsonColumn(v, readTaskListItemBody, `${TASK_LIST_ITEM_TABLE}.item_json ${where}`);
|
|
28
28
|
}
|
|
29
29
|
catch (e) {
|
|
30
|
-
throw foldJsonColumn(e, (m) =>
|
|
30
|
+
throw foldJsonColumn(e, (m) => `TaskListStore: ${m} — refusing rather than hand the model a row nobody can read as a task`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
function assertListKeyBytes(listKey) {
|
|
@@ -18,7 +18,7 @@ function parseRecord(raw, key) {
|
|
|
18
18
|
return decodeJsonColumn(raw, readUsageWindowRecordBody, `${USAGE_WINDOW_TABLE}.record for key "${key}"`);
|
|
19
19
|
}
|
|
20
20
|
catch (e) {
|
|
21
|
-
throw foldJsonColumn(e, (m) =>
|
|
21
|
+
throw foldJsonColumn(e, (m) => `${m} — refusing to mis-count a governance window`);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
function validateUsageWindowRecord(v) {
|