@sema-agent/server 7.115.0 → 7.116.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 +167 -4
- package/USAGE.md +6 -3
- package/dist/approval-ask-audit-store.d.ts +25 -6
- package/dist/approval-ask-audit-store.js +6 -1
- package/dist/approval-card.d.ts +8 -4
- package/dist/approval-card.js +2 -0
- package/dist/approval-crash-convergence.d.ts +60 -0
- package/dist/approval-crash-convergence.js +75 -0
- package/dist/approval-deny-reasons.d.ts +13 -0
- package/dist/approval-deny-reasons.js +2 -0
- package/dist/approval.js +2 -1
- package/dist/boot/coordinators.js +0 -12
- package/dist/boot/execution-env.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/stage-07-capability-layer.js +21 -15
- package/dist/boot/stores.js +4 -4
- package/dist/capabilities/hands-lane.d.ts +0 -23
- package/dist/capabilities/hands-lane.js +3 -2
- package/dist/capabilities/send-user-file-tool.d.ts +10 -22
- package/dist/capabilities/send-user-file-tool.js +1 -8
- package/dist/capability-facts.d.ts +296 -0
- package/dist/capability-facts.js +354 -0
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config.d.ts +16 -13
- package/dist/config.js +12 -12
- package/dist/http/admission.d.ts +6 -0
- package/dist/http/admission.js +22 -5
- package/dist/http/principal-gate.d.ts +3 -0
- package/dist/http/principal-gate.js +3 -0
- package/dist/http/resume-legs.js +10 -1
- package/dist/http/retired-skill-manifest.d.ts +23 -0
- package/dist/http/retired-skill-manifest.js +22 -0
- package/dist/http/route-table.d.ts +3 -2
- package/dist/http/route-table.js +5 -5
- package/dist/http/routes/a2a-serve.js +14 -21
- package/dist/http/routes/admin-config-refresh.js +7 -2
- package/dist/http/routes/admin-drain.js +7 -2
- package/dist/http/routes/admin-memory-optout.js +20 -6
- package/dist/http/routes/adoption.js +19 -5
- package/dist/http/routes/agents-roster.js +4 -2
- package/dist/http/routes/approvals-assistant.js +41 -18
- package/dist/http/routes/background.d.ts +0 -2
- package/dist/http/routes/background.js +4 -5
- package/dist/http/routes/capabilities.d.ts +2 -1
- package/dist/http/routes/capabilities.js +62 -65
- package/dist/http/routes/config-catalog.d.ts +4 -5
- package/dist/http/routes/config-catalog.js +16 -2
- package/dist/http/routes/devices.js +17 -11
- package/dist/http/routes/diagnostics.js +6 -1
- package/dist/http/routes/fleet.js +4 -2
- package/dist/http/routes/images.js +30 -3
- package/dist/http/routes/leader.js +7 -1
- package/dist/http/routes/mcp-reconnect.js +17 -10
- package/dist/http/routes/memory-bundle.js +20 -6
- package/dist/http/routes/memory-compliance.js +19 -5
- package/dist/http/routes/memory-consolidation.d.ts +9 -12
- package/dist/http/routes/memory-consolidation.js +17 -3
- package/dist/http/routes/memory-origin.js +19 -5
- package/dist/http/routes/notify-wake.js +5 -3
- package/dist/http/routes/observability.js +19 -5
- package/dist/http/routes/retention-ops.js +6 -1
- package/dist/http/routes/rules.js +49 -21
- package/dist/http/routes/run-memory-capture.js +5 -2
- package/dist/http/routes/runs.js +90 -60
- package/dist/http/routes/session-sync.js +64 -53
- package/dist/http/routes/sessions-list.js +8 -5
- package/dist/http/routes/sessions.js +39 -24
- package/dist/http/routes/shared-memory.js +5 -3
- package/dist/http/routes/tasks.js +2 -1
- package/dist/http/routes/trace-usage.js +18 -11
- package/dist/http/routes/workflows.js +36 -13
- package/dist/http/server.js +4 -1
- package/dist/http/wire-types.d.ts +6 -0
- package/dist/namespaced-rule-name.d.ts +54 -0
- package/dist/namespaced-rule-name.js +33 -0
- package/dist/observability/corrupt-read-seat.d.ts +4 -3
- package/dist/observability/corrupt-read-seat.js +4 -3
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/plugins/file-run-store.d.ts +7 -1
- package/dist/plugins/file-run-store.js +20 -7
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/one-time-migrations.d.ts +4 -2
- package/dist/plugins/one-time-migrations.js +26 -3
- package/dist/plugins/pg-session-storage.js +1 -1
- package/dist/plugins/run-store-sql.js +4 -4
- package/dist/plugins/session-policy-store-sql.js +4 -2
- package/dist/plugins/store-backend.d.ts +6 -1
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/store-contracts.d.ts +27 -8
- package/dist/plugins/store-contracts.js +12 -3
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.d.ts +37 -3
- package/dist/run-local.js +48 -5
- package/dist/runs.d.ts +0 -58
- package/dist/runs.js +15 -20
- package/dist/session-policy-wire.d.ts +40 -60
- package/dist/session-policy-wire.js +60 -32
- package/dist/spec-fields.d.ts +32 -0
- package/dist/spec-fields.js +31 -16
- package/dist/standing-answer-grant.d.ts +8 -0
- package/dist/standing-answer-grant.js +5 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/tool-approval.d.ts +16 -0
- package/dist/tool-approval.js +29 -7
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/self-minted-wire.d.ts +42 -0
- package/dist/trace/self-minted-wire.js +13 -0
- package/dist/user-input-ledger.d.ts +8 -5
- package/package.json +4 -2
- package/scripts/drain-check-local.mjs +115 -25
- package/scripts/drain-check.mysql.sql +67 -0
- package/scripts/drain-check.pg.sql +71 -0
|
@@ -12,6 +12,7 @@ const journalProjectionMemo = new Map();
|
|
|
12
12
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
13
13
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
14
14
|
import { assertNever } from "../route-ctx.js";
|
|
15
|
+
import * as facts from "../../capability-facts.js";
|
|
15
16
|
export async function handleWorkflows(req, res, match, ctx) {
|
|
16
17
|
const { deps } = ctx;
|
|
17
18
|
const { runSessionAcceptOk } = ctx.helpers;
|
|
@@ -21,10 +22,21 @@ export async function handleWorkflows(req, res, match, ctx) {
|
|
|
21
22
|
sendPrincipalRefusal(res, req, deps.config);
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
let journalStore;
|
|
26
|
+
if (match.decl.id === "workflow-journal") {
|
|
27
|
+
const journalFace = facts.workflowsJournal(deps);
|
|
28
|
+
if (!journalFace.available) {
|
|
29
|
+
sendWorkflowsUnavailable(res, journalFace.why);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
journalStore = journalFace.journal;
|
|
33
|
+
}
|
|
34
|
+
const runs = facts.workflowRuns(deps);
|
|
35
|
+
if (!runs.available) {
|
|
36
|
+
sendWorkflowsUnavailable(res, runs.why);
|
|
26
37
|
return;
|
|
27
38
|
}
|
|
39
|
+
const runStore = runs.store;
|
|
28
40
|
const scope = principal ?? "default";
|
|
29
41
|
const wfId = match.params.runId;
|
|
30
42
|
const isStream = match.decl.id === "workflow-stream";
|
|
@@ -33,7 +45,7 @@ export async function handleWorkflows(req, res, match, ctx) {
|
|
|
33
45
|
const statusFilter = q.get("status");
|
|
34
46
|
const limit = Math.min(100, Math.max(1, Number(q.get("limit") ?? 50) || 50));
|
|
35
47
|
const sessionFilter = q.get("session");
|
|
36
|
-
const sessionRuns = await
|
|
48
|
+
const sessionRuns = await runStore.listByScope(scope, {
|
|
37
49
|
...(statusFilter ? { status: statusFilter } : {}),
|
|
38
50
|
limit,
|
|
39
51
|
...(sessionFilter !== null ? { session: sessionFilter } : {}),
|
|
@@ -47,7 +59,7 @@ export async function handleWorkflows(req, res, match, ctx) {
|
|
|
47
59
|
sendJson(res, 200, { workflows: redacted });
|
|
48
60
|
return;
|
|
49
61
|
}
|
|
50
|
-
const run = await getWorkflowRun(
|
|
62
|
+
const run = await getWorkflowRun(runStore, wfId, scope);
|
|
51
63
|
if (!run || run.scope !== scope) {
|
|
52
64
|
sendError(res, 404, "not_found.workflow", "workflow not found");
|
|
53
65
|
return;
|
|
@@ -55,10 +67,8 @@ export async function handleWorkflows(req, res, match, ctx) {
|
|
|
55
67
|
if (!runSessionAcceptOk(req, res, { sessionId: run.originatingSessionId ?? null }, match.decl.id === "workflow-journal" ? "workflow.journal" : isStream ? "workflow.stream" : "workflow.detail", "workflow not found"))
|
|
56
68
|
return;
|
|
57
69
|
if (match.decl.id === "workflow-journal") {
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
70
|
+
if (journalStore === undefined)
|
|
71
|
+
throw new Error("workflow-journal reached without the workflowsJournal journal witness");
|
|
62
72
|
const jq = new URLSearchParams(match.search);
|
|
63
73
|
const jLimit = Math.min(50, Math.max(1, Number(jq.get("limit") ?? 20) || 20));
|
|
64
74
|
const jOffset = Math.max(0, Number(jq.get("offset") ?? 0) || 0);
|
|
@@ -100,7 +110,7 @@ export async function handleWorkflows(req, res, match, ctx) {
|
|
|
100
110
|
...(r.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}),
|
|
101
111
|
};
|
|
102
112
|
};
|
|
103
|
-
const js =
|
|
113
|
+
const js = journalStore;
|
|
104
114
|
let projected;
|
|
105
115
|
if (js.loadPage) {
|
|
106
116
|
const page = await js.loadPage(wfId, scope, { offset: jOffset, limit: jLimit, maxResultBytes: MAX_ROW_BYTES });
|
|
@@ -118,7 +128,7 @@ export async function handleWorkflows(req, res, match, ctx) {
|
|
|
118
128
|
});
|
|
119
129
|
}
|
|
120
130
|
else {
|
|
121
|
-
const entries = await
|
|
131
|
+
const entries = await journalStore.load(wfId, scope);
|
|
122
132
|
projected = entries
|
|
123
133
|
.sort((a, b) => callKeyOrdinal(a.callKey) - callKeyOrdinal(b.callKey))
|
|
124
134
|
.slice(jOffset, jOffset + jLimit)
|
|
@@ -150,8 +160,9 @@ export async function handleWorkflowAgentSteer(req, res, match, ctx) {
|
|
|
150
160
|
{
|
|
151
161
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
152
162
|
return;
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
const runs = facts.workflowRuns(deps);
|
|
164
|
+
if (!runs.available) {
|
|
165
|
+
sendWorkflowsUnavailable(res, runs.why);
|
|
155
166
|
return;
|
|
156
167
|
}
|
|
157
168
|
const principal = gatedPrincipal(req, deps.config);
|
|
@@ -183,7 +194,7 @@ export async function handleWorkflowAgentSteer(req, res, match, ctx) {
|
|
|
183
194
|
}
|
|
184
195
|
const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
185
196
|
const ownerScope = principal ?? "default";
|
|
186
|
-
const wfRun = await getWorkflowRun(
|
|
197
|
+
const wfRun = await getWorkflowRun(runs.store, wfRunId, ownerScope);
|
|
187
198
|
if (!trusted && !wfRun) {
|
|
188
199
|
sendError(res, 404, "not_found.workflow", "workflow not found");
|
|
189
200
|
return;
|
|
@@ -389,6 +400,18 @@ async function streamWorkflowRun(req, res, runId, scope) {
|
|
|
389
400
|
if (!res.writableEnded)
|
|
390
401
|
res.end();
|
|
391
402
|
}
|
|
403
|
+
function sendWorkflowsUnavailable(res, why) {
|
|
404
|
+
switch (why) {
|
|
405
|
+
case "workflow_run_store_absent":
|
|
406
|
+
sendError(res, 501, "capability.self_orchestration_required", "workflow runs require self-orchestration (SELF_ORCHESTRATION_ENABLED)");
|
|
407
|
+
return;
|
|
408
|
+
case "workflow_journal_store_absent":
|
|
409
|
+
sendError(res, 501, "capability.workflow_store_required", "workflow journal requires the journal store (SELF_ORCHESTRATION_ENABLED + a store backend)");
|
|
410
|
+
return;
|
|
411
|
+
default:
|
|
412
|
+
return assertNever(why);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
392
415
|
export const WORKFLOWS_ROUTES = [
|
|
393
416
|
{ id: "workflows-list", path: "/v1/workflows", methods: ["GET"] },
|
|
394
417
|
{ id: "workflow-stream", pattern: /^\/v1\/workflows\/(?<runId>[^/]+)\/stream$/, label: "/v1/workflows/:id/$sub", methods: ["GET"] },
|
package/dist/http/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import { sendError, sendSessionError, httpErrorCode, msg } from "./send.js";
|
|
|
23
23
|
import { createRequestGuards } from "./request-guards.js";
|
|
24
24
|
import { createAdmission, MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills } from "./admission.js";
|
|
25
25
|
import { createResumeLegs } from "./resume-legs.js";
|
|
26
|
+
import * as facts from "../capability-facts.js";
|
|
26
27
|
import { createDispatch, hasServiceAuth } from "./dispatch.js";
|
|
27
28
|
import { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, isProbePath } from "./route-table.js";
|
|
28
29
|
import { createApprovalSweeps } from "./approval-sweeps.js";
|
|
@@ -118,7 +119,9 @@ export function createHttpServer(rawDeps) {
|
|
|
118
119
|
sendError(res, 500, "internal.error", "internal error");
|
|
119
120
|
});
|
|
120
121
|
});
|
|
121
|
-
|
|
122
|
+
const deviceLane = facts.deviceLane(deps);
|
|
123
|
+
if (deviceLane.available)
|
|
124
|
+
deviceLane.hub.attach(server);
|
|
122
125
|
function runnerFor(spec) {
|
|
123
126
|
return deps.runnerFor(spec);
|
|
124
127
|
}
|
|
@@ -301,6 +301,12 @@ export interface TaskRequestBody {
|
|
|
301
301
|
* 续跑重放逐字重放存量体 ⇒ 同一只写点同判(坏形 defensive:只认 literal `true`)。无租户门(收窄,不跨租户)。
|
|
302
302
|
* 探测位 `GET /v1/capabilities` 的 `disableSkills`(与本键同一个词)。 */
|
|
303
303
|
disableSkills?: boolean;
|
|
304
|
+
/** core 7.41.0 #1303 —— **人用名字点过的技能**(壳从人敲的 `/name` 解析出来的,按 `SkillSpec.name`),原样交给 core 的
|
|
305
|
+
* `TaskSpec.userInvokedSkills`:这些技能的 `allowedTools` / `disallowedTools` 开跑即生效,不出 `Skill` 卡(人自己点的名就是那句表态),
|
|
306
|
+
* 只管这一次 run(steer 不终止它,下一次 run 不带)。每个名字必须是这一 run 装着的技能(场景出厂 ∪ 本请求 `skills`),否则引擎
|
|
307
|
+
* 拒启 `config.user_invoked_skill_unknown`(同步腿 400)。值形:非空串数组(受理门 400 `request.field_invalid`);续跑重放逐字重放存量体,
|
|
308
|
+
* 坏形不挂键。不投递技能正文(正文照旧由 `Skill` 工具 / 调用方负责)。 */
|
|
309
|
+
userInvokedSkills?: string[];
|
|
304
310
|
/** [ref]② (core 1.314 工具面控制批): roster 真卸载清单(schema 不上 wire,manifest 收窄)。wire 工具名;
|
|
305
311
|
* tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。 */
|
|
306
312
|
excludeTools?: string[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议规则名(`mcp__…` / `a2a__…`)够不够得着一只挂载工具 —— **一只**判官 + **一只**句子。调它的面(点名,不是「每个面」):
|
|
3
|
+
* · 运维四表与中心下发的同名表、请求 `settings.permissions.{allow,ask,deny}`(经 `config.ts` 的 `unmatchableToolNamesRefusal`);
|
|
4
|
+
* · 会话规则写口两腿与 SQL 会话规则店的写口(经 `session-policy-wire.ts`);
|
|
5
|
+
* · 技能的两只清单 `allowedTools` / `disallowedTools`,两处受理口:顶层 `skills[]`(`http/admission.ts` 的 `validateUserSkills`)与子代
|
|
6
|
+
* `agents[].skills[]`(`spec-fields.ts` 的 `validateTaskAgents`),经 `spec-fields.ts` 的 `unreachableSkillToolListEntryOf`。
|
|
7
|
+
* **不调它的面**(如实,未接):`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`)、子代的工具名表 `agents[].allowTools` /
|
|
8
|
+
* `denyTools`(`spec-fields.ts` 只判形与条数)。在这两处写铸不出的服务器段仍被收下、永不命中。
|
|
9
|
+
* 规则文本里「工具名那一段」怎么取也只有一种读法:{@link ruleEntryToolName}(配置表与技能清单同读)。
|
|
10
|
+
*
|
|
11
|
+
* 规则:一条协议规则名够得着挂载工具 ⇔
|
|
12
|
+
* ① 分得出对端段 —— 三段名(`ns.parse` 认得:首段为对端、余段非空)取 `parse().peer`;两段覆盖形 `<ns>__<peer>`(core `isNamespacedCoveringRuleName`
|
|
13
|
+
* 认、`ns.parse` 不认)取前缀之后的整段;两者都不是(`mcp__` / `mcp__srv__` / `mcp____x`)= **缺段**;
|
|
14
|
+
* ② 对端段是引擎**自铸**的那一段:`mintNamespacePrefix(ns, peer) === ns.prefix + peer + "__"`。引擎铸注册名时归一对端段(字符表外换 `_`、超长截断加摘要、
|
|
15
|
+
* 折连串下划线、去尾下划线),而规则匹配对对端段逐字比(覆盖形 `namespacedRuleNameCovers`,精确名整串比)—— 铸不出的对端段一只工具都匹配不到。
|
|
16
|
+
* 服务器段里带 `*`(`mcp__*`、`mcp__git*__x`)是同一个「够不着」,但出路不同:那是在写通配,而服务器名不支持通配 —— 给一条铸名写法
|
|
17
|
+
* (`mcp__<摘要>__*`)没人照做得了 ⇒ 判词单列 `wildcard_peer`,句子说实话(写具体的服务器名)。
|
|
18
|
+
* 判据不看规则是覆盖形还是精确名:两种形的对端段命运相同。精确名的**工具段**归一不在这里判(包根没有导出工具段的铸法,不手抄)。
|
|
19
|
+
*
|
|
20
|
+
* core 下一版(7.42.0)的规则名判官会自己给这个判词;到货那天本仓这只对照整只退役(连同会话规则写口这一问的码由 `request.field_invalid` 归回
|
|
21
|
+
* `rules.legacy_tool_name`)。
|
|
22
|
+
*/
|
|
23
|
+
import { type ProtocolNamespace } from "@sema-agent/core";
|
|
24
|
+
/**
|
|
25
|
+
* 一条**规则文本**里的工具名那一段(判官与旧名判据的主语)= core 包根 `parsePermissionRule` 的 `toolName`,原样 —— 引擎的技能编译器
|
|
26
|
+
* (`compileSkillGrant`)与规则编译器读条目用的就是这一只:`Bash(npm:*)` ⇒ `Bash`;`mcp__srv(*)` / `mcp__srv()` / 带首尾空格的 `mcp__srv ` ⇒ 光杆名 `mcp__srv`。
|
|
27
|
+
* 配置表的规则编译器消费者与技能的两只清单读同一只。不在这里另写解析、不回落整串:修前用 `parseRuleText`(只认 `工具(内容)` 形,光杆名解析不出就回落整串),
|
|
28
|
+
* 整串带着 `(*)` / 尾空格进判官 ⇒ 合法条目被判「铸不出」(修复验证轮 gpt-6 腿)。条目若是引擎文法本身就拒的形(配置表上 MCP 规则带括号:
|
|
29
|
+
* `unsupported.mcp_paren`),拒由引擎的文法那一问说(配置表的判据链里它排在本判官之前),本判官不抢答。
|
|
30
|
+
*/
|
|
31
|
+
export declare function ruleEntryToolName(entry: string): string;
|
|
32
|
+
/** 一条协议规则名够不着任何挂载工具的判词(闭集)。 */
|
|
33
|
+
export type UnreachableNamespacedRuleName =
|
|
34
|
+
/** 分不出对端段:光杆前缀、对端段为空、或分隔符之后工具段为空。 */
|
|
35
|
+
{
|
|
36
|
+
readonly kind: "incomplete";
|
|
37
|
+
readonly ns: ProtocolNamespace;
|
|
38
|
+
}
|
|
39
|
+
/** 对端段不是引擎自铸的那一段;`minted` = 引擎为这样一个对端名铸出的注册名前缀(`mintNamespacePrefix` 的返回值,原样)。 */
|
|
40
|
+
| {
|
|
41
|
+
readonly kind: "unminted_peer";
|
|
42
|
+
readonly ns: ProtocolNamespace;
|
|
43
|
+
readonly minted: string;
|
|
44
|
+
}
|
|
45
|
+
/** 对端段里带 `*`:写的是通配,而服务器 / 对端名不支持通配(规则对这一段逐字比)。 */
|
|
46
|
+
| {
|
|
47
|
+
readonly kind: "wildcard_peer";
|
|
48
|
+
readonly ns: ProtocolNamespace;
|
|
49
|
+
};
|
|
50
|
+
/** 判官:不是协议名、或够得着 ⇒ `undefined`;否则给判词。纯字符串运算,不做 I/O。 */
|
|
51
|
+
export declare function unreachableNamespacedRuleNameOf(name: string): UnreachableNamespacedRuleName | undefined;
|
|
52
|
+
/** 判词 → 句子(「为什么永远匹配不到 + 怎么写」;不复述条目本身 —— 调用方的句子已点名它)。闭集穷尽:判词加一种 ⇒ 编译红。 */
|
|
53
|
+
export declare function describeUnreachableNamespacedRuleName(v: UnreachableNamespacedRuleName): string;
|
|
54
|
+
//# sourceMappingURL=namespaced-rule-name.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isNamespacedCoveringRuleName, mintNamespacePrefix, parsePermissionRule, protocolOf } from "@sema-agent/core";
|
|
2
|
+
export function ruleEntryToolName(entry) {
|
|
3
|
+
return parsePermissionRule(entry).toolName;
|
|
4
|
+
}
|
|
5
|
+
export function unreachableNamespacedRuleNameOf(name) {
|
|
6
|
+
const ns = protocolOf(name);
|
|
7
|
+
if (ns === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
const peer = ns.parse(name)?.peer ?? (isNamespacedCoveringRuleName(name) ? name.slice(ns.prefix.length) : undefined);
|
|
10
|
+
if (peer === undefined)
|
|
11
|
+
return { kind: "incomplete", ns };
|
|
12
|
+
if (peer.includes("*"))
|
|
13
|
+
return { kind: "wildcard_peer", ns };
|
|
14
|
+
const minted = mintNamespacePrefix(ns, peer);
|
|
15
|
+
return minted === `${ns.prefix}${peer}__` ? undefined : { kind: "unminted_peer", ns, minted };
|
|
16
|
+
}
|
|
17
|
+
export function describeUnreachableNamespacedRuleName(v) {
|
|
18
|
+
switch (v.kind) {
|
|
19
|
+
case "incomplete":
|
|
20
|
+
return `incomplete ${v.ns.id} name — the live form is ${v.ns.prefix}<server>__<tool>, or ${v.ns.prefix}<server> / ${v.ns.prefix}<server>__* to cover every tool on that server`;
|
|
21
|
+
case "wildcard_peer":
|
|
22
|
+
return `the ${v.ns.id} server segment does not take a wildcard — a rule compares the server segment literally, so the entry matches nothing; write the concrete server name: ${v.ns.prefix}<server>__* to cover every tool on that server, or ${v.ns.prefix}<server>__<tool>`;
|
|
23
|
+
case "unminted_peer":
|
|
24
|
+
return (`the engine never mints this server segment as written — a server configured under that name has its tools registered as ${v.minted}<tool>, ` +
|
|
25
|
+
`and a rule compares the server segment literally (no wildcard, no normalization), so the entry matches nothing; if that is the configured name, ` +
|
|
26
|
+
`write ${v.minted}* to cover every tool on that server, or ${v.minted}<tool>`);
|
|
27
|
+
default: {
|
|
28
|
+
const unhandled = v;
|
|
29
|
+
throw new Error(`unhandled namespaced rule name verdict: ${JSON.stringify(unhandled)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=namespaced-rule-name.js.map
|
|
@@ -43,9 +43,10 @@ export declare function createCorruptReadFailOpenSeat(tag: FailOpenTag, leg?: (i
|
|
|
43
43
|
*
|
|
44
44
|
* ## 逐命中响亮不降级
|
|
45
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
|
-
*
|
|
46
|
+
* · session-policy 脸 ⇒ `P-DEBT` tag(`server.session-policy.corrupt-read-fail-open`)—— 计数逐次 + 探针行 + 一次性 warn。core 7.39.2 起按键读拒;
|
|
47
|
+
* core 7.41.0 #1309 起本会话的枚举拒、别的会话的抹除不动它、重绑带走它 —— 但只对**有归属**的坏行(行里的 `__sid`,否则同名高水位标记的 `__sid`);
|
|
48
|
+
* 无归属的坏行仍按「无规则」(会话规则只许收紧 ⇒ 丢的恰是收紧);座上分不出读法 ⇒ 这一脸每一发都计(含已拒的)。
|
|
49
|
+
* 抹除扫走无归属坏行那一形 core 是否披露、座上数不数得到,本车未亲核(未闭环,收车档登)。放行是 core 的裁定、本仓不包层(源头修复纪律),所以**只登债、不改放行**;
|
|
49
50
|
* · 其余脸 ⇒ 不计(见 {@link DurableAbsenceFace})。
|
|
50
51
|
* 记账**先于** warn:记账是总函数,warn 可能抛(core 的 guard 会吞掉那个抛),一次日志故障不许吃掉计数。
|
|
51
52
|
*
|
|
@@ -86,9 +86,10 @@ export function createDurableAbsenceSeat(warn, root, thirdFace) {
|
|
|
86
86
|
}
|
|
87
87
|
function buildDurableAbsenceNote(thirdFace) {
|
|
88
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,
|
|
90
|
-
"
|
|
91
|
-
"
|
|
89
|
+
"Consequence depends on which store read it: session-policy = a keyed read and this session's enumeration (a task's session rules, " +
|
|
90
|
+
"GET/PUT .../policy, the session sync export) were REFUSED (code \"corrupt\"); another session's erase keeps the row and a data-root rebind carries it, " +
|
|
91
|
+
"but a damaged row nothing on disk attributes to a session (no session id in its bytes and no high-water mark beside it) is still omitted from listings, " +
|
|
92
|
+
"swept by any session's erase when it is not JSON and left behind by a rebind, i.e. read as NO RULES — fix or delete the named file (deleting it means no session rules); session repo = a listing silently " +
|
|
92
93
|
`omitted rows; ${thirdFace}. Inspect the named path before deciding.`);
|
|
93
94
|
}
|
|
94
95
|
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)";
|
|
@@ -368,7 +368,11 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
368
368
|
};
|
|
369
369
|
readonly "server.session-policy.corrupt-read-fail-open": {
|
|
370
370
|
readonly cls: "P-DEBT";
|
|
371
|
-
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
|
|
371
|
+
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 7.41.0(#1309)起另三张脸对**有归属**的坏行(行里的 `__sid`,否则同名高水位标记的 `__sid`)也收紧了:① 本会话的 `listBySession` 拒(`corrupt`;会话同步导出 `GET /v1/sessions/:id/sync/manifest` 转 500 `corrupt`);② 别的会话的 `deleteBySession` 不动它;③ 数据根重绑带走它(新键上按键读照拒)。**仍按「无规则」的只剩无归属的坏行**(行里读不出 `__sid`、同名高水位标记也不在;core 包内 CHANGELOG 逐字「keeps its old fate」):枚举略过并披露、它「根本不是 JSON」时被任一会话的抹除扫掉(此后按键读得 ENOENT = 无规则)、重绑留在旧键下 —— 钉 `test/corrupt-read-f-seats-s990.test.ts` 的 🚧「S-1016 残余」格。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / 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 把无归属行也改成拒,届时撤本 tag**(7.116.0 读不出能撤 ⇒ 照实留着)。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。";
|
|
372
|
+
};
|
|
373
|
+
readonly "server.approval-crash-convergence.cause-unreadable": {
|
|
374
|
+
readonly cls: "F";
|
|
375
|
+
readonly note: "7.116.0 修复轮(`approval-crash-convergence.ts` 的 `convergeCrashedAsksAtBoot`;local 车道启动收敛):前世留下的一条 `STREAM_PENDING` 问行,它的卡信封(`card_json`)读不出 ⇒ 读不出 wire `approvalId`、无从向审计账要成因词 ⇒ 成因**回落**成 `crashed_before_park`。行照样收 VOID(决断口照样 410 —— 可决性 fail-closed,不受影响);最坏后果 = 一张其实是被有序关停截断的卡,在 410 体的 `deniedReason` 上说成「进程崩了」(`crashConverged` 读面若有它的审计组,那里的成因仍按审计账判,两处可能不同词)。计数非零 = 问账里有读不出卡信封的行(位腐 / 手改 / 版本错位),去看探针 detail 点名的 askId。读数另在启动日志 `approval_ask_audit_crash_converged` 的 `causeUnreadable` 格单列。";
|
|
372
376
|
};
|
|
373
377
|
readonly "server.outbound.connect-observer-threw": {
|
|
374
378
|
readonly cls: "F";
|
|
@@ -341,7 +341,11 @@ export const FAIL_OPEN_TAGS = {
|
|
|
341
341
|
},
|
|
342
342
|
"server.session-policy.corrupt-read-fail-open": {
|
|
343
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
|
|
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 7.41.0(#1309)起另三张脸对**有归属**的坏行(行里的 `__sid`,否则同名高水位标记的 `__sid`)也收紧了:① 本会话的 `listBySession` 拒(`corrupt`;会话同步导出 `GET /v1/sessions/:id/sync/manifest` 转 500 `corrupt`);② 别的会话的 `deleteBySession` 不动它;③ 数据根重绑带走它(新键上按键读照拒)。**仍按「无规则」的只剩无归属的坏行**(行里读不出 `__sid`、同名高水位标记也不在;core 包内 CHANGELOG 逐字「keeps its old fate」):枚举略过并披露、它「根本不是 JSON」时被任一会话的抹除扫掉(此后按键读得 ENOENT = 无规则)、重绑留在旧键下 —— 钉 `test/corrupt-read-f-seats-s990.test.ts` 的 🚧「S-1016 残余」格。会话规则**只许收紧** ⇒ 丢的恰是调用方加的收紧(部署策略 / 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 把无归属行也改成拒,届时撤本 tag**(7.116.0 读不出能撤 ⇒ 照实留着)。计数非零 = 有会话的规则文件坏了:运维按探针里的 `sessionId` / `path` 去修(删文件即放弃那层规则,删前先看 `reason`)。认脸判据见 `durableAbsenceFaceOf`(披露路径落在本数据根的 `session-policy/` 之下);认不出的脸不计。7.114.0 合流时车 UY 曾以「core 已修」整只撤掉本 tag,修复轮恢复(那次删除未发版)。",
|
|
345
|
+
},
|
|
346
|
+
"server.approval-crash-convergence.cause-unreadable": {
|
|
347
|
+
cls: "F",
|
|
348
|
+
note: "7.116.0 修复轮(`approval-crash-convergence.ts` 的 `convergeCrashedAsksAtBoot`;local 车道启动收敛):前世留下的一条 `STREAM_PENDING` 问行,它的卡信封(`card_json`)读不出 ⇒ 读不出 wire `approvalId`、无从向审计账要成因词 ⇒ 成因**回落**成 `crashed_before_park`。行照样收 VOID(决断口照样 410 —— 可决性 fail-closed,不受影响);最坏后果 = 一张其实是被有序关停截断的卡,在 410 体的 `deniedReason` 上说成「进程崩了」(`crashConverged` 读面若有它的审计组,那里的成因仍按审计账判,两处可能不同词)。计数非零 = 问账里有读不出卡信封的行(位腐 / 手改 / 版本错位),去看探针 detail 点名的 askId。读数另在启动日志 `approval_ask_audit_crash_converged` 的 `causeUnreadable` 格单列。",
|
|
345
349
|
},
|
|
346
350
|
"server.outbound.connect-observer-threw": {
|
|
347
351
|
cls: "F",
|
|
@@ -8,6 +8,9 @@ export declare const PERMISSION_RULE_EVENT_TABLE: readonly [{
|
|
|
8
8
|
}, {
|
|
9
9
|
readonly event: "read_only_allowed";
|
|
10
10
|
readonly meaning: "the engine's read-only shell verdict cleared an ask that was NOT an operator-written rule (core decisionReason \"read_only\"; an operator-declared ask carries matchedAskRule and is never cleared this way)";
|
|
11
|
+
}, {
|
|
12
|
+
readonly event: "skill_allowed";
|
|
13
|
+
readonly meaning: "an activated skill's allowed-tools list cleared the ask for the rest of that run (core decisionReason \"skill\"; today only a skill the request itself carried can do this — this deployment's own skills carry no lists; it clears only the unstated / ordinary questions a persisted allow rule may clear, never a deny / ask rule, an operator list, a governance or shell-floor question, or a mandated one)";
|
|
11
14
|
}];
|
|
12
15
|
/** 词表里的一个词(闭集)。 */
|
|
13
16
|
export type PermissionRuleEventWord = (typeof PERMISSION_RULE_EVENT_TABLE)[number]["event"];
|
|
@@ -12,6 +12,10 @@ export const PERMISSION_RULE_EVENT_TABLE = [
|
|
|
12
12
|
event: "read_only_allowed",
|
|
13
13
|
meaning: "the engine's read-only shell verdict cleared an ask that was NOT an operator-written rule (core decisionReason \"read_only\"; an operator-declared ask carries matchedAskRule and is never cleared this way)",
|
|
14
14
|
},
|
|
15
|
+
{
|
|
16
|
+
event: "skill_allowed",
|
|
17
|
+
meaning: "an activated skill's allowed-tools list cleared the ask for the rest of that run (core decisionReason \"skill\"; today only a skill the request itself carried can do this — this deployment's own skills carry no lists; it clears only the unstated / ordinary questions a persisted allow rule may clear, never a deny / ask rule, an operator list, a governance or shell-floor question, or a mandated one)",
|
|
18
|
+
},
|
|
15
19
|
];
|
|
16
20
|
const PERMISSION_RULE_EVENT_KINDS = new Set(PERMISSION_RULE_EVENT_TABLE.map((r) => `${PERMISSION_TRACE_PREFIX}${r.event}`));
|
|
17
21
|
export function isPermissionRuleEventKind(kind) {
|
|
@@ -182,7 +182,7 @@ export declare class FileRunStore {
|
|
|
182
182
|
/** Release any claim whose row is now terminal; KEEP suspended/needs_review (resume re-finds them). One place for
|
|
183
183
|
* the claim-release invariant, shared by reapRunning + reclaimOrphanedAtBoot. */
|
|
184
184
|
private sweepClaims;
|
|
185
|
-
/** Flip every `running` row matching `pred` → failed (with `reason`), then sweep terminal claims. */
|
|
185
|
+
/** Flip every `running` row matching `pred` → failed (with the death sentence `reason` + its machine `code`), then sweep terminal claims. */
|
|
186
186
|
private reapRunning;
|
|
187
187
|
reapStale(olderThanMs: number): Promise<number>;
|
|
188
188
|
/** TOC integration ask③ (INTEGRATION-DESIGN §11): a host-lane engine restart leaves the PREVIOUS process's
|
|
@@ -320,6 +320,12 @@ export declare class FileRunStore {
|
|
|
320
320
|
* @returns 被改写的账本**行**数(不是 run 数)。
|
|
321
321
|
*/
|
|
322
322
|
migrateRetiredAskOriginRows(): Promise<number>;
|
|
323
|
+
/**
|
|
324
|
+
* 一次性迁移 **M6 的 local 腿**(窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):把 ≤7.115.x 写下的死亡判决行
|
|
325
|
+
* (`failed` ∧ 码列空 ∧ 句子是那两句,判据 = `readLegacyHolderDeathCode`)补上机读码,盘上(`run.json` 整份原子替换,同 `persist`)与内存同改。
|
|
326
|
+
* 起服时调(`LocalBackend.runOneTimeMigrations`,任何读者之前);数据根独占 ⇒ 本车道没有滚动升级窗,不需要读侧半场。幂等:第二跑零命中零写盘。
|
|
327
|
+
*/
|
|
328
|
+
migrateLegacyHolderDeathCodes(): number;
|
|
323
329
|
/** Release every open event-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
|
|
324
330
|
dispose(): void;
|
|
325
331
|
}
|
|
@@ -7,7 +7,7 @@ import { randomBytes } from "node:crypto";
|
|
|
7
7
|
import { AppendLog, atomicWriteFile, ensureDir, isProcessLive, processFingerprint, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
|
|
8
8
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
9
9
|
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
10
|
-
import { RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError } from "./store-contracts.js";
|
|
10
|
+
import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR, StoreBootRefusalError, buildRunOrphanedError, readLegacyHolderDeathCode } from "./store-contracts.js";
|
|
11
11
|
import { notifyRunTerminal } from "../observability/run-terminal.js";
|
|
12
12
|
import { migrateRetiredAskOrigin } from "./one-time-migrations.js";
|
|
13
13
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
@@ -573,13 +573,14 @@ export class FileRunStore {
|
|
|
573
573
|
this.releaseClaim(sessionId);
|
|
574
574
|
}
|
|
575
575
|
}
|
|
576
|
-
reapRunning(pred, reason) {
|
|
576
|
+
reapRunning(pred, reason, code) {
|
|
577
577
|
let reaped = 0;
|
|
578
578
|
for (const r of this.runs.values()) {
|
|
579
579
|
if (r.status === "running" && pred(r)) {
|
|
580
580
|
r.status = "failed";
|
|
581
581
|
r.result = null;
|
|
582
582
|
r.error = reason;
|
|
583
|
+
r.errorCode = code;
|
|
583
584
|
r.updatedAt = new Date();
|
|
584
585
|
this.persist(r);
|
|
585
586
|
reaped += 1;
|
|
@@ -590,7 +591,7 @@ export class FileRunStore {
|
|
|
590
591
|
}
|
|
591
592
|
async reapStale(olderThanMs) {
|
|
592
593
|
const cutoff = Date.now() - olderThanMs;
|
|
593
|
-
return this.reapRunning((r) => r.updatedAt.getTime() < cutoff, RUN_STALLED_ERROR);
|
|
594
|
+
return this.reapRunning((r) => r.updatedAt.getTime() < cutoff, RUN_STALLED_ERROR, RUN_HOLDER_DEATH_CODES.stalled);
|
|
594
595
|
}
|
|
595
596
|
async reclaimOrphanedAtBoot(isResumable) {
|
|
596
597
|
const orphans = [];
|
|
@@ -602,6 +603,7 @@ export class FileRunStore {
|
|
|
602
603
|
r.status = "failed";
|
|
603
604
|
r.result = null;
|
|
604
605
|
r.error = RUN_ORPHANED_ERROR;
|
|
606
|
+
r.errorCode = RUN_HOLDER_DEATH_CODES.orphaned;
|
|
605
607
|
r.updatedAt = new Date();
|
|
606
608
|
this.persist(r);
|
|
607
609
|
}
|
|
@@ -730,7 +732,7 @@ export class FileRunStore {
|
|
|
730
732
|
const verdict = await this.judgeClaim(sessionId, claim, holder);
|
|
731
733
|
if (verdict.kind === "live")
|
|
732
734
|
return { ok: false, activeTaskId: verdict.holderTaskId ?? holder };
|
|
733
|
-
this.takeOverClaim(sessionId, verdict.reason, verdict.snapshot);
|
|
735
|
+
this.takeOverClaim(sessionId, verdict.reason, verdict.snapshot, verdict.code);
|
|
734
736
|
}
|
|
735
737
|
}
|
|
736
738
|
try {
|
|
@@ -806,9 +808,9 @@ export class FileRunStore {
|
|
|
806
808
|
pidStillRunning: owner !== undefined && isProcessLive({ pid: owner.pid }),
|
|
807
809
|
note: "the claim's owner process is provably not the one that wrote it (fingerprint falsified, or the pid is gone) and the row has nothing left to resume — taking the claim over",
|
|
808
810
|
});
|
|
809
|
-
return { kind: "stale", reason: buildRunOrphanedError(who), snapshot };
|
|
811
|
+
return { kind: "stale", reason: buildRunOrphanedError(who), snapshot, code: RUN_HOLDER_DEATH_CODES.orphaned };
|
|
810
812
|
}
|
|
811
|
-
takeOverClaim(sessionId, reason, snapshot) {
|
|
813
|
+
takeOverClaim(sessionId, reason, snapshot, code) {
|
|
812
814
|
const nowClaim = readClaimRecord(singleRecordOf(this.readRecords(this.activePath(sessionId), CLAIM_RECORD_SHAPE)), sessionId);
|
|
813
815
|
if (snapshot.torn) {
|
|
814
816
|
if (nowClaim !== undefined)
|
|
@@ -820,7 +822,7 @@ export class FileRunStore {
|
|
|
820
822
|
if (row?.status !== snapshot.status || row?.updatedAt.getTime() !== snapshot.updatedAtMs)
|
|
821
823
|
return false;
|
|
822
824
|
if (row && (row.status === "running" || row.status === "suspended" || row.status === "needs_review")) {
|
|
823
|
-
this.persist({ ...row, status: "failed", error: reason, updatedAt: new Date() });
|
|
825
|
+
this.persist({ ...row, status: "failed", error: reason, ...(code !== undefined ? { errorCode: code } : {}), updatedAt: new Date() });
|
|
824
826
|
}
|
|
825
827
|
const quarantine = join(this.tmpDir, `stale-claim.${sanitizePathComponent(sessionId)}.${process.pid}.${randomBytes(6).toString("hex")}`);
|
|
826
828
|
try {
|
|
@@ -899,6 +901,17 @@ export class FileRunStore {
|
|
|
899
901
|
}
|
|
900
902
|
return migrated;
|
|
901
903
|
}
|
|
904
|
+
migrateLegacyHolderDeathCodes() {
|
|
905
|
+
let migrated = 0;
|
|
906
|
+
for (const r of this.runs.values()) {
|
|
907
|
+
const code = readLegacyHolderDeathCode(r);
|
|
908
|
+
if (code === null || r.errorCode === code)
|
|
909
|
+
continue;
|
|
910
|
+
this.persist({ ...r, errorCode: code });
|
|
911
|
+
migrated += 1;
|
|
912
|
+
}
|
|
913
|
+
return migrated;
|
|
914
|
+
}
|
|
902
915
|
dispose() {
|
|
903
916
|
for (const log of this.eventLogs.values()) {
|
|
904
917
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { redactErrorMessage, redactLedgerEventData, sealRunRowResult } from "../observability/run-terminal.js";
|
|
2
2
|
import { outwardPlane } from "../terminal.js";
|
|
3
3
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
4
|
-
import { RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
4
|
+
import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
5
5
|
import { notifyRunTerminal } from "../observability/run-terminal.js";
|
|
6
6
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
7
7
|
export class MemoryRunStore {
|
|
@@ -300,6 +300,7 @@ export class MemoryRunStore {
|
|
|
300
300
|
r.status = "failed";
|
|
301
301
|
r.result = null;
|
|
302
302
|
r.error = RUN_STALLED_ERROR;
|
|
303
|
+
r.errorCode = RUN_HOLDER_DEATH_CODES.stalled;
|
|
303
304
|
r.updatedAt = new Date();
|
|
304
305
|
reaped += 1;
|
|
305
306
|
}
|
|
@@ -22,6 +22,8 @@ export interface OneTimeMigrationReport {
|
|
|
22
22
|
readonly sessionIncarnationColumn: "added" | "present" | "no-table" | "lazy-on-read";
|
|
23
23
|
/** M5:本次给空化身铸了令牌的行数。非零 = 这次迁移真写了存量行,必须让运维看见(与 M4 同一条纪律)。 */
|
|
24
24
|
readonly sessionIncarnationsBackfilled: number;
|
|
25
|
+
/** M6:本次给「只有句子」的死亡判决行补上机读码的 run 行数(SQL `task_run` / local `run.json`)。非零 = 这次迁移真写了存量行(与 M4 同一条纪律)。 */
|
|
26
|
+
readonly holderDeathCodeRowsBackfilled: number;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
|
|
@@ -54,8 +56,8 @@ export interface OneTimeMigrationWindow {
|
|
|
54
56
|
* 而留着 M3 会让 M3 永远判「跑过了」。三条同版引入、同一天到期,本来就同一次删。
|
|
55
57
|
*/
|
|
56
58
|
export declare const ONE_TIME_MIGRATIONS: readonly OneTimeMigrationWindow[];
|
|
57
|
-
/** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)
|
|
58
|
-
export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated">;
|
|
59
|
+
/** local 车道上 SQL 独有的四条(M1 / M2 / M4 / M5)的读数(M3 / M6 两车道都有,local 腿的读数由 `LocalBackend.runOneTimeMigrations` 现算):那几张表 / 列在文件底座上从来不存在 ⇒ 如实答 absent / 0。 */
|
|
60
|
+
export declare const NO_SQL_MIGRATIONS: Omit<OneTimeMigrationReport, "askOriginRowsMigrated" | "holderDeathCodeRowsBackfilled">;
|
|
59
61
|
/** 启动期的日志口(logger 的子集)。两级各有其用,见 {@link logOneTimeMigrations}。 */
|
|
60
62
|
export interface MigrationLogSink {
|
|
61
63
|
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
@@ -2,6 +2,7 @@ import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js
|
|
|
2
2
|
import { acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
|
|
3
3
|
import { jsonColumnTextExpr } from "./sql-json-column.js";
|
|
4
4
|
import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
|
|
5
|
+
import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
5
6
|
export const ONE_TIME_MIGRATIONS = [
|
|
6
7
|
{
|
|
7
8
|
id: "M1",
|
|
@@ -62,6 +63,19 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
62
63
|
"`session.incarnation_missing`(parked 决定腿因此拒、记忆座退回按 id 绑定并 warn)—— 运维手工给那些行填一只随机令牌即可",
|
|
63
64
|
reportKeys: ["sessionIncarnationColumn", "sessionIncarnationsBackfilled"],
|
|
64
65
|
},
|
|
66
|
+
{
|
|
67
|
+
id: "M6",
|
|
68
|
+
introducedIn: "7.116.0",
|
|
69
|
+
releasedOn: "2026-10-05",
|
|
70
|
+
removableAfter: "2026-11-04",
|
|
71
|
+
removal: "删 migrateOn 的 M6 步(`task_run` 两条 `UPDATE … SET error_code`)与 local 腿 `FileRunStore.migrateLegacyHolderDeathCodes`(及 " +
|
|
72
|
+
"`LocalBackend.runOneTimeMigrations` 里那一格);报告与日志行的 `holderDeathCodeRowsBackfilled`;**连同读侧半场一起删**:" +
|
|
73
|
+
"`store-contracts.ts` 的 `readLegacyHolderDeathCode` 与 SQL 行映射 `run-store-sql.ts#rowToRun` 里对它的调用(码列改回原样读)",
|
|
74
|
+
afterRemoval: "没有响亮臂:窗关删码之后仍只有句子、码列为空的行(跳过了所有带 M6 的版本直接升的库,或窗关之后还在写的 ≤7.115.x 副本)不再被判成持有者死亡 —— " +
|
|
75
|
+
"用户输入受理账本的收敛判词(`user-input-ledger.ts` `judgeOpen`)对这种行落到「其余」臂 `leg_over`:补落 `settled{leg_end}`、不告知,那条 run 上" +
|
|
76
|
+
"丢掉的 steer 不再告诉用户;`GET /v1/runs/:id` 上 `errorCode` 缺席、只剩 `error` 句子",
|
|
77
|
+
reportKeys: ["holderDeathCodeRowsBackfilled"],
|
|
78
|
+
},
|
|
65
79
|
];
|
|
66
80
|
export const NO_SQL_MIGRATIONS = {
|
|
67
81
|
retiredSnapshotTable: "absent",
|
|
@@ -204,6 +218,12 @@ async function migrateOn(db, dialect) {
|
|
|
204
218
|
: await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
|
|
205
219
|
`WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
|
|
206
220
|
const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
|
|
221
|
+
const holderDeathCodeRowsBackfilled = (await db.exec(dialect === "tidb"
|
|
222
|
+
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
|
|
223
|
+
: "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR])) +
|
|
224
|
+
(await db.exec(dialect === "tidb"
|
|
225
|
+
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
|
|
226
|
+
: "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
|
|
207
227
|
let retiredSnapshotTable = "absent";
|
|
208
228
|
let retiredSnapshotRows = 0;
|
|
209
229
|
let retiredSteerSeatColumn = "absent";
|
|
@@ -236,7 +256,7 @@ async function migrateOn(db, dialect) {
|
|
|
236
256
|
}
|
|
237
257
|
return {
|
|
238
258
|
retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows,
|
|
239
|
-
sessionIncarnationColumn, sessionIncarnationsBackfilled,
|
|
259
|
+
sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled,
|
|
240
260
|
};
|
|
241
261
|
}
|
|
242
262
|
const INCARNATION_BACKFILL_BATCH = 500;
|
|
@@ -309,7 +329,7 @@ export function migrateRetiredAskOrigin(type, data) {
|
|
|
309
329
|
}
|
|
310
330
|
export function logOneTimeMigrations(log, backend, report) {
|
|
311
331
|
const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0 ||
|
|
312
|
-
report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0;
|
|
332
|
+
report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0;
|
|
313
333
|
(changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
314
334
|
backend,
|
|
315
335
|
retiredSnapshotTable: report.retiredSnapshotTable,
|
|
@@ -320,11 +340,14 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
320
340
|
auditDetailJsonNullRows: report.auditDetailJsonNullRows,
|
|
321
341
|
sessionIncarnationColumn: report.sessionIncarnationColumn,
|
|
322
342
|
sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
|
|
343
|
+
holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
|
|
323
344
|
removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
|
|
324
345
|
note: "one-time persisted-data migrations (M1: retired whole-tree snapshot index table dropped; M2: retired single-seat " +
|
|
325
346
|
"steering column dropped; M3: retired ask-origin spelling rewritten in place; M4: audit details written as the JSON " +
|
|
326
347
|
"text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
|
|
327
|
-
"session incarnation column added and every session row without one minted its own token
|
|
348
|
+
"session incarnation column added and every session row without one minted its own token; M6: run rows failed by a " +
|
|
349
|
+
"holder-death verdict written by an earlier version with only the sentence got the machine-readable code run.stalled / " +
|
|
350
|
+
"run.orphaned). Each step " +
|
|
328
351
|
"runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
|
|
329
352
|
"removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
|
|
330
353
|
"version that still carries a step at least once before upgrading past its deletion.",
|
|
@@ -317,7 +317,7 @@ export class PgSessionStore {
|
|
|
317
317
|
return `$${params.length}`;
|
|
318
318
|
};
|
|
319
319
|
const where = ["sm.placement_kind IS NULL"];
|
|
320
|
-
if (opts.owner)
|
|
320
|
+
if (opts.owner !== undefined)
|
|
321
321
|
where.push(opts.includeUnowned ? `(sm.owner = ${ph(opts.owner)} OR sm.owner IS NULL)` : `sm.owner = ${ph(opts.owner)}`);
|
|
322
322
|
if (opts.q)
|
|
323
323
|
where.push(`EXISTS (SELECT 1 FROM task_run tr WHERE tr.session_id = sm.session_id AND LOWER(tr.objective_preview) LIKE LOWER(${ph(`%${escapeLike(opts.q)}%`)}) ESCAPE '\\')`);
|
|
@@ -6,7 +6,7 @@ import { pgSanitizeText } from "./pg-safe-json.js";
|
|
|
6
6
|
import { toIso as iso } from "./sql-row-helpers.js";
|
|
7
7
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
8
8
|
import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readJsonObjectColumn, readNonNullJsonValueColumn } from "./sql-json-column.js";
|
|
9
|
-
import { RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
9
|
+
import { RUN_HOLDER_DEATH_CODES, RUN_STALLED_ERROR, readLegacyHolderDeathCode } from "./store-contracts.js";
|
|
10
10
|
import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal.js";
|
|
11
11
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
12
12
|
import { isDupKeyError } from "./sql-errors.js";
|
|
@@ -214,7 +214,7 @@ export class SqlRunStore {
|
|
|
214
214
|
status: r.status,
|
|
215
215
|
result: refusing("serve a finished run as if it had produced nothing", () => decodeResultColumn(r.result, String(r.task_id))) ?? null,
|
|
216
216
|
error: r.error ?? null,
|
|
217
|
-
errorCode: r.error_code ?? null,
|
|
217
|
+
errorCode: readLegacyHolderDeathCode({ status: String(r.status), error: r.error ?? null, errorCode: r.error_code ?? null }),
|
|
218
218
|
jobId: r.job_id ?? null,
|
|
219
219
|
source: r.source ?? null,
|
|
220
220
|
instanceId: r.instance_id ?? null,
|
|
@@ -424,8 +424,8 @@ export class SqlRunStore {
|
|
|
424
424
|
const cutoff = new Date(Date.now() - olderThanMs);
|
|
425
425
|
let reaped = 0;
|
|
426
426
|
await this.tx(async (conn) => {
|
|
427
|
-
const { affected } = await conn.query(this.q(`UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', updated_at = ? ` +
|
|
428
|
-
"WHERE status = 'running' AND updated_at < ?", `UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', updated_at = $1 ` +
|
|
427
|
+
const { affected } = await conn.query(this.q(`UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', error_code = '${RUN_HOLDER_DEATH_CODES.stalled}', updated_at = ? ` +
|
|
428
|
+
"WHERE status = 'running' AND updated_at < ?", `UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', error_code = '${RUN_HOLDER_DEATH_CODES.stalled}', updated_at = $1 ` +
|
|
429
429
|
"WHERE status = 'running' AND updated_at < $2"), [new Date(), cutoff]);
|
|
430
430
|
reaped = affected;
|
|
431
431
|
await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
|