@sema-agent/server 7.54.0 → 7.56.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 +3 -3
- package/README.md +12 -6
- package/README.zh-CN.md +9 -5
- package/USAGE.md +108 -52
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-ask-audit-store.d.ts +129 -0
- package/dist/approval-ask-audit-store.js +284 -0
- package/dist/approval-card.d.ts +18 -6
- package/dist/approval-card.js +2 -2
- package/dist/approval-content-kind.d.ts +22 -0
- package/dist/approval-content-kind.js +5 -0
- package/dist/approval-reconciler.d.ts +2 -1
- package/dist/approval-reconciler.js +1 -1
- package/dist/boot/coordinators.d.ts +2 -0
- package/dist/boot/coordinators.js +18 -1
- package/dist/boot/leader.d.ts +21 -0
- package/dist/boot/leader.js +6 -0
- package/dist/boot/reapers.d.ts +2 -1
- package/dist/boot/runtime-caps.d.ts +2 -1
- package/dist/boot/runtime-caps.js +3 -1
- package/dist/boot/stores.js +8 -1
- package/dist/config-center/types.d.ts +2 -1
- package/dist/config-provider.js +1 -0
- package/dist/device-store.d.ts +66 -2
- package/dist/device-store.js +35 -0
- package/dist/device-ws-hub.d.ts +8 -0
- package/dist/device-ws-hub.js +6 -0
- package/dist/http/route-ctx.d.ts +15 -3
- package/dist/http/routes/approvals-assistant.js +3 -2
- package/dist/http/routes/devices.d.ts +64 -0
- package/dist/http/routes/devices.js +173 -0
- package/dist/http/routes/leader.js +2 -2
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.d.ts +14 -0
- package/dist/http/server.js +30 -5
- package/dist/leader/endpoint.js +5 -4
- package/dist/leader/wire.d.ts +50 -0
- package/dist/leader/wire.js +19 -7
- package/dist/main.js +4 -2
- package/dist/observability/fail-open.d.ts +7 -3
- package/dist/observability/fail-open.js +7 -3
- package/dist/plugins/approval-ask-store-memory.d.ts +11 -1
- package/dist/plugins/approval-ask-store-memory.js +20 -3
- package/dist/plugins/approval-ask-store-sql.d.ts +82 -0
- package/dist/plugins/approval-ask-store-sql.js +41 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
- package/dist/plugins/checkpoint-store-sql.js +4 -1
- package/dist/plugins/device-store-sql.d.ts +38 -1
- package/dist/plugins/device-store-sql.js +82 -2
- package/dist/plugins/file-run-store.js +2 -0
- package/dist/plugins/local-checkpoint-store.js +2 -0
- package/dist/plugins/local-session-store.d.ts +9 -9
- package/dist/plugins/local-session-store.js +5 -3
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/permission-rule-store-file.d.ts +22 -4
- package/dist/plugins/permission-rule-store-file.js +17 -8
- package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
- package/dist/plugins/permission-rule-store-sql.js +23 -13
- package/dist/plugins/pg-session-storage.js +17 -13
- package/dist/plugins/remote-env-device.js +8 -1
- package/dist/plugins/run-store-sql.js +6 -4
- package/dist/plugins/sql-errors.d.ts +12 -0
- package/dist/plugins/sql-errors.js +10 -0
- package/dist/plugins/store-contracts.d.ts +4 -0
- package/dist/plugins/tidb-session-store.js +18 -14
- package/dist/rules-consent.d.ts +5 -4
- package/dist/rules-consent.js +5 -33
- package/dist/runs.js +1 -0
- package/dist/runtime-caps-resolver.js +3 -1
- package/dist/security.d.ts +7 -0
- package/dist/tool-approval.d.ts +66 -83
- package/dist/tool-approval.js +222 -186
- package/dist/trace/ledger-events.d.ts +9 -0
- package/package.json +3 -3
- package/skills/find-skills.md +1 -1
- package/skills/loop.md +1 -1
package/dist/rules-consent.js
CHANGED
|
@@ -230,27 +230,6 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
230
230
|
const monotonic = opts?.monotonic ?? (() => performance.now());
|
|
231
231
|
const sleep = opts?.sleep ?? (async (ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
232
232
|
const localImportLogger = createLogger();
|
|
233
|
-
const anchors = new Map();
|
|
234
|
-
const ANCHOR_CAP = 4096;
|
|
235
|
-
const pruneAnchors = (nowMono) => {
|
|
236
|
-
for (const [id, a] of anchors) {
|
|
237
|
-
if (nowMono - a.mintedAtMono > ticketTtlMs)
|
|
238
|
-
anchors.delete(id);
|
|
239
|
-
}
|
|
240
|
-
if (anchors.size <= ANCHOR_CAP)
|
|
241
|
-
return;
|
|
242
|
-
const excess = anchors.size - ANCHOR_CAP;
|
|
243
|
-
let dropped = 0;
|
|
244
|
-
for (const id of anchors.keys()) {
|
|
245
|
-
if (dropped++ >= excess)
|
|
246
|
-
break;
|
|
247
|
-
anchors.delete(id);
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
const decisionClock = (ticketId) => {
|
|
251
|
-
const a = anchors.get(ticketId);
|
|
252
|
-
return a === undefined ? undefined : { decisionNowMs: Math.ceil(a.mintedAtWall + (monotonic() - a.mintedAtMono)) };
|
|
253
|
-
};
|
|
254
233
|
return {
|
|
255
234
|
async persistCardRule(input) {
|
|
256
235
|
let prepared;
|
|
@@ -360,14 +339,10 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
360
339
|
}
|
|
361
340
|
return { ok: false, reason: "too-many-candidates", candidates: preview.candidates.length };
|
|
362
341
|
}
|
|
363
|
-
const mintedAtMono = monotonic();
|
|
364
342
|
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "cc-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
365
|
-
pruneAnchors(mintedAtMono);
|
|
366
|
-
anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
|
|
367
343
|
return { ok: true, preview, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
368
344
|
},
|
|
369
345
|
async redeemImport(principal, ticket) {
|
|
370
|
-
const clock = decisionClock(ticket);
|
|
371
346
|
const consumeOutcomeAfterThrow = async (err) => {
|
|
372
347
|
localImportLogger.warn("cc_import_claim_write_failed", {
|
|
373
348
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -398,7 +373,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
398
373
|
};
|
|
399
374
|
let consumed;
|
|
400
375
|
try {
|
|
401
|
-
consumed = await stores.tickets.consume(ticket, principal, "cc-import"
|
|
376
|
+
consumed = await stores.tickets.consume(ticket, principal, "cc-import");
|
|
402
377
|
}
|
|
403
378
|
catch (err) {
|
|
404
379
|
return await consumeOutcomeAfterThrow(err);
|
|
@@ -407,7 +382,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
407
382
|
return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
|
|
408
383
|
const releaseClaim = async () => {
|
|
409
384
|
try {
|
|
410
|
-
return await stores.tickets.release(ticket, principal, consumed.claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000
|
|
385
|
+
return await stores.tickets.release(ticket, principal, consumed.claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
|
|
411
386
|
}
|
|
412
387
|
catch (err) {
|
|
413
388
|
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
@@ -496,10 +471,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
496
471
|
}
|
|
497
472
|
if (approvalId === undefined)
|
|
498
473
|
return { ok: true, preview: { members } };
|
|
499
|
-
const mintedAtMono = monotonic();
|
|
500
474
|
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "local-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
501
|
-
pruneAnchors(mintedAtMono);
|
|
502
|
-
anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
|
|
503
475
|
return { ok: true, preview: { members }, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
504
476
|
},
|
|
505
477
|
async redeemLocalImport(principal, ticket) {
|
|
@@ -530,7 +502,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
530
502
|
const driveAndSettle = async (approvalId, payloadHash, claimId) => {
|
|
531
503
|
const tryRelease = async () => {
|
|
532
504
|
try {
|
|
533
|
-
return await stores.tickets.release(ticket, principal, claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000
|
|
505
|
+
return await stores.tickets.release(ticket, principal, claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
|
|
534
506
|
}
|
|
535
507
|
catch (err) {
|
|
536
508
|
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
@@ -638,7 +610,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
638
610
|
};
|
|
639
611
|
let consumed;
|
|
640
612
|
try {
|
|
641
|
-
consumed = await stores.tickets.consume(ticket, principal, "local-import"
|
|
613
|
+
consumed = await stores.tickets.consume(ticket, principal, "local-import");
|
|
642
614
|
}
|
|
643
615
|
catch (err) {
|
|
644
616
|
return await claimOutcomeAfterThrow(err, "consume");
|
|
@@ -660,7 +632,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
660
632
|
case "minted": {
|
|
661
633
|
let again;
|
|
662
634
|
try {
|
|
663
|
-
again = await stores.tickets.consume(ticket, principal, "local-import"
|
|
635
|
+
again = await stores.tickets.consume(ticket, principal, "local-import");
|
|
664
636
|
}
|
|
665
637
|
catch (err) {
|
|
666
638
|
return await claimOutcomeAfterThrow(err, "consume");
|
package/dist/runs.js
CHANGED
|
@@ -400,6 +400,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
400
400
|
abortSignal: cancelCtrl.signal,
|
|
401
401
|
...(spec.sessionId ? { sessionId: spec.sessionId } : {}),
|
|
402
402
|
emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
|
|
403
|
+
emitRevoke: (f) => approvalEmit(f),
|
|
403
404
|
legKey: "",
|
|
404
405
|
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
405
406
|
}, withQuestion)
|
|
@@ -31,7 +31,7 @@ export function toCoreRuntimeCaps(ent) {
|
|
|
31
31
|
return undefined;
|
|
32
32
|
const autoMode = ent.autoMode;
|
|
33
33
|
const badCap = (v) => v !== undefined && typeof v !== "boolean";
|
|
34
|
-
if (badCap(ent.allowWorkflows) || badCap(ent.forceDurableGate) || badCap(ent.allowFork) || badCap(ent.allowObservers) || badCap(autoMode)) {
|
|
34
|
+
if (badCap(ent.allowWorkflows) || badCap(ent.forceDurableGate) || badCap(ent.allowFork) || badCap(ent.allowObservers) || badCap(autoMode) || badCap(ent.allowMemoryOptOut)) {
|
|
35
35
|
throw new Error("runtimeCaps from center carries a non-boolean cap (contract breach — fail closed)");
|
|
36
36
|
}
|
|
37
37
|
const caps = {};
|
|
@@ -45,6 +45,8 @@ export function toCoreRuntimeCaps(ent) {
|
|
|
45
45
|
caps.allowObservers = ent.allowObservers;
|
|
46
46
|
if (typeof autoMode === "boolean")
|
|
47
47
|
caps.autoMode = autoMode;
|
|
48
|
+
if (typeof ent.allowMemoryOptOut === "boolean")
|
|
49
|
+
caps.allowMemoryOptOut = ent.allowMemoryOptOut;
|
|
48
50
|
return Object.keys(caps).length ? caps : undefined;
|
|
49
51
|
}
|
|
50
52
|
export function centerEntitlementSourceWired(config) {
|
package/dist/security.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface SessionListItem {
|
|
|
15
15
|
lastStatus: string;
|
|
16
16
|
/** K-5c (shell §K): the latest run's task_id — the shell's one-hop live-tail re-attach anchor on resume. */
|
|
17
17
|
lastRunId: string | null;
|
|
18
|
+
/** [ref]([ref]/[ref]):the LATEST run's **engine run id**(`TaskResult.runId`)when known —— OMIT-when-absent,
|
|
19
|
+
* 恒不铸 null。runs-ledger listers 从 latest run 的终局 `result` JSON 提取(⇒ 在跑 latest 键诚实缺席);
|
|
20
|
+
* local lister 走 `noteTaskRun` 席实时记录(⇒ 在跑也在场)——两面缺席集合不完全相等,读法一致:
|
|
21
|
+
* 缺席 = 「此行无已知 engine runId」,禁读成其它含义。⚠️ 与 `lastRunId`(= latest run 的 **task_id**)
|
|
22
|
+
* 是两条身份轴,命名对齐 core `SessionStoreSummary.lastTaskRunId`。wire:additive 键零投影透传
|
|
23
|
+
* (sessions-list.ts);sema-sdk spec `SessionSummary` 闭集候 SDK 半场开键([ref]/.82 班车)。 */
|
|
24
|
+
lastTaskRunId?: string;
|
|
18
25
|
/** Auto-generated session title (null = untitled / lister face has no session_meta). */
|
|
19
26
|
title: string | null;
|
|
20
27
|
}
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { IncomingMessage } from "node:http";
|
|
|
2
2
|
import { type AskRequest, type AskOutcome, type RuleOffer } from "@sema-agent/core";
|
|
3
3
|
import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
|
|
4
4
|
import { type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
|
|
6
|
+
import type { ApprovalAskAuditSink } from "./approval-ask-audit-store.js";
|
|
6
7
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
7
8
|
/** [ref]:审批 gate kind 闭集(core gateMatch 的 `human`/`irreversible_ask` ↔ outcome.gate "policy_ask")。
|
|
8
9
|
* 此前 7 份手写副本散在两只 checkpoint store 的数组/SQL 字面与 /decide 守卫——core 加审批味 kind 时
|
|
@@ -21,10 +22,10 @@ export declare const APPROVAL_GATE_KINDS_SQL_IN: string;
|
|
|
21
22
|
* 因此续跑」。反面(真被消费)不铸键 —— additive 只记真,与 `updatedInputForwarded`/`decisionNote`
|
|
22
23
|
* 的先例逐字同族。**缺席禁读作「有消费者」的证明**:老版本 server 从不发这个键。
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* 成因两形([ref] 起;[ref] 复审件3 曾补的第三形 = 弃单墓碑守卫翻 park,随守卫整删退役。进程内仍可
|
|
26
|
+
* 短暂出现「腿在场而零交付」:编辑在飞让路 —— 标记放开后的下一拍按行兑现):①持有悬挂 promise 的是
|
|
27
|
+
* **另一个副本**;②本副本重启后本地窗已空。
|
|
28
|
+
* 两形的处置同一句:壳读 run 面判续跑没有,绝不把 200 无键当成「run 已续跑」的证明。
|
|
28
29
|
*
|
|
29
30
|
* 为什么不是 409/410(施工时的二选一,裁定写在这里):409/410 说的是「你的请求没有生效」,而这次决议
|
|
30
31
|
* **确实生效了**(行是 DECIDED,幂等回放读得到、审计面记得住)——把它渲成错误会让壳把一次成功的
|
|
@@ -322,6 +323,10 @@ export interface LivePendingRow {
|
|
|
322
323
|
* 子代 ask)上恒等成立。缺席 = 顶层 ask。`fromSubagent` 保留不动(byte-compat:先于本键的消费方仍按
|
|
323
324
|
* 它判别)。 */
|
|
324
325
|
originTaskId?: string;
|
|
326
|
+
/** [ref]([ref]②):内容问句分型 —— `"content_ask"` 当 `toolName` 为 AskUserQuestion(词属主
|
|
327
|
+
* `approval-content-kind.ts`,与 durable 两读面 / core summarize 面同词,OMIT 契约同族)。
|
|
328
|
+
* 缺席 = 普通工具 ask。展示/分诊分型用,永不参与决议路由。 */
|
|
329
|
+
contentKind?: "content_ask";
|
|
325
330
|
}
|
|
326
331
|
export interface ToolApprovalRunContext {
|
|
327
332
|
taskId: string;
|
|
@@ -329,8 +334,9 @@ export interface ToolApprovalRunContext {
|
|
|
329
334
|
owner: string | null;
|
|
330
335
|
emit: (frame: ToolApprovalFrame) => void | Promise<void>;
|
|
331
336
|
/**
|
|
332
|
-
* [ref] 车6:**批级撤卡帧**(`approval_revoke
|
|
333
|
-
* `ApprovalRevokeFrame` 顶注)
|
|
337
|
+
* [ref] 车6:**批级撤卡帧**(`approval_revoke`,语义与发射面见 approval-card.ts 的
|
|
338
|
+
* `ApprovalRevokeFrame` 顶注 —— [ref] 起 bg/resume durable 腿的钩子写本腿账本,sync 腿仍 live)
|
|
339
|
+
* 的投递口。
|
|
334
340
|
*
|
|
335
341
|
* 🔴 为什么是**独立的可选钩子**而不是给 `emit` 的入参加宽:`emit` 是既有 `tool_approval` 活卡腿的
|
|
336
342
|
* 通道,它的消费方(装配点 + 全部存量帧断言面)今天只认那两种帧;把入参改成联合会强迫每一个消费点
|
|
@@ -339,9 +345,8 @@ export interface ToolApprovalRunContext {
|
|
|
339
345
|
* 车4 域)可以与 `approval_request` 的发射点**同批**接上。
|
|
340
346
|
*
|
|
341
347
|
* 缺席 ⇒ 本连接不收撤卡帧;壳侧的结构补偿恒是重连 preamble 的全量对账基准。
|
|
342
|
-
* (
|
|
343
|
-
*
|
|
344
|
-
* 「两族都还没接线」。)
|
|
348
|
+
* (接线现状:[ref] 起三腿全接 —— sync 腿 live 口(`routes/tasks.ts`),bg/resume 两腿 durable
|
|
349
|
+
* append(`runs.ts` / `http/server.ts`),与 `emitCard` 的三腿同批对齐。)
|
|
345
350
|
*/
|
|
346
351
|
emitRevoke?: (frame: ApprovalRevokeFrame) => void | Promise<void>;
|
|
347
352
|
/**
|
|
@@ -603,6 +608,17 @@ export declare function resolveApprovalLeg(input: {
|
|
|
603
608
|
export declare class ToolApprovalCoordinator {
|
|
604
609
|
private readonly als;
|
|
605
610
|
private readonly pending;
|
|
611
|
+
/** [ref] codex R3-1 / R4-1:终局阶梯**在飞店调用**(claim + 让路探针读)的计数 —— 按**持久 askId** 记(不按
|
|
612
|
+
* 本地登记闭包:同 askId 的重复登记 `pendingByAskId` 各领一顶帽 = 帽可被登记数绕开,池位按行数才是帽的
|
|
613
|
+
* 本义)。原始店 promise settle(resolve/reject 皆是)归还,归零即删键;帽值 {@link MAX_INFLIGHT_TERMINAL_CLAIMS}。 */
|
|
614
|
+
private readonly terminalClaimsInFlight;
|
|
615
|
+
/** {@link terminalClaimsInFlight} 的唯一写口:申请一格(帽满 ⇒ `undefined`),返回归还闭包。 */
|
|
616
|
+
private admitTerminalClaim;
|
|
617
|
+
/** 某 askId 此刻的在飞终局店调用数(claim + 让路探针)。阶梯据它分「调度争用」与「无人在飞」(R6-1);
|
|
618
|
+
* 判据钩子同读此口。 */
|
|
619
|
+
private terminalClaimsInFlightCount;
|
|
620
|
+
/** 判据钩子(生产路径不调)。 */
|
|
621
|
+
terminalClaimsInFlightForTest(askId: string): number;
|
|
606
622
|
/** [ref] 车2(codex 交叉复审 round3 抓获真 finding,round5 精化成 Set):次级索引,键 = 持久层 askId
|
|
607
623
|
* (与 {@link pending} 的 wire-面 uuidv7 `id` 是两条独立的身份轴,顶注同精神)——只在 askStore 在场且
|
|
608
624
|
* 这只 ask 真有 askId 时才登记。**同一 askId 下可能同时挂着不止一条本地条目**(round5 抓获:
|
|
@@ -658,10 +674,6 @@ export declare class ToolApprovalCoordinator {
|
|
|
658
674
|
/** [ref] + codex R3-[critical]:askId → **此刻正带着 `updatedInput` 决它的请求数**(见
|
|
659
675
|
* {@link markEditedDecisionInFlight} 顶注)。只在 `decideAsk` 那一次调用期间非空,故天然有界。 */
|
|
660
676
|
private readonly editedDecisionsInFlight;
|
|
661
|
-
/** [ref] + codex R6-[critical]:askId → **弃单墓碑**到期时刻。看门狗放弃一枚在飞编辑标记时落一条,
|
|
662
|
-
* 此后行派生的 approve 对这只 ask 改走 park 路由(理由见 {@link notifyExternalDecision} 里那段)。
|
|
663
|
-
* 有界两道:条目自带 TTL(惰性过期)+ 表大小 FIFO 上限,同 park 墓碑表的纪律。 */
|
|
664
|
-
private readonly abandonedEditedDecisions;
|
|
665
677
|
/** [ref] 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */
|
|
666
678
|
private storeErrorWarned;
|
|
667
679
|
private storeErrorTally;
|
|
@@ -696,6 +708,11 @@ export declare class ToolApprovalCoordinator {
|
|
|
696
708
|
* 构造之后才装配;同款 holder 先例 = main.ts 的 `getRunDenySweep`)。取到 undefined ⇒ 该臂逐字回落
|
|
697
709
|
* 修前 404,partial 部署安全。 */
|
|
698
710
|
private readonly parkedRedeem;
|
|
711
|
+
/** [ref] 裁 (c):local 车道的 File ask **审计** sink(在场 ⇔ `DB_BACKEND=local` ∧ 审批面开着,装配点
|
|
712
|
+
* `boot/coordinators.ts`)。三个挂线点:铸造(askBroadcast register 后)/ 决议(`settle()` 公共咽喉)/
|
|
713
|
+
* 腿闭(`runWithContext` finally)。**纯观察面**:sink 自己吞写失败并 `recordFailOpen`,任何一次审计
|
|
714
|
+
* 写都不得改变审批终局(店头注成文)。SQL 车道恒缺席(那里有真 ask 店)。 */
|
|
715
|
+
private readonly askAudit;
|
|
699
716
|
constructor(opts?: {
|
|
700
717
|
ttlMs?: number;
|
|
701
718
|
/** [ref](案A):零活流铸造的 ask 的窗(见 {@link unreachedTtlMs})。装配点在协议**上场**时才传
|
|
@@ -721,6 +738,8 @@ export declare class ToolApprovalCoordinator {
|
|
|
721
738
|
parkTombstoneTtlMs?: number;
|
|
722
739
|
/** [ref]:per-principal 子帽(同上不接 env;生效值恒 `min(本值, parkTombstoneMax)`)。 */
|
|
723
740
|
parkTombstonePerPrincipalMax?: number;
|
|
741
|
+
/** [ref] (c):local 车道审计 sink(见 {@link ToolApprovalCoordinator.askAudit})。 */
|
|
742
|
+
askAudit?: ApprovalAskAuditSink;
|
|
724
743
|
});
|
|
725
744
|
/**
|
|
726
745
|
* [ref] R-13 C —— 「**无人可答**」这一类终局的**唯一**成形口(park 路由 vs deny 政策)。
|
|
@@ -732,8 +751,8 @@ export declare class ToolApprovalCoordinator {
|
|
|
732
751
|
* (e) 断连 `forceParkNow` / emit 全灭 / 写侧准入超限 —— 走本方法。
|
|
733
752
|
*
|
|
734
753
|
* **持久回放的两个口也归它管**(codex R1-F1 验真后收口,红先钉在
|
|
735
|
-
* `test/unattended-approval-policy.test.ts`)
|
|
736
|
-
*
|
|
754
|
+
* `test/unattended-approval-policy.test.ts`):claim 败方真相的结算([ref] `settleFromClaimLoss`,前身
|
|
755
|
+
* `convergeFromDurableState`)与 `ensureAsk` 幂等重入,只要回放出来的是 `"unavailable"`(行 PARKING/PARKED)就过这个口。理由:那三处与臂 (a) 是**同一件
|
|
737
756
|
* 事**(本 ask 的等待以「行进了投递面」收场,赢家可能是别的副本 / 上一次调用),不统一的话同一只 ask
|
|
738
757
|
* 的终局会取决于「谁赢了那把 CAS」「重试没重试」—— 比残余更坏的不可解释性。人的**真决议**(回放出
|
|
739
758
|
* `true`/`false`)当然不过这个口:那不是「无人可答」。
|
|
@@ -881,23 +900,32 @@ export declare class ToolApprovalCoordinator {
|
|
|
881
900
|
*/
|
|
882
901
|
private sessionGrantUncontestedByRow;
|
|
883
902
|
/**
|
|
884
|
-
* [ref]
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
* 行派生 approve
|
|
896
|
-
* · 弃单墓碑在场 ⇒ `parked`(park 路由,可逆可再批 —— 绝不把一个证不出载荷的放行交出去);
|
|
903
|
+
* [ref]:claim 败方读数的分类器 —— **零读**(真相已随 `claimed:false` 回来,384 §4.3-1「settle 自足」),
|
|
904
|
+
* 把 {@link AskTerminalClaimCurrent} 翻译成 {@link RecoveryRead} 五态。两处消费 —— `resolveLocalOutcome`
|
|
905
|
+
* 主阶梯与三臂的迟到回调(迟到输同样自足,律 3)—— 共用这**一份**映射(`replayTerminalAskRow` 仍是唯一的
|
|
906
|
+
* 三态行映射,这里只把它接到结算动作上)。
|
|
907
|
+
*
|
|
908
|
+
* 沿革:[ref] 时代这里是恢复读口 `readRowForRecovery`(一次带墙钟的 `getAsk`,七态含 pending/unreadable),
|
|
909
|
+
* 前身 `readDecidedForRecovery`([ref] codex R3-F1)。「CAS 与读真相是两次店往返」= [ref]-R1 残余窗的病根
|
|
910
|
+
* ([ref]①):CAS 迟到干净输时真相不在答案里,补读又撞店不可用 ⇒ 人类决议丢失。[ref] 把读口整个撤掉:
|
|
911
|
+
* 争终局与读真相并成店的一次原子 claim,分类器只剩纯函数半场。[ref](行派生 approve 丢编辑)已随
|
|
912
|
+
* [ref] 落列收口:`approved` 臂带行上的 `updatedInput`。
|
|
913
|
+
*
|
|
914
|
+
* 行派生 approve 的守卫只剩一道**在这里**施加([ref]:弃单墓碑守卫随 [ref] 落列整删,见文件头 🪦 注):
|
|
897
915
|
* · 编辑在飞 ⇒ `edit-in-flight`(codex R4-[critical] 同族口:载荷还在别人手里的那一拍按行结算 = 用裸
|
|
898
916
|
* `true` 抢在它前面;持有者随后必定自己结算)。
|
|
899
917
|
*/
|
|
900
|
-
|
|
918
|
+
/**
|
|
919
|
+
* [ref] 让路探针(**只读、不争、不定局**):编辑在飞标记在场时 claim 阶梯刻意**不重打** claim(顶注:本臂
|
|
920
|
+
* 抢赢会把持有者正在提交的带编辑批准翻成 park),但一次**已落盘**的决议不该等到标记放开才被消费 ——
|
|
921
|
+
* codex [ref] R3-F1 的真窗:持有者回读慢于让路预算而标记仍在看门狗内 ⇒ 让路用尽走 park = 把已落盘批准
|
|
922
|
+
* 翻成 unavailable。探针 = 一次带墙钟的 `getAsk`;它**不是**败方真相的来源(那是 claim 答案,律 1),
|
|
923
|
+
* 只在 claim 被扣住的那几拍上不干扰地看一眼行:终态 ⇒ 交分类器(带载荷 approve 当拍兑现 / 负终态、
|
|
924
|
+
* PARKING 当拍结算 / 裸 approve 继续让);pending、行不在、读不出 ⇒ `undefined`,继续让路(有界)。
|
|
925
|
+
* 它只能**提前**兑现一个已落的决议,永远不会按意图定局 —— 这是它与 [ref] 恢复读口的本质差别。
|
|
926
|
+
*/
|
|
927
|
+
private probeRowWhileYielding;
|
|
928
|
+
private classifyClaimLoss;
|
|
901
929
|
/** [ref] codex R3-F2:同一 askId 下**其余本地注册**(重复注册,见 {@link pendingByAskId} 顶注)按
|
|
902
930
|
* **park 路由**收尾 —— 行已 PARKING,它们看的是同一条行,终局必须与赢家同形(`unattendedPolicy`
|
|
903
931
|
* 由各自的闭包读同一个口,deny 部署上一起是 deny)。赢家自己此刻已 settle 过、已从集合里摘除,
|
|
@@ -943,11 +971,13 @@ export declare class ToolApprovalCoordinator {
|
|
|
943
971
|
settled: number;
|
|
944
972
|
};
|
|
945
973
|
/**
|
|
946
|
-
* [ref] 车6:把一张**批级撤卡帧**投给给定的一组连接(
|
|
947
|
-
*
|
|
974
|
+
* [ref] 车6:把一张**批级撤卡帧**投给给定的一组连接(逐 ctx 分发 —— 见 `ApprovalRevokeFrame` 顶注:
|
|
975
|
+
* [ref] 起 durable 腿的钩子把帧落进本腿账本、sync 腿仍 live;丢帧的结构补偿是重连 preamble 的
|
|
976
|
+
* 全量对账基准)。
|
|
948
977
|
*
|
|
949
978
|
* 空名单不发(零信息的帧只会让壳多一次无意义的对账)。每路独立 catch:一路 emit 失败(连接刚死、
|
|
950
|
-
* durable-append 目标抖动)**绝不回滚已经落定的持久 CAS** —— 帧是通知,行才是真源。
|
|
979
|
+
* durable-append 目标抖动)**绝不回滚已经落定的持久 CAS** —— 帧是通知,行才是真源。[ref] 的 durable
|
|
980
|
+
* append 失败同落这只 catch(不新铸失败臂;账本行 VOID 是真源,撤帧丢失由 preamble 对账补偿)。
|
|
951
981
|
*/
|
|
952
982
|
private emitRevokeTo;
|
|
953
983
|
/**
|
|
@@ -1193,15 +1223,6 @@ export declare class ToolApprovalCoordinator {
|
|
|
1193
1223
|
backlog: number;
|
|
1194
1224
|
truncated: boolean;
|
|
1195
1225
|
}>;
|
|
1196
|
-
/**
|
|
1197
|
-
* [ref](案A)+ codex [critical] 的**前置门读口**:本副本对这条持久 askId 还有几条**未结算**的活体登记。
|
|
1198
|
-
*
|
|
1199
|
-
* 唯一消费者 = durable 回决口(`routes/runs.ts`)在 CAS **之前**判「这次带来的 `updatedInput` 交得出去
|
|
1200
|
-
* 吗」—— 交不出去就响亮拒,绝不静默把编辑降级成不带编辑的批准(判据与理由全文在那个调用点)。
|
|
1201
|
-
* 返回 0 的两种成因(**刻意不分**):等待的 promise 在**别的副本**,或那条 run 已经死了(重启后本地窗空)。
|
|
1202
|
-
* 两者的处置相同(这次编辑在这里都落不了地),而分辨它们需要读 run 面 —— 那不是本口能回答的问题。
|
|
1203
|
-
*/
|
|
1204
|
-
liveConsumerCount(askId: string): number;
|
|
1205
1226
|
/**
|
|
1206
1227
|
* 🔴 [ref] + codex R3-[critical](真缺陷,红先复现器在 `test/case-a-reachability-queue.test.ts` 同名格)
|
|
1207
1228
|
* —— **带编辑的决议在飞期间,观察者不结算**。
|
|
@@ -1229,48 +1250,10 @@ export declare class ToolApprovalCoordinator {
|
|
|
1229
1250
|
editedDecisionInFlightCount(askId: string): number;
|
|
1230
1251
|
/** {@link markEditedDecisionInFlight} 的读口:此刻有没有**别的**请求正带着编辑决这只 ask。 */
|
|
1231
1252
|
editedDecisionInFlight(askId: string): boolean;
|
|
1232
|
-
/**
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
* 🔴 **过期/逐出必须 fail-closed**(codex R7-[high],验真后采纳):首版是「到点就删」——而它保护的是
|
|
1237
|
-
* 一条**仍然挂着**的本地等待腿。店退化到十分钟以上、或并发弃单超过表帽时,墓碑先消失、腿还在,下一次
|
|
1238
|
-
* 行派生 approve 照样把原始命令放出去,正好复活它要防的那件事。判据改成:**丢墓碑之前先把它保护的
|
|
1239
|
-
* 消费者收掉**(park 路由,可逆可再批)。收完之后再删就是安全的 —— 没有腿可被误放行了。
|
|
1240
|
-
*/
|
|
1241
|
-
private abandonedEditedDecisionPending;
|
|
1242
|
-
/**
|
|
1243
|
-
* 🔴 **行派生 approve 的统一守卫**(codex R7-[critical],验真后修)。
|
|
1244
|
-
*
|
|
1245
|
-
* R6 把弃单墓碑的兑现点收在 {@link notifyExternalDecision} 上,并自称「公共咽喉」—— 那句话**不成立**:
|
|
1246
|
-
* 树上还有三处**直接** `settle(true, …)` / 直接把 `true` 交回 core 的行派生回放(`convergeFromDurableState`
|
|
1247
|
-
* 的收敛臂、`ensureAsk` 幂等命中终局行的回放、窗到期报错臂的 `readDecidedForRecovery` —— [ref] 起前者与
|
|
1248
|
-
* 后者合并成 `readRowForRecovery` 一个读口、三臂共用),它们跳过那个口。于是弃单之后从这三条路上仍能放出
|
|
1249
|
-
* 原始命令。
|
|
1250
|
-
*
|
|
1251
|
-
* 收口形 = **一个判据函数、五处消费**(上面三处 + 轮询 + HTTP 观察者):把「行说 approve」这件事
|
|
1252
|
-
* 在弃单墓碑在场时翻译成 `"unavailable"`(park 路由,可逆可再批),其余取值逐字透传。
|
|
1253
|
-
* 判据只认**不带编辑的行派生**放行(裸 `true`)—— 人的当次决议带着 `updatedInput` 走的是另一条路,不经
|
|
1254
|
-
* 本函数;[ref] 之后行上带编辑的 approve 回放出来是对象臂 `{allow:true, updatedInput}`,载荷**在行上**,
|
|
1255
|
-
* 墓碑要护的「证不出载荷」这件事对它不成立,同样原样透传。
|
|
1256
|
-
* ⚠️ 既有边界如实记([ref] 随批复核):列在之后,一只 ask 的决议**要么**带编辑落列(回放恒对象臂,本
|
|
1257
|
-
* 守卫永不命中)**要么**从没带过编辑(从没设过在飞标记,永无墓碑)⇒ 本守卫在新行上结构上不再可达,
|
|
1258
|
-
* 留着只为老进程/老行的并存窗与既有判据格;拆除是独立件([ref] 登记),不搭本批。
|
|
1259
|
-
*/
|
|
1260
|
-
private guardRowDerivedApprove;
|
|
1261
|
-
/** 丢一枚弃单墓碑的**唯一**口(过期 / 容量逐出共用):先把它还在保护的本地消费者按 park 路由收掉,
|
|
1262
|
-
* 再删表项 —— 顺序是硬的(先删表再收人 = 中间那一瞬正是可被误放行的窗口)。 */
|
|
1263
|
-
private dropAbandonedEditTombstone;
|
|
1264
|
-
/** 判据钩子:把「看门狗那一拍」显式地跑一次(留弃单墓碑 + 让在飞标记归零)。
|
|
1265
|
-
* 存在理由 = 那条臂的真实触发要等 {@link EDITED_DECISION_MARK_CAP_MS} 的墙钟,而本仓禁固定睡眠钉
|
|
1266
|
-
* (fixed-sleep 棘轮);把**语义**开一个口比让判据去睡 10 秒诚实得多。生产路径不调它。 */
|
|
1253
|
+
/** 判据钩子:把「看门狗那一拍」显式地跑一次(= 在飞标记被放弃归零;[ref] 起看门狗不再铸弃单墓碑,
|
|
1254
|
+
* 本钩子随之只剩这一半)。存在理由 = 那条臂的真实触发要等 {@link EDITED_DECISION_MARK_CAP_MS} 的
|
|
1255
|
+
* 墙钟,而本仓禁固定睡眠钉(fixed-sleep 棘轮)。生产路径不调它。 */
|
|
1267
1256
|
expireEditedDecisionMarkForTest(askId: string): void;
|
|
1268
|
-
/** 判据钩子:把一枚弃单墓碑**拨到已过期**并当场走它的丢弃路(= 真实 TTL 到点那一拍的语义)。
|
|
1269
|
-
* 同上理由:真触发要等 {@link ABANDONED_EDIT_TOMBSTONE_TTL_MS} 的墙钟。生产路径不调它。 */
|
|
1270
|
-
forceAbandonedEditTombstoneExpiryForTest(askId: string): void;
|
|
1271
|
-
/** 判据钩子:把一枚弃单墓碑的到期时刻**改写**成给定值(不触发惰性过期口本身)。用来构造
|
|
1272
|
-
* 「墓碑已过期、但还没有任何人来读过它」这一拍 —— 那正是「触发过期的那次调用还护不护」的判据点。 */
|
|
1273
|
-
setAbandonedEditTombstoneDeadlineForTest(askId: string, atMs: number): void;
|
|
1274
1257
|
/** Test/observability hooks. */
|
|
1275
1258
|
pendingCount(): number;
|
|
1276
1259
|
/** [ref]:当前挂着的**悬挂**条目数(零投递集合铸的那一类)——运维/判据读它证「悬挂队列有多长」。 */
|