@sema-agent/server 7.143.0 → 7.145.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 +295 -0
- package/README.md +3 -0
- package/README.zh-CN.md +1 -0
- package/USAGE.md +34 -16
- package/dist/approval-card.d.ts +31 -14
- package/dist/approval-card.js +8 -8
- package/dist/audit.d.ts +2 -21
- package/dist/audit.js +0 -9
- package/dist/boot/execution-env.js +1 -1
- package/dist/boot/runner-deps.d.ts +10 -5
- package/dist/boot/runner-deps.js +5 -4
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +3 -3
- package/dist/boot/shutdown.d.ts +1 -1
- package/dist/boot/shutdown.js +4 -2
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +4 -9
- package/dist/capabilities/scenarios.d.ts +14 -18
- package/dist/capabilities/scenarios.js +1 -2
- package/dist/config-d-migration.d.ts +3 -0
- package/dist/delegated-session-owner.d.ts +33 -151
- package/dist/delegated-session-owner.js +1 -83
- package/dist/entry-mode-clamp-notice.d.ts +1 -1
- package/dist/execution-placement.d.ts +7 -4
- package/dist/fleet/fleet-bus.d.ts +13 -3
- package/dist/fleet/fleet-bus.js +17 -3
- package/dist/hooks/classic-stations.d.ts +3 -1
- package/dist/hooks/classic-stations.js +2 -0
- package/dist/http/admission.js +2 -1
- package/dist/http/checkpoint-ctx.d.ts +1 -0
- package/dist/http/checkpoint-ctx.js +1 -0
- package/dist/http/durable-run-leg.js +7 -6
- package/dist/http/principal-gate.d.ts +1 -1
- package/dist/http/refusal-body.d.ts +7 -3
- package/dist/http/refusal-body.js +1 -0
- package/dist/http/resume-legs.js +16 -7
- package/dist/http/route-ctx.d.ts +15 -0
- package/dist/http/routes/approvals-assistant.d.ts +20 -1
- package/dist/http/routes/approvals-assistant.js +28 -6
- package/dist/http/routes/rules.d.ts +1 -1
- package/dist/http/routes/rules.js +5 -2
- package/dist/http/routes/session-sync.js +12 -0
- package/dist/http/routes/tasks.js +12 -9
- package/dist/http/run-meta.d.ts +14 -7
- package/dist/http/run-meta.js +5 -2
- package/dist/http/server.d.ts +13 -5
- package/dist/http/server.js +3 -0
- package/dist/http/submit-ctx.d.ts +12 -0
- package/dist/http/submit-ctx.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lane-home-probe.d.ts +15 -0
- package/dist/lane-home-probe.js +4 -0
- package/dist/observability/corrupt-read-seat.d.ts +5 -9
- package/dist/observability/corrupt-read-seat.js +1 -2
- package/dist/observability/fail-open.d.ts +26 -10
- package/dist/observability/fail-open.js +26 -10
- package/dist/observability/run-terminal.d.ts +1 -1
- package/dist/observability/run-terminal.js +2 -2
- package/dist/parked-revive-surface.d.ts +6 -7
- package/dist/peer-inbound-face.d.ts +29 -34
- package/dist/peer-inbound-face.js +28 -63
- package/dist/plugins/approval-ask-store-file.d.ts +16 -2
- package/dist/plugins/approval-ask-store-file.js +21 -1
- package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
- package/dist/plugins/approval-ask-store-memory.js +11 -8
- package/dist/plugins/caching-session-store.d.ts +3 -3
- package/dist/plugins/caching-session-store.js +3 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -37
- package/dist/plugins/checkpoint-store-sql.js +123 -40
- package/dist/plugins/file-run-store.d.ts +13 -1
- package/dist/plugins/file-run-store.js +11 -1
- package/dist/plugins/fork-routing-session-store.d.ts +8 -1
- package/dist/plugins/fork-routing-session-store.js +6 -0
- package/dist/plugins/local-checkpoint-store.d.ts +17 -8
- package/dist/plugins/local-checkpoint-store.js +26 -7
- package/dist/plugins/local-session-store.d.ts +105 -34
- package/dist/plugins/local-session-store.js +168 -65
- package/dist/plugins/local-session-window.d.ts +4 -2
- package/dist/plugins/memory-run-store.d.ts +2 -0
- package/dist/plugins/memory-run-store.js +6 -0
- package/dist/plugins/one-time-migrations.d.ts +12 -12
- package/dist/plugins/one-time-migrations.js +13 -13
- package/dist/plugins/pg-session-storage.d.ts +1 -9
- package/dist/plugins/pg-session-storage.js +2 -19
- package/dist/plugins/remote-env-host.d.ts +8 -4
- package/dist/plugins/remote-env-host.js +7 -9
- package/dist/plugins/remote-shell.js +12 -2
- package/dist/plugins/run-store-sql.d.ts +4 -0
- package/dist/plugins/run-store-sql.js +3 -0
- package/dist/plugins/schema-consistency.d.ts +5 -1
- package/dist/plugins/session-fork.d.ts +1 -1
- package/dist/plugins/session-ownership.d.ts +9 -0
- package/dist/plugins/session-ownership.js +6 -0
- package/dist/plugins/session-store.d.ts +13 -16
- package/dist/plugins/session-store.js +9 -10
- package/dist/plugins/sql-json-column.d.ts +24 -2
- package/dist/plugins/sql-json-column.js +38 -3
- package/dist/plugins/store-backend.js +3 -2
- package/dist/plugins/tidb-session-store.d.ts +2 -14
- package/dist/plugins/tidb-session-store.js +2 -20
- package/dist/prompt-screen.d.ts +21 -0
- package/dist/prompt-screen.js +8 -0
- package/dist/rules-consent.d.ts +63 -51
- package/dist/rules-consent.js +6 -10
- package/dist/run-acceptance.d.ts +40 -10
- package/dist/run-acceptance.js +38 -5
- package/dist/run-local.js +2 -1
- package/dist/runs.js +5 -1
- package/dist/security.d.ts +10 -7
- package/dist/session-agent.d.ts +39 -0
- package/dist/session-agent.js +16 -0
- package/dist/session-error-wire.d.ts +25 -26
- package/dist/session-error-wire.js +4 -5
- package/dist/session-incarnation.d.ts +2 -2
- package/dist/session-notify-door.d.ts +2 -2
- package/dist/session-notify-door.js +5 -1
- package/dist/tool-approval.d.ts +3 -1
- package/dist/tool-approval.js +47 -18
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +1 -1
- package/dist/trace/ledger-sink.js +12 -4
- package/dist/trace/model-segment.d.ts +26 -6
- package/dist/trace/model-segment.js +58 -9
- package/dist/trace/project.d.ts +8 -2
- package/dist/trace/project.js +6 -4
- package/dist/trace/redact.d.ts +77 -2
- package/dist/trace/redact.js +234 -99
- package/dist/trace/self-minted-wire.d.ts +160 -9
- package/dist/trace/self-minted-wire.js +52 -3
- package/package.json +3 -3
- package/wire-changes/server@7.144.0.json +291 -0
- package/wire-changes/server@7.145.0.json +146 -0
- package/dist/session-records-skipped-notice.d.ts +0 -21
- package/dist/session-records-skipped-notice.js +0 -27
|
@@ -24,17 +24,16 @@
|
|
|
24
24
|
* 近似如实记:重解的列表是**部署缺省**那一份,读不回那一 run 的逐请求差异(`settings.webSearch` 后端、center overlay 场景)——
|
|
25
25
|
* 座只判 bundle 实例的成员性,继承面是 `[...web, ...scaffolds]`,那两项差异不改变谁在面内。
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `{
|
|
31
|
-
*
|
|
32
|
-
* (批准之后删了、同 id 被重新登记)⇒ 停驻否决 —— 两件都在挂点那一侧判,与这里无关。
|
|
27
|
+
* **委派树根见证([ref])与交座是两个独立判据**:Runner 之外复活的后台子代的完成 relay(会话通知门,`session-notify-door.ts`)要**委派树根的见证**:
|
|
28
|
+
* 根 id(行上本来就带 `rootSessionId`)+ 判断那一刻的根行住客 = 写口放行的那一位(赎回腿解析器那一读;不是那一位时写口整条拒,走不到这里)—— 见证恒带住客。
|
|
29
|
+
* 所以**每一行**都交出它({@link ScenarioContext} 的 `delegationRoot`):交座的一层行以 `{ sessionId: root, delegationRoot }` 重解,不交座的行(更深的行、根属主不是卡的租户的
|
|
30
|
+
* 一层行)以 `{ delegationRoot }` 重解 —— 只喂 relay,不挂任何会话级设施、不带 principal,交座判据(上面三条)一个字不动。行上没有根 ⇒ boot 期那只裸 Agent(没有根可交 ⇒ 不接 relay)。
|
|
31
|
+
* 复活的子代再要审批时的停驻挂点(`plugins/session-store.ts`)不读见证:引擎交它那一行自己的属主,挂点只比对(core 7.62.0 [ref];根见证在挂点的读法随 [ref] P-5 退役)。
|
|
33
32
|
*/
|
|
34
33
|
import { type BackgroundAgentRecord, type ToolSpec } from "@sema-agent/core";
|
|
35
34
|
import type { Scenario } from "./capabilities/scenarios.js";
|
|
36
35
|
import type { SessionFace } from "./session-incarnation.js";
|
|
37
|
-
/** 一次赎回用的面:驱动的 Agent 工具(
|
|
36
|
+
/** 一次赎回用的面:驱动的 Agent 工具(它的完成 relay 绑着行上的根)+ (交座时)与它同出一张列表的继承座。 */
|
|
38
37
|
export interface ParkedReviveSurface {
|
|
39
38
|
readonly reviveTool: ToolSpec;
|
|
40
39
|
/** 缺席 = 不交座(core fail-closed:复活的子代不继承任何场景层工具)。 */
|
|
@@ -23,33 +23,28 @@
|
|
|
23
23
|
* 成因会在重判时移动,列 / 决每次都读卡上**当下**的成因。
|
|
24
24
|
* · **一个调用方、一张卡、一个答案**:决断口只认列表会列出的卡(`decision === "held"` ∧ 可审成因);已批待投(`approved`)/ 已交接(`delivered`)的卡不列、不收。
|
|
25
25
|
*
|
|
26
|
-
* ##
|
|
27
|
-
* core
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `peer.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
26
|
+
* ## 枚举(core 7.62.0 `listPeerHeldQueues`)
|
|
27
|
+
* 列与关停都读 core 进程注册表的快照(`listPeerHeldQueues()`:每只队列的 scope / 打开它的会话 / 卡来自哪只箱 / 同一时刻的卡),本面不记任何表 ——
|
|
28
|
+
* 一张卡在不在、属于哪个 scope、哪只箱,全是那一读。决断口按行 id 的 (会话, 车道, seq) 回 `peerHeldQueueIfAny(调用方 scope, …)` 取同一只队列。
|
|
29
|
+
* 箱只有两种(会话自己的箱 / 它也排空的 principal 收件箱,core 的快照注逐字);读到第三种 ⇒ 那只队列的卡不列(没有车道就拼不出决断口 id)+ F 类
|
|
30
|
+
* `server.peer-inbound.queue-box-unknown` 留痕 —— 信仍被扣、按 core 的期限带回执过期(安全默认)。
|
|
31
|
+
*
|
|
32
|
+
* ## 到期
|
|
33
|
+
* 卡上的到期时刻是 core 的 `PeerHeldEntry.deadlineAt`(到期计时武装时在场:可审成因 ∧ 有限 `dialogExpiry` ∧ 卡仍 `held`;成因一动 core 重武装或清掉它)。
|
|
34
|
+
* 行的 `deadline` 就是它的原值,本仓不按 `heldAt + dialogExpiry` 自算(成因一动就是第二份、错的读法)。
|
|
35
35
|
*
|
|
36
36
|
* ## 关停
|
|
37
37
|
* CC `HBe` 在宿主:core 不调 `PeerHeldQueue.shutdown()`(全库零生产调用点)。{@link PeerInboundFace.shutdown} 在有序关停的审批收尾那一拍
|
|
38
|
-
* (`boot/shutdown.ts`,关池之前)
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
* 🚧 core 的结清回执是投出即返(排空汇点 `void routePeerDeliveryReceipt(…)`):`shutdown()` 兑现时回执可能还没落箱,关池在它之后 ⇒ 那一条回执可能丢
|
|
43
|
-
* (信本身已结清)。宿主没有可等的句柄;core 请托 = `shutdown()` 等回执落箱再兑现。
|
|
44
|
-
*/
|
|
38
|
+
* (`boot/shutdown.ts`,关池之前)对注册表里**每只**队列调它(进程的队列都是本进程的;不求 scope)—— 仍被扣的信按 core 的规则带 `expired` 回执结清
|
|
39
|
+
* (reason `shutdown`)。core 的 `shutdown()` 等它发出的每条回执落箱再兑现,没落的点名 seq 拒(卡照样全结);各队列互不等,一只拒不挡别的
|
|
40
|
+
* (`Promise.allSettled`),拒句进读数 `receiptFaults`,由关停链写一行 warn。
|
|
41
|
+
*/
|
|
45
42
|
import { type PeerHeldEntry, type PeerHeldSettleOutcome } from "@sema-agent/core";
|
|
46
43
|
import { type PeerInboundLane } from "./org-delivery-gate.js";
|
|
47
44
|
/**
|
|
48
|
-
*
|
|
49
|
-
* 列 / 决用调用方的 principal,关停用会话行属主;两者在属主围栏下同值(`resolve-spec.ts` 的 `fenceSessionOwner`),
|
|
50
|
-
* 无主存量会话被具名壳续聊的混属主形是既有残余(`boot/session-faces.ts` 信箱腿同一条登记)。
|
|
45
|
+
* 调用方 principal 的信箱 scope —— core 写侧 `taskScope = spec.principal ?? "default"` 的同一条读法(无主 ⇒ `"default"`)。列 / 决都按它找队列。
|
|
51
46
|
*/
|
|
52
|
-
export declare function peerBoxScope(
|
|
47
|
+
export declare function peerBoxScope(principal: string | null | undefined): string;
|
|
53
48
|
/** 决断口 id 的坐标(id 是它的唯一拼法,见 {@link peerInboundIdOf})。 */
|
|
54
49
|
export interface PeerInboundCoord {
|
|
55
50
|
readonly sessionId: string;
|
|
@@ -68,9 +63,9 @@ export declare function parsePeerInboundId(id: string): PeerInboundCoord | undef
|
|
|
68
63
|
* · `cause` —— core 的扣信成因(`PeerHeldEntry.cause`,`CrossSessionHoldCause` 闭集);
|
|
69
64
|
* · `content` / `fromName` —— 信的存储正文(`[summary] message`,core 的规范存储形)与发信方的显示名,**过凭据脱敏**(显示面;与停驻行的工具入参同一条);
|
|
70
65
|
* · `fromSession` / `fromScope` —— 发信会话 / 跨 principal 投递的发信 scope(core 的 typed 归属锚),在场才带;
|
|
71
|
-
* · `sentAt` / `heldAt` —— epoch ms
|
|
72
|
-
*
|
|
73
|
-
*
|
|
66
|
+
* · `sentAt` / `heldAt` —— epoch ms;
|
|
67
|
+
* · `deadline` —— core 卡上的到期时刻 `deadlineAt` 原值(绝对 epoch ms,与停驻行 `deadline` 同名同义;壳用 `deadline − serverNowMs` 渲倒计时),
|
|
68
|
+
* 到期计时没武装(`dialogExpiry` 为 `null`)⇒ 缺席。
|
|
74
69
|
*/
|
|
75
70
|
export interface PeerInboundPendingRow {
|
|
76
71
|
readonly kind: "peer_inbound";
|
|
@@ -81,6 +76,7 @@ export interface PeerInboundPendingRow {
|
|
|
81
76
|
readonly content: string;
|
|
82
77
|
readonly sentAt: number;
|
|
83
78
|
readonly heldAt: number;
|
|
79
|
+
readonly deadline?: number;
|
|
84
80
|
readonly fromSession?: string;
|
|
85
81
|
readonly fromName?: string;
|
|
86
82
|
readonly fromScope?: string;
|
|
@@ -103,25 +99,24 @@ export type PeerInboundDecideResult = {
|
|
|
103
99
|
readonly kind: "unrecorded";
|
|
104
100
|
readonly error: unknown;
|
|
105
101
|
};
|
|
106
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* 关停读数:`queues` = 关了几只队列(注册表里的每一只,含空的);`cards` = 关停那一刻快照里的卡数;`receiptFaults` = 各只拒了的队列的拒句
|
|
104
|
+
* (core 点名回执没落箱的 seq —— 卡已结清,只是发信方可能没收到那条 `expired`)。
|
|
105
|
+
*/
|
|
107
106
|
export interface PeerInboundShutdownReading {
|
|
108
107
|
readonly queues: number;
|
|
109
|
-
readonly
|
|
108
|
+
readonly cards: number;
|
|
109
|
+
readonly receiptFaults: readonly string[];
|
|
110
110
|
}
|
|
111
111
|
export interface PeerInboundFace {
|
|
112
|
-
/**
|
|
113
|
-
observe(notice: {
|
|
114
|
-
readonly code: string;
|
|
115
|
-
readonly detail?: Record<string, unknown>;
|
|
116
|
-
}): void;
|
|
117
|
-
/** `scope` 里此刻等人审的信({@link awaitsReview} 的卡),按 `heldAt` 升序(同刻按 id)。 */
|
|
112
|
+
/** `scope` 里此刻等人审的信({@link awaitsReview} 的卡;读 core 的队列枚举),按 `heldAt` 升序(同刻按 id)。 */
|
|
118
113
|
list(scope: string): PeerInboundPendingRow[];
|
|
119
114
|
/** 决断转发:只认 `scope` 里此刻等人审的那张卡(与 {@link list} 同一只判据);`settle` 抛(店拒了 CAS)⇒ `unrecorded`。 */
|
|
120
115
|
decide(scope: string, coord: PeerInboundCoord, decision: PeerInboundDecision): Promise<PeerInboundDecideResult>;
|
|
121
|
-
/**
|
|
122
|
-
shutdown(
|
|
116
|
+
/** 有序关停:core 注册表里每只队列 `shutdown()`(core 的规则:仍 held 的卡带 `expired` 回执结清,reason `shutdown`;等回执落箱)。永不抛。 */
|
|
117
|
+
shutdown(): Promise<PeerInboundShutdownReading>;
|
|
123
118
|
}
|
|
124
119
|
export declare function createPeerInboundFace(): PeerInboundFace;
|
|
125
|
-
/** 进程单例 ——
|
|
120
|
+
/** 进程单例 —— 决断尾(一张卡一次只过一只决断)是进程级的;审批路由与关停链读的是同一只。 */
|
|
126
121
|
export declare const defaultPeerInboundFace: PeerInboundFace;
|
|
127
122
|
//# sourceMappingURL=peer-inbound-face.d.ts.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { PEER_HELD_REVIEW_CAUSES, isPeerSessionId, peerHeldQueueIfAny, } from "@sema-agent/core";
|
|
1
|
+
import { PEER_HELD_REVIEW_CAUSES, isPeerSessionId, listPeerHeldQueues, peerHeldQueueIfAny, } from "@sema-agent/core";
|
|
2
2
|
import { PEER_INBOUND_LANES, boxOfLane, laneOfBox } from "./org-delivery-gate.js";
|
|
3
3
|
import { PEER_INBOUND_ID_PREFIX } from "./approval-queue-kind.js";
|
|
4
4
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
5
5
|
import { seatText } from "./trace/exit-seat.js";
|
|
6
|
-
export function peerBoxScope(
|
|
7
|
-
return
|
|
6
|
+
export function peerBoxScope(principal) {
|
|
7
|
+
return principal ?? "default";
|
|
8
8
|
}
|
|
9
9
|
export function peerInboundIdOf(c) {
|
|
10
10
|
return `${PEER_INBOUND_ID_PREFIX}${c.sessionId}:${c.lane}:${c.seq}`;
|
|
@@ -39,23 +39,18 @@ function rowOf(sessionId, lane, card) {
|
|
|
39
39
|
content: seatText("peer-inbound-card-text", card.content),
|
|
40
40
|
sentAt: card.sentAt,
|
|
41
41
|
heldAt: card.heldAt,
|
|
42
|
+
...(card.deadlineAt !== undefined ? { deadline: card.deadlineAt } : {}),
|
|
42
43
|
...(fromSession !== undefined ? { fromSession } : {}),
|
|
43
44
|
...(card.from !== undefined ? { fromName: seatText("peer-inbound-card-text", card.from) } : {}),
|
|
44
45
|
...(fromScope !== undefined ? { fromScope } : {}),
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
|
-
function
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (lane !== undefined)
|
|
53
|
-
return { sessionId, lane };
|
|
54
|
-
}
|
|
55
|
-
recordFailOpen("server.peer-inbound.notice-coordinates-unreadable", `code=${code}`);
|
|
56
|
-
return undefined;
|
|
48
|
+
function laneOfQueue(q) {
|
|
49
|
+
const lane = laneOfBox(q.box, q.sessionId);
|
|
50
|
+
if (lane === undefined)
|
|
51
|
+
recordFailOpen("server.peer-inbound.queue-box-unknown", `cards=${q.size}`);
|
|
52
|
+
return lane;
|
|
57
53
|
}
|
|
58
|
-
const indexKey = (e) => JSON.stringify([e.lane, e.sessionId.toLowerCase()]);
|
|
59
54
|
function awaitsReview(card) {
|
|
60
55
|
return card.decision === "held" && isReviewCause(card.cause);
|
|
61
56
|
}
|
|
@@ -66,7 +61,6 @@ function queueOf(scope, e) {
|
|
|
66
61
|
return peerHeldQueueIfAny(scope, e.sessionId, boxOfLane(e.lane));
|
|
67
62
|
}
|
|
68
63
|
export function createPeerInboundFace() {
|
|
69
|
-
const index = new Map();
|
|
70
64
|
const tails = new WeakMap();
|
|
71
65
|
const serially = (q, seq, step) => {
|
|
72
66
|
const perQueue = tails.get(q) ?? new Map();
|
|
@@ -80,37 +74,19 @@ export function createPeerInboundFace() {
|
|
|
80
74
|
});
|
|
81
75
|
};
|
|
82
76
|
return {
|
|
83
|
-
observe(notice) {
|
|
84
|
-
switch (notice.code) {
|
|
85
|
-
case "peer.inbound_disposition": {
|
|
86
|
-
if (notice.detail?.disposition !== "held" || !isReviewCause(notice.detail.cause))
|
|
87
|
-
return;
|
|
88
|
-
const e = coordinatesOf(notice.code, notice.detail);
|
|
89
|
-
if (e !== undefined && !index.has(indexKey(e)))
|
|
90
|
-
index.set(indexKey(e), e);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
case "peer.held_settled": {
|
|
94
|
-
if (notice.detail?.heldCount !== 0)
|
|
95
|
-
return;
|
|
96
|
-
const e = coordinatesOf(notice.code, notice.detail);
|
|
97
|
-
if (e !== undefined)
|
|
98
|
-
index.delete(indexKey(e));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
default:
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
77
|
list(scope) {
|
|
106
78
|
const rows = [];
|
|
107
|
-
for (const
|
|
108
|
-
|
|
109
|
-
|
|
79
|
+
for (const q of listPeerHeldQueues()) {
|
|
80
|
+
if (q.scope !== scope)
|
|
81
|
+
continue;
|
|
82
|
+
const reviewable = q.entries.filter(awaitsReview);
|
|
83
|
+
if (reviewable.length === 0)
|
|
84
|
+
continue;
|
|
85
|
+
const lane = laneOfQueue(q);
|
|
86
|
+
if (lane === undefined)
|
|
110
87
|
continue;
|
|
111
|
-
for (const card of
|
|
112
|
-
|
|
113
|
-
rows.push(rowOf(e.sessionId, e.lane, card));
|
|
88
|
+
for (const card of reviewable)
|
|
89
|
+
rows.push(rowOf(q.sessionId, lane, card));
|
|
114
90
|
}
|
|
115
91
|
return rows.sort((a, b) => a.heldAt - b.heldAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
116
92
|
},
|
|
@@ -131,26 +107,15 @@ export function createPeerInboundFace() {
|
|
|
131
107
|
return outcome === "gone" ? { kind: "not_found" } : { kind: "settled", outcome };
|
|
132
108
|
});
|
|
133
109
|
},
|
|
134
|
-
async shutdown(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (scope === undefined) {
|
|
144
|
-
scopeUnresolved += 1;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const q = queueOf(scope, e);
|
|
148
|
-
if (q === undefined)
|
|
149
|
-
continue;
|
|
150
|
-
await q.shutdown();
|
|
151
|
-
queues += 1;
|
|
152
|
-
}
|
|
153
|
-
return { queues, scopeUnresolved };
|
|
110
|
+
async shutdown() {
|
|
111
|
+
const snapshots = listPeerHeldQueues();
|
|
112
|
+
const queues = snapshots.flatMap((s) => {
|
|
113
|
+
const q = peerHeldQueueIfAny(s.scope, s.sessionId, s.box);
|
|
114
|
+
return q === undefined ? [] : [q];
|
|
115
|
+
});
|
|
116
|
+
const settled = await Promise.allSettled(queues.map((q) => q.shutdown()));
|
|
117
|
+
const receiptFaults = settled.flatMap((r) => (r.status === "rejected" ? [r.reason instanceof Error ? r.reason.message : String(r.reason)] : []));
|
|
118
|
+
return { queues: queues.length, cards: snapshots.reduce((n, s) => n + s.size, 0), receiptFaults };
|
|
154
119
|
},
|
|
155
120
|
};
|
|
156
121
|
}
|
|
@@ -130,6 +130,11 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
|
|
|
130
130
|
private journalV1Records;
|
|
131
131
|
/** 盘上还有 v1 信封记录(随 M34):重放到 v1 记录时置位,任何一次压实(整本重写成 v2)清位 —— 已被压实过的账本不再为升级重写第二次。 */
|
|
132
132
|
private journalV1OnDisk;
|
|
133
|
+
/**
|
|
134
|
+
* 盘上还有删行记录({@link ROW_REMOVAL_OPS}):写下一条 / 重放到一条时置位,任何一次压实(整本重写成快照)清位 —— 与 {@link journalV1OnDisk} 同形。
|
|
135
|
+
* 置位期间压实到期(顶注「压实」段的起因 ②):同一拍压实;压实失败 ⇒ 位留着,下一次写(或下一次起服)再试。
|
|
136
|
+
*/
|
|
137
|
+
private removalRecordsOnDisk;
|
|
133
138
|
/** 本次启动隔离出去的原件路径(无坏字节 ⇒ `undefined`)。 */
|
|
134
139
|
readonly corruptQuarantinePath: string | undefined;
|
|
135
140
|
/**
|
|
@@ -159,11 +164,16 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
|
|
|
159
164
|
* 代价如实:不可编码的值(BigInt / 循环引用)在本形上**抛**,而 InMemory 形收得下 —— 那与 SQL 孪生
|
|
160
165
|
* 同向(它也存不下),是**介质事实**,写进顶注的边界段。
|
|
161
166
|
*
|
|
162
|
-
* 🔴 **压实失败不许落到调用方头上**(替补对抗复审 F3 [medium],实测后修):走到
|
|
167
|
+
* 🔴 **压实失败不许落到调用方头上**(替补对抗复审 F3 [medium],实测后修):走到 {@link compactOrRecord} 时这
|
|
163
168
|
* 一次写**已经 fsync 落盘、也已经翻进内存**,把一次纯管家动作的 I/O 错误抛给调用方 = 把一次已生效的
|
|
164
169
|
* 人类批准报成失败(客户端重试会拿到 `ask_not_pending`)。压实是 F 类兜底:失败留痕、下一次写再试。
|
|
170
|
+
* 两个起因同一条:删行记录引起的压实失败,位({@link removalRecordsOnDisk})留着,下一次**任何**写(与下一次起服)再试 ——
|
|
171
|
+
* 不是等下一次真删了行的调用。三只删行动词的内核体全是同步前缀,所以走到这里时被删的行已经不在内存里,快照里没有它们。
|
|
165
172
|
*/
|
|
166
173
|
private write;
|
|
174
|
+
/** 压实到期就压实;失败 ⇒ 登记过的 F 类兜底(计数 + 留痕),不上抛 —— `write()` 尾段与起服各调一次,同一句话。 */
|
|
175
|
+
private compactOrRecord;
|
|
176
|
+
/** 压实是否到期(顶注「压实」段两个起因)。到期 ⇒ 压实;失败原样抛给 {@link compactOrRecord}。 */
|
|
167
177
|
private maybeCompact;
|
|
168
178
|
/** 整本重写成行快照 + 原子替换。失败 ⇒ 原账本原样留着、`records` 不动(下一次写再试),日志由
|
|
169
179
|
* core 的 [ref] 惰性重开接住 —— 一次瞬时 I/O 错误绝不把这条日志变成砖。 */
|
|
@@ -193,8 +203,12 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
|
|
|
193
203
|
abortBatch(batchId: string): Promise<string[]>;
|
|
194
204
|
deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
|
|
195
205
|
resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
206
|
+
/** 删一个 run 名下的全部问行与批行。与两只留存动词同律:内核的纯读选择为空 ⇒ 不落账本、不压实;否则「先落账、再翻内存」并同拍压实(顶注「压实」段起因 ②)。 */
|
|
196
207
|
deleteByTask(taskId: string): Promise<void>;
|
|
197
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* 留存律的问行半场(谓词在内核,见接口注)。空的一次(内核的纯读选择为空)不落账本、不碰内存、不压实;否则照常「先落账、再翻内存」,
|
|
210
|
+
* 并在 `write()` 尾段同一拍压实(顶注「压实」段起因 ②)—— 选择非空 ⇒ 内核同步前缀里必删 ≥ 1 行,被删行的字节不留在盘上。
|
|
211
|
+
*/
|
|
198
212
|
reapSettledAsks(cutoffMs: number, limit: number, keepAnchors: ReadonlySet<string>): Promise<number>;
|
|
199
213
|
/** 留存律的批行半场(同上)。 */
|
|
200
214
|
reapEmptyBatches(cutoffMs: number, limit: number): Promise<number>;
|
|
@@ -28,6 +28,7 @@ const JOURNAL_OPS = [
|
|
|
28
28
|
];
|
|
29
29
|
const JOURNAL_OP_WORDS = new Set(JOURNAL_OPS);
|
|
30
30
|
export const JOURNAL_OP_TABLE_GATE = true;
|
|
31
|
+
const ROW_REMOVAL_OPS = new Set(["reapSettledAsks", "reapEmptyBatches", "deleteByTask"]);
|
|
31
32
|
const JOURNAL_V1_CLAIM_INTENTS = ["expire", "cancel"];
|
|
32
33
|
const JOURNAL_V1_CLAIM_INTENT_WORDS = new Set(JOURNAL_V1_CLAIM_INTENTS);
|
|
33
34
|
const JOURNAL_ENVELOPE = 2;
|
|
@@ -94,6 +95,7 @@ export class FileApprovalAskStore {
|
|
|
94
95
|
corruptLineCount = 0;
|
|
95
96
|
journalV1Records = 0;
|
|
96
97
|
journalV1OnDisk = false;
|
|
98
|
+
removalRecordsOnDisk = false;
|
|
97
99
|
corruptQuarantinePath;
|
|
98
100
|
constructor(root) {
|
|
99
101
|
const dir = join(root, APPROVAL_ASK_DIR);
|
|
@@ -117,6 +119,9 @@ export class FileApprovalAskStore {
|
|
|
117
119
|
`and rewrote the live journal from the replayed state. A dropped record is a LOST approval-ask transition: inspect the quarantined file.`);
|
|
118
120
|
this.compact();
|
|
119
121
|
}
|
|
122
|
+
else if (this.removalRecordsOnDisk) {
|
|
123
|
+
this.compactOrRecord();
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
clockRead() {
|
|
122
127
|
const pinned = this.pinnedNow;
|
|
@@ -136,6 +141,8 @@ export class FileApprovalAskStore {
|
|
|
136
141
|
const onDisk = { v: JOURNAL_ENVELOPE, k: "op", t, ...callToDisk(encoded) };
|
|
137
142
|
this.logFor().append(onDisk, true);
|
|
138
143
|
this.records += 1;
|
|
144
|
+
if (ROW_REMOVAL_OPS.has(call.op))
|
|
145
|
+
this.removalRecordsOnDisk = true;
|
|
139
146
|
this.pinnedNow = t;
|
|
140
147
|
let out;
|
|
141
148
|
try {
|
|
@@ -144,15 +151,22 @@ export class FileApprovalAskStore {
|
|
|
144
151
|
finally {
|
|
145
152
|
this.pinnedNow = null;
|
|
146
153
|
}
|
|
154
|
+
this.compactOrRecord();
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
compactOrRecord() {
|
|
147
158
|
try {
|
|
148
159
|
this.maybeCompact();
|
|
149
160
|
}
|
|
150
161
|
catch (e) {
|
|
151
162
|
recordFailOpen("server.approval-ask-journal.compaction-failed", `path=${this.path} err=${e instanceof Error ? e.message : String(e)}`);
|
|
152
163
|
}
|
|
153
|
-
return out;
|
|
154
164
|
}
|
|
155
165
|
maybeCompact() {
|
|
166
|
+
if (this.removalRecordsOnDisk) {
|
|
167
|
+
this.compact();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
156
170
|
if (this.records < ASK_JOURNAL_COMPACT_MIN_RECORDS)
|
|
157
171
|
return;
|
|
158
172
|
const snapshot = this.core.exportRows();
|
|
@@ -168,6 +182,7 @@ export class FileApprovalAskStore {
|
|
|
168
182
|
this.log?.closeForSwap();
|
|
169
183
|
atomicWriteFile(this.tmpDir, this.path, lines.join(""));
|
|
170
184
|
this.journalV1OnDisk = false;
|
|
185
|
+
this.removalRecordsOnDisk = false;
|
|
171
186
|
this.records = lines.length;
|
|
172
187
|
}
|
|
173
188
|
replayRecord(disk) {
|
|
@@ -203,6 +218,8 @@ export class FileApprovalAskStore {
|
|
|
203
218
|
refuse("unknown record kind");
|
|
204
219
|
if (typeof probe.op !== "string" || !JOURNAL_OP_WORDS.has(probe.op))
|
|
205
220
|
refuse("verb outside this build's closed set (a DOWNGRADE: a newer server wrote it)");
|
|
221
|
+
if (typeof probe.op === "string" && ROW_REMOVAL_OPS.has(probe.op))
|
|
222
|
+
this.removalRecordsOnDisk = true;
|
|
206
223
|
if (typeof probe.t !== "number" || !Number.isFinite(probe.t))
|
|
207
224
|
refuse("call record without a clock reading");
|
|
208
225
|
if (probe.op === "claimTerminal" && !JOURNAL_V1_CLAIM_INTENT_WORDS.has(String(raw.intent)))
|
|
@@ -301,6 +318,9 @@ export class FileApprovalAskStore {
|
|
|
301
318
|
return this.write({ op: "resolveProvisional", askId, from, to, patch }, (c) => this.core.resolveProvisional(c.askId, c.from, c.to, c.patch));
|
|
302
319
|
}
|
|
303
320
|
async deleteByTask(taskId) {
|
|
321
|
+
const plan = this.core.planDeleteByTask(taskId);
|
|
322
|
+
if (plan.askIds.length === 0 && plan.batchIds.length === 0)
|
|
323
|
+
return;
|
|
304
324
|
return this.write({ op: "deleteByTask", taskId }, (c) => this.core.deleteByTask(c.taskId));
|
|
305
325
|
}
|
|
306
326
|
async reapSettledAsks(cutoffMs, limit, keepAnchors) {
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* · {@link InMemoryApprovalAskStore.exportRows} / {@link InMemoryApprovalAskStore.importRows} —— 压实用的
|
|
17
17
|
* 整本快照口(纯搬行,不经任何谓词)。压实要把「一串调用」折成「一串终局行」,没有这一对就只能在
|
|
18
18
|
* File 形里另写一套「哪些行还算数」的判据 —— 那才是第二份手抄。
|
|
19
|
-
* · {@link InMemoryApprovalAskStore.planReapSettledAsks} / {@link InMemoryApprovalAskStore.planReapEmptyBatches}
|
|
20
|
-
*
|
|
19
|
+
* · {@link InMemoryApprovalAskStore.planReapSettledAsks} / {@link InMemoryApprovalAskStore.planReapEmptyBatches} /
|
|
20
|
+
* {@link InMemoryApprovalAskStore.planDeleteByTask} —— 三只删行动词的**纯读选择**(删哪些行)。File 形据它判「这一次有没有东西可删」,空的一次不落账本;
|
|
21
|
+
* 删除本身仍是接口动词,与选择共用这一只。
|
|
21
22
|
* · {@link InMemoryApprovalAskStore.peekAsk} —— 一行的**同步**点读(纯读,零判定)。File 形在写口的同步前缀里据它判「这次决议在不在
|
|
22
23
|
* 属主域内」(出域的一次不落账本)、重放时据它喂行自己的属主([ref],理由在 File 形的 `decideAsk`)。
|
|
23
24
|
*
|
|
@@ -118,6 +119,11 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
|
|
|
118
119
|
getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
|
|
119
120
|
resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
|
|
120
121
|
deleteByTask(taskId: string): Promise<void>;
|
|
122
|
+
/** {@link deleteByTask} 会删哪些行(纯读,零写;选择与删除共用这一只,没有第二份谓词)。 */
|
|
123
|
+
planDeleteByTask(taskId: string): {
|
|
124
|
+
readonly askIds: readonly string[];
|
|
125
|
+
readonly batchIds: readonly string[];
|
|
126
|
+
};
|
|
121
127
|
getBatch(batchId: string): Promise<BatchRow | null>;
|
|
122
128
|
/**
|
|
123
129
|
* 留存律的问行半场(谓词见接口注,与 SQL twin 的 `DELETE … LIMIT` 同答)。方法体全同步(零 `await`)⇒ File 形把它当一次落账的调用
|
|
@@ -365,14 +365,17 @@ export class InMemoryApprovalAskStore {
|
|
|
365
365
|
return true;
|
|
366
366
|
}
|
|
367
367
|
async deleteByTask(taskId) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
368
|
+
const plan = this.planDeleteByTask(taskId);
|
|
369
|
+
for (const id of plan.askIds)
|
|
370
|
+
this.asks.delete(id);
|
|
371
|
+
for (const id of plan.batchIds)
|
|
372
|
+
this.batches.delete(id);
|
|
373
|
+
}
|
|
374
|
+
planDeleteByTask(taskId) {
|
|
375
|
+
return {
|
|
376
|
+
askIds: [...this.asks.values()].filter((r) => r.taskId === taskId).map((r) => r.askId),
|
|
377
|
+
batchIds: [...this.batches.values()].filter((b) => b.taskId === taskId).map((b) => b.batchId),
|
|
378
|
+
};
|
|
376
379
|
}
|
|
377
380
|
async getBatch(batchId) {
|
|
378
381
|
const b = this.batches.get(batchId);
|
|
@@ -4,7 +4,6 @@ import type { StagingHandle } from "../session-sync-kernel.js";
|
|
|
4
4
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
5
5
|
import type { InstructionSourceStore } from "../instruction-source-notice.js";
|
|
6
6
|
import type { SessionWorkspaceStore } from "../session-workspace.js";
|
|
7
|
-
import type { OwnerlessClaim } from "../delegated-session-owner.js";
|
|
8
7
|
export interface CachingOptions {
|
|
9
8
|
/** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */
|
|
10
9
|
ttlSec: number;
|
|
@@ -57,8 +56,6 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
57
56
|
* k8s 工作区空间 / SSE 探针 / resume-at 读的都是本类。 */
|
|
58
57
|
readonly headOf: SessionHeadReader["headOf"];
|
|
59
58
|
readonly register?: (sessionId: string, owner: string | null) => Promise<void>;
|
|
60
|
-
/** [ref] 委派继承律的认领口,纯转发(写的是行上的属主那一格 —— 只 CAS 子行自己,不碰暖缓存里的会话;停驻挂点在默认部署上读的就是本类 —— 与 `placementOf` 同转发同缺:内店记录委派树 ⇒ 两只都在)。 */
|
|
61
|
-
readonly claimOwnerless?: OwnerlessClaim;
|
|
62
59
|
/** Delegated SessionRepo seam (§0.5) — list/fork/delete operate on the DURABLE tables directly (they bypass the
|
|
63
60
|
* warm cache by design; a fork/delete must read/write durable state, not a possibly-stale cached view). */
|
|
64
61
|
readonly listSessions?: (opts: {
|
|
@@ -119,6 +116,9 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
119
116
|
* 裸店、不经本包装,但 security.ts 的 OwnerAwareSessionStore 契约列了它 —— 经 sessionStorage 消费的
|
|
120
117
|
* 未来调用方不该静默 no-op,与 beginImportStaging 同批补齐(同一类白名单缺口)。 */
|
|
121
118
|
readonly sweepStagingSessions?: () => Promise<number>;
|
|
119
|
+
/** 带戳存档的早拒口(local 店独有;`OwnerAwareSessionStore.assertArchiveAnonymous`),读持久存档头、不经暖缓存。
|
|
120
|
+
* 🔴 必须转发(与 staging 族同一条教训):默认部署把真店包进本类,会话同步第一阶段的路由读到的是包装层 —— 不转发 = 这一口在默认部署上静默不发(第一阶段仍答通用冲突),没有任何一处报错。 */
|
|
121
|
+
readonly assertArchiveAnonymous?: (sessionId: string) => Promise<void>;
|
|
122
122
|
/** 2c session-sync (§7/§8) — delegated IDEMPOTENT replace; like importEntries it writes durable state under
|
|
123
123
|
* `sessionId`, so a stale warm handle for that id is evicted on success (next acquire re-wakes from the new log). */
|
|
124
124
|
readonly replaceEntries?: (sessionId: string, owner: string | null, entries: SessionTreeEntry[]) => Promise<void>;
|
|
@@ -9,7 +9,6 @@ export class CachingSessionStore {
|
|
|
9
9
|
onStaleHit;
|
|
10
10
|
headOf;
|
|
11
11
|
register;
|
|
12
|
-
claimOwnerless;
|
|
13
12
|
listSessions;
|
|
14
13
|
fork;
|
|
15
14
|
forkTo;
|
|
@@ -30,6 +29,7 @@ export class CachingSessionStore {
|
|
|
30
29
|
importEntries;
|
|
31
30
|
beginImportStaging;
|
|
32
31
|
sweepStagingSessions;
|
|
32
|
+
assertArchiveAnonymous;
|
|
33
33
|
replaceEntries;
|
|
34
34
|
retention;
|
|
35
35
|
placements;
|
|
@@ -56,8 +56,6 @@ export class CachingSessionStore {
|
|
|
56
56
|
const ownerAware = inner;
|
|
57
57
|
if (ownerAware.register)
|
|
58
58
|
this.register = ownerAware.register.bind(inner);
|
|
59
|
-
if (ownerAware.claimOwnerless)
|
|
60
|
-
this.claimOwnerless = ownerAware.claimOwnerless.bind(inner);
|
|
61
59
|
if (ownerAware.listSessions)
|
|
62
60
|
this.listSessions = ownerAware.listSessions.bind(inner);
|
|
63
61
|
if (ownerAware.sessionSummaryOf)
|
|
@@ -88,6 +86,8 @@ export class CachingSessionStore {
|
|
|
88
86
|
this.listEntryIds = ownerAware.listEntryIds.bind(inner);
|
|
89
87
|
if (ownerAware.exportEntriesStream)
|
|
90
88
|
this.exportEntriesStream = ownerAware.exportEntriesStream.bind(inner);
|
|
89
|
+
if (ownerAware.assertArchiveAnonymous)
|
|
90
|
+
this.assertArchiveAnonymous = ownerAware.assertArchiveAnonymous.bind(inner);
|
|
91
91
|
this.forget = (sessionId) => {
|
|
92
92
|
this.evict(sessionId);
|
|
93
93
|
return inner.forget?.(sessionId);
|