@sema-agent/server 7.53.0 → 7.54.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/USAGE.md +5 -0
- package/dist/boot/config-center.js +2 -2
- package/dist/boot/leader.js +2 -0
- package/dist/boot/resolve-spec.js +1 -3
- package/dist/boot/runner-deps.d.ts +20 -0
- package/dist/boot/runner-deps.js +14 -8
- package/dist/boot/shutdown.js +1 -1
- package/dist/capabilities/team.d.ts +10 -9
- package/dist/config-center/apply-effective.d.ts +25 -26
- package/dist/config-center/apply-effective.js +28 -66
- package/dist/config-center/effective-keys.d.ts +30 -0
- package/dist/config-center/effective-keys.js +49 -0
- package/dist/config-center/facade.d.ts +3 -3
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/restart-signal.js +3 -9
- package/dist/config-center/types.d.ts +19 -40
- package/dist/config-lkg.js +7 -1
- package/dist/config-provider.js +1 -1
- package/dist/device-enrollment.d.ts +5 -3
- package/dist/device-store.d.ts +15 -1
- package/dist/device-store.js +2 -0
- package/dist/device-ws-hub.d.ts +7 -2
- package/dist/device-ws-hub.js +25 -5
- package/dist/http/server.js +19 -0
- package/dist/http/wire-types.d.ts +19 -0
- package/dist/leader/wire.d.ts +12 -0
- package/dist/leader/wire.js +6 -5
- package/dist/observability/fail-open.d.ts +16 -0
- package/dist/observability/fail-open.js +16 -0
- package/dist/plugins/pg-pool.d.ts +29 -2
- package/dist/plugins/pg-pool.js +45 -2
- package/dist/plugins/remote-env-device.d.ts +4 -2
- package/dist/plugins/remote-env-device.js +4 -1
- package/dist/plugins/roster-store-sql.d.ts +1 -3
- package/dist/plugins/store-backend.js +1 -1
- package/dist/plugins/tidb-pool.d.ts +13 -0
- package/dist/runtime-governance.d.ts +4 -0
- package/dist/runtime-governance.js +23 -1
- package/dist/tool-approval.d.ts +21 -28
- package/dist/tool-approval.js +220 -98
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/injection-tier.d.ts +11 -21
- package/dist/trace/injection-tier.js +3 -4
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
2
|
import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
|
|
3
|
-
import { DEVICE_ERROR_CODES,
|
|
3
|
+
import { DEVICE_ERROR_CODES, } from "../device-store.js";
|
|
4
4
|
export const DEVICE_PROVIDER = "device";
|
|
5
5
|
const ok = (value) => ({ ok: true, value });
|
|
6
6
|
const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
|
|
@@ -60,6 +60,7 @@ export const DEVICE_REJECT_TO_EXEC_ERROR = {
|
|
|
60
60
|
"device.enrollment_invalid": "auth_failed",
|
|
61
61
|
"not_found.device": "auth_failed",
|
|
62
62
|
"limit.rate_exceeded": "target_unavailable",
|
|
63
|
+
"draining": "target_unavailable",
|
|
63
64
|
};
|
|
64
65
|
export const DEVICE_REJECT_TO_FILE_ERROR = {
|
|
65
66
|
"device.not_bound": "transport_lost",
|
|
@@ -73,6 +74,7 @@ export const DEVICE_REJECT_TO_FILE_ERROR = {
|
|
|
73
74
|
"device.enrollment_invalid": "permission_denied",
|
|
74
75
|
"not_found.device": "permission_denied",
|
|
75
76
|
"limit.rate_exceeded": "transport_lost",
|
|
77
|
+
"draining": "transport_lost",
|
|
76
78
|
};
|
|
77
79
|
const DEVICE_REJECT_TO_REMOTE_ERROR = {
|
|
78
80
|
"device.not_bound": "connect_failed",
|
|
@@ -86,6 +88,7 @@ const DEVICE_REJECT_TO_REMOTE_ERROR = {
|
|
|
86
88
|
"device.enrollment_invalid": "auth_failed",
|
|
87
89
|
"not_found.device": "auth_failed",
|
|
88
90
|
"limit.rate_exceeded": "connect_failed",
|
|
91
|
+
"draining": "connect_failed",
|
|
89
92
|
};
|
|
90
93
|
function rejectText(reject) {
|
|
91
94
|
return `[${reject.code}] ${reject.message}`;
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
* 是「core 变语义必须先过迁移裁量」的告警钉)。
|
|
22
22
|
*/
|
|
23
23
|
import type { Pool } from "mysql2/promise";
|
|
24
|
-
import { type RosterStore, type RosterEntry } from "@sema-agent/core";
|
|
25
|
-
type RosterAccess = Parameters<RosterStore["resolve"]>[1];
|
|
24
|
+
import { type RosterStore, type RosterEntry, type RosterAccess } from "@sema-agent/core";
|
|
26
25
|
import type { PgQueryFn } from "./pg-query.js";
|
|
27
26
|
export declare const ROSTER_TABLE = "agent_roster";
|
|
28
27
|
export declare function ensureTiDBRosterSchema(pool: Pool): Promise<void>;
|
|
@@ -51,5 +50,4 @@ export declare class PgRosterStore implements RosterStore {
|
|
|
51
50
|
/** [ref] MED2(TiDB twin 同案注)。 */
|
|
52
51
|
reapOlderThan(cutoffMs: number): Promise<number>;
|
|
53
52
|
}
|
|
54
|
-
export {};
|
|
55
53
|
//# sourceMappingURL=roster-store-sql.d.ts.map
|
|
@@ -319,7 +319,7 @@ export function createStoreBackend(config, onWarn, onError) {
|
|
|
319
319
|
if (config.dbBackend === "pg") {
|
|
320
320
|
if (!config.pg)
|
|
321
321
|
return undefined;
|
|
322
|
-
const pool = createPgPool(pgPoolOptions(config.pg, config.dbQueryTimeoutMs));
|
|
322
|
+
const pool = createPgPool(pgPoolOptions(config.pg, config.dbQueryTimeoutMs), onWarn);
|
|
323
323
|
return new PgBackend(pool, config);
|
|
324
324
|
}
|
|
325
325
|
if (!config.tidb)
|
|
@@ -10,6 +10,19 @@ import type { ServiceConfig } from "../config-types.js";
|
|
|
10
10
|
*/
|
|
11
11
|
/** Build the mysql2 pool options from TiDB config (pure → unit-testable apart from the live connection). */
|
|
12
12
|
export declare function tidbPoolOptions(tidb: NonNullable<ServiceConfig["tidb"]>): mysql.PoolOptions;
|
|
13
|
+
/** ── [ref] 同查结论:mysql2 与 pg-pool **不同病**,本池不需要(也不应)挂进程保命监听 ────────────────
|
|
14
|
+
* pg 侧的病是「idle client 错误 emit 到 Pool 且无监听 ⇒ 未捕获 ⇒ 整进程 exit 1」(修在 pg-pool.ts
|
|
15
|
+
* createPgPool 头注)。mysql2@3.22.6 亲读(不信文档,以实现为准):
|
|
16
|
+
* · `lib/pool_connection.js:14-16` —— PoolConnection 构造器**恒挂** `once('error', () =>
|
|
17
|
+
* this._removeFromPool())`:每条池连接天生有 error 监听(**借出期同在**——不像 pg-pool 借出时
|
|
18
|
+
* 摘监听,mysql2 的 checked-out 连接也无裸奔窗),EventEmitter 不会把 idle 死转 throw,
|
|
19
|
+
* 坏连接就地淘汰;
|
|
20
|
+
* · `lib/base/connection.js` `_notifyError` —— fatal 闩(`if (this._fatalError) return`)挡同一
|
|
21
|
+
* 连接的二次 emit;`_handleFatalError` 摘 stream data 监听 ⇒ 闩后 `protocolError` 结构不可达;
|
|
22
|
+
* · `lib/base/pool.js` / `lib/promise/pool.js` —— 池层与 promise 包装层都**不**把连接 error 再
|
|
23
|
+
* emit 到池对象上(池层只 emit acquire/connection/enqueue/release)。
|
|
24
|
+
* ⇒ 空闲连接被 KILL / 故障切换杀掉 = 库内吞并淘汰,进程不死、池自愈。真 TiDB KILL 钉在
|
|
25
|
+
* `test/pool-idle-error-db-integration.test.ts` 第③格。升 mysql2 大版时按上面三坐标复核。 */
|
|
13
26
|
export declare function createTidbPool(config: ServiceConfig): Pool;
|
|
14
27
|
/**
|
|
15
28
|
* Idempotent schema. `session_event` is the append-only L1 event log (positional slicing by
|
|
@@ -126,6 +126,10 @@ export declare function governanceMandatesShellGateAlways(governance: {
|
|
|
126
126
|
autonomy?: Autonomy;
|
|
127
127
|
manualModeShellGate?: "always" | "classify";
|
|
128
128
|
}): boolean;
|
|
129
|
+
/** spec 序里的秩(0 起,越大越严)。入参收 `string`(fail-loud 边界:schema `trustRank` 同款姿势)——词表外 ⇒
|
|
130
|
+
* RangeError,绝不 undefined/NaN 让比较静默成 false。导出只为对表钉(test/safety-merge-spec-consumption.test.ts,
|
|
131
|
+
* [ref] 件4)——运行时消费仍走上表(键类型闭集)。 */
|
|
132
|
+
export declare function shellGateRank(word: string): number;
|
|
129
133
|
/**
|
|
130
134
|
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
|
|
131
135
|
* `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, } from "@sema-agent/core";
|
|
2
|
+
import { SAFETY_MERGE_SPEC } from "@sema-agent/settings-schema";
|
|
2
3
|
import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
|
|
3
4
|
export function isDelegationToolName(name) {
|
|
4
5
|
return name === DEFAULT_SUBAGENT_TOOL_NAME;
|
|
@@ -116,7 +117,28 @@ function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS
|
|
|
116
117
|
},
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
|
-
const SHELL_GATE_RANK =
|
|
120
|
+
const SHELL_GATE_RANK = (() => {
|
|
121
|
+
const tighten = SAFETY_MERGE_SPEC.shellGate.tighten;
|
|
122
|
+
const rule = tighten.rule;
|
|
123
|
+
if (tighten.rule !== "enumRank")
|
|
124
|
+
throw new Error(`SAFETY_MERGE_SPEC.shellGate.tighten.rule=${rule} — 不是 enumRank,shellGate 秩无从派生`);
|
|
125
|
+
const table = { off: -1, classify: -1, always: -1 };
|
|
126
|
+
for (const [i, w] of tighten.order.entries()) {
|
|
127
|
+
if (!(w in table))
|
|
128
|
+
throw new Error(`SAFETY_MERGE_SPEC.shellGate 序里有本仓词表外的词:${w}`);
|
|
129
|
+
table[w] = i;
|
|
130
|
+
}
|
|
131
|
+
for (const [w, r] of Object.entries(table))
|
|
132
|
+
if (r < 0)
|
|
133
|
+
throw new Error(`SAFETY_MERGE_SPEC.shellGate 序缺词:${w}`);
|
|
134
|
+
return table;
|
|
135
|
+
})();
|
|
136
|
+
export function shellGateRank(word) {
|
|
137
|
+
const r = SHELL_GATE_RANK[word];
|
|
138
|
+
if (r === undefined)
|
|
139
|
+
throw new RangeError(`unknown shellGate word: ${word}`);
|
|
140
|
+
return r;
|
|
141
|
+
}
|
|
120
142
|
export function applyRuntimeGovernance(base, governance) {
|
|
121
143
|
const marksOf = () => governance.askMarks ?? currentGovernanceAskMarks();
|
|
122
144
|
const overrides = { ...autonomyOverrides(governance.autonomy) };
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -842,30 +842,6 @@ export declare class ToolApprovalCoordinator {
|
|
|
842
842
|
* 面)—— 把它混进本名单会让「同 askId 的重复本地注册」拿到裸 deny,而赢家拿的是 park 路由:同一条
|
|
843
843
|
* 持久行、两种 live 终局。那一支改走 {@link settleSameAskIdParkRoute}。 */
|
|
844
844
|
private settleVoidedSiblings;
|
|
845
|
-
/**
|
|
846
|
-
* [ref] codex R3-F1 —— 「行上此刻有没有一个**真决议**」的一次性窄读(窗到期臂的店报错支专用)。
|
|
847
|
-
*
|
|
848
|
-
* 返回 `true`/`false` = 行是 `DECIDED` 且决议是 approve/deny(**人的**裁决,或车4 端点代人落的那一次);
|
|
849
|
-
* `undefined` = 其它一切(读失败、行不在、行是 STREAM_PENDING/PARKING/PARKED/DENIED/VOID)——调用方按
|
|
850
|
-
* 自己的臂继续。判别复用 {@link replayTerminalAskRow}(不造第二套映射),这里只把它的**布尔臂**取出来:
|
|
851
|
-
* `"unavailable"`(PARKING/PARKED)刻意**不**在本口消费,那属于路由面、归 `unattendedPolicy` 管。
|
|
852
|
-
*
|
|
853
|
-
* 一次读 + 墙钟 deadline(不重试):店已经在报错的语境里,重试只会把窗到期的收尾拖成秒级,而这一口要
|
|
854
|
-
* 回答的问题(「有没有人已经答过了」)读一次就够 —— 读不到就按「不知道」处置,与本臂原语义一致。
|
|
855
|
-
*
|
|
856
|
-
* ⚠️ **已登记的残余(codex R4-F1/F2,验真后不在本批修)**:本读与随后的本地 settle 之间没有持久 CAS,
|
|
857
|
-
* 所以「读到 STREAM_PENDING、另一副本**随后**才提交 DECIDED」这条窄窗仍会得到 park/deny 终局(行是
|
|
858
|
-
* approve 而本腿不执行);`emit` 全灭臂与取消臂的 store-未知态 catch 有**同形**缺口(两者都早于本件、
|
|
859
|
-
* 各自有既有判据钉着)。方向仍是 fail-closed(没有任何东西在未经人批时执行),补偿是 core 侧那张仍然
|
|
860
|
-
* pending 的 checkpoint —— 人再批一次就走。真正的收口 = 把窗/emit-全灭/取消三条 ambiguous-CAS catch
|
|
861
|
-
* 统一到一次**持久**恢复流程(带 CAS 与终态回读),那是店语义级改动,须由本模块属主立设计件,施工车
|
|
862
|
-
* 不私开(本批已试过整段复用 `convergeFromDurableState`,被自己的全量跑打回,理由见其顶注)。
|
|
863
|
-
*
|
|
864
|
-
* ✅ **第二条残余已收口([ref],2026-08-19 登记 → 2026-08-31 [ref] 销)**:本口读出的放行此前交给
|
|
865
|
-
* `settle` 时第三形参 `updatedInput` 恒缺席(行上没有那一列),core 落回原始实参执行 = 执行面**宽于**
|
|
866
|
-
* 人所批准。现在行上有 `updated_input` 列,本口回 `{allowed, updatedInput?}` 两件,`settle` 拿到的就是
|
|
867
|
-
* 人批准的那份编辑;P-DEBT tag 随列撤线(fail-open.ts 撤线备注 + census 第 31 行终态)。
|
|
868
|
-
*/
|
|
869
845
|
/**
|
|
870
846
|
* 🔴 session grant 短路前的**持久出处复核**([ref] 的 codex R1-[high] 补丁;[ref] 换判据)。
|
|
871
847
|
*
|
|
@@ -904,8 +880,24 @@ export declare class ToolApprovalCoordinator {
|
|
|
904
880
|
* 多半头一次到,那等于把一揽子放行整只废掉。窗的上界由本判据定死:行**一提交**窗即关。
|
|
905
881
|
*/
|
|
906
882
|
private sessionGrantUncontestedByRow;
|
|
907
|
-
/**
|
|
908
|
-
|
|
883
|
+
/**
|
|
884
|
+
* [ref] 恢复读口:一次带墙钟的 `getAsk`,把行翻译成 {@link RecoveryRead} 七态。**只读不写**(单赢者:终局
|
|
885
|
+
* 写点只有各臂的 CAS)。两处消费 —— 干净输后的 `convergeFromDurableState`(在 pending/unreadable 上退避)
|
|
886
|
+
* 与店未知态后的 `recoverFromUnknownStore`(只在 unreadable 上退避)—— 共用这**一份**映射,不各自
|
|
887
|
+
* `switch` 一遍(`replayTerminalAskRow` 仍是唯一的三态映射,这里只把它接到结算动作上)。
|
|
888
|
+
*
|
|
889
|
+
* 沿革:前身 `readDecidedForRecovery`([ref] codex R3-F1,窗到期臂店报错支专用)只答「有没有真决议」,
|
|
890
|
+
* 且把「读失败」与「行不是 DECIDED」折成同一个 `undefined` —— 调用臂分不清「店真不可用」与「行就是没定」,
|
|
891
|
+
* 于是取消臂 / emit 全灭臂的同形缺口一直没法接同一条路(R4-F1/F2 登记的残余)。[ref] 把两种「不知道」
|
|
892
|
+
* 拆开(设计件 §2 K1/K2),三臂 catch 才统一得起来。[ref] 那条(行派生 approve 丢编辑)已随 [ref] 落列
|
|
893
|
+
* 收口:`approved` 臂带行上的 `updatedInput`。
|
|
894
|
+
*
|
|
895
|
+
* 行派生 approve 的两道守卫**在这里**施加(五处消费一份判据,见 {@link guardRowDerivedApprove}):
|
|
896
|
+
* · 弃单墓碑在场 ⇒ `parked`(park 路由,可逆可再批 —— 绝不把一个证不出载荷的放行交出去);
|
|
897
|
+
* · 编辑在飞 ⇒ `edit-in-flight`(codex R4-[critical] 同族口:载荷还在别人手里的那一拍按行结算 = 用裸
|
|
898
|
+
* `true` 抢在它前面;持有者随后必定自己结算)。
|
|
899
|
+
*/
|
|
900
|
+
private readRowForRecovery;
|
|
909
901
|
/** [ref] codex R3-F2:同一 askId 下**其余本地注册**(重复注册,见 {@link pendingByAskId} 顶注)按
|
|
910
902
|
* **park 路由**收尾 —— 行已 PARKING,它们看的是同一条行,终局必须与赢家同形(`unattendedPolicy`
|
|
911
903
|
* 由各自的闭包读同一个口,deny 部署上一起是 deny)。赢家自己此刻已 settle 过、已从集合里摘除,
|
|
@@ -1252,8 +1244,9 @@ export declare class ToolApprovalCoordinator {
|
|
|
1252
1244
|
*
|
|
1253
1245
|
* R6 把弃单墓碑的兑现点收在 {@link notifyExternalDecision} 上,并自称「公共咽喉」—— 那句话**不成立**:
|
|
1254
1246
|
* 树上还有三处**直接** `settle(true, …)` / 直接把 `true` 交回 core 的行派生回放(`convergeFromDurableState`
|
|
1255
|
-
* 的收敛臂、`ensureAsk` 幂等命中终局行的回放、窗到期报错臂的 `readDecidedForRecovery`
|
|
1256
|
-
*
|
|
1247
|
+
* 的收敛臂、`ensureAsk` 幂等命中终局行的回放、窗到期报错臂的 `readDecidedForRecovery` —— [ref] 起前者与
|
|
1248
|
+
* 后者合并成 `readRowForRecovery` 一个读口、三臂共用),它们跳过那个口。于是弃单之后从这三条路上仍能放出
|
|
1249
|
+
* 原始命令。
|
|
1257
1250
|
*
|
|
1258
1251
|
* 收口形 = **一个判据函数、五处消费**(上面三处 + 轮询 + HTTP 观察者):把「行说 approve」这件事
|
|
1259
1252
|
* 在弃单墓碑在场时翻译成 `"unavailable"`(park 路由,可逆可再批),其余取值逐字透传。
|
package/dist/tool-approval.js
CHANGED
|
@@ -46,6 +46,20 @@ export const ASK_DECISION_CONSUMER_ABSENT = "absent";
|
|
|
46
46
|
const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
|
|
47
47
|
const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
|
|
48
48
|
const DURABLE_CALL_TIMEOUT_MS = 5_000;
|
|
49
|
+
function ambiguousCasFailOpenTag(intent) {
|
|
50
|
+
switch (intent) {
|
|
51
|
+
case "expire":
|
|
52
|
+
return "server.approval.ambiguous-cas.expire";
|
|
53
|
+
case "cancel":
|
|
54
|
+
return "server.approval.ambiguous-cas.cancel";
|
|
55
|
+
case "unreachable":
|
|
56
|
+
return "server.approval.ambiguous-cas.unreachable";
|
|
57
|
+
default: {
|
|
58
|
+
const unreachable = intent;
|
|
59
|
+
throw new Error(`ambiguousCasFailOpenTag: unknown intent ${String(unreachable)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
49
63
|
class ApprovalStoreDeadlineError extends Error {
|
|
50
64
|
constructor(label, timeoutMs) {
|
|
51
65
|
super(`approval store call ${label} timed out after ${timeoutMs}ms`);
|
|
@@ -440,26 +454,32 @@ export class ToolApprovalCoordinator {
|
|
|
440
454
|
return false;
|
|
441
455
|
}
|
|
442
456
|
}
|
|
443
|
-
async
|
|
457
|
+
async readRowForRecovery(askId, where) {
|
|
444
458
|
if (!this.askStore)
|
|
445
|
-
return
|
|
459
|
+
return { kind: "absent" };
|
|
460
|
+
let row;
|
|
446
461
|
try {
|
|
447
|
-
|
|
448
|
-
if (row === null || row.state !== "DECIDED")
|
|
449
|
-
return undefined;
|
|
450
|
-
if (replayTerminalAskRow(row) === true && this.abandonedEditedDecisionPending(askId)) {
|
|
451
|
-
this.guardRowDerivedApprove(askId, true, "expire-error-recovery");
|
|
452
|
-
return undefined;
|
|
453
|
-
}
|
|
454
|
-
if (this.editedDecisionInFlight(askId))
|
|
455
|
-
return undefined;
|
|
456
|
-
const replayed = settleArgsOf(replayTerminalAskRow(row));
|
|
457
|
-
return replayed === "unavailable" ? undefined : replayed;
|
|
462
|
+
row = await this.withStoreDeadline(this.askStore.getAsk(askId), `getAsk(${where})`, DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
458
463
|
}
|
|
459
464
|
catch (err) {
|
|
460
|
-
this.noteStoreError(err,
|
|
461
|
-
return
|
|
465
|
+
this.noteStoreError(err, `getAsk(${where})`);
|
|
466
|
+
return { kind: "unreadable" };
|
|
467
|
+
}
|
|
468
|
+
if (row === null)
|
|
469
|
+
return { kind: "absent" };
|
|
470
|
+
if (row.state === "STREAM_PENDING") {
|
|
471
|
+
return this.editedDecisionInFlight(askId) ? { kind: "edit-in-flight" } : { kind: "pending" };
|
|
462
472
|
}
|
|
473
|
+
const replayed = settleArgsOf(this.guardRowDerivedApprove(askId, replayTerminalAskRow(row), where));
|
|
474
|
+
if (replayed === "unavailable")
|
|
475
|
+
return { kind: "parked" };
|
|
476
|
+
if (!replayed.allowed)
|
|
477
|
+
return { kind: "denied" };
|
|
478
|
+
if (replayed.updatedInput !== undefined)
|
|
479
|
+
return { kind: "approved", updatedInput: replayed.updatedInput };
|
|
480
|
+
if (this.editedDecisionInFlight(askId))
|
|
481
|
+
return { kind: "edit-in-flight" };
|
|
482
|
+
return { kind: "approved" };
|
|
463
483
|
}
|
|
464
484
|
hasOtherSuspendedOwner(askId, self) {
|
|
465
485
|
if (askId === undefined)
|
|
@@ -490,13 +510,17 @@ export class ToolApprovalCoordinator {
|
|
|
490
510
|
const now = Date.now();
|
|
491
511
|
let ownCount = 0;
|
|
492
512
|
let ownExpiredVictim;
|
|
513
|
+
let ownBridgeVictim;
|
|
493
514
|
let ownPlainVictim;
|
|
494
515
|
let ownEvictable;
|
|
495
516
|
let globalExpiredVictim;
|
|
517
|
+
let globalBridgeVictim;
|
|
496
518
|
for (const [k, t] of this.parkTombstones) {
|
|
497
519
|
const expired = now - t.parkedAtMs > this.parkTombstoneTtlMs;
|
|
498
520
|
if (globalExpiredVictim === undefined && expired && t.inflight === undefined)
|
|
499
521
|
globalExpiredVictim = k;
|
|
522
|
+
if (globalBridgeVictim === undefined && t.kind === "voidBridge" && t.inflight === undefined)
|
|
523
|
+
globalBridgeVictim = k;
|
|
500
524
|
if (t.owner !== tomb.owner)
|
|
501
525
|
continue;
|
|
502
526
|
ownCount++;
|
|
@@ -506,13 +530,16 @@ export class ToolApprovalCoordinator {
|
|
|
506
530
|
ownEvictable = k;
|
|
507
531
|
if (ownExpiredVictim === undefined && expired)
|
|
508
532
|
ownExpiredVictim = k;
|
|
509
|
-
if (
|
|
533
|
+
if (ownBridgeVictim === undefined && t.kind === "voidBridge")
|
|
534
|
+
ownBridgeVictim = k;
|
|
535
|
+
if (ownPlainVictim === undefined && t.kind === "park" && t.redeemed === undefined)
|
|
510
536
|
ownPlainVictim = k;
|
|
511
537
|
}
|
|
538
|
+
const insertingBridge = tomb.kind === "voidBridge";
|
|
512
539
|
if (ownCount >= effectivePerPrincipalMax) {
|
|
513
|
-
const victim = ownExpiredVictim ?? ownPlainVictim ?? ownEvictable;
|
|
540
|
+
const victim = insertingBridge ? (ownExpiredVictim ?? ownBridgeVictim) : (ownExpiredVictim ?? ownBridgeVictim ?? ownPlainVictim ?? ownEvictable);
|
|
514
541
|
if (victim === undefined) {
|
|
515
|
-
recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"} reason
|
|
542
|
+
recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"} reason=${insertingBridge ? "bridge-would-evict-redeemable" : "all-inflight"}`);
|
|
516
543
|
return;
|
|
517
544
|
}
|
|
518
545
|
this.parkTombstones.delete(victim);
|
|
@@ -520,8 +547,11 @@ export class ToolApprovalCoordinator {
|
|
|
520
547
|
else if (this.parkTombstones.size >= this.parkTombstoneMax && globalExpiredVictim !== undefined) {
|
|
521
548
|
this.parkTombstones.delete(globalExpiredVictim);
|
|
522
549
|
}
|
|
550
|
+
else if (this.parkTombstones.size >= this.parkTombstoneMax && !insertingBridge && globalBridgeVictim !== undefined) {
|
|
551
|
+
this.parkTombstones.delete(globalBridgeVictim);
|
|
552
|
+
}
|
|
523
553
|
else if (this.parkTombstones.size >= this.parkTombstoneMax) {
|
|
524
|
-
recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"}`);
|
|
554
|
+
recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"}${insertingBridge ? " reason=bridge-refused-at-global-cap" : ""}`);
|
|
525
555
|
return;
|
|
526
556
|
}
|
|
527
557
|
this.parkTombstones.set(approvalId, tomb);
|
|
@@ -878,7 +908,7 @@ export class ToolApprovalCoordinator {
|
|
|
878
908
|
sig.removeEventListener("abort", fn);
|
|
879
909
|
this.pending.delete(id);
|
|
880
910
|
if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedOutcome() !== false) {
|
|
881
|
-
this.recordParkTombstone(wireApprovalId, { askId, batchId, owner: origin.owner, parkedAtMs: Date.now() });
|
|
911
|
+
this.recordParkTombstone(wireApprovalId, { kind: "park", askId, batchId, owner: origin.owner, parkedAtMs: Date.now() });
|
|
882
912
|
parkRouted = true;
|
|
883
913
|
}
|
|
884
914
|
if (askId !== undefined) {
|
|
@@ -898,39 +928,37 @@ export class ToolApprovalCoordinator {
|
|
|
898
928
|
const convergeFromDurableState = async (where) => {
|
|
899
929
|
if (!this.askStore || askId === undefined)
|
|
900
930
|
return;
|
|
901
|
-
const store = this.askStore;
|
|
902
931
|
const pinnedAskId = askId;
|
|
903
|
-
for (let attempt = 0; attempt
|
|
932
|
+
for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
|
|
904
933
|
if (done)
|
|
905
934
|
return;
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
935
|
+
const read = await this.readRowForRecovery(pinnedAskId, `converge:${where}`);
|
|
936
|
+
if (done)
|
|
937
|
+
return;
|
|
938
|
+
switch (read.kind) {
|
|
939
|
+
case "absent":
|
|
911
940
|
settle(false, "expired");
|
|
912
941
|
return;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
942
|
+
case "pending":
|
|
943
|
+
case "edit-in-flight":
|
|
944
|
+
case "unreadable":
|
|
945
|
+
if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
|
|
946
|
+
await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
|
|
916
947
|
continue;
|
|
917
|
-
|
|
918
|
-
const outcome = settleArgsOf(this.guardRowDerivedApprove(pinnedAskId, replayTerminalAskRow(row), `converge:${where}`));
|
|
919
|
-
if (outcome === "unavailable") {
|
|
948
|
+
case "parked":
|
|
920
949
|
windowRouteUnavailable = true;
|
|
921
950
|
settle(false, "expired");
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
settle(true, "allowed",
|
|
925
|
-
|
|
926
|
-
|
|
951
|
+
return;
|
|
952
|
+
case "approved":
|
|
953
|
+
settle(true, "allowed", read.updatedInput);
|
|
954
|
+
return;
|
|
955
|
+
case "denied":
|
|
927
956
|
settle(false, "denied");
|
|
957
|
+
return;
|
|
958
|
+
default: {
|
|
959
|
+
const unreachable = read;
|
|
960
|
+
throw new Error(`convergeFromDurableState: unknown recovery read ${String(unreachable)}`);
|
|
928
961
|
}
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
catch (err) {
|
|
932
|
-
this.noteStoreError(err, `getAsk(${where})`);
|
|
933
|
-
await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
|
|
934
962
|
}
|
|
935
963
|
}
|
|
936
964
|
if (!done) {
|
|
@@ -943,6 +971,90 @@ export class ToolApprovalCoordinator {
|
|
|
943
971
|
}
|
|
944
972
|
}
|
|
945
973
|
};
|
|
974
|
+
const recoverFromUnknownStore = async (arm, casStillInFlight) => {
|
|
975
|
+
if (askId === undefined) {
|
|
976
|
+
arm.settleByIntent();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const pinnedAskId = askId;
|
|
980
|
+
for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
|
|
981
|
+
if (done)
|
|
982
|
+
return;
|
|
983
|
+
const read = await this.readRowForRecovery(pinnedAskId, `recovery:${arm.intent}`);
|
|
984
|
+
if (done)
|
|
985
|
+
return;
|
|
986
|
+
switch (read.kind) {
|
|
987
|
+
case "approved":
|
|
988
|
+
settle(true, "allowed", read.updatedInput);
|
|
989
|
+
return;
|
|
990
|
+
case "denied":
|
|
991
|
+
settle(false, "denied");
|
|
992
|
+
return;
|
|
993
|
+
case "parked":
|
|
994
|
+
windowRouteUnavailable = true;
|
|
995
|
+
settle(false, "expired");
|
|
996
|
+
return;
|
|
997
|
+
case "pending": {
|
|
998
|
+
if (casStillInFlight) {
|
|
999
|
+
arm.settleByIntent();
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
let result;
|
|
1003
|
+
try {
|
|
1004
|
+
result = await arm.cas();
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
this.noteStoreError(err, arm.storeLabel);
|
|
1008
|
+
arm.settleByIntent();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (arm.won(result)) {
|
|
1012
|
+
arm.onWon(result);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
await convergeFromDurableState(arm.where);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
case "absent":
|
|
1019
|
+
arm.settleByIntent();
|
|
1020
|
+
return;
|
|
1021
|
+
case "edit-in-flight":
|
|
1022
|
+
case "unreadable":
|
|
1023
|
+
if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
|
|
1024
|
+
await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
|
|
1025
|
+
continue;
|
|
1026
|
+
default: {
|
|
1027
|
+
const unreachable = read;
|
|
1028
|
+
throw new Error(`recoverFromUnknownStore: unknown recovery read ${String(unreachable)}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (done)
|
|
1033
|
+
return;
|
|
1034
|
+
if (this.editedDecisionInFlight(pinnedAskId)) {
|
|
1035
|
+
windowRouteUnavailable = true;
|
|
1036
|
+
settle(false, "expired");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
recordFailOpen(ambiguousCasFailOpenTag(arm.intent), `askId=${pinnedAskId} reads=${DURABLE_CONVERGE_BACKOFF_MS.length + 1}`);
|
|
1040
|
+
arm.settleByIntent();
|
|
1041
|
+
};
|
|
1042
|
+
const resolveLocalOutcome = async (arm) => {
|
|
1043
|
+
let result;
|
|
1044
|
+
try {
|
|
1045
|
+
result = await arm.cas();
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
this.noteStoreError(err, arm.storeLabel);
|
|
1049
|
+
await recoverFromUnknownStore(arm, err instanceof ApprovalStoreDeadlineError);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (arm.won(result)) {
|
|
1053
|
+
arm.onWon(result);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
await convergeFromDurableState(arm.where);
|
|
1057
|
+
};
|
|
946
1058
|
const settleParkRouteIfUnsettled = (hostSettledBy) => {
|
|
947
1059
|
if (done)
|
|
948
1060
|
return;
|
|
@@ -955,34 +1067,30 @@ export class ToolApprovalCoordinator {
|
|
|
955
1067
|
settleParkRouteIfUnsettled(hostSelfReport);
|
|
956
1068
|
return;
|
|
957
1069
|
}
|
|
958
|
-
|
|
959
|
-
|
|
1070
|
+
const store = this.askStore;
|
|
1071
|
+
const pinnedAskId = askId;
|
|
1072
|
+
const pinnedBatchId = batchId;
|
|
1073
|
+
await resolveLocalOutcome({
|
|
1074
|
+
intent: "expire",
|
|
1075
|
+
storeLabel: "expireAsk",
|
|
1076
|
+
where: "windowExpired",
|
|
1077
|
+
cas: () => this.withStoreDeadline(store.expireAsk(pinnedAskId, pinnedBatchId), "expireAsk(window)", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
960
1078
|
if (!late.won)
|
|
961
1079
|
return;
|
|
962
|
-
this.settleSameAskIdParkRoute(
|
|
1080
|
+
this.settleSameAskIdParkRoute(pinnedAskId);
|
|
963
1081
|
this.settleVoidedSiblings(late.voidedSiblings);
|
|
964
|
-
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(
|
|
965
|
-
})
|
|
966
|
-
|
|
1082
|
+
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, late.voidedSiblings, "superseded_by_park", Date.now()));
|
|
1083
|
+
}),
|
|
1084
|
+
won: (res) => res.won,
|
|
1085
|
+
onWon: (res) => {
|
|
967
1086
|
windowRouteUnavailable = true;
|
|
968
1087
|
settle(false, "expired");
|
|
969
|
-
this.settleSameAskIdParkRoute(
|
|
1088
|
+
this.settleSameAskIdParkRoute(pinnedAskId);
|
|
970
1089
|
this.settleVoidedSiblings(res.voidedSiblings);
|
|
971
|
-
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
catch (err) {
|
|
978
|
-
this.noteStoreError(err, "expireAsk");
|
|
979
|
-
const decided = await this.readDecidedForRecovery(askId);
|
|
980
|
-
if (decided !== undefined && !done) {
|
|
981
|
-
settle(decided.allowed, decided.allowed ? "allowed" : "denied", decided.updatedInput);
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
settleParkRouteIfUnsettled(hostSelfReport);
|
|
985
|
-
}
|
|
1090
|
+
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, res.voidedSiblings, "superseded_by_park", Date.now()));
|
|
1091
|
+
},
|
|
1092
|
+
settleByIntent: () => settleParkRouteIfUnsettled(hostSelfReport),
|
|
1093
|
+
});
|
|
986
1094
|
})();
|
|
987
1095
|
};
|
|
988
1096
|
const forceParkNow = () => {
|
|
@@ -1005,26 +1113,29 @@ export class ToolApprovalCoordinator {
|
|
|
1005
1113
|
settle(false, "expired");
|
|
1006
1114
|
return;
|
|
1007
1115
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1116
|
+
const store = this.askStore;
|
|
1117
|
+
const pinnedAskId = askId;
|
|
1118
|
+
const pinnedBatchId = batchId;
|
|
1119
|
+
await resolveLocalOutcome({
|
|
1120
|
+
intent: "cancel",
|
|
1121
|
+
storeLabel: "transitionAsk(cancel)",
|
|
1122
|
+
where: "runCancel",
|
|
1123
|
+
cas: () => this.withStoreDeadline(store.transitionAsk(pinnedAskId, "STREAM_PENDING", "VOID", { updatedAtMs: Date.now() }), "transitionAsk(cancel)", DURABLE_CALL_TIMEOUT_MS, (lateWon) => {
|
|
1010
1124
|
if (!lateWon)
|
|
1011
1125
|
return;
|
|
1012
|
-
this.settleVoidedSiblings([
|
|
1013
|
-
this.
|
|
1014
|
-
|
|
1015
|
-
|
|
1126
|
+
this.settleVoidedSiblings([pinnedAskId]);
|
|
1127
|
+
this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
|
|
1128
|
+
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now()));
|
|
1129
|
+
}),
|
|
1130
|
+
won: (won) => won,
|
|
1131
|
+
onWon: () => {
|
|
1016
1132
|
settle(false, "expired");
|
|
1017
|
-
this.settleVoidedSiblings([
|
|
1018
|
-
this.
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
catch (err) {
|
|
1025
|
-
this.noteStoreError(err, "transitionAsk(cancel)");
|
|
1026
|
-
settle(false, "expired");
|
|
1027
|
-
}
|
|
1133
|
+
this.settleVoidedSiblings([pinnedAskId]);
|
|
1134
|
+
this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
|
|
1135
|
+
this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now()));
|
|
1136
|
+
},
|
|
1137
|
+
settleByIntent: () => settle(false, "expired"),
|
|
1138
|
+
});
|
|
1028
1139
|
};
|
|
1029
1140
|
if (signal) {
|
|
1030
1141
|
const onTaskAbort = () => void runCancel();
|
|
@@ -1114,31 +1225,42 @@ export class ToolApprovalCoordinator {
|
|
|
1114
1225
|
const emitAllP = (async () => {
|
|
1115
1226
|
const results = await Promise.all(openByCtx.values());
|
|
1116
1227
|
const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || results.some((r) => r.legacy || r.card);
|
|
1228
|
+
emitSettled = true;
|
|
1117
1229
|
if (!anySucceeded && !done) {
|
|
1118
1230
|
if (this.askStore && askId !== undefined && batchId !== undefined) {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1231
|
+
const store = this.askStore;
|
|
1232
|
+
const pinnedAskId = askId;
|
|
1233
|
+
const pinnedBatchId = batchId;
|
|
1234
|
+
const settleUnreachable = () => {
|
|
1235
|
+
if (done)
|
|
1236
|
+
return;
|
|
1237
|
+
emitFailed = true;
|
|
1238
|
+
settle(false, "expired");
|
|
1239
|
+
};
|
|
1240
|
+
await resolveLocalOutcome({
|
|
1241
|
+
intent: "unreachable",
|
|
1242
|
+
storeLabel: "expireAsk(emit-failed)",
|
|
1243
|
+
where: "emitFailed",
|
|
1244
|
+
cas: () => this.withStoreDeadline(store.expireAsk(pinnedAskId, pinnedBatchId), "expireAsk(emit-failed)", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
1245
|
+
if (!late.won)
|
|
1246
|
+
return;
|
|
1247
|
+
this.settleSameAskIdParkRoute(pinnedAskId);
|
|
1248
|
+
this.settleVoidedSiblings(late.voidedSiblings);
|
|
1249
|
+
}),
|
|
1250
|
+
won: (res) => res.won,
|
|
1251
|
+
onWon: (res) => {
|
|
1252
|
+
settleUnreachable();
|
|
1253
|
+
this.settleSameAskIdParkRoute(pinnedAskId);
|
|
1125
1254
|
this.settleVoidedSiblings(res.voidedSiblings);
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
this.noteStoreError(err, "expireAsk(emit-failed)");
|
|
1130
|
-
if (!done) {
|
|
1131
|
-
emitFailed = true;
|
|
1132
|
-
settle(false, "expired");
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1255
|
+
},
|
|
1256
|
+
settleByIntent: settleUnreachable,
|
|
1257
|
+
});
|
|
1135
1258
|
}
|
|
1136
1259
|
else if (!done) {
|
|
1137
1260
|
emitFailed = true;
|
|
1138
1261
|
settle(false, "expired");
|
|
1139
1262
|
}
|
|
1140
1263
|
}
|
|
1141
|
-
emitSettled = true;
|
|
1142
1264
|
})();
|
|
1143
1265
|
await Promise.race([emitAllP, allowedP]);
|
|
1144
1266
|
const settled = await allowedP;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
|
|
17
17
|
* 刚立,立完就在下一次加码时自己咬住了)。
|
|
18
18
|
*/
|
|
19
|
-
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted"];
|
|
19
|
+
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval"];
|
|
20
20
|
export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
|
|
21
21
|
/** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
|
|
22
22
|
export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
|