@sema-agent/server 7.112.0 → 7.114.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 +95 -0
- package/USAGE.md +4 -4
- package/dist/approval-ask-audit-store.d.ts +3 -4
- package/dist/approval-ask-audit-store.js +3 -3
- package/dist/approval-card.d.ts +4 -4
- package/dist/boot/coordinators.js +1 -1
- package/dist/boot/memory-boundary.d.ts +2 -1
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/resolve-spec.js +2 -2
- package/dist/boot/stores.js +4 -6
- package/dist/boot/workflow-orchestration.js +4 -4
- package/dist/config-catalog.js +6 -5
- package/dist/config-invariants.d.ts +27 -0
- package/dist/config-invariants.js +54 -39
- package/dist/config-types.d.ts +5 -11
- package/dist/config-types.js +0 -1
- package/dist/config.d.ts +0 -16
- package/dist/config.js +44 -69
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +5 -1
- package/dist/execution-lane-caps.d.ts +54 -30
- package/dist/execution-lane-caps.js +48 -20
- package/dist/hooks/hook-runner.d.ts +87 -7
- package/dist/hooks/hook-runner.js +113 -20
- package/dist/host-decision.d.ts +5 -3
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -4
- package/dist/http/request-guards.js +2 -2
- package/dist/http/resume-legs.d.ts +9 -10
- package/dist/http/resume-legs.js +64 -18
- package/dist/http/route-ctx.d.ts +38 -8
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/routes/background.js +2 -2
- package/dist/http/routes/capabilities.js +2 -2
- package/dist/http/routes/mcp-reconnect.js +2 -2
- package/dist/http/routes/notify-wake.js +3 -3
- package/dist/http/routes/runs.js +19 -13
- package/dist/http/routes/session-sync.js +6 -6
- package/dist/http/routes/sessions.js +20 -20
- package/dist/http/routes/tasks.js +1 -0
- package/dist/http/send.d.ts +1 -1
- package/dist/http/send.js +1 -1
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/local-data-root.d.ts +14 -0
- package/dist/local-data-root.js +5 -0
- package/dist/observability/corrupt-read-seat.d.ts +46 -66
- package/dist/observability/corrupt-read-seat.js +79 -63
- package/dist/observability/fail-open.d.ts +28 -0
- package/dist/observability/fail-open.js +28 -0
- package/dist/observability/run-terminal.js +39 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +1 -5
- package/dist/orchestration/workflow-agent-session-index.js +2 -2
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -2
- package/dist/plugins/approval-ask-store-file.d.ts +1 -4
- package/dist/plugins/approval-ask-store-file.js +2 -2
- package/dist/plugins/file-resume-anchor-store.d.ts +1 -6
- package/dist/plugins/file-resume-anchor-store.js +2 -4
- package/dist/plugins/file-run-store.d.ts +0 -4
- package/dist/plugins/file-run-store.js +1 -3
- package/dist/plugins/remote-env-host.d.ts +0 -35
- package/dist/plugins/remote-env-host.js +0 -8
- package/dist/plugins/remote-env-local-docker.d.ts +5 -13
- package/dist/plugins/remote-env-local-docker.js +2 -4
- package/dist/plugins/send-file-ledger.d.ts +1 -5
- package/dist/plugins/send-file-ledger.js +2 -2
- package/dist/plugins/store-backend.js +8 -9
- package/dist/plugins/user-input-ledger-file.d.ts +1 -5
- package/dist/plugins/user-input-ledger-file.js +2 -2
- package/dist/run-local.d.ts +1 -1
- package/dist/run-local.js +2 -2
- package/dist/runs.js +1 -0
- package/dist/spec-fields.js +0 -1
- package/dist/store-posture.d.ts +7 -7
- package/dist/store-posture.js +0 -1
- package/dist/task-settings.d.ts +13 -0
- package/dist/task-settings.js +3 -0
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/project.js +2 -3
- package/dist/trace/redact.d.ts +18 -0
- package/dist/trace/redact.js +4 -0
- package/dist/unattended-permission-mode.d.ts +9 -2
- package/dist/unattended-permission-mode.js +5 -1
- package/dist/user-input-ledger.d.ts +24 -5
- package/dist/user-input-ledger.js +46 -11
- package/package.json +4 -3
- package/scripts/retired-mode-seed-drain-local.mjs +317 -0
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* 修复腿(集成沙箱修复 `mkRepair`、模块门修复 `leader.ts` `runModuleGate`、单代理修复环 `repair-wire.ts` `attachRepairLoopDeps`)、run-local
|
|
7
7
|
* (`src/run-local.ts`,一次性 CLI)与这些腿的 durable 续跑重挂(`CheckpointCtx.unattendedLeg`)—— 经 {@link withUnattendedLeg} 这**一只**
|
|
8
8
|
* 合并口铸两键:`permissionMode: "default"`(显式:续跑时盖过 checkpoint 记下的旧模式)与 `onAsk` = {@link createUnattendedOnAsk}。
|
|
9
|
-
* 那只 onAsk 读 `AskRequest.classes` **整体**:集非空且每一类都在 {@link UNATTENDED_ANSWERED_CLASSES}(= `{ordinary}`,常量,不是旋钮)⇒ 答 yes
|
|
9
|
+
* 那只 onAsk 读 `AskRequest.classes` **整体**:集非空且每一类都在 {@link UNATTENDED_ANSWERED_CLASSES}(= `{ordinary}`,常量,不是旋钮)⇒ 答 yes
|
|
10
|
+
* —— **以策略的名义**(S-953 / core 7.39.0 #1265:`{allow:true, settledBy:"policy", approver:"unattended_class_answer"}` ⇒ core 结算 `policy_allowed` /
|
|
11
|
+
* `who.party:"policy"`;修前是裸 `true`,core 把机器的放行记成**人**批 `human_allowed` / `person`);
|
|
10
12
|
* 否则不答 —— 本腿**有席**(run-local 的 `createLocalApprover`:有 TTY 问人、无 TTY 当场拒;续跑腿的协调器席)⇒ 交给席;**无席** ⇒ 答
|
|
11
13
|
* `"unavailable"` = 交回 core 的缺席路(本腿武装了 durable park ⇒ park,等人经 `/decide` 答;否则 headless 拒 —— fail-closed)。
|
|
12
14
|
*
|
|
@@ -47,12 +49,17 @@ import type { AskClass, AskRequest, OnAsk, TaskSpec } from "@sema-agent/core";
|
|
|
47
49
|
* 不变量(测试钉):答集 ⊆ core `RELEASABLE_ASK_CLASSES`,且与带 `requiresRealApproval` 的类(core `RRA_EXPLAINERS` ∪ `policy_rra`)不交;`mode` 不在其中。
|
|
48
50
|
*/
|
|
49
51
|
export declare const UNATTENDED_ANSWERED_CLASSES: ReadonlySet<AskClass>;
|
|
52
|
+
/**
|
|
53
|
+
* S-953(core 7.39.0 #1265):按类作答层这只席的**名字** —— 本仓自己的闭词(core 只把它转录到结算记录的 `who.approver`,不认证、不回读;
|
|
54
|
+
* `AskOutcome.approver` 契约逐字「a closed word the host owns, never a person」)。审计读者按它认出「这一次放行是无人值守腿按类答的」。
|
|
55
|
+
*/
|
|
56
|
+
export declare const UNATTENDED_POLICY_APPROVER = "unattended_class_answer";
|
|
50
57
|
/** onAsk 的函数臂(`"allow"` / `"deny"` 两个字面臂是姿态设置,不是席)—— 本腿的活审批席与本层自己都是这一形。 */
|
|
51
58
|
export type UnattendedSeat = Extract<OnAsk, (req: AskRequest, signal?: AbortSignal) => unknown>;
|
|
52
59
|
/** 这一问是不是机器答得了的:读 `classes` **整体** —— 在场、非空、且每一类都在答集;缺席 / 空 / 任一类在答集外 ⇒ 否。 */
|
|
53
60
|
export declare function unattendedAnswers(req: Pick<AskRequest, "classes">): boolean;
|
|
54
61
|
/**
|
|
55
|
-
* 按类作答的 onAsk(活对象 ⇒ `create*`)。答得了 ⇒
|
|
62
|
+
* 按类作答的 onAsk(活对象 ⇒ `create*`)。答得了 ⇒ 策略的 yes(这一次调用的答,记在策略名下 —— S-953);答不了 ⇒ 有席交席(席的答原样返回,席自己的
|
|
56
63
|
* fail-closed 语义不动),无席 ⇒ `"unavailable"`(core G1:这一问此刻没有人够得着 ⇒ 改道 durable park;本腿没武装 park ⇒ headless 拒)。
|
|
57
64
|
* 不答的臂**从不**自己拒成 `false`:那会把「交给 park 的人」折成「机器拒了」,durable 审批块的 leader worker 就再也 park 不下来。
|
|
58
65
|
*/
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export const UNATTENDED_ANSWERED_CLASSES = new Set(["ordinary"]);
|
|
2
|
+
export const UNATTENDED_POLICY_APPROVER = "unattended_class_answer";
|
|
3
|
+
function unattendedPolicyYes() {
|
|
4
|
+
return { allow: true, settledBy: "policy", approver: UNATTENDED_POLICY_APPROVER };
|
|
5
|
+
}
|
|
2
6
|
export function unattendedAnswers(req) {
|
|
3
7
|
const classes = req.classes;
|
|
4
8
|
return classes !== undefined && classes.length > 0 && classes.every((c) => UNATTENDED_ANSWERED_CLASSES.has(c));
|
|
5
9
|
}
|
|
6
10
|
export function createUnattendedOnAsk(seat) {
|
|
7
|
-
return (req, signal) => (unattendedAnswers(req) ?
|
|
11
|
+
return (req, signal) => (unattendedAnswers(req) ? unattendedPolicyYes() : seat !== undefined ? seat(req, signal) : "unavailable");
|
|
8
12
|
}
|
|
9
13
|
export function withUnattendedLeg(spec, seat) {
|
|
10
14
|
return { ...spec, permissionMode: "default", onAsk: createUnattendedOnAsk(seat) };
|
|
@@ -27,10 +27,14 @@
|
|
|
27
27
|
* · `accepted` —— 受理点(`routes/runs.ts` 的 live 腿,**调 `TaskStream.steer` 之前**、回 200 之前)写;写不进 ⇒ 503
|
|
28
28
|
* `steering.ledger_unavailable`,引擎一个字节都没收到(fail-closed:没账的 200 是空头承诺)。身份 = `acceptId`
|
|
29
29
|
* (每次受理一枚;同 Idempotency-Key 的重放共享 `messageId`,但各是一次受理)。`writer` = 受理进程的 `instanceId`。
|
|
30
|
-
* · `settled` —— 本进程对这次受理的守护**正常结束**:`
|
|
30
|
+
* · `settled` —— 本进程对这次受理的守护**正常结束**:`consumed`(S-968 / core 7.39.0 #1273:引擎在承载它的腿上铸了
|
|
31
|
+
* `human_input{delivery:"applied"}`、`inputId` = 本受理的 `messageId` —— 引擎确认把它消费进了转录;腿的事件环把每一帧交
|
|
32
|
+
* {@link UserInputLedger.observeLegEvent},认哪一帧只在本文件判;且引擎收下了**这次受理的调用** —— 路由报 {@link UserInputLedger.confirm},
|
|
33
|
+
* [ref] 7.113.0 opus 腿 L4:同 key 异正文的那次受理会被引擎拒,不能凭同名消费证据先结清)、`leg_end`(承载它的腿在本进程收场 —— done / suspended / needs_review / failed
|
|
31
34
|
* 一律,未消费的那一半 core 已用 `leg_end` 自己告知,本仓不重算「消费了没有」)或 `not_applied`(`TaskStream.steer`
|
|
32
|
-
* 抛了,这次受理没有变成 200;路由随即落这一行)
|
|
33
|
-
* `
|
|
35
|
+
* 抛了,这次受理没有变成 200;路由随即落这一行)。子代腿的 `leg_end`:core 铸在子代自己的会话上,core 7.39.0 #1279(S-980)起同一次收场
|
|
36
|
+
* **再铸一条到父腿**(`detail.sessionId` = 父会话、`taskId` = 父 run、带 `childSessionId`)⇒ 经分流器到人;本账对子代受理落的
|
|
37
|
+
* `settled{leg_end}`(行记父 run,S-966)与那条父口帧说的是同一件事(钉在 `test/engine-notice-wire.test.ts` S-980 两格)。
|
|
34
38
|
* · `converged` —— 收敛器判「受理它的进程已经不在、而它从没 settle」⇒ `cause:"crashed"`(core 闭集里服务层那一臂)。
|
|
35
39
|
* · `announced` —— 通告**落进新 run 的事件账之后**写(`taskId` = 告知它的那条**新** run;`EngineNoticeRouter.routeConfirmed` 等口的落账结果,
|
|
36
40
|
* codex r1-F2):落账失败 ⇒ 不写,下一条 run 再说;重启再重启不重复。唯一的重复形 = 帧已落账而这一行没写成(写失败 / 进程死在两步之间)
|
|
@@ -78,7 +82,7 @@
|
|
|
78
82
|
* 不开旋钮(没有「关掉告知」的正当需求)· 不改 steer 的 200 体(回执仍是「已交付」,账本是它的影子)· 不重放丢失的输入(core 明写
|
|
79
83
|
* NOT redelivered;本账只告知,不替人决定重发)· 不存输入正文(行里只有身份与时刻)。
|
|
80
84
|
*/
|
|
81
|
-
import { type EngineNoticeCode, type UserSteerUndrainedCause } from "@sema-agent/core";
|
|
85
|
+
import { type EngineNoticeCode, type TaskEvent, type UserSteerUndrainedCause } from "@sema-agent/core";
|
|
82
86
|
import { type RunRecord } from "./plugins/store-contracts.js";
|
|
83
87
|
import { type EngineNoticeRouter } from "./trace/engine-notice-wire.js";
|
|
84
88
|
/** local 数据根下的子目录(File 孪生;与 `approval-ask-audit/` 同层同形)。 */
|
|
@@ -90,10 +94,12 @@ export declare const USER_INPUT_KIND_WORDS: {
|
|
|
90
94
|
readonly steer: true;
|
|
91
95
|
};
|
|
92
96
|
/** `settled` 的两种收场(闭集,见顶注)。 */
|
|
93
|
-
export type SettleHow = "leg_end" | "not_applied";
|
|
97
|
+
export type SettleHow = "leg_end" | "not_applied" | "consumed";
|
|
98
|
+
/** 读盘词表(File 孪生的折叠守卫 / SQL 孪生的 `settle_how` 列读腿同读这一张;`consumed` 自 7.113.0 起,≤7.112.x 的读腿不认它 —— 回滚须知在 MIGRATION)。 */
|
|
94
99
|
export declare const SETTLE_HOW_WORDS: {
|
|
95
100
|
readonly leg_end: true;
|
|
96
101
|
readonly not_applied: true;
|
|
102
|
+
readonly consumed: true;
|
|
97
103
|
};
|
|
98
104
|
/** 本服务铸的那一臂(core 删掉这个词 ⇒ `Extract` 得 `never` ⇒ 下面的常量编译红)。 */
|
|
99
105
|
export type CrashedCause = Extract<UserSteerUndrainedCause, "crashed">;
|
|
@@ -293,8 +299,21 @@ export interface UserInputLedger {
|
|
|
293
299
|
}): Promise<UserInputAcceptReceipt>;
|
|
294
300
|
/** 引擎没收下(`TaskStream.steer` 抛)⇒ 落 `settled{not_applied}`。不抛(失败留痕,受理仍挂在腿上,腿收场时照常 settle)。 */
|
|
295
301
|
withdraw(receipt: UserInputAcceptReceipt): Promise<void>;
|
|
302
|
+
/**
|
|
303
|
+
* 引擎收下了这次受理的那一次调用(`TaskStream.steer` / 子代句柄 `steer` resolve)—— {@link withdraw} 的对偶,路由对每次受理恰报其一(经 `routes/runs.ts`
|
|
304
|
+
* 的 `relayAcceptedInput`)。收下之后这次受理才可被消费证据结清([ref] 7.113.0 opus 腿 L4,见 {@link observeLegEvent});消费证据此刻已在这条腿上
|
|
305
|
+
* (消费帧先于本调用到达 / 同 Idempotency-Key 的重放在消费之后才来)⇒ 当场落 `settled{consumed}`。不抛(结清写失败留在腿上,收场照常 `leg_end`)。
|
|
306
|
+
*/
|
|
307
|
+
confirm(receipt: UserInputAcceptReceipt): Promise<void>;
|
|
296
308
|
/** 腿在本进程收场 ⇒ 这条腿上的每一次受理落 `settled{leg_end}`。不抛。 */
|
|
297
309
|
settleLeg(leg: object): Promise<void>;
|
|
310
|
+
/**
|
|
311
|
+
* S-968(core 7.39.0 #1273):腿的事件环把**每一帧**引擎事件交进来(三条腿同一只口;不认的帧零成本返回)。唯一认的一形 =
|
|
312
|
+
* `human_input{delivery:"applied"}`:引擎确认把那条输入消费进了转录 ⇒ 这条腿上 `messageId` = 帧 `inputId`、**且引擎已收下其调用**({@link confirm})的受理
|
|
313
|
+
* (同 Idempotency-Key 的幂等重放一并)落 `settled{consumed}`,离开 open 集 —— 之后写者死也不再按崩溃告知。结清写失败 ⇒ 留在腿上(收场照常落 `leg_end`),不抛。
|
|
314
|
+
* 引擎还没判的受理不结清:messageId 只由 key 派生、账本看不见正文,同 key 异正文的那一次会被引擎拒(`steering.duplicate_input_id`)、走 {@link withdraw}。
|
|
315
|
+
*/
|
|
316
|
+
observeLegEvent(leg: object, ev: TaskEvent): Promise<void>;
|
|
298
317
|
/** 收敛扫描(boot 一次 + 每个维护 tick;判据见顶注)。不抛;返回本次收敛条数。 */
|
|
299
318
|
converge(): Promise<number>;
|
|
300
319
|
/** 新 run 首拍:把本会话已收敛、未告知的受理按旧 run 分组铸通告、投出后落 `announced`。不抛。 */
|
|
@@ -4,7 +4,7 @@ import { isHolderDeathVerdict } from "./plugins/store-contracts.js";
|
|
|
4
4
|
import { defaultEngineNoticeRouter } from "./trace/engine-notice-wire.js";
|
|
5
5
|
export const USER_INPUT_LEDGER_DIR = "user-input-ledger";
|
|
6
6
|
export const USER_INPUT_KIND_WORDS = { steer: true };
|
|
7
|
-
export const SETTLE_HOW_WORDS = { leg_end: true, not_applied: true };
|
|
7
|
+
export const SETTLE_HOW_WORDS = { leg_end: true, not_applied: true, consumed: true };
|
|
8
8
|
const UNDRAINED_CAUSE_MINTER = { leg_end: "engine", crashed: "server" };
|
|
9
9
|
export const CRASHED_CAUSE = "crashed";
|
|
10
10
|
export function isServingLayerUndrainedCause(v) {
|
|
@@ -128,7 +128,7 @@ const USER_STEER_UNDRAINED = "task.user_steer_undrained";
|
|
|
128
128
|
export function buildCrashedSteerNotice(input) {
|
|
129
129
|
return {
|
|
130
130
|
code: USER_STEER_UNDRAINED,
|
|
131
|
-
message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.steer} user steer(s) for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the
|
|
131
|
+
message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.steer} user steer(s) for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the engine confirmed consuming them. They are NOT redelivered.`,
|
|
132
132
|
detail: { steer: input.steer, cause: CRASHED_CAUSE, taskId: input.taskId, sessionId: input.sessionId },
|
|
133
133
|
sessionId: input.sessionId,
|
|
134
134
|
};
|
|
@@ -157,13 +157,25 @@ export function createUserInputLedger(opts) {
|
|
|
157
157
|
const { store, writer, evidence } = opts;
|
|
158
158
|
const now = opts.now ?? Date.now;
|
|
159
159
|
const legs = new WeakMap();
|
|
160
|
+
const consumedOnLeg = new WeakMap();
|
|
161
|
+
const settleConsumed = async (leg, acceptId) => {
|
|
162
|
+
try {
|
|
163
|
+
await store.settle(acceptId, "consumed", now());
|
|
164
|
+
legs.get(leg)?.delete(acceptId);
|
|
165
|
+
liveHere.delete(acceptId);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
recordFailOpen("server.user-input-ledger.append-failed", `op=consumed ${errText(e)}`);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
160
171
|
const liveHere = new Set();
|
|
161
|
-
const track = (leg, acceptId) => {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
const track = (leg, acceptId, messageId) => {
|
|
173
|
+
const entry = { messageId, taken: false };
|
|
174
|
+
const onLeg = legs.get(leg);
|
|
175
|
+
if (onLeg)
|
|
176
|
+
onLeg.set(acceptId, entry);
|
|
165
177
|
else
|
|
166
|
-
legs.set(leg, new
|
|
178
|
+
legs.set(leg, new Map([[acceptId, entry]]));
|
|
167
179
|
};
|
|
168
180
|
const pageSize = opts.scanBudget?.pageSize ?? USER_INPUT_OPEN_SCAN_MAX;
|
|
169
181
|
const maxPages = opts.scanBudget?.maxPages ?? USER_INPUT_OPEN_SCAN_PAGES;
|
|
@@ -239,9 +251,17 @@ export function createUserInputLedger(opts) {
|
|
|
239
251
|
liveHere.delete(acceptId);
|
|
240
252
|
throw e;
|
|
241
253
|
}
|
|
242
|
-
track(input.leg, acceptId);
|
|
254
|
+
track(input.leg, acceptId, input.messageId);
|
|
243
255
|
return { acceptId, leg: input.leg };
|
|
244
256
|
},
|
|
257
|
+
async confirm(receipt) {
|
|
258
|
+
const entry = legs.get(receipt.leg)?.get(receipt.acceptId);
|
|
259
|
+
if (entry === undefined)
|
|
260
|
+
return;
|
|
261
|
+
entry.taken = true;
|
|
262
|
+
if (consumedOnLeg.get(receipt.leg)?.has(entry.messageId) === true)
|
|
263
|
+
await settleConsumed(receipt.leg, receipt.acceptId);
|
|
264
|
+
},
|
|
245
265
|
async withdraw(receipt) {
|
|
246
266
|
legs.get(receipt.leg)?.delete(receipt.acceptId);
|
|
247
267
|
try {
|
|
@@ -255,11 +275,11 @@ export function createUserInputLedger(opts) {
|
|
|
255
275
|
}
|
|
256
276
|
},
|
|
257
277
|
async settleLeg(leg) {
|
|
258
|
-
const
|
|
259
|
-
if (!
|
|
278
|
+
const onLeg = legs.get(leg);
|
|
279
|
+
if (!onLeg)
|
|
260
280
|
return;
|
|
261
281
|
legs.delete(leg);
|
|
262
|
-
for (const acceptId of
|
|
282
|
+
for (const acceptId of onLeg.keys()) {
|
|
263
283
|
try {
|
|
264
284
|
await store.settle(acceptId, "leg_end", now());
|
|
265
285
|
}
|
|
@@ -271,6 +291,21 @@ export function createUserInputLedger(opts) {
|
|
|
271
291
|
}
|
|
272
292
|
}
|
|
273
293
|
},
|
|
294
|
+
async observeLegEvent(leg, ev) {
|
|
295
|
+
if (ev.type !== "human_input" || ev.delivery !== "applied")
|
|
296
|
+
return;
|
|
297
|
+
const seen = consumedOnLeg.get(leg);
|
|
298
|
+
if (seen)
|
|
299
|
+
seen.add(ev.inputId);
|
|
300
|
+
else
|
|
301
|
+
consumedOnLeg.set(leg, new Set([ev.inputId]));
|
|
302
|
+
const onLeg = legs.get(leg);
|
|
303
|
+
if (!onLeg)
|
|
304
|
+
return;
|
|
305
|
+
const due = [...onLeg].flatMap(([acceptId, entry]) => (entry.messageId === ev.inputId && entry.taken ? [acceptId] : []));
|
|
306
|
+
for (const acceptId of due)
|
|
307
|
+
await settleConsumed(leg, acceptId);
|
|
308
|
+
},
|
|
274
309
|
converge() {
|
|
275
310
|
inFlight ??= convergePass().finally(() => {
|
|
276
311
|
inFlight = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.114.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"!dist/run-local-linux-x64",
|
|
29
29
|
"!dist/run-local-darwin-arm64",
|
|
30
30
|
"skills",
|
|
31
|
+
"scripts/retired-mode-seed-drain-local.mjs",
|
|
31
32
|
"deploy/sema-up",
|
|
32
33
|
"docs/DEVICE-EXECUTOR-PROTOCOL.md",
|
|
33
34
|
"USAGE.md",
|
|
@@ -61,8 +62,8 @@
|
|
|
61
62
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
|
-
"@sema-agent/core": "7.
|
|
65
|
-
"@sema-agent/settings-schema": "
|
|
65
|
+
"@sema-agent/core": "7.39.2",
|
|
66
|
+
"@sema-agent/settings-schema": "7.0.1",
|
|
66
67
|
"e2b": "^2.28.0",
|
|
67
68
|
"libsodium-wrappers": "^0.8.4",
|
|
68
69
|
"mysql2": "^3.22.4",
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 换钉 7.113.0(core 7.39.0 删掉退役 mode-seed 的一次性迁移)前的**排空检查 —— `DB_BACKEND=local`(File 店)车道**,只读。
|
|
4
|
+
* 随 npm 包出(`scripts/retired-mode-seed-drain-local.mjs`;SQL 车道的同一判据是 `docs/DEPLOY-PREREQS.md` 同名节的两条只读 SQL)。
|
|
5
|
+
*
|
|
6
|
+
* ## 为什么是一只脚本,不是一行 grep
|
|
7
|
+
* ① 判据有三条成员(core `refuseRetiredModeSeed`):`autoModeRequested` / `approverPosture` **任何值**(含 JSON null)、
|
|
8
|
+
* `shellGate` **不是 `"always"`**(含 JSON null)。`"always"` 是 7.34.0 起的合法新形,grep 表达不了「值不是 always」;
|
|
9
|
+
* ② core 的 File 店是**快照 + 账本**两只文件(`checkpoints/checkpoints.snapshot.jsonl` 每行一条行,`checkpoints/checkpoints.jsonl` 是快照之后的事件):
|
|
10
|
+
* 压实之后旧行**只在快照里**,只扫账本会把最该排的那批(≤7.105.0 停放的老行)整批漏掉;
|
|
11
|
+
* ③ 账本是追加写:同一条行先 `put` 后 `resolve` / `expire` / `reopen`,只有折叠到**当前状态**才知道它还 pending 没有。
|
|
12
|
+
* ⇒ 本脚本照 core 的同一组事件把两只文件折成当前行,再按上面三条成员判 pending 行。
|
|
13
|
+
*
|
|
14
|
+
* ## 与 core 同判 —— 判据坐标(core 7.39.1 安装树 `node_modules/@sema-agent/core/dist/`;core 改了这些,本脚本要跟着改)
|
|
15
|
+
* · 行形:`stores/file/checkpoint-store.js:61` `isCheckpointRow`(`token` / `scope` / `status` 三个串)。快照行(`:70` `judgeRow`)与账本 `put` 的 `cp`(`:71-78` `judgeEvent`)
|
|
16
|
+
* **同一只判**。不合形的记录 core **跳过并披露、保留旧行**(`stores/file/fs-atomic.js:122` `readJsonlRecords`)—— 升级后那张旧卡照样在、照样被拒;所以本脚本对不合形的 `put`
|
|
17
|
+
* **不覆盖**旧行、计入 `corrupt`(⇒ rc 2),旧卡仍被数到;不合形的快照行(含缺 `scope`)不当行数、计入 `corrupt`。
|
|
18
|
+
* · 事件词:`stores/file/checkpoint-store.js:7-14` 注册表六词(put / resolve / reopen / execution / expire / steer_append);不认的词 core **拒绝重放**
|
|
19
|
+
* (`:16-19` `applyCheckpointEvent` 抛 `ledger_event_unknown`)⇒ 本脚本计入 `unknownEvents`(⇒ rc 2),不替它猜。
|
|
20
|
+
* · 重放序:`stores/file/shared-ledger.js:20-31` 先快照、后账本。压实 `stores/file/shared-ledger.js:63-72` `compact()` 先**原子换新快照**、再**清空账本**。
|
|
21
|
+
* · 退役成员:`core/checkpoint-store.js:739-756` `RETIRED_MODE_SEED_MEMBERS` / `refuseRetiredModeSeed`。
|
|
22
|
+
*
|
|
23
|
+
* ## 只读、不取锁;服务运行中也可以跑,但脚本只对**一致的**快照 + 账本组合下结论
|
|
24
|
+
* 不打开 core 的店(那会取 `checkpoints/LOCK`、与在跑的服务争锁),只读两只 JSONL。服务在写时,两次读之间可能发生压实 ——
|
|
25
|
+
* 先读快照(旧)、压实(新快照含账本里的行,账本清空)、再读账本(空)⇒ 只在账本里的 pending 退役卡两边都读不到,读出一个假的「干净」。
|
|
26
|
+
* 所以读前读后各取两文件的身份(dev / ino / size / mtimeNs),任一变了(压实 = 原子替换 ⇒ ino 变;追加 ⇒ size 变)就**重读**,至多 3 次;
|
|
27
|
+
* 仍不稳 ⇒ rc 2 + 一句话(服务在写:停写或重跑),不给结论。
|
|
28
|
+
*
|
|
29
|
+
* ## 数据根
|
|
30
|
+
* · 带参:`node scripts/retired-mode-seed-drain-local.mjs <数据根>` —— 读那个目录;
|
|
31
|
+
* · 不带参:数据根 = **服务自己的那只解析**(`dist/local-data-root.js` 的 `resolveLocalDataRoot`,`loadConfig` 调的同一只函数,脚本不自抄那条链),
|
|
32
|
+
* 所以要在**与服务同一份 env**(同样的数据根相关 env、同一个 home)下跑,读到的才是服务用的那个目录;末行 `root=` 回显实际读了哪个目录。
|
|
33
|
+
* 随包形里 `dist/` 在脚本隔壁;源码树没 build 过又没给根 ⇒ rc 2 说明(见下)。
|
|
34
|
+
* · **数据根缺席 ≠ 干净**:根目录不存在 / 不是目录 / 根下没有 `checkpoints/`(core File 店在后端构造时就建这个目录,所以它是「这里跑过 local 后端」的标记;
|
|
35
|
+
* 根 `LOCK` 是 pidfile、干净退出即删,不能当标记)⇒ rc 2,不给结论 —— 路径打错、env 没对上服务,都会读到一个什么都没有的目录。
|
|
36
|
+
*
|
|
37
|
+
* ## 读数(机读末行 + 退出码)
|
|
38
|
+
* · 每条命中一行:`RETIRED-ROW token=<token> sessionId=<id> createdAt=<ms> member=<成员>`;
|
|
39
|
+
* · 末行恒有:`RETIRED-MODE-SEED rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`;
|
|
40
|
+
* 没读成 / 不能下结论(rc 2 的几种缺席与不稳形)时前五个计数是 `-`、多一个 `reason=<词>`,**不会出现 `rows=0`**(下游读 `rows=0` 即「干净」);
|
|
41
|
+
* · rc 0 = 零命中(可换钉)· rc 1 = 有命中(逐行列出;答完或取消再跑)· rc 2 = 读不懂 / 没读成 / 不能下结论(账本里有本脚本不认的事件词 / 不合形的记录 /
|
|
42
|
+
* 数据根缺席 / 缺省根解析不可用 / 读期间文件一直在变 / 读文件出错 / 用法错)—— 不给结论(缺席 ≠ 干净:一条读不懂的记录可能正是让某行回到 pending 的那一条)。
|
|
43
|
+
* rc 2 与 rc 1 同时成立时(有命中、也有读不懂的记录)报 rc 2,命中行照列。
|
|
44
|
+
*/
|
|
45
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
46
|
+
import { homedir } from "node:os";
|
|
47
|
+
import { join } from "node:path";
|
|
48
|
+
import { fileURLToPath } from "node:url";
|
|
49
|
+
|
|
50
|
+
const TAIL_PREFIX = "RETIRED-MODE-SEED";
|
|
51
|
+
/** 读期间文件在变时的有界重读次数(含第一次)。 */
|
|
52
|
+
const STABLE_READ_ATTEMPTS = 3;
|
|
53
|
+
/** core 账本事件注册表(`stores/file/checkpoint-store.js:7-14`)。 */
|
|
54
|
+
const KNOWN_LEDGER_EVENTS = new Set(["put", "resolve", "reopen", "execution", "expire", "steer_append"]);
|
|
55
|
+
|
|
56
|
+
/** @param {string} path */
|
|
57
|
+
const dirKind = (path) => {
|
|
58
|
+
try {
|
|
59
|
+
return statSync(path).isDirectory() ? "dir" : "file";
|
|
60
|
+
} catch {
|
|
61
|
+
return "absent";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** 文件全文;不存在 = 空(与 core `readJsonlRecords` 的 ENOENT 臂同);其余错误抛出(由 {@link drainReport} 转 rc 2)。
|
|
66
|
+
* @param {string} path */
|
|
67
|
+
const textOf = (path) => {
|
|
68
|
+
try {
|
|
69
|
+
return readFileSync(path, "utf8");
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") return "";
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** @param {string} path */
|
|
77
|
+
const linesOf = (path) => textOf(path).split("\n").filter((l) => l.trim().length > 0);
|
|
78
|
+
|
|
79
|
+
/** @param {string} line */
|
|
80
|
+
const parse = (line) => {
|
|
81
|
+
try {
|
|
82
|
+
const v = JSON.parse(line);
|
|
83
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** core `isCheckpointRow`(`stores/file/checkpoint-store.js:61`)逐字:`token` / `scope` / `status` 三个串。快照行与账本 `put` 的 `cp` 同一只判。
|
|
90
|
+
* @param {unknown} x
|
|
91
|
+
* @returns {x is Record<string, unknown> & { token: string, scope: string, status: string }} */
|
|
92
|
+
const isCheckpointRow = (x) =>
|
|
93
|
+
x !== null && typeof x === "object" && typeof (/** @type {Record<string, unknown>} */ (x).token) === "string" && typeof (/** @type {Record<string, unknown>} */ (x).scope) === "string" && typeof (/** @type {Record<string, unknown>} */ (x).status) === "string";
|
|
94
|
+
|
|
95
|
+
/** 一只文件的身份;不存在 = `absent`(账本可以合法不存在);其余 stat 错误抛出。
|
|
96
|
+
* @param {string} path */
|
|
97
|
+
const identityOf = (path) => {
|
|
98
|
+
try {
|
|
99
|
+
const st = statSync(path, { bigint: true });
|
|
100
|
+
return `${st.dev}:${st.ino}:${st.size}:${st.mtimeNs}`;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") return "absent";
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** 两只文件的身份(快照 | 账本)。
|
|
108
|
+
* @param {string} dir */
|
|
109
|
+
const identitiesOf = (dir) => `${identityOf(join(dir, "checkpoints.snapshot.jsonl"))}|${identityOf(join(dir, "checkpoints.jsonl"))}`;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 读一遍(不判一致性):快照 → 钩子 → 账本,折成当前行。
|
|
113
|
+
* @param {string} dir
|
|
114
|
+
* @param {() => void | Promise<void>} betweenReads
|
|
115
|
+
* @returns {Promise<{ rows: Map<string, Record<string, unknown>>, corrupt: number, unknownEvents: number }>}
|
|
116
|
+
*/
|
|
117
|
+
async function foldOnce(dir, betweenReads) {
|
|
118
|
+
/** @type {Map<string, Record<string, unknown>>} token → 行(当前状态) */
|
|
119
|
+
const rows = new Map();
|
|
120
|
+
let corrupt = 0;
|
|
121
|
+
let unknownEvents = 0;
|
|
122
|
+
|
|
123
|
+
// ① 快照:每行一条行;不合形(core `isCheckpointRow`)⇒ core 跳过 ⇒ 本脚本也不当行数,计 corrupt
|
|
124
|
+
for (const line of linesOf(join(dir, "checkpoints.snapshot.jsonl"))) {
|
|
125
|
+
const r = parse(line);
|
|
126
|
+
if (r === undefined || !isCheckpointRow(r)) {
|
|
127
|
+
corrupt++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
rows.set(r.token, r);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await betweenReads();
|
|
134
|
+
|
|
135
|
+
// ② 账本:快照之后的事件,照 core `applyCheckpointEvent` 的同一组词折叠(状态相关的三词改 status;其余两词与 pending 判定无关)
|
|
136
|
+
for (const line of linesOf(join(dir, "checkpoints.jsonl"))) {
|
|
137
|
+
const ev = parse(line);
|
|
138
|
+
if (ev === undefined || typeof ev.t !== "string") {
|
|
139
|
+
corrupt++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!KNOWN_LEDGER_EVENTS.has(ev.t)) {
|
|
143
|
+
unknownEvents++; // 新版本写的事件词:本脚本不替它猜(core 对未知词拒绝重放)
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (typeof ev.token !== "string") {
|
|
147
|
+
corrupt++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ev.t === "put") {
|
|
151
|
+
// core:`put` 的 cp 不合形 ⇒ 跳过该事件、保留旧行。本脚本同:不覆盖,计 corrupt(⇒ rc 2),旧行照数。
|
|
152
|
+
if (!isCheckpointRow(ev.cp)) {
|
|
153
|
+
corrupt++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
rows.set(ev.token, ev.cp);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const row = rows.get(ev.token);
|
|
160
|
+
switch (ev.t) {
|
|
161
|
+
case "resolve":
|
|
162
|
+
if (row) row.status = "resolved";
|
|
163
|
+
break;
|
|
164
|
+
case "reopen":
|
|
165
|
+
if (row) row.status = "pending";
|
|
166
|
+
break;
|
|
167
|
+
case "expire":
|
|
168
|
+
if (row) row.status = "expired";
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
break; // execution / steer_append:不改 status
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { rows, corrupt, unknownEvents };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 把 `<dir>` 下的快照 + 账本折成当前行,**只对一致的组合交结果**(导出:测试与 CLI 共用同一只;CLI 不传钩子)。
|
|
179
|
+
* 读前读后各取两文件身份,变了就重读,至多 {@link STABLE_READ_ATTEMPTS} 次;仍变 ⇒ `stable:false`(不交行)。
|
|
180
|
+
* @param {string} dir `<数据根>/checkpoints`
|
|
181
|
+
* @param {{ afterSnapshotRead?: (attempt: number) => void | Promise<void> }} [opts] 测试钩子:快照读完、账本还没读的那一刻(`attempt` 从 1 起)
|
|
182
|
+
* @returns {Promise<{ stable: true, attempts: number, rows: Map<string, Record<string, unknown>>, corrupt: number, unknownEvents: number } | { stable: false, attempts: number }>}
|
|
183
|
+
*/
|
|
184
|
+
export async function scanCheckpointDir(dir, opts = {}) {
|
|
185
|
+
for (let attempt = 1; attempt <= STABLE_READ_ATTEMPTS; attempt++) {
|
|
186
|
+
const before = identitiesOf(dir);
|
|
187
|
+
const folded = await foldOnce(dir, async () => {
|
|
188
|
+
await opts.afterSnapshotRead?.(attempt);
|
|
189
|
+
});
|
|
190
|
+
if (identitiesOf(dir) === before) return { stable: true, attempts: attempt, ...folded };
|
|
191
|
+
}
|
|
192
|
+
return { stable: false, attempts: STABLE_READ_ATTEMPTS };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** core `refuseRetiredModeSeed` 的三条成员(`core/checkpoint-store.js:739-756` 逐字):前两条任何值都算(含 null),`shellGate` 不是 "always" 才算。
|
|
196
|
+
* @param {unknown} gate
|
|
197
|
+
* @returns {string | undefined} */
|
|
198
|
+
function retiredMember(gate) {
|
|
199
|
+
if (gate === null || typeof gate !== "object") return undefined;
|
|
200
|
+
const g = /** @type {Record<string, unknown>} */ (gate);
|
|
201
|
+
if (g.autoModeRequested !== undefined) return "autoModeRequested";
|
|
202
|
+
if (g.approverPosture !== undefined) return "approverPosture";
|
|
203
|
+
if (g.shellGate !== undefined && g.shellGate !== "always") return "shellGate";
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 没读成 / 不能下结论的报告:一句人话进 stderr、机读末行进 stdout(计数全 `-`,不出现 `rows=0`),rc 2。
|
|
208
|
+
* @param {string} reason 机读词
|
|
209
|
+
* @param {string} sentence
|
|
210
|
+
* @param {string} rootEcho */
|
|
211
|
+
function unreadableReport(reason, sentence, rootEcho) {
|
|
212
|
+
return { rc: /** @type {const} */ (2), stdout: [`${TAIL_PREFIX} rows=- pending=- total=- unknownEvents=- corrupt=- reason=${reason} root=${rootEcho}`], stderr: [sentence] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 一次排空检查的完整报告(导出:CLI 与测试共用)。
|
|
217
|
+
* @param {string} root 数据根
|
|
218
|
+
* @param {{ afterSnapshotRead?: (attempt: number) => void | Promise<void> }} [opts]
|
|
219
|
+
* @returns {Promise<{ rc: 0 | 1 | 2, stdout: string[], stderr: string[] }>}
|
|
220
|
+
*/
|
|
221
|
+
export async function drainReport(root, opts = {}) {
|
|
222
|
+
const dir = join(root, "checkpoints");
|
|
223
|
+
switch (dirKind(root)) {
|
|
224
|
+
case "absent":
|
|
225
|
+
return unreadableReport("root-absent", `data root ${root} does not exist — no verdict (absent is not clean): check that this process has the same data-root env as the service, or pass the data root as the argument`, root);
|
|
226
|
+
case "file":
|
|
227
|
+
return unreadableReport("root-not-a-directory", `data root ${root} is not a directory — no verdict: pass the service's data root`, root);
|
|
228
|
+
case "dir":
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (dirKind(dir) !== "dir") {
|
|
232
|
+
return unreadableReport(
|
|
233
|
+
"no-checkpoints-dir",
|
|
234
|
+
`no checkpoints/ directory under ${root} — no verdict (absent is not clean): this is not the data root of a local-backend service that has booted (the service creates checkpoints/ when it starts); check that this process has the same data-root env as the service, or pass the data root as the argument`,
|
|
235
|
+
root,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
let scan;
|
|
239
|
+
try {
|
|
240
|
+
scan = await scanCheckpointDir(dir, opts);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
// 读文件出错(权限 / 路径是目录 / …)⇒ 不给结论。未捕获异常的退出码是 1 = 「有命中」,会被下游读成有停放行。
|
|
243
|
+
return unreadableReport("read-error", `cannot read the checkpoint files under ${dir}: ${e instanceof Error ? e.message : String(e)} — no verdict`, root);
|
|
244
|
+
}
|
|
245
|
+
if (!scan.stable) {
|
|
246
|
+
return unreadableReport(
|
|
247
|
+
"unstable-read",
|
|
248
|
+
`the checkpoint files under ${dir} changed while being read (${scan.attempts} reads in a row) — the service is writing to its checkpoint store, so no consistent snapshot + ledger could be read: no verdict; stop the service (or its checkpoint traffic) and rerun`,
|
|
249
|
+
root,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const { rows, corrupt, unknownEvents } = scan;
|
|
253
|
+
const stdout = [];
|
|
254
|
+
let hits = 0;
|
|
255
|
+
let pending = 0;
|
|
256
|
+
for (const [token, row] of rows) {
|
|
257
|
+
if (row.status !== "pending") continue;
|
|
258
|
+
pending++;
|
|
259
|
+
const state = row.state !== null && typeof row.state === "object" ? /** @type {Record<string, unknown>} */ (row.state) : {};
|
|
260
|
+
const member = retiredMember(state.inheritedGate);
|
|
261
|
+
if (member === undefined) continue;
|
|
262
|
+
hits++;
|
|
263
|
+
stdout.push(`RETIRED-ROW token=${token} sessionId=${String(row.sessionId)} createdAt=${String(row.createdAt)} member=${member}`);
|
|
264
|
+
}
|
|
265
|
+
stdout.push(`${TAIL_PREFIX} rows=${hits} pending=${pending} total=${rows.size} unknownEvents=${unknownEvents} corrupt=${corrupt} root=${root}`);
|
|
266
|
+
return { rc: unknownEvents > 0 || corrupt > 0 ? 2 : hits > 0 ? 1 : 0, stdout, stderr: [] };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** CLI 入口:解析实参与缺省根,打印报告,按报告的 rc 退出。 */
|
|
270
|
+
async function main() {
|
|
271
|
+
const usage = "usage: retired-mode-seed-drain-local.mjs [<data root>]";
|
|
272
|
+
const args = process.argv.slice(2);
|
|
273
|
+
/** @param {{ rc: number, stdout: string[], stderr: string[] }} r */
|
|
274
|
+
const finish = (r) => {
|
|
275
|
+
for (const l of r.stderr) process.stderr.write(`${l}\n`);
|
|
276
|
+
for (const l of r.stdout) process.stdout.write(`${l}\n`);
|
|
277
|
+
process.exit(r.rc);
|
|
278
|
+
};
|
|
279
|
+
if (args.length > 1 || (args[0] !== undefined && args[0].startsWith("-"))) finish(unreadableReport("usage", usage, "-"));
|
|
280
|
+
/** @type {string} */
|
|
281
|
+
let root;
|
|
282
|
+
if (args[0] !== undefined) {
|
|
283
|
+
root = args[0];
|
|
284
|
+
} else {
|
|
285
|
+
try {
|
|
286
|
+
const { resolveLocalDataRoot } = await import("../dist/local-data-root.js");
|
|
287
|
+
root = resolveLocalDataRoot(process.env, homedir());
|
|
288
|
+
} catch (e) {
|
|
289
|
+
finish(
|
|
290
|
+
unreadableReport(
|
|
291
|
+
"default-root-unavailable",
|
|
292
|
+
`cannot resolve the default data root (dist/local-data-root.js next to scripts/ is not loadable: ${e instanceof Error ? e.message : String(e)}) — run the copy inside an installed @sema-agent/server package (or after \`npm run build\`), or pass the data root as the argument`,
|
|
293
|
+
"-",
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
finish(await drainReport(root));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 作脚本跑才进 CLI;被 import(测试)时只导出函数。
|
|
302
|
+
// 7.113.0 合并复审的修复验证两腿同根发现(主会话按「门轴新代码 fail-closed」收口):入口判定**自己不许失败成「什么都不跑、rc 0」**
|
|
303
|
+
// (rc 0 = 「干净,可换钉」)。两边都取 realpath 再比(`--preserve-symlinks-main` 下 `import.meta.url` 留软链路径、argv[1] 解析成真路径 ⇒ 修前
|
|
304
|
+
// 两边不等 ⇒ main 不跑 ⇒ 静默 rc 0);argv[1] 缺席 / 为 `-`(stdin 导入方)/ 解析不到 ⇒ 当 import,不抛(修前 realpathSync 对 `-` 抛 ENOENT);
|
|
305
|
+
// Node ≥ 22.18 有 `import.meta.main`,在场就以它为准(平台自己的判定,比路径比对可靠)。
|
|
306
|
+
function invokedAsMain() {
|
|
307
|
+
const metaMain = /** @type {{ main?: unknown }} */ (import.meta).main;
|
|
308
|
+
if (typeof metaMain === "boolean") return metaMain;
|
|
309
|
+
const arg = process.argv[1];
|
|
310
|
+
if (arg === undefined || arg === "-" || arg === "") return false;
|
|
311
|
+
try {
|
|
312
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(arg);
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (invokedAsMain()) await main();
|