@sema-agent/server 7.73.0 → 7.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +8 -0
- package/dist/boot/runner-deps.d.ts +0 -4
- package/dist/boot/runner-deps.js +1 -2
- package/dist/boot/session-faces.js +0 -2
- package/dist/boot/stores.js +3 -16
- package/dist/config-types.d.ts +1 -1
- package/dist/deployment-governance.d.ts +23 -3
- package/dist/deployment-governance.js +23 -2
- package/dist/env-facts.d.ts +8 -1
- package/dist/http/route-ctx.d.ts +9 -8
- package/dist/http/routes/approvals-assistant.js +2 -2
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/workflows.js +4 -0
- package/dist/http/server.js +1 -0
- package/dist/main.js +0 -1
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/orchestration/workflow-agent-session-index.d.ts +3 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +5 -0
- package/dist/plugins/approval-ask-store-sql.js +8 -1
- package/dist/plugins/blob-backend.d.ts +6 -7
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -10
- package/dist/plugins/checkpoint-store-sql.js +6 -24
- package/dist/plugins/file-run-store.d.ts +18 -0
- package/dist/plugins/file-run-store.js +27 -0
- package/dist/plugins/one-time-migrations.d.ts +66 -0
- package/dist/plugins/one-time-migrations.js +193 -0
- package/dist/plugins/pg-pool.d.ts +6 -0
- package/dist/plugins/pg-pool.js +5 -14
- package/dist/plugins/retention-store-sql.js +0 -1
- package/dist/plugins/store-backend.d.ts +11 -15
- package/dist/plugins/store-backend.js +7 -26
- package/dist/plugins/task-attachment-store.d.ts +2 -2
- package/dist/plugins/tidb-pool.js +11 -23
- package/dist/plugins/workflow-run-store-sql.d.ts +9 -0
- package/dist/task-cwd.d.ts +12 -1
- package/dist/task-settings.d.ts +6 -0
- package/dist/tool-approval.js +1 -1
- package/dist/trace/core-keyset-guard.d.ts +11 -2
- package/dist/trace/project.d.ts +28 -29
- package/dist/trace/project.js +12 -17
- package/dist/trace/redact.js +5 -1
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -974,6 +974,14 @@ curl -s http://<host>:8090/v1/runs/<taskId> -H 'x-agent-principal: user:42' #
|
|
|
974
974
|
> (仅 contents/pull-requests 只读)。未知词拒启;`GIT_API_BASEURL` 指向 github.com 族而 kind 仍是
|
|
975
975
|
> gitea 也拒启并指路(Gitea 形状对 GitHub 恒 404,启动即报优于运行时逐调用 404)。
|
|
976
976
|
|
|
977
|
+
> **⚠️ 升级注记(7.74.0):升级前把未决的 workflow park 决掉。** 本版起引擎对**任何**没有 `parks` 这一位的
|
|
978
|
+
> 历史 workflow run 记录,在派发任何 agent **之前**整条拒绝 `Workflow({resumeFromRunId})`
|
|
979
|
+
> (`workflow.park_truth_unreadable`,`detail.reason: "parks_unreadable"`)。那一位是本版引擎才开始写的,所以
|
|
980
|
+
> 所有**升级前**写下的 workflow run 记录都算数。这是刻意的:把旧记录读成「这条 run 没有 park」会让一只已经
|
|
981
|
+
> 停在人工审批上的子代被**重跑一遍**(付费 + 外部副作用)。**处置**:升级窗内把未决的 park 走
|
|
982
|
+
> `POST /v1/approvals/:sessionId/decide` 决掉,或对那些 run 起一条**新**的 run —— 不要 resume 旧记录。存量行
|
|
983
|
+
> 不需要清理(只在被 resume 时读,拒绝是逐次的、不留残留状态)。**本版不做兼容读腿。**
|
|
984
|
+
|
|
977
985
|
> **升级注记(7.7.0)**:本版起 `code-review` / `scan` / `discuss`(7.28.0 前叫 `team`)/ `toolset: repo-readonly|none` 的场景改跑
|
|
978
986
|
> 无执行环境的引擎。**升级前**若还有这些场景的 durable 挂起任务(需同时满足:配了 `REMOTE_EXEC`、开了
|
|
979
987
|
> `DURABLE_APPROVAL`、任务已 park),它们在新版上续跑会以 409 `checkpoint.unsupported_version` 响亮失败
|
|
@@ -160,10 +160,6 @@ export interface RunnerDepsCtx {
|
|
|
160
160
|
/** [ref] per-edited-file rewind history (core `RunnerDeps.fileHistoryStore`; replaces the retired E19
|
|
161
161
|
* whole-tree snapshot seat — core 94632197 renamed the seat, not a coexistence). WIRING IS THE OPT-IN. */
|
|
162
162
|
fileHistoryStore: ReturnType<StoreBackend["fileHistory"]> | undefined;
|
|
163
|
-
/** S-1 legacy-epoch discriminant (core `RunnerDeps.legacyRewindBoundaryProbe`): lets the rewind refusal
|
|
164
|
-
* distinguish "never existed / reaped" from "exists in the RETIRED whole-tree epoch". Consulted only on
|
|
165
|
-
* the refusal path; diagnostic input, never a control input (a bad probe cannot change the outcome). */
|
|
166
|
-
legacyRewindBoundaryProbe: RunnerDeps["legacyRewindBoundaryProbe"];
|
|
167
163
|
/** [ref] 车二:持久化权限规则店 provider(core `RunnerDeps.permissionRuleStore`)。缺席 ⇒ 引擎的
|
|
168
164
|
* `permissionRules.storeWired` 如实报 false、`AskRequest.ruleOffers` 不铸(诚实缺席)。 */
|
|
169
165
|
permissionRuleStore: RunnerDeps["permissionRuleStore"];
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -91,7 +91,7 @@ export function createSharedRunnerDeps(ctx) {
|
|
|
91
91
|
return shared;
|
|
92
92
|
}
|
|
93
93
|
export function createRunnerDeps(ctx) {
|
|
94
|
-
const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecords, memorySyncRunner, runtimeCapsResolver, fileHistoryStore,
|
|
94
|
+
const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecords, memorySyncRunner, runtimeCapsResolver, fileHistoryStore, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, sessionShellGateExplicitlyOff, } = ctx;
|
|
95
95
|
const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
|
|
96
96
|
const sharedRunnerDeps = createSharedRunnerDeps(ctx);
|
|
97
97
|
const workflowModelAllowlist = workflowModelAllowlistFor(config);
|
|
@@ -185,7 +185,6 @@ export function createRunnerDeps(ctx) {
|
|
|
185
185
|
: undefined,
|
|
186
186
|
runtimeCapsResolver: runtimeCapsResolver ? runtimeCapsResolver : undefined,
|
|
187
187
|
fileHistoryStore: fileHistoryStore ? fileHistoryStore : undefined,
|
|
188
|
-
legacyRewindBoundaryProbe: legacyRewindBoundaryProbe ? legacyRewindBoundaryProbe : undefined,
|
|
189
188
|
permissionRuleStore: permissionRuleStore ? permissionRuleStore : undefined,
|
|
190
189
|
onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
|
|
191
190
|
loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
|
|
@@ -77,8 +77,6 @@ export function createSessionFaces(ctx) {
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
if (backend?.legacySnapshotPurge)
|
|
81
|
-
await backend.legacySnapshotPurge(sessionId);
|
|
82
80
|
if (workflowCompletionInbox)
|
|
83
81
|
await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
|
|
84
82
|
await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
|
package/dist/boot/stores.js
CHANGED
|
@@ -15,6 +15,7 @@ import { PgRosterStore, TiDBRosterStore, ensurePgRosterSchema, ensureTiDBRosterS
|
|
|
15
15
|
import { LocalTaskAttachmentStore } from "../plugins/local-task-attachment-store.js";
|
|
16
16
|
import { PgTaskAttachmentStore, TiDBTaskAttachmentStore, ensurePgTaskAttachmentSchema, ensureTiDBTaskAttachmentSchema } from "../plugins/task-attachment-store.js";
|
|
17
17
|
import { createSessionStore } from "../plugins/session-store.js";
|
|
18
|
+
import { logOneTimeMigrations } from "../plugins/one-time-migrations.js";
|
|
18
19
|
import { createTaskListLane } from "./task-list-lane.js";
|
|
19
20
|
import { ensurePgTaskListSchema, ensureTiDBTaskListSchema } from "../plugins/task-list-store-sql.js";
|
|
20
21
|
import { assertDeviceAuditRebindEventSchema, ensurePgDeviceSchema, ensureTiDBDeviceSchema } from "../plugins/device-store-sql.js";
|
|
@@ -85,23 +86,9 @@ export async function openStores(ctx) {
|
|
|
85
86
|
await assertFileHistorySchema(async (sql) => ({ rows: (await pgPool.query(sql)).rows }), "pg");
|
|
86
87
|
else if (mysqlPool)
|
|
87
88
|
await assertFileHistorySchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
|
|
88
|
-
{
|
|
89
|
-
const legacyProbeSql = "SELECT 1 FROM snapshot_manifest LIMIT 1";
|
|
90
|
-
const hasLegacyRows = await (pgPool
|
|
91
|
-
? pgPool.query(legacyProbeSql).then((r) => r.rows.length > 0)
|
|
92
|
-
: mysqlPool
|
|
93
|
-
? mysqlPool.query(legacyProbeSql).then(([rows]) => rows.length > 0)
|
|
94
|
-
: Promise.resolve(false)).catch(() => false);
|
|
95
|
-
if (hasLegacyRows) {
|
|
96
|
-
logger.warn("legacy_snapshot_epoch_rows_present", {
|
|
97
|
-
note: "rows from the RETIRED whole-tree snapshot epoch (snapshot_manifest/snapshot_blob) exist. They are NOT " +
|
|
98
|
-
"migrated (whole-tree manifests and per-file history are structurally different); rewind refusals for " +
|
|
99
|
-
"legacy-era boundaries name the retired epoch via the boundary probe; the retention lane keeps sweeping " +
|
|
100
|
-
"these rows by session, and the maintenance tick GCs their orphaned blob bytes.",
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
89
|
}
|
|
90
|
+
if (backend)
|
|
91
|
+
logOneTimeMigrations(logger, backend.kind, await backend.runOneTimeMigrations());
|
|
105
92
|
if ((process.env.REWIND_SNAPSHOT_MAX_MB ?? "") !== "") {
|
|
106
93
|
logger.warn("rewind_snapshot_max_mb_retired", {
|
|
107
94
|
note: "REWIND_SNAPSHOT_MAX_MB no longer has an effect: the whole-tree snapshot epoch is retired (S-15); per-edited-file history captures only files the agent touches, with no tree walk and no size gate. Remove the env var. Per-blob SQL sizing is governed by SNAPSHOT_BLOB_SQL_MAX_BYTES / object storage as before.",
|
package/dist/config-types.d.ts
CHANGED
|
@@ -861,7 +861,7 @@ export interface ServiceConfigFlat {
|
|
|
861
861
|
dbQueryTimeoutMs?: number;
|
|
862
862
|
/** Object-store offload for the E19/2c file-snapshot BLOBS (clay 2026-06-26). The conversation TRANSCRIPT stays
|
|
863
863
|
* DB-first; ONLY the snapshot BYTES (binary file content, up to ~64 MiB/file — a perf-suboptimal TiDB blob row) move
|
|
864
|
-
* to MinIO. The
|
|
864
|
+
* to MinIO. The file-history MANIFEST (relPath→hash) STAYS in SQL, so the transcript hot read never
|
|
865
865
|
* touches MinIO. Built (by the tidb/pg StoreBackend) into a MinioBlobBackend iff MINIO_ENDPOINT + MINIO_ACCESS_KEY +
|
|
866
866
|
* MINIO_SECRET_KEY are all set — else the file-snapshot stores keep the SQL `snapshot_blob` default (byte-identical to
|
|
867
867
|
* today). Reuses the SAME MinIO env names as the k8s workspace-snapshot lane (zero remap). */
|
|
@@ -118,8 +118,10 @@ export declare function createDurableQuestionGate(live: LiveQuestionFace | undef
|
|
|
118
118
|
* · **覆盖面(2026-08-08 按 core 5.19.0 [ref] 校正;旧文见下方「历史」段)**:本层只在
|
|
119
119
|
* `ToolCallRequest.cwd` **缺席**那一形上说话。core 5.19.0 起每条路径解析型守卫按 `req.cwd ?? rootPath`
|
|
120
120
|
* 解析写目标(dist `core/sensitive-path-policy.js`),而 `canonicalizeTarget` 拿到 baseCwd 后会先把
|
|
121
|
-
* 相对形**拼成绝对形**再交给 env(dist `tools/fs/safety.js` 的 `
|
|
122
|
-
*
|
|
121
|
+
* 相对形**拼成绝对形**再交给 env(dist `tools/fs/safety.js` 的 `canonicalizeTarget`,7.17.0 起该分支是
|
|
122
|
+
* `baseCwd && !isAbsoluteForFamily(family, spelled) ? joinForFamily(family, baseCwd, spelled) : spelled`
|
|
123
|
+
* —— [ref] 把「算不算绝对 / 怎么拼」双双改成**按树的家族**判,旧注引的 `isAbsolutePathForm` 那一行已不是
|
|
124
|
+
* 这里的实现行;**结论不变**,本层仍是「有戳时自动让位」)—— 本层的 `absolutePath` 对绝对形一律报错弃权 ⇒ **有戳时本层自动让位**,由真身那一层
|
|
123
125
|
* (沙箱 `DeferredSandboxPathEnv` / host `NodeExecutionEnv`)按活 cwd 裁决,cwd 里的守卫段现在真看得见。
|
|
124
126
|
* ⇒ 本层今天的射程 = 「引擎没盖戳」的调用:相对形**自身拼写**里带守卫段的那一类(`Write(".env")`、
|
|
125
127
|
* `Write("cfg/.ssh/id_rsa")`),仍由本层 fail-closed 兜住。**不删臂**:让位与冗余不是一回事——
|
|
@@ -137,8 +139,25 @@ export declare function createDurableQuestionGate(live: LiveQuestionFace | undef
|
|
|
137
139
|
* [ref],已在 5.19.0 到货**,两条钉按上述翻面完毕。另一条备选收口(守卫集开启即把沙箱 lane 相对写
|
|
138
140
|
* 一律 deny,有真受损方且无对应旋钮)因此作废,无需部署方拍板。
|
|
139
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* 沙箱 lane 两层守卫的**显式**分工(core 7.17.1 起必须显式,不能再靠「让 `absolutePath` 失败」隐式弃权):
|
|
144
|
+
* `createSensitivePathPolicy` 对 canon 失败自 7.17.1 一律 `deny` 并点名 cause(它闭掉的正是本仓 [ref]② 上板的
|
|
145
|
+
* 自拒形放行洞)。旧形里两层都拿「判不了 ⇒ core 判 allow」当让位:真身层对**缺戳相对形**报错(沙箱 cwd 不是
|
|
146
|
+
* 门能猜的,[ref] 裁定 1)、词法层对**绝对形**报错 —— 新引擎上两处都翻成硬拒,沙箱 lane 的每一次写都被拒(提货批
|
|
147
|
+
* 14 格红)。让位与「判不了」是两件事:让位是**本层的**决定,要由本层自己说出来。分工(与两层类注一致,一字未改):
|
|
148
|
+
* · 真身层(`DeferredSandboxPathEnv`)判 **绝对形 / 带 `req.cwd` 戳的相对形**(core 按戳解析进真身);
|
|
149
|
+
* · 词法层(`RelativeTargetLexicalEnv`)判 **缺戳相对形**(自身拼写里的守卫段,fail-closed);
|
|
150
|
+
* 两层射程互补、无重叠、无缝隙;各自射程外**显式 allow**(= 让位给另一层),canon 失败留给真的判不了的形
|
|
151
|
+
* (UNC/NUL/越根)—— 那正是 7.17.1 要 deny 的。提取写目标用 core 自家 `protectivePathTargetOf` + `pathTargetValue`
|
|
152
|
+
* (与 core 守卫策略同一只提取器,零复刻);「算不算绝对」用 core `isAbsoluteForFamily(undefined, …)`(沙箱 lane
|
|
153
|
+
* 无 root/cwd 声明 ⇒ 家族 undefined ⇒ 只认 `/`-rooted,与引擎同一律)。
|
|
154
|
+
*/
|
|
155
|
+
type WriteTargetForm = "unstamped-relative" | "absolute-or-stamped";
|
|
156
|
+
/** 只在 `form` 那一类写目标上说话,其余显式 allow(让位);非写调用与提不出目标的调用原样交给 policy(它自己判 allow)。 */
|
|
157
|
+
export declare function scopedToWriteTargetForm(policy: ToolPolicy, form: WriteTargetForm): ToolPolicy;
|
|
140
158
|
export declare class RelativeTargetLexicalEnv extends StubExecutionEnv {
|
|
141
|
-
/** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(
|
|
159
|
+
/** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(fail-closed 守底:`relativeTargetsOnly` 已把这些形挡在外面,
|
|
160
|
+
* 这里不会再被绝对形命中;若有人绕过包装直接用本 env,报错在 7.17.1 上读作 deny,方向仍是保守)。 */
|
|
142
161
|
absolutePath(path: string): Promise<Result<string, FileError>>;
|
|
143
162
|
/** 恒「不存在」⇒ core 的 `canonicalizeNewPath` 逐级回退,最终把词法归一形当 canonical key 交给段匹配。
|
|
144
163
|
* 这里绝不能报错:报错会被 core 读成 `unresolvedSymlink` 而对**每一个**相对目标 deny(含普通文件)。 */
|
|
@@ -205,4 +224,5 @@ export declare function createDeploymentGovernanceInputs(config: DeploymentGover
|
|
|
205
224
|
* 本口只按调用方给的形铸策略,绝不替它判「这个部署该不该有门」。
|
|
206
225
|
*/
|
|
207
226
|
export declare function createApprovalBaselinePolicy(config: ApprovalBaselineConfigView, durable?: DurableApprovalSeat): ToolPolicy;
|
|
227
|
+
export {};
|
|
208
228
|
//# sourceMappingURL=deployment-governance.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join, posix } from "node:path";
|
|
2
|
-
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, createTranscriptIntegrityPolicy, err, ok, } from "@sema-agent/core";
|
|
2
|
+
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, createTranscriptIntegrityPolicy, err, isAbsoluteForFamily, isProtectedWrite, ok, pathTargetValue, protectivePathTargetOf, } from "@sema-agent/core";
|
|
3
3
|
import { createDurableAskPolicy } from "./approval.js";
|
|
4
4
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
|
|
5
5
|
export function createDurableQuestionGate(live) {
|
|
@@ -15,6 +15,27 @@ export function createDurableQuestionGate(live) {
|
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
function writeTargetFormOf(req) {
|
|
19
|
+
if (!isProtectedWrite(req))
|
|
20
|
+
return undefined;
|
|
21
|
+
const pt = protectivePathTargetOf(req);
|
|
22
|
+
const target = pt === undefined ? undefined : pathTargetValue(req.args, pt);
|
|
23
|
+
if (typeof target !== "string" || target.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (req.cwd !== undefined || target.includes("\u0000") || isAbsoluteForFamily(undefined, target))
|
|
26
|
+
return "absolute-or-stamped";
|
|
27
|
+
return "unstamped-relative";
|
|
28
|
+
}
|
|
29
|
+
export function scopedToWriteTargetForm(policy, form) {
|
|
30
|
+
return {
|
|
31
|
+
async check(req, signal) {
|
|
32
|
+
const actual = writeTargetFormOf(req);
|
|
33
|
+
if (actual !== undefined && actual !== form)
|
|
34
|
+
return { action: "allow" };
|
|
35
|
+
return policy.check(req, signal);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
18
39
|
export class RelativeTargetLexicalEnv extends StubExecutionEnv {
|
|
19
40
|
absolutePath(path) {
|
|
20
41
|
if (path.length === 0 || path.startsWith("/") || path.includes("\u0000")) {
|
|
@@ -64,7 +85,7 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
|
|
|
64
85
|
const withTranscripts = (p) => (transcriptGuard ? combinePolicies(p, transcriptGuard) : p);
|
|
65
86
|
if (pathAdjudication.cwd !== undefined)
|
|
66
87
|
return withTranscripts(realTarget);
|
|
67
|
-
return withTranscripts(combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt })));
|
|
88
|
+
return withTranscripts(combinePolicies(scopedToWriteTargetForm(realTarget, "absolute-or-stamped"), scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt }), "unstamped-relative")));
|
|
68
89
|
})()
|
|
69
90
|
: undefined;
|
|
70
91
|
return {
|
package/dist/env-facts.d.ts
CHANGED
|
@@ -83,7 +83,14 @@ export declare function ensureScratchpadDir(localDataRoot: string, sessionId: st
|
|
|
83
83
|
* - mkdir -p 确保存在(gate 的 canonicalize 与 core 根围栏都要真目录)。
|
|
84
84
|
* 敏感路径的最后一道**不在**写门折叠里([ref] 起搬家):守卫集 `SENSITIVE_WRITE_PATTERNS` 由
|
|
85
85
|
* governance 拍(boot/resolve-spec.ts → applyRuntimeGovernance)无条件铸成 DENY 基线,经 tightenTaskSpec
|
|
86
|
-
* 与写门 deny-wins 折叠 —— 结论不变(deny 恒赢,本函数放出的 exemptDirs 越不过它),但施加点已换。
|
|
86
|
+
* 与写门 deny-wins 折叠 —— 结论不变(deny 恒赢,本函数放出的 exemptDirs 越不过它),但施加点已换。
|
|
87
|
+
*
|
|
88
|
+
* 🔴 **接缝登记(7.74.0 / core 7.17.0 [ref];今日无可达分歧)**:下面那道「绝对路径」闸回答的是「引擎眼里
|
|
89
|
+
* 这条豁免目录算不算绝对」——引擎自本版起按**树的家族**判(`isAbsoluteForFamily`,家族从树的 root/cwd 拼法
|
|
90
|
+
* 读,不看宿主 OS),本函数用的是 Node 宿主口径。**posix 家族两者逐字同判**(2026-09-12 实测六形全同),
|
|
91
|
+
* 而本闸又只在 `hostSemanticsLane` 上场(路径就是这台 worker 自己的盘)⇒ 今天零分歧,不改。**分歧只在 win
|
|
92
|
+
* 家族的 `/`-开头无盘符那一类**(Node 说绝对、引擎说未完全限定)。判据与坐标属主见 `task-cwd.ts` 的
|
|
93
|
+
* {@link import("./task-cwd.js").isValidCwd} 顶注(同一条接缝,那里是唯一成文点,这里不复述第二份)。 */
|
|
87
94
|
export declare function acceptShellScratchpadDir(raw: unknown, opts: {
|
|
88
95
|
requirePrincipal: boolean;
|
|
89
96
|
hostSemanticsLane: boolean;
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
18
|
import type { HostDecision } from "../host-decision.js";
|
|
19
|
-
import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner, EngineNotice } from "@sema-agent/core";
|
|
19
|
+
import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, RunInternals, Runner, EngineNotice } from "@sema-agent/core";
|
|
20
20
|
import type { FlatServiceDeps, RequestAuth } from "./server.js";
|
|
21
21
|
import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
|
|
22
22
|
import type { IdempotencyCache } from "./idempotency.js";
|
|
@@ -180,13 +180,14 @@ export interface DriveResumeArgs {
|
|
|
180
180
|
* 🔴 **TRUSTED**:它绝不是 `TaskSpec` 字段、也绝不是工具参数(模型不能决定一次审批)。唯一的铸点是
|
|
181
181
|
* `/decide` 的第三条车道(判据属主 = `parked-decide.ts` 的 `planWorkflowParkedDecide` 顶注)。
|
|
182
182
|
* 缺席 ⇒ 本位对这条腿是完整 no-op,与本批之前逐字相同。
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
183
|
+
*
|
|
184
|
+
* 🔴 **形不手抄:直接是 core 的那一位**(`NonNullable<RunInternals["workflowParkedResume"]>`)。手抄一份
|
|
185
|
+
* 结构等价的字面(本位 7.73.0 及以前就是)会让 `token: string` / `inheritedGate: unknown` 这种**更宽**的
|
|
186
|
+
* 形悄悄通过,再靠下游一个 `as NonNullable<RunInternals[...]>` 把它洗进 core 的 branded 位 —— 那个转型
|
|
187
|
+
* 正是「边界必 schema、禁自铸」要挡的东西(裸 string 当赎回键传进去,编译期一个字都不响)。派生之后
|
|
188
|
+
* core 改这一位(本版就把 `WorkflowParkedResume.outcome` 改成了可选)在本仓是**编译事件**,不是靠人去
|
|
189
|
+
* 对表的一行注。 */
|
|
190
|
+
workflowParkedResume?: NonNullable<RunInternals["workflowParkedResume"]>;
|
|
190
191
|
/** [ref]([ref] Inkglow 案 → [ref]① 定谳):**200 受理语义**——调用方声明「我的消费方按 poll/SSE 跟
|
|
191
192
|
* 终态,别让 HTTP 等模型往返」。在场为 `true` 时本腿在**受理点**(`resumeStream` 解析之后)立刻把
|
|
192
193
|
* `200 {taskId, status:"resuming", bindingEnforced:true}` 交回调用方,模型腿(流迭代 + 行驱动)继续
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { screenedGate } from "../../trace/project.js";
|
|
2
2
|
import { personDecision } from "../../host-decision.js";
|
|
3
3
|
import { sameExecutionRecord } from "@sema-agent/core";
|
|
4
4
|
import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
|
|
@@ -87,7 +87,7 @@ async function decideIdempotentReplay(cs, args) {
|
|
|
87
87
|
return undefined;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
return
|
|
90
|
+
return screenedGate(first);
|
|
91
91
|
})();
|
|
92
92
|
return {
|
|
93
93
|
status: 200,
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -789,7 +789,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
789
789
|
taskId,
|
|
790
790
|
status: run.status,
|
|
791
791
|
turnCut: haltReceipt.turnCut,
|
|
792
|
-
note: "halt accepted — cut + stop: turnCut reports whether THIS call cut a live turn (repeat calls answer turnCut:false once the stop is latched).
|
|
792
|
+
note: "halt accepted — cut + stop: turnCut reports whether THIS call cut a live turn (repeat calls answer turnCut:false once the stop is latched). A halt that CONSUMES (it cut a turn, or stopped the loop at a boundary) ends the run at that boundary as status:\"completed\" with haltedByUser:true — resume by submitting the next objective on the same session. Each real cut is marked by a task.turn_interrupted event with detail.cause:\"user_halt\". A halt racing the run's natural last moments consumes NOTHING: the run's own ending stands (completed with the answer as result, or failed when the model's own abort owns the final) and the result does NOT carry haltedByUser — the only trace of that halt is a task.halt_unconsumed engine notice.",
|
|
793
793
|
});
|
|
794
794
|
return;
|
|
795
795
|
}
|
|
@@ -245,6 +245,9 @@ function projectLegApprovalKeys(agent) {
|
|
|
245
245
|
...(approvalWaitedMs !== undefined ? { approvalWaitedMs } : {}),
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
+
function projectParkRow(p) {
|
|
249
|
+
return { callKey: p.callKey, sessionId: p.sessionId, originRunId: p.originRunId };
|
|
250
|
+
}
|
|
248
251
|
function summarizeWorkflowDetail(run) {
|
|
249
252
|
const isDone = (s) => s === "completed" || s === "failed";
|
|
250
253
|
const durationOf = (a) => a.startedAt === undefined || a.endedAt === undefined ? undefined : a.endedAt - a.startedAt;
|
|
@@ -339,6 +342,7 @@ function summarizeWorkflowDetail(run) {
|
|
|
339
342
|
rev: run.rev,
|
|
340
343
|
...(run.error !== undefined ? { error: redactErrorMessage(run.error) ?? "" } : {}),
|
|
341
344
|
...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
|
|
345
|
+
...(run.parks !== undefined ? { parks: run.parks.map(projectParkRow) } : {}),
|
|
342
346
|
...(run.result !== undefined ? { result: redactSecrets(run.result) } : {}),
|
|
343
347
|
phases,
|
|
344
348
|
...(unphasedView ? { unphased: unphasedView } : {}),
|
package/dist/http/server.js
CHANGED
|
@@ -2525,6 +2525,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
2525
2525
|
env_factory_missing: true,
|
|
2526
2526
|
governed_unwired: true,
|
|
2527
2527
|
origin_missing: true,
|
|
2528
|
+
seat_retired: true,
|
|
2528
2529
|
ledger_event_unknown: true,
|
|
2529
2530
|
real_approval_damaged: false,
|
|
2530
2531
|
real_approval_forged: false,
|
package/dist/main.js
CHANGED
|
@@ -277,7 +277,6 @@ async function main() {
|
|
|
277
277
|
pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
|
|
278
278
|
sessionCaptureRecords,
|
|
279
279
|
memorySyncRunner, toolResultStore: runnerOffloadStore, checkpointStore, sessionPolicyStore, runtimeCapsResolver, fileHistoryStore,
|
|
280
|
-
legacyRewindBoundaryProbe: backend?.legacyRewindBoundaryProbe ? backend.legacyRewindBoundaryProbe.bind(backend) : undefined,
|
|
281
280
|
permissionRuleStore: permissionRuleProvider,
|
|
282
281
|
executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
|
|
283
282
|
workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
|
|
@@ -19,7 +19,7 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
19
19
|
};
|
|
20
20
|
readonly "server.trace.gate-outcome-defective": {
|
|
21
21
|
readonly cls: "F";
|
|
22
|
-
readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)
|
|
22
|
+
readonly note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)同走这一只筛,零双读 —— 本服务上一版写下的退役词旧拼法由启动期的一次性迁移在库里原地改完,读腿因此没有第二种读法):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集),**或投影期抛出**(S-239:`settlement.note` / `who.approver` 在 ≥2^23 单段上撞 `redactSecrets` 既有的回溯上界 —— 筛子总函数化后同走这一口,而不是逃出去断掉整条账本流)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。";
|
|
23
23
|
};
|
|
24
24
|
readonly "server.trace.tool-roster-malformed": {
|
|
25
25
|
readonly cls: "F";
|
|
@@ -10,7 +10,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
10
10
|
},
|
|
11
11
|
"server.trace.gate-outcome-defective": {
|
|
12
12
|
cls: "F",
|
|
13
|
-
note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)
|
|
13
|
+
note: "S-136(core 7.6.0 design/390 S6-A,`trace/project.ts` 的 `screenedGate`:写腿 `toolEndEventData` 直用;**三条**耐久读腿(投影回放 `toolResultFieldsOf`,`GET /v1/tasks/:id/turns` 与 `GET /v1/tasks/:id/stream` 共用 / 账本裸回放 `GET /v1/runs/:id/events`,经 `screenedStoredEventData` / `/decide` 幂等回放 `executionOutcome`)同走这一只筛,零双读 —— 本服务上一版写下的退役词旧拼法由启动期的一次性迁移在库里原地改完,读腿因此没有第二种读法):一条 `tool_end` / 账本行携带的 `GateOutcome` 过不了 core 的 `screenGateOutcome`(I1–I4 不变量,或某一格词出闭集),**或投影期抛出**(S-239:`settlement.note` / `who.approver` 在 ≥2^23 单段上撞 `redactSecrets` 既有的回溯上界 —— 筛子总函数化后同走这一口,而不是逃出去断掉整条账本流)⇒ **整条 gate 记录不上帧**(report + withhold,core 契约逐字给出的两种响亮形之一;绝不「静默修补」成一条自洽的记录)。放行的最坏后果=这一帧的消费端渲染不出「谁拒的 / 那次等待怎么结束的」,退回到 `isError` + 文案;它**不改变任何裁决**(门早已在 core 里判完,这里只是观测面)。反方向(把一条自相矛盾的记录照发)更坏:消费端按 I2/I3 读出来的结论会是假的。计数让「某个世代的产生者一直在发坏记录」显形。",
|
|
14
14
|
},
|
|
15
15
|
"server.trace.tool-roster-malformed": {
|
|
16
16
|
cls: "F",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** 一条索引行:**子会话 id 是主键**(会话 id 全局唯一 ⇒ 读口天然是单行精确查,无分页假阴性)。 */
|
|
2
2
|
export interface WorkflowAgentSessionRow {
|
|
3
|
-
/** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = checkpoint
|
|
3
|
+
/** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = 那条 checkpoint 行的 `sessionId`;
|
|
4
|
+
* **不是** `sourceTaskId` —— 后者在 core 契约上是 ECHO-ONLY、明令不可作门输入)。 */
|
|
4
5
|
sessionId: string;
|
|
5
6
|
/** 它所属的 workflow run(`w*`)。 */
|
|
6
7
|
runId: string;
|
|
@@ -49,7 +50,7 @@ export declare const PARKED_IDENTITY_LABEL_MAX = 200;
|
|
|
49
50
|
* 🔴 **一条 parked 行的「恢复身份」形**(S-192)—— 店层瘦身时**最后才放弃**的那一份。
|
|
50
51
|
*
|
|
51
52
|
* 它是一条**合法的** `WorkflowAgentRun`(core 的必填四位 `label`/`callKey`/`status`/`queuedAt` 齐全),
|
|
52
|
-
* 加上赎回路由要的两位(`sessionId` = checkpoint
|
|
53
|
+
* 加上赎回路由要的两位(`sessionId` = 那条 checkpoint 行的 `sessionId`,`parkedCheckpointToken` = 赎回键)。
|
|
53
54
|
* 展示载荷(`prompt`/`output`/`activity`/`errorMessage`/`phase`/`model`…)一律不带 —— 那正是被裁掉的东西。
|
|
54
55
|
*
|
|
55
56
|
* 返回的是**普通数据**(build*),消费方是 `plugins/workflow-run-store-sql.ts` 的 `slimOversizeRun`。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canAskTransition } from "../approval-ask-machine.js";
|
|
2
|
-
import { assertIdempotencyKeyShape, ApprovalAskReadAbortedError, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
2
|
+
import { assertIdempotencyKeyShape, assertIntegralEpochMs, ApprovalAskReadAbortedError, projectAskTerminalClaimTruth } from "./approval-ask-store-sql.js";
|
|
3
3
|
function applyPatch(row, patch) {
|
|
4
4
|
if (patch.decision !== undefined)
|
|
5
5
|
row.decision = patch.decision;
|
|
@@ -24,6 +24,8 @@ export class InMemoryApprovalAskStore {
|
|
|
24
24
|
asks = new Map();
|
|
25
25
|
batches = new Map();
|
|
26
26
|
async ensureAsk(row) {
|
|
27
|
+
assertIntegralEpochMs("expiresAtMs", row.expiresAtMs);
|
|
28
|
+
assertIntegralEpochMs("createdAtMs", row.createdAtMs);
|
|
27
29
|
if (!this.batches.has(row.batchId)) {
|
|
28
30
|
this.batches.set(row.batchId, {
|
|
29
31
|
batchId: row.batchId,
|
|
@@ -226,6 +226,11 @@ export type DecideResult = {
|
|
|
226
226
|
* `idempotency_conflict`/回放结果。键是调用方铸的 opaque 值,首尾空白没有任何合法语义,
|
|
227
227
|
* 在边界上 fail-loud 拒掉,方言分歧面整个消失(比把列改 VARBINARY 更窄、且对三形同时成立)。
|
|
228
228
|
*/
|
|
229
|
+
/** BIGINT 毫秒列的整数守卫(两 twin 同一只):`expires_at_ms` / `created_at_ms` / `updated_at_ms` 落库前必须是
|
|
230
|
+
* 安全整数。带小数的墙钟(`Date.now() + performance.now()` 派生窗)在 PG 上被 `bigint` 拒收、在 MySQL 协议上被
|
|
231
|
+
* 隐式截断 —— 两方言不同答;这里改成两方言同一句 TypeError(响亮,点名字段与值),铸点侧(`tool-approval.ts`
|
|
232
|
+
* 的 deadline 铸点)负责给整数,本守卫只拦漏网。 */
|
|
233
|
+
export declare function assertIntegralEpochMs(field: string, value: number): void;
|
|
229
234
|
export declare function assertIdempotencyKeyShape(key: string): void;
|
|
230
235
|
/**
|
|
231
236
|
* [ref] 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
2
2
|
import { canAskTransition } from "../approval-ask-machine.js";
|
|
3
3
|
import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
|
|
4
|
+
export function assertIntegralEpochMs(field, value) {
|
|
5
|
+
if (!Number.isSafeInteger(value)) {
|
|
6
|
+
throw new TypeError(`approval-ask store: ${field} must be an integer epoch-ms (BIGINT column), got ${String(value)}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
4
9
|
export function assertIdempotencyKeyShape(key) {
|
|
5
10
|
if (key.length === 0 || key.length > 255 || key.trim() !== key) {
|
|
6
11
|
throw new Error(`invalid idempotency key: must be 1-255 chars with no leading/trailing whitespace (got ${JSON.stringify(key.slice(0, 32))}${key.length > 32 ? "…" : ""}, length ${key.length})`);
|
|
@@ -91,7 +96,7 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
|
|
|
91
96
|
gate_bound_call_id VARCHAR(255) NULL,
|
|
92
97
|
gate_bound_input_hash VARCHAR(255) NULL,
|
|
93
98
|
idempotency_key VARCHAR(255) NULL,
|
|
94
|
-
-- LONGTEXT 照 image-bake/background-agent
|
|
99
|
+
-- LONGTEXT 照 image-bake/background-agent 的载荷列先例:重放帧载荷可能超 64K TEXT 墙。
|
|
95
100
|
card_json LONGTEXT NOT NULL,
|
|
96
101
|
-- #363 规则车道素材两列(语义/类型理由逐字见 AskRow.ruleCommand / ruleScopeRoot 的顶注)。
|
|
97
102
|
-- rule_command 与 permission_rule_approval.command 同款 LONGTEXT:命令行没有入口上限,VARCHAR
|
|
@@ -314,6 +319,8 @@ export class SqlApprovalAskStore {
|
|
|
314
319
|
return rows[0] ? mapBatchRow(rows[0]) : null;
|
|
315
320
|
}
|
|
316
321
|
async ensureAsk(row) {
|
|
322
|
+
assertIntegralEpochMs("expiresAtMs", row.expiresAtMs);
|
|
323
|
+
assertIntegralEpochMs("createdAtMs", row.createdAtMs);
|
|
317
324
|
const now = row.createdAtMs;
|
|
318
325
|
let insertedRows = 0;
|
|
319
326
|
const conn = await this.db.connect();
|
|
@@ -39,9 +39,8 @@ export interface BlobBackend {
|
|
|
39
39
|
* dialect twins selected by `dialect`. Keeping the SQL byte-identical means the existing behaviour + the env-gated
|
|
40
40
|
* real-DB integration suite are unchanged when MinIO is unset (the DEFAULT backend the file-snapshot stores construct).
|
|
41
41
|
*
|
|
42
|
-
* NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the `
|
|
43
|
-
*
|
|
44
|
-
* and hands it here). The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob
|
|
42
|
+
* NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the reference (`not in the manifest`) filter: that
|
|
43
|
+
* check stays in the owning store, which computes the orphan hash SET from its own domain's manifest and hands it here. The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob
|
|
45
44
|
* that a concurrent in-flight snapshot still needs is never byte-deleted (delete-in-use is data loss; under-delete is
|
|
46
45
|
* harmless — the next sweep collects it).
|
|
47
46
|
*/
|
|
@@ -64,10 +63,10 @@ export declare class SqlBlobBackend implements BlobBackend {
|
|
|
64
63
|
getBlob(hash: string): Promise<Uint8Array | undefined>;
|
|
65
64
|
/** The subset of `hashes` that exist — a single `blob_hash IN (...)` SELECT (idempotency / reap probe). */
|
|
66
65
|
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
67
|
-
/** Byte-delete the given hashes (the orphan SET is computed by the
|
|
68
|
-
* grace-window guard so a just-written blob a concurrent in-flight
|
|
69
|
-
* the tidb/pg stores' gcOrphanBlobs delete, minus the
|
|
70
|
-
*
|
|
66
|
+
/** Byte-delete the given hashes (the orphan SET is computed by the CALLER from its own domain's manifest). Retains
|
|
67
|
+
* the grace-window guard so a just-written blob a concurrent in-flight write needs is never delete-in-use — VERBATIM
|
|
68
|
+
* the tidb/pg stores' gcOrphanBlobs delete, minus the reference clause (the caller already excluded referenced
|
|
69
|
+
* hashes). Best-effort: a delete error is swallowed (under-delete is harmless). */
|
|
71
70
|
deleteBlobs(hashes: string[]): Promise<number>;
|
|
72
71
|
}
|
|
73
72
|
/** SHARED index op(workspace 浏览面 #3,[ref]②)—— per-blob 字节数,读 SQL INDEX 的 `byte_len`
|
|
@@ -369,9 +369,9 @@ export declare const DECIDED_REPLAY_SCAN_CAP = 8;
|
|
|
369
369
|
* (plan_review / dry_run_review)的判词,那些门的决议入口不是本腿,回放它们等于跨门作答。
|
|
370
370
|
*/
|
|
371
371
|
export declare function approvalDecisionOfWinner(winner: ResolvedOutcome | undefined, boundCallId: string): "approve" | "deny" | null;
|
|
372
|
-
/** `
|
|
373
|
-
* 的入参形)。列是**唯一**权威(suspend 时写的 blob
|
|
374
|
-
type SteerColumnState = Pick<CheckpointState, "
|
|
372
|
+
/** `pending_steer_queue` 列承载的 CheckpointState 字段(= core `appendPendingSteer` / `readPendingSteerQueue`
|
|
373
|
+
* 的入参形)。列是**唯一**权威(suspend 时写的 blob 从不带它)。 */
|
|
374
|
+
type SteerColumnState = Pick<CheckpointState, "pendingSteerQueue">;
|
|
375
375
|
/** 读列结果。`lossy` = 列里有东西但**没能完整读出来**(JSON 坏 / 条目形不认)。两个调用点对它的处置
|
|
376
376
|
* 刻意相反,见各自的旁注:`get()` 容忍(坏列不许把一条挂起的 run 变成不可 resume),`setPendingSteer`
|
|
377
377
|
* fail-loud(往一份读不全的队列上追加 = 把别人 park 的指令悄悄写没,正是 [ref] 要消灭的那件事)。 */
|
|
@@ -380,14 +380,17 @@ interface SteerColumnRead {
|
|
|
380
380
|
lossy: boolean;
|
|
381
381
|
}
|
|
382
382
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
383
|
+
* steering 列 → `{pendingSteerQueue?}`(= core `appendPendingSteer`/`readPendingSteerQueue` 的入参形)。
|
|
384
|
+
* **一列一形**:`pending_steer_queue` 存 `PendingSteerEntry[]`。
|
|
385
|
+
*
|
|
386
|
+
* 🔴 7.74.0 起只剩这**一**列。队列化之前的裸单座列 `pending_steer` 随它的席位一起退役了(core 7.17.0
|
|
387
|
+
* [ref] d:引擎对带席的行 PRE-CAS 拒 `seat_retired`),存量列由启动期的一次性迁移删掉
|
|
388
|
+
* (`plugins/one-time-migrations.ts`:列上还有非空值 ⇒ **响亮拒启**,清空后 `DROP COLUMN`)。
|
|
389
|
+
* ⇒ 这里刻意**没有**第二条读腿:双读臂的寿命就是那一次迁移,不是永远。
|
|
390
|
+
*
|
|
391
|
+
* 列有内容却读不出 ⇒ `lossy`(两个调用点对它的处置见 {@link SteerColumnRead})。
|
|
389
392
|
*/
|
|
390
|
-
export declare function
|
|
393
|
+
export declare function readPendingSteerQueueColumn(queueRaw: unknown): SteerColumnRead;
|
|
391
394
|
/** Dual-dialect durable CheckpointStore. See the file header for the dialect-delta ledger. */
|
|
392
395
|
export declare class SqlCheckpointStore implements CheckpointStore {
|
|
393
396
|
protected readonly db: SqlDriver;
|
|
@@ -210,15 +210,6 @@ function readActorAssertion(v) {
|
|
|
210
210
|
const issuer = "issuer" in v ? v.issuer : undefined;
|
|
211
211
|
return { id, hostAsserted, ...(typeof issuer === "string" ? { issuer } : {}) };
|
|
212
212
|
}
|
|
213
|
-
function readLegacySeat(v) {
|
|
214
|
-
if (typeof v !== "object" || v === null)
|
|
215
|
-
return undefined;
|
|
216
|
-
const text = "text" in v ? v.text : undefined;
|
|
217
|
-
const trusted = "trusted" in v ? v.trusted : undefined;
|
|
218
|
-
if (typeof text !== "string" || typeof trusted !== "boolean")
|
|
219
|
-
return undefined;
|
|
220
|
-
return { text, trusted };
|
|
221
|
-
}
|
|
222
213
|
function readQueueEntry(v) {
|
|
223
214
|
if (typeof v !== "object" || v === null)
|
|
224
215
|
return undefined;
|
|
@@ -243,7 +234,7 @@ function readQueueEntry(v) {
|
|
|
243
234
|
...(priority === "now" || priority === "next" || priority === "later" ? { priority } : {}),
|
|
244
235
|
};
|
|
245
236
|
}
|
|
246
|
-
export function
|
|
237
|
+
export function readPendingSteerQueueColumn(queueRaw) {
|
|
247
238
|
let lossy = false;
|
|
248
239
|
const parse = (raw) => {
|
|
249
240
|
if (raw === null || raw === undefined)
|
|
@@ -258,10 +249,6 @@ export function readPendingSteerColumns(seatRaw, queueRaw) {
|
|
|
258
249
|
return undefined;
|
|
259
250
|
}
|
|
260
251
|
};
|
|
261
|
-
const seatParsed = parse(seatRaw);
|
|
262
|
-
const seat = seatParsed === undefined ? undefined : readLegacySeat(seatParsed);
|
|
263
|
-
if (seatParsed !== undefined && seat === undefined)
|
|
264
|
-
lossy = true;
|
|
265
252
|
const queueParsed = parse(queueRaw);
|
|
266
253
|
let queue;
|
|
267
254
|
if (queueParsed !== undefined) {
|
|
@@ -278,10 +265,7 @@ export function readPendingSteerColumns(seatRaw, queueRaw) {
|
|
|
278
265
|
}
|
|
279
266
|
}
|
|
280
267
|
}
|
|
281
|
-
return {
|
|
282
|
-
state: { ...(seat !== undefined ? { pendingSteer: seat } : {}), ...(queue !== undefined ? { pendingSteerQueue: queue } : {}) },
|
|
283
|
-
lossy,
|
|
284
|
-
};
|
|
268
|
+
return { state: { ...(queue !== undefined ? { pendingSteerQueue: queue } : {}) }, lossy };
|
|
285
269
|
}
|
|
286
270
|
const STEER_CAS_ATTEMPTS = 8;
|
|
287
271
|
const EXECUTION_RECORD_CAS_ATTEMPTS = 8;
|
|
@@ -375,7 +359,7 @@ export class SqlCheckpointStore {
|
|
|
375
359
|
}
|
|
376
360
|
}
|
|
377
361
|
async get(token) {
|
|
378
|
-
const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev,
|
|
362
|
+
const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer_queue, execution_outcome, execution_at_ms FROM checkpoint WHERE token = $1"), [token]);
|
|
379
363
|
const r = rows[0];
|
|
380
364
|
if (!r)
|
|
381
365
|
return null;
|
|
@@ -396,13 +380,11 @@ export class SqlCheckpointStore {
|
|
|
396
380
|
if (r.execution_at_ms != null)
|
|
397
381
|
cp.executionAtMs = Number(r.execution_at_ms);
|
|
398
382
|
}
|
|
399
|
-
const steer =
|
|
383
|
+
const steer = readPendingSteerQueueColumn(r.pending_steer_queue);
|
|
400
384
|
if (steer.lossy) {
|
|
401
385
|
this.logger?.info?.("checkpoint_pending_steer_unreadable", { tokenFp: tokenFingerprint(token) });
|
|
402
386
|
}
|
|
403
387
|
if (cp.state) {
|
|
404
|
-
if (steer.state.pendingSteer !== undefined)
|
|
405
|
-
cp.state.pendingSteer = steer.state.pendingSteer;
|
|
406
388
|
if (steer.state.pendingSteerQueue !== undefined)
|
|
407
389
|
cp.state.pendingSteerQueue = steer.state.pendingSteerQueue;
|
|
408
390
|
}
|
|
@@ -454,11 +436,11 @@ export class SqlCheckpointStore {
|
|
|
454
436
|
async setPendingSteer(token, scope, steer) {
|
|
455
437
|
const clean = validatePendingSteer(steer);
|
|
456
438
|
for (let attempt = 0; attempt < STEER_CAS_ATTEMPTS; attempt++) {
|
|
457
|
-
const { rows } = await this.db.query(this.q("SELECT status,
|
|
439
|
+
const { rows } = await this.db.query(this.q("SELECT status, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = ? AND scope = ?", "SELECT status, pending_steer_queue, pending_steer_rev FROM checkpoint WHERE token = $1 AND scope = $2"), [token, scope]);
|
|
458
440
|
const row = rows[0];
|
|
459
441
|
if (!row || row.status !== "pending")
|
|
460
442
|
return false;
|
|
461
|
-
const read =
|
|
443
|
+
const read = readPendingSteerQueueColumn(row.pending_steer_queue);
|
|
462
444
|
if (read.lossy) {
|
|
463
445
|
throw new Error(`the parked steering queue on checkpoint ${tokenFingerprint(token)} is unreadable — refusing to append onto it ` +
|
|
464
446
|
`(appending would silently drop the entries this build cannot parse)`);
|
|
@@ -284,6 +284,24 @@ export declare class FileRunStore {
|
|
|
284
284
|
private releaseClaim;
|
|
285
285
|
/** Drop a run entirely (registry + event log + open fd + on-disk dir) — used by deleteBySession. */
|
|
286
286
|
private dropRun;
|
|
287
|
+
/**
|
|
288
|
+
* 🔴 **一次性迁移(随 7.75.0 连同 `one-time-migrations.ts` 整删)** —— 退役词旧拼法的账本改写,
|
|
289
|
+
* SQL 两孪生那条整表 UPDATE 的 local 形。逐条走**同一只**纯改写函数,所以三条车道改出来的字节一致。
|
|
290
|
+
*
|
|
291
|
+
* 为什么是 store 上的一只动词、而不是外面一个文件走查:本店的账本有**两份**在场 —— 盘上的 jsonl
|
|
292
|
+
* 与 hydrate 进来的内存索引。外部走查只改得到前者,读腿读的却是后者(`getEvents` 从 Map 取),
|
|
293
|
+
* 于是迁移在本次进程里完全不生效、下次重启才"忽然"生效。两份一起改的唯一正确位置就是这里。
|
|
294
|
+
*
|
|
295
|
+
* 写盘用整文件原子替换(不是追加):改的是**已有行**,而 `AppendLog` 只会往后写。替换之前把**全部**
|
|
296
|
+
* append fd 撤掉(rename 之后旧 fd 指向的是被 unlink 的 inode,继续往那儿追加 = 之后的事件全部写进一个
|
|
297
|
+
* 没人看得见的文件),下一次 `appendEvent` 会按惰性规则重新开。撤 fd 复用**既有的那一只** {@link dispose}
|
|
298
|
+
* —— 刻意不在这里另写一段 close+catch:那会是本文件第二处同形的兜底臂,而规则要变少不要变多。
|
|
299
|
+
* 常态下这一撤是空操作:本动词跑在 boot 期,那时一个 append fd 都还没惰性开出来。
|
|
300
|
+
*
|
|
301
|
+
* 幂等:零命中 ⇒ 一个字节都不写、一只 fd 都不撤(第二跑零 I/O)。
|
|
302
|
+
* @returns 被改写的账本**行**数(不是 run 数)。
|
|
303
|
+
*/
|
|
304
|
+
migrateRetiredAskOriginRows(): Promise<number>;
|
|
287
305
|
/** Release every open event-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
|
|
288
306
|
dispose(): void;
|
|
289
307
|
}
|