@sema-agent/server 7.68.0 → 7.69.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 +19 -2
- package/dist/adoption/plan.js +11 -0
- package/dist/approval-card.d.ts +26 -0
- package/dist/approval-card.js +11 -0
- package/dist/approval.js +5 -4
- package/dist/boot/config-center.d.ts +6 -7
- package/dist/boot/config-center.js +31 -8
- package/dist/boot/leader.js +1 -1
- package/dist/boot/workflow-orchestration.d.ts +2 -0
- package/dist/boot/workflow-orchestration.js +10 -2
- package/dist/budget.js +1 -1
- package/dist/config-catalog.js +20 -1
- package/dist/config-center/apply-effective.d.ts +8 -1
- package/dist/config-center/apply-effective.js +23 -9
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/read-face.d.ts +30 -1
- package/dist/config-center/read-face.js +33 -15
- package/dist/config-center/restart-signal.js +2 -1
- package/dist/config-center/types.d.ts +20 -5
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +17 -2
- package/dist/config.d.ts +0 -21
- package/dist/config.js +19 -6
- package/dist/http/route-ctx.d.ts +39 -1
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/workflows.js +15 -7
- package/dist/http/server.d.ts +10 -2
- package/dist/http/server.js +70 -24
- package/dist/leader/wire.d.ts +9 -2
- package/dist/leader/wire.js +6 -6
- package/dist/main.js +6 -5
- package/dist/model-compat.d.ts +81 -0
- package/dist/model-compat.js +52 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/operator-ask.d.ts +46 -0
- package/dist/operator-ask.js +4 -0
- package/dist/orchestration/workflow-agent-session-index.d.ts +96 -0
- package/dist/orchestration/workflow-agent-session-index.js +146 -0
- package/dist/orchestration/workflow-journal-entry.d.ts +78 -0
- package/dist/orchestration/workflow-journal-entry.js +20 -0
- package/dist/orchestration/workflow-notify-journal.d.ts +43 -1
- package/dist/orchestration/workflow-notify-journal.js +43 -3
- package/dist/parked-decide.d.ts +115 -5
- package/dist/parked-decide.js +94 -26
- package/dist/plugins/pg-pool.js +8 -0
- package/dist/plugins/store-backend.d.ts +5 -0
- package/dist/plugins/store-backend.js +3 -1
- package/dist/plugins/tidb-pool.js +8 -0
- package/dist/plugins/workflow-journal-store-sql.js +10 -3
- package/dist/plugins/workflow-run-store-sql.d.ts +30 -0
- package/dist/plugins/workflow-run-store-sql.js +40 -0
- package/dist/router/route-orchestration.js +2 -1
- package/dist/run-local.js +1 -1
- package/dist/runs.js +2 -0
- package/dist/runtime-governance.js +5 -4
- package/dist/tool-approval.d.ts +20 -0
- package/dist/tool-approval.js +2 -1
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** 一条索引行:**子会话 id 是主键**(会话 id 全局唯一 ⇒ 读口天然是单行精确查,无分页假阴性)。 */
|
|
2
|
+
export interface WorkflowAgentSessionRow {
|
|
3
|
+
/** parked 的那只 workflow 子代自己的会话 id(= `WorkflowAgentRun.sessionId` = checkpoint 的 `sourceTaskId`)。 */
|
|
4
|
+
sessionId: string;
|
|
5
|
+
/** 它所属的 workflow run(`w*`)。 */
|
|
6
|
+
runId: string;
|
|
7
|
+
/** 该 agent 调用的稳定顺位身份(`WorkflowAgentRun.callKey`)—— 行自描述用,取 run 后仍以行内实体为准。 */
|
|
8
|
+
agentKey: string;
|
|
9
|
+
/** run 的租户键(`WorkflowRun.scope`)。 */
|
|
10
|
+
scope: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 索引面的两个动词。**写者只有一个**(见文件头),读者是判别器/收敛器/读面。
|
|
14
|
+
*
|
|
15
|
+
* `syncRun` 语义 = 「这条 run 当前的 parked 行集**就是** rows」:多出来的旧行删,rows 里的行 upsert。
|
|
16
|
+
* 幂等、可重放、与 run 的一次成功写同拍。
|
|
17
|
+
*/
|
|
18
|
+
export interface WorkflowAgentSessionIndex {
|
|
19
|
+
syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
|
|
20
|
+
findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
|
|
21
|
+
}
|
|
22
|
+
/** 派生的**唯一属主**:一只 `WorkflowRun` 当前的 parked 索引行。
|
|
23
|
+
*
|
|
24
|
+
* 三个键缺任何一个都不入索引 —— 缺 `sessionId` 的 park 行 join 不到(checkpoint 那侧的键就是它),
|
|
25
|
+
* 缺 `parkedCheckpointToken` 的行不是一条可赎回的 park(core 契约:在场 ⟺ `status:"parked"`)。
|
|
26
|
+
* ⚠️ token **不进索引行**:它是赎回能力,家在 run 的 blob 里(与 bg 行同判),索引只回答「去哪条 run 找」。 */
|
|
27
|
+
export declare function buildParkedAgentSessionRows(runId: string, run: {
|
|
28
|
+
scope: string;
|
|
29
|
+
agents?: ReadonlyArray<{
|
|
30
|
+
status?: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
callKey?: string;
|
|
33
|
+
parkedCheckpointToken?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}): WorkflowAgentSessionRow[];
|
|
36
|
+
/** 两处(装饰器的变更检测、local 后端的重放)共用的行集等值判据 —— 顺序无关的集合比较,单一属主。 */
|
|
37
|
+
export declare function sameAgentSessionRows(a: readonly WorkflowAgentSessionRow[], b: readonly WorkflowAgentSessionRow[]): boolean;
|
|
38
|
+
/** 账本一行的**内域形**:一次 `syncRun` 的整份行集快照(`{runId, rows}`)。 */
|
|
39
|
+
export interface WorkflowAgentSessionLedgerLine {
|
|
40
|
+
runId: string;
|
|
41
|
+
rows: WorkflowAgentSessionRow[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 盘上账本行 → 内域形的**唯一读者**(宪法 [ref] §2:进程外字节禁裸 `as` 铸形)。
|
|
45
|
+
*
|
|
46
|
+
* 姿态与 SQL 孪生逐字同源:那边 `findByAgentSession` 逐字段收窄(`String(r.session_id)` …),这边逐字段
|
|
47
|
+
* 判形 —— 一张索引面、两个后端、**一种**入域姿态。
|
|
48
|
+
*
|
|
49
|
+
* **整行成败制**(与既有的撕裂行臂同一条规则):一行 = 一次 syncRun 的**整份**快照,任一位读不出来就说明
|
|
50
|
+
* 这份快照重建不出来 ⇒ 整行不 apply(那条 run 保留上一份已知良好的快照)。不做「丢掉坏成员、留下好成员」
|
|
51
|
+
* ——那会静默铸出一份**比真相少几条 park** 的行集,而这条索引的消费面正是 park 的赎回路由。
|
|
52
|
+
* 读不出来 ⇒ 调用方**响亮**记一笔([ref]:安全轴不许静默),不吞整档。
|
|
53
|
+
*/
|
|
54
|
+
export declare function readAgentSessionLedgerLine(v: unknown): WorkflowAgentSessionLedgerLine | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* 单机车道的索引(`FileWorkflowRunStore` / `InMemoryWorkflowRunStore` 的同室孪生)。
|
|
57
|
+
*
|
|
58
|
+
* `root` 在场 ⇒ 追加式 JSONL 账本(与姊妹 {@link import("./workflow-notify-journal.js").FileWorkflowNotifyJournalStore}
|
|
59
|
+
* 逐字同形:0o700 目录 + 0o600 账本、按行重放、末行撕裂即丢),重启后 park 仍可 join;缺席 ⇒ 纯内存
|
|
60
|
+
* (`WORKFLOW_RUN_STORE=memory` 的显式易失车道,与它的 run 店同寿命)。
|
|
61
|
+
*
|
|
62
|
+
* 账本的一行 = **一次 `syncRun` 的整份行集**(按 runId 后写覆盖),所以重放即收敛,不需要压缩腿。
|
|
63
|
+
*/
|
|
64
|
+
export declare class LocalWorkflowAgentSessionIndex implements WorkflowAgentSessionIndex {
|
|
65
|
+
private readonly fsyncEnabled;
|
|
66
|
+
/** 坏账本行的**响亮**出口(可选;缺席 ⇒ 那一行照样不入索引,只是没人听见)。形与 workflow 店族的
|
|
67
|
+
* `onWarn` 逐字同源(`workflow-completion-inbox.ts` 的那一只),不另立第二种披露通道。 */
|
|
68
|
+
private readonly onWarn?;
|
|
69
|
+
private readonly ledgerPath;
|
|
70
|
+
private readonly byRun;
|
|
71
|
+
private readonly bySession;
|
|
72
|
+
private fd;
|
|
73
|
+
constructor(root?: string, fsyncEnabled?: boolean,
|
|
74
|
+
/** 坏账本行的**响亮**出口(可选;缺席 ⇒ 那一行照样不入索引,只是没人听见)。形与 workflow 店族的
|
|
75
|
+
* `onWarn` 逐字同源(`workflow-completion-inbox.ts` 的那一只),不另立第二种披露通道。 */
|
|
76
|
+
onWarn?: ((msg: string, meta: Record<string, unknown>) => void) | undefined);
|
|
77
|
+
/**
|
|
78
|
+
* 打开账本时把**撕裂尾**(崩在 append 中途、末尾没有换行的那截字节)截回最后一个完整行边界。
|
|
79
|
+
*
|
|
80
|
+
* 🔴 为什么必须在**任何 append 之前**做(codex 修复验证轮 [medium],故障注入复现):残尾留在盘上时,
|
|
81
|
+
* 下一次 `syncRun` 会紧贴着它落字节 ⇒ `<残尾><新记录>` 在 replay 里是**一行**,JSON 解析失败被整行丢掉。
|
|
82
|
+
* 丢的**不是**那截未提交的残尾(丢它是对的),而是紧随其后那次**成功重试**写下的行 —— 于是「写失败了
|
|
83
|
+
* 下一次写会重试救回来」这条本模块与 `syncAgentSessionIndex` 都在依赖的性质当场失效,park 索引静默消失。
|
|
84
|
+
* 截尾之后 append 永远从一个干净的行边界开始,重试才真的救得回来。
|
|
85
|
+
*
|
|
86
|
+
* 截掉的只可能是**未提交**的字节(一条记录的耐久性以它那个换行为界,与 notify journal 同一条崩溃序),
|
|
87
|
+
* 所以这里不是丢数据;但它是一次真实的崩溃留痕,**响亮**一次([ref]:安全轴不许静默)。
|
|
88
|
+
*/
|
|
89
|
+
private truncateTornTail;
|
|
90
|
+
private replay;
|
|
91
|
+
/** 内存两张表的收敛(重放与写路径共用一份逻辑)。 */
|
|
92
|
+
private apply;
|
|
93
|
+
syncRun(runId: string, rows: readonly WorkflowAgentSessionRow[]): Promise<void>;
|
|
94
|
+
findByAgentSession(sessionId: string): Promise<WorkflowAgentSessionRow | null>;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=workflow-agent-session-index.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { existsSync, fsyncSync, mkdirSync, openSync, readFileSync, truncateSync, writeSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function buildParkedAgentSessionRows(runId, run) {
|
|
4
|
+
const rows = [];
|
|
5
|
+
for (const a of run.agents ?? []) {
|
|
6
|
+
if (a.status !== "parked")
|
|
7
|
+
continue;
|
|
8
|
+
if (typeof a.sessionId !== "string" || a.sessionId === "")
|
|
9
|
+
continue;
|
|
10
|
+
if (typeof a.parkedCheckpointToken !== "string" || a.parkedCheckpointToken === "")
|
|
11
|
+
continue;
|
|
12
|
+
rows.push({ sessionId: a.sessionId, runId, agentKey: typeof a.callKey === "string" ? a.callKey : "", scope: run.scope });
|
|
13
|
+
}
|
|
14
|
+
return rows;
|
|
15
|
+
}
|
|
16
|
+
export function sameAgentSessionRows(a, b) {
|
|
17
|
+
if (a.length !== b.length)
|
|
18
|
+
return false;
|
|
19
|
+
const key = (r) => `${r.sessionId}\u0000${r.runId}\u0000${r.agentKey}\u0000${r.scope}`;
|
|
20
|
+
const left = new Set(a.map(key));
|
|
21
|
+
for (const r of b)
|
|
22
|
+
if (!left.has(key(r)))
|
|
23
|
+
return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
export function readAgentSessionLedgerLine(v) {
|
|
27
|
+
if (typeof v !== "object" || v === null)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!("runId" in v) || !("rows" in v))
|
|
30
|
+
return undefined;
|
|
31
|
+
const { runId, rows } = v;
|
|
32
|
+
if (typeof runId !== "string" || runId === "")
|
|
33
|
+
return undefined;
|
|
34
|
+
if (!Array.isArray(rows))
|
|
35
|
+
return undefined;
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const raw of rows) {
|
|
38
|
+
const row = readAgentSessionRow(raw);
|
|
39
|
+
if (row === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
out.push(row);
|
|
42
|
+
}
|
|
43
|
+
return { runId, rows: out };
|
|
44
|
+
}
|
|
45
|
+
function readAgentSessionRow(v) {
|
|
46
|
+
if (typeof v !== "object" || v === null)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (!("sessionId" in v) || !("runId" in v) || !("agentKey" in v) || !("scope" in v))
|
|
49
|
+
return undefined;
|
|
50
|
+
const { sessionId, runId, agentKey, scope } = v;
|
|
51
|
+
if (typeof sessionId !== "string" || sessionId === "")
|
|
52
|
+
return undefined;
|
|
53
|
+
if (typeof runId !== "string" || runId === "")
|
|
54
|
+
return undefined;
|
|
55
|
+
if (typeof agentKey !== "string")
|
|
56
|
+
return undefined;
|
|
57
|
+
if (typeof scope !== "string" || scope === "")
|
|
58
|
+
return undefined;
|
|
59
|
+
return { sessionId, runId, agentKey, scope };
|
|
60
|
+
}
|
|
61
|
+
export class LocalWorkflowAgentSessionIndex {
|
|
62
|
+
fsyncEnabled;
|
|
63
|
+
onWarn;
|
|
64
|
+
ledgerPath;
|
|
65
|
+
byRun = new Map();
|
|
66
|
+
bySession = new Map();
|
|
67
|
+
fd;
|
|
68
|
+
constructor(root, fsyncEnabled = true, onWarn) {
|
|
69
|
+
this.fsyncEnabled = fsyncEnabled;
|
|
70
|
+
this.onWarn = onWarn;
|
|
71
|
+
if (root === undefined) {
|
|
72
|
+
this.ledgerPath = undefined;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const dir = join(root, "workflow-agent-sessions");
|
|
76
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
77
|
+
this.ledgerPath = join(dir, "index.jsonl");
|
|
78
|
+
this.truncateTornTail();
|
|
79
|
+
this.replay();
|
|
80
|
+
}
|
|
81
|
+
truncateTornTail() {
|
|
82
|
+
const path = this.ledgerPath;
|
|
83
|
+
if (path === undefined || !existsSync(path))
|
|
84
|
+
return;
|
|
85
|
+
const bytes = readFileSync(path);
|
|
86
|
+
if (bytes.length === 0 || bytes[bytes.length - 1] === 0x0a)
|
|
87
|
+
return;
|
|
88
|
+
const keep = bytes.lastIndexOf(0x0a) + 1;
|
|
89
|
+
truncateSync(path, keep);
|
|
90
|
+
this.onWarn?.("workflow_agent_session_ledger_torn_tail_truncated", {
|
|
91
|
+
path,
|
|
92
|
+
droppedBytes: bytes.length - keep,
|
|
93
|
+
note: "the last append never finished (no trailing newline) — those uncommitted bytes were cut back to the last complete line so the NEXT append starts on a clean boundary. Without the cut, the retry that follows would be glued onto the broken suffix and dropped as one unparsable line, i.e. the retry — not the torn write — is what would be lost.",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
replay() {
|
|
97
|
+
if (this.ledgerPath === undefined || !existsSync(this.ledgerPath))
|
|
98
|
+
return;
|
|
99
|
+
for (const line of readFileSync(this.ledgerPath, "utf8").split("\n")) {
|
|
100
|
+
if (!line.trim())
|
|
101
|
+
continue;
|
|
102
|
+
let rec;
|
|
103
|
+
try {
|
|
104
|
+
rec = JSON.parse(line);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const entry = readAgentSessionLedgerLine(rec);
|
|
110
|
+
if (entry === undefined) {
|
|
111
|
+
this.onWarn?.("workflow_agent_session_ledger_line_rejected", {
|
|
112
|
+
path: this.ledgerPath,
|
|
113
|
+
note: "a committed ledger line did not read back as {runId, rows[]} — that run keeps its previous snapshot; a /decide for a park it should have indexed falls back to the legacy lane (honest miss, never a mis-route)",
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
this.apply(entry.runId, entry.rows);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
apply(runId, rows) {
|
|
121
|
+
for (const old of this.byRun.get(runId) ?? []) {
|
|
122
|
+
if (this.bySession.get(old.sessionId)?.runId === runId)
|
|
123
|
+
this.bySession.delete(old.sessionId);
|
|
124
|
+
}
|
|
125
|
+
if (rows.length === 0)
|
|
126
|
+
this.byRun.delete(runId);
|
|
127
|
+
else
|
|
128
|
+
this.byRun.set(runId, [...rows]);
|
|
129
|
+
for (const r of rows)
|
|
130
|
+
this.bySession.set(r.sessionId, r);
|
|
131
|
+
}
|
|
132
|
+
async syncRun(runId, rows) {
|
|
133
|
+
if (this.ledgerPath !== undefined) {
|
|
134
|
+
if (this.fd === undefined)
|
|
135
|
+
this.fd = openSync(this.ledgerPath, "a", 0o600);
|
|
136
|
+
writeSync(this.fd, `${JSON.stringify({ runId, rows })}\n`);
|
|
137
|
+
if (this.fsyncEnabled)
|
|
138
|
+
fsyncSync(this.fd);
|
|
139
|
+
}
|
|
140
|
+
this.apply(runId, rows);
|
|
141
|
+
}
|
|
142
|
+
async findByAgentSession(sessionId) {
|
|
143
|
+
return this.bySession.get(sessionId) ?? null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=workflow-agent-session-index.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-185(core 7.10.0 [ref] / B-049)—— `WorkflowJournalEntry` **两臂**的单点读写。
|
|
3
|
+
*
|
|
4
|
+
* core 7.10.0 把 journal 记录从一个 interface 换成了判别联合:
|
|
5
|
+
* `{ callKey, result }` —— 这一序数的子代跑完了,`result` 是它的 `TaskResult`;
|
|
6
|
+
* `{ callKey, parked }` —— 这一序数**停在耐久审批门上**,`parked` 是子代那份 `terminal.kind:"paused"`
|
|
7
|
+
* 的 `TaskResult`(令牌 / 门 / 钉住的 session / 花费都在里面),**没有 `result`**。
|
|
8
|
+
*
|
|
9
|
+
* 两臂的载荷同型(都是一只 `TaskResult`),差的只是**这一位是不是 park**。所以本模块把整条规则收敛成
|
|
10
|
+
* 两只互逆的函数:读一条记录 ⇒ `(payload, parked)` 二元组;拿到 `(payload, parked)` ⇒ 铸回记录。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **为什么必须单点**:本仓有三个消费面要判这一臂 —— SQL 店的写(`append`)、SQL 店的读(`load`)、
|
|
13
|
+
* HTTP journal 读面(`GET /v1/workflows/:id/journal`)。每处各写一次 `entry.parked !== undefined ? … : …`
|
|
14
|
+
* 就是同一条判别式的三个写者;core 哪天加第三臂(它已经加过一次),三处里漏改一处就是**静默丢一条
|
|
15
|
+
* 承重记录**——而 journal 是 LOAD-BEARING(丢一条 = 一次已完成的子代被当成没跑过而重跑,外部副作用重复)。
|
|
16
|
+
* 收敛到这里之后,第三臂到货 ⇒ 本文件**一处**编译红。
|
|
17
|
+
*
|
|
18
|
+
* 🔴 **park 臂的形是 fail-closed 的**:core 的类型要求 `parked.terminal.kind === "paused"`。从盘上读回来的
|
|
19
|
+
* 字节没有类型,所以 {@link journalEntryOfRow} 在铸 park 臂时**亲验**这一位,不合就**抛**——它是 LOAD-BEARING
|
|
20
|
+
* 面上的坏行(一条说自己 park 了却拿不出暂停终局的记录,既不能驱动、也不能当成跑完),吞掉它会让 resume
|
|
21
|
+
* 在一个被钉住的子代 session 旁边重新起一只活跑,正是 core 那条「一次性、响亮」的 schema 台阶要防的形。
|
|
22
|
+
*
|
|
23
|
+
* 🔴 **盘上的编码是那道台阶的一部分**,不是实现细节:见 {@link journalRowPayloadOf} 的顶注(park 载荷走
|
|
24
|
+
* `{"parked": …}` 信封,好让**回滚/混版**的老读者拿到一个没有 `terminal` 的对象、落进 core 的响亮拒绝臂,
|
|
25
|
+
* 而不是把一个暂停终局当成可重放结果)。
|
|
26
|
+
*/
|
|
27
|
+
import type { TaskResult, WorkflowJournalEntry } from "@sema-agent/core";
|
|
28
|
+
/** park 臂的载荷型(core 的 `WorkflowJournalEntry` park 成员逐字):一只 `terminal` 收敛到 `paused` 的 `TaskResult`。 */
|
|
29
|
+
export type ParkedTaskResult = TaskResult & {
|
|
30
|
+
terminal: Extract<TaskResult["terminal"], {
|
|
31
|
+
kind: "paused";
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* 读一条记录的两臂 —— **唯一**的判别点。`parked` 为真时 `payload` 是子代那份暂停终局,否则是它的完成结果。
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ 判据逐字照 core 自己的 file 店(`entry.parked !== undefined`),不是真值判:一只 `parked` 位上的
|
|
38
|
+
* 假值在 core 的类型上拼不出来,而按真值判会在将来某个 falsy 载荷上把 park 读成 result。
|
|
39
|
+
*/
|
|
40
|
+
export declare function journalEntryPayload(entry: WorkflowJournalEntry): {
|
|
41
|
+
payload: TaskResult;
|
|
42
|
+
parked: boolean;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* 盘上一行的 **payload 文本编码** —— SQL 店 `append` 的唯一写者。
|
|
46
|
+
*
|
|
47
|
+
* · result 臂 ⇒ `JSON.stringify(entry.result)`,与 7.68.0 及以前**逐字节相同**(历史行零迁移);
|
|
48
|
+
* · park 臂 ⇒ **信封** `{"parked": <TaskResult>}`。
|
|
49
|
+
*
|
|
50
|
+
* 🔴 **信封不是包装,它是 core 那道「一次性、响亮」的 schema 台阶在 SQL 店上的兑现**(codex 对抗复审
|
|
51
|
+
* r1 [high],验真后采纳)。core 的设计逐字是:park 序数journal 在 `parked` 键下、**没有 `result`**,
|
|
52
|
+
* 于是「一个本版本之前的 journal 读者找不到终局因由,会**拒绝**整次 resume(`WorkflowJournalIncompatibleError`)
|
|
53
|
+
* 而不是把一个暂停终局当成跑完了拿去重跑」。
|
|
54
|
+
*
|
|
55
|
+
* 本仓最初的实现把 park 载荷直接写进 `result` 列、判别位另开一列 —— 那**破坏了这条屏障**:一台回滚到
|
|
56
|
+
* 7.68.0(core 7.9.2)的副本、或滚动升级期的混版机队,它的 `load` 是 `SELECT call_key, result` + 逐行
|
|
57
|
+
* `{callKey, result: JSON.parse(result)}`,于是拿到一条**带 paused 终局的 result 臂**,照 core 的老语义
|
|
58
|
+
* 当作可重放结果 —— 在那个子代 session 已被一条 pending checkpoint 钉住的同时**再起一只活跑**,
|
|
59
|
+
* 批准前的外部副作用重复一遍。这正是那道屏障存在的理由。
|
|
60
|
+
*
|
|
61
|
+
* 信封形让老读者拿到一个**没有 `terminal`** 的对象 ⇒ 落进 core 那条「找不到终局因由 ⇒ 拒绝 resume」的
|
|
62
|
+
* 臂,方向与 core 的 file 店逐字一致。附带的好处是 **零 DDL、零重建库**:判别位活在载荷里,不占列。
|
|
63
|
+
*/
|
|
64
|
+
export declare function journalRowPayloadOf(entry: WorkflowJournalEntry): string;
|
|
65
|
+
/**
|
|
66
|
+
* {@link journalRowPayloadOf} 的逆 —— 盘上一行的 payload 文本 → 记录。**唯一**的回装点(SQL 店的 `load`
|
|
67
|
+
* 与 HTTP journal 读面共读它)。
|
|
68
|
+
*
|
|
69
|
+
* 判别式 = 解出来的对象上有没有**自有** `parked` 键。它与 `TaskResult` 结构上不相交(core 的 `TaskResult`
|
|
70
|
+
* 没有这个成员),所以历史字节(裸 TaskResult)恒判 result 臂 —— 不需要任何「老字节 = result」的兼容规则,
|
|
71
|
+
* 判别式自己就说清了。
|
|
72
|
+
*
|
|
73
|
+
* park 臂验 `terminal.kind === "paused"`;不合 ⇒ **抛**,由 LOAD-BEARING 的 `load` 把它当店故障传上去。
|
|
74
|
+
* 一条「说自己 park 了却拿不出暂停终局」的行既驱动不了、也不能当跑完,吞掉它会让 resume 在被钉住的子代
|
|
75
|
+
* session 旁边重新起一只活跑 —— 与上面那条屏障要防的是同一件事。
|
|
76
|
+
*/
|
|
77
|
+
export declare function journalEntryOfRow(callKey: string, json: string): WorkflowJournalEntry;
|
|
78
|
+
//# sourceMappingURL=workflow-journal-entry.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function journalEntryPayload(entry) {
|
|
2
|
+
return entry.parked !== undefined ? { payload: entry.parked, parked: true } : { payload: entry.result, parked: false };
|
|
3
|
+
}
|
|
4
|
+
export function journalRowPayloadOf(entry) {
|
|
5
|
+
const { payload, parked } = journalEntryPayload(entry);
|
|
6
|
+
return JSON.stringify(parked ? { parked: payload } : payload);
|
|
7
|
+
}
|
|
8
|
+
export function journalEntryOfRow(callKey, json) {
|
|
9
|
+
const raw = JSON.parse(json);
|
|
10
|
+
if (typeof raw === "object" && raw !== null && Object.hasOwn(raw, "parked")) {
|
|
11
|
+
const payload = raw.parked;
|
|
12
|
+
const terminal = payload.terminal;
|
|
13
|
+
if (terminal === undefined || terminal.kind !== "paused") {
|
|
14
|
+
throw new Error(`workflow journal: entry ${callKey} is recorded as PARKED but its stored terminal is "${terminal === undefined ? "absent" : terminal.kind}" (expected "paused") — refusing to admit it (a park row that cannot be driven must not be replayed as a completed result)`);
|
|
15
|
+
}
|
|
16
|
+
return { callKey, parked: { ...payload, terminal } };
|
|
17
|
+
}
|
|
18
|
+
return { callKey, result: raw };
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=workflow-journal-entry.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WorkflowRun, WorkflowRunStore, WorkflowRunStatus, WorkflowRunSummary } from "@sema-agent/core";
|
|
2
2
|
import { type FleetEventBus } from "../fleet/fleet-bus.js";
|
|
3
|
+
import { type WorkflowAgentSessionIndex } from "./workflow-agent-session-index.js";
|
|
3
4
|
/** A workflow completion notification's terminal payload — the bounded, redacted shape core's notifier seam
|
|
4
5
|
* carries, re-derivable from a durable {@link import("@sema-agent/core").WorkflowRun} on the recovery path. */
|
|
5
6
|
export interface WorkflowCompletionPayload {
|
|
@@ -272,6 +273,10 @@ export declare class JournalingWorkflowRunStore implements WorkflowRunStore {
|
|
|
272
273
|
/** MF-Fleet (data contract): publish the workflow's fleet row on every put/update (the same write-observation
|
|
273
274
|
* point as the notify journal). Optional — undefined ⇒ no fleet publish. */
|
|
274
275
|
private readonly fleetBus?;
|
|
276
|
+
/** S-185 车CM:workflow 出身 park 的 join 索引。**本装饰器是它全仓唯一的写者** —— 索引是 `WorkflowRun`
|
|
277
|
+
* 的派生投影,写点必须与 fleet 行投影/notify journal 同一个(见 index 模块的文件头「单写者」段)。
|
|
278
|
+
* 可选:缺席 ⇒ 一位不维护(与 fleet 半场同姿势,行为逐字不变)。 */
|
|
279
|
+
private readonly agentSessionIndex?;
|
|
275
280
|
constructor(inner: WorkflowRunStore,
|
|
276
281
|
/** [ref] optional since the fleet decoupling: undefined ⇒ no start-time journal (ephemeral memory
|
|
277
282
|
* backend has nothing to recover), the FLEET half still publishes — the panel is replica-local UI
|
|
@@ -280,7 +285,44 @@ export declare class JournalingWorkflowRunStore implements WorkflowRunStore {
|
|
|
280
285
|
gate?: Pick<WorkflowNotifyGate, "onWorkflowStart"> | undefined,
|
|
281
286
|
/** MF-Fleet (data contract): publish the workflow's fleet row on every put/update (the same write-observation
|
|
282
287
|
* point as the notify journal). Optional — undefined ⇒ no fleet publish. */
|
|
283
|
-
fleetBus?: FleetEventBus | undefined
|
|
288
|
+
fleetBus?: FleetEventBus | undefined,
|
|
289
|
+
/** S-185 车CM:workflow 出身 park 的 join 索引。**本装饰器是它全仓唯一的写者** —— 索引是 `WorkflowRun`
|
|
290
|
+
* 的派生投影,写点必须与 fleet 行投影/notify journal 同一个(见 index 模块的文件头「单写者」段)。
|
|
291
|
+
* 可选:缺席 ⇒ 一位不维护(与 fleet 半场同姿势,行为逐字不变)。 */
|
|
292
|
+
agentSessionIndex?: WorkflowAgentSessionIndex | undefined);
|
|
293
|
+
/** 上一次**本进程**为某条 run 写进索引的行集 —— 变更检测,不是缓存。
|
|
294
|
+
*
|
|
295
|
+
* 为什么需要:workflow 的进度写是每只 agent 状态变化一次,而 park 是罕见事件 ⇒ 无条件同步会给
|
|
296
|
+
* 「一条 park 都没有」的绝大多数 run 每次写多打一发删除(SQL)或多追加一行账本(file,无界增长)。
|
|
297
|
+
* 判据是**行集是否变化**,与后端无关 ⇒ 规则只有一条、两个后端同享。
|
|
298
|
+
* 跨副本安全性:一条 run 的写者按 core 契约唯一(`store` 的 owner),而同步只发生在一次**成功 CAS 之后**;
|
|
299
|
+
* 失败切换后新 owner 的表是空的 ⇒ 它的第一次写必落盘,收敛。
|
|
300
|
+
*
|
|
301
|
+
* 🔴 **空集也记**(7.69.0 合并重扫 c0):此前成功臂写作「空集 delete、非空集 set」,于是「一条 park
|
|
302
|
+
* 都没有」的那条 run —— 也就是上面这段头注**点名要短路的绝大多数形** —— memo 恒回 `undefined`、短路
|
|
303
|
+
* 判据恒假,每一次 put/update 都重跑一遍 `syncRun(id, [])`(file 车道每次多一行账本 + 一次 fsync,而
|
|
304
|
+
* 这本账本按设计没有压缩腿;SQL 车道每次多一发 `DELETE`)。代码与自己成文的判据相反。
|
|
305
|
+
* 当时那条 delete 臂承担的是**有界性**:memo 若无条件记,表会随本进程见过的每一条 run 单调增长。
|
|
306
|
+
* 现在有界性由下面那条**终态驱逐**独立承担(run 终态 ⇒ 它不会再被写 ⇒ 摘表),两件事各归各位:
|
|
307
|
+
* 记不记 = 变更检测的正确性;摘不摘 = 表的有界性。**不**加「首次空集跳过」这类依赖进程内历史的特判。 */
|
|
308
|
+
private readonly indexedRows;
|
|
309
|
+
/**
|
|
310
|
+
* 写腿的**不变式守卫**(codex r1 [medium],验真后修):`indexedRows` 只保留一次**我们亲眼看着落地**
|
|
311
|
+
* 的写。持久化抛错时这条 run 的盘上状态未知(而 core 会吞掉这次失败、这条 run 往往再也不被写),
|
|
312
|
+
* 若把 memo 留着,那一条 `{runId → rows}` 就永久留在表里 —— 终态驱逐等不到它的终态写。
|
|
313
|
+
*
|
|
314
|
+
* ⚠️ 如实登记的残余:一条**最后一次写成功、随后被放弃**的 run(进程崩前没跑到终态)仍会在本进程的
|
|
315
|
+
* memo 里留一条空集。它与 fleet 活行登记簿同一量级(都随本世进程消亡),刻意**不**为它加清扫腿:
|
|
316
|
+
* 那是给一个不产生错误答案的形加机制。
|
|
317
|
+
*/
|
|
318
|
+
private forgetIndexMemoIfWriteOutcomeUnknown;
|
|
319
|
+
/** 索引的一次收敛(put/update 成功后调用)。
|
|
320
|
+
*
|
|
321
|
+
* **自吞错**:索引是观察面的派生物,它的故障绝不能把一次已成功的 run 持久化变成失败 —— 与
|
|
322
|
+
* `publishFleet` 的姿态同源。但自吞 ≠ 静默([ref]):留痕走**登记过的** fail-open tag
|
|
323
|
+
* `server.workflow-park-index.sync-failed`(P-DEBT;射程、后果与欠账的唯一属主 = 那条词表项的
|
|
324
|
+
* `note`,census 里有对应行)。 */
|
|
325
|
+
private syncAgentSessionIndex;
|
|
284
326
|
/** Derive + publish the MF-Fleet workflow row from a WorkflowRun (doneCount/totalCount from agents, tokens from
|
|
285
327
|
* stats); a terminal workflow LEAVES the fleet (publish terminal then remove), a running one stays/updates. */
|
|
286
328
|
private publishFleet;
|
|
@@ -2,6 +2,8 @@ import { mkdirSync, readFileSync, existsSync, openSync, writeSync, fsyncSync, cl
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { buildFleetWorkflowRow } from "../fleet/fleet-bus.js";
|
|
4
4
|
import { isTerminalWorkflowRowStatus, seedTerminalWorkflowRow } from "../fleet/fleet-terminal-window.js";
|
|
5
|
+
import { buildParkedAgentSessionRows, sameAgentSessionRows } from "./workflow-agent-session-index.js";
|
|
6
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
5
7
|
const SUMMARY_MAX = 500;
|
|
6
8
|
export function recoverySummary(status, error) {
|
|
7
9
|
if (status === "completed")
|
|
@@ -271,10 +273,43 @@ export class JournalingWorkflowRunStore {
|
|
|
271
273
|
inner;
|
|
272
274
|
gate;
|
|
273
275
|
fleetBus;
|
|
274
|
-
|
|
276
|
+
agentSessionIndex;
|
|
277
|
+
constructor(inner, gate, fleetBus, agentSessionIndex) {
|
|
275
278
|
this.inner = inner;
|
|
276
279
|
this.gate = gate;
|
|
277
280
|
this.fleetBus = fleetBus;
|
|
281
|
+
this.agentSessionIndex = agentSessionIndex;
|
|
282
|
+
}
|
|
283
|
+
indexedRows = new Map();
|
|
284
|
+
async forgetIndexMemoIfWriteOutcomeUnknown(id, write) {
|
|
285
|
+
try {
|
|
286
|
+
return await write();
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
this.indexedRows.delete(id);
|
|
290
|
+
throw e;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async syncAgentSessionIndex(id, run) {
|
|
294
|
+
const index = this.agentSessionIndex;
|
|
295
|
+
if (index === undefined)
|
|
296
|
+
return;
|
|
297
|
+
const rows = buildParkedAgentSessionRows(id, run);
|
|
298
|
+
const prior = this.indexedRows.get(id);
|
|
299
|
+
if (prior === undefined || !sameAgentSessionRows(prior, rows)) {
|
|
300
|
+
try {
|
|
301
|
+
await index.syncRun(id, rows);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
this.indexedRows.delete(id);
|
|
305
|
+
recordFailOpen("server.workflow-park-index.sync-failed", `runId=${id} rows=${rows.length} err=${err instanceof Error ? err.message : String(err)}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (isTerminalWorkflowRowStatus(run.status))
|
|
310
|
+
this.indexedRows.delete(id);
|
|
311
|
+
else
|
|
312
|
+
this.indexedRows.set(id, rows);
|
|
278
313
|
}
|
|
279
314
|
publishFleet(id, run) {
|
|
280
315
|
if (!this.fleetBus)
|
|
@@ -298,11 +333,16 @@ export class JournalingWorkflowRunStore {
|
|
|
298
333
|
runId: id,
|
|
299
334
|
scope: run.scope,
|
|
300
335
|
});
|
|
301
|
-
await this.inner.put(id, run);
|
|
336
|
+
await this.forgetIndexMemoIfWriteOutcomeUnknown(id, () => this.inner.put(id, run));
|
|
337
|
+
await this.syncAgentSessionIndex(id, run);
|
|
302
338
|
this.publishFleet(id, run);
|
|
303
339
|
}
|
|
304
340
|
async update(id, scope, run, expect) {
|
|
305
|
-
const ok = await this.inner.update(id, scope, run, expect);
|
|
341
|
+
const ok = await this.forgetIndexMemoIfWriteOutcomeUnknown(id, () => this.inner.update(id, scope, run, expect));
|
|
342
|
+
if (!ok)
|
|
343
|
+
this.indexedRows.delete(id);
|
|
344
|
+
if (ok)
|
|
345
|
+
await this.syncAgentSessionIndex(id, run);
|
|
306
346
|
if (ok)
|
|
307
347
|
this.publishFleet(id, run);
|
|
308
348
|
return ok;
|
package/dist/parked-decide.d.ts
CHANGED
|
@@ -9,11 +9,59 @@
|
|
|
9
9
|
* 行永 parked、生命周期分叉)。
|
|
10
10
|
*/
|
|
11
11
|
import type { HostDecision } from "./host-decision.js";
|
|
12
|
-
import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, TaskRegistry, ToolSpec } from "@sema-agent/core";
|
|
12
|
+
import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, ResumeOutcome, TaskRegistry, ToolSpec, WorkflowAgentRun, WorkflowRun, WorkflowRunStore } from "@sema-agent/core";
|
|
13
|
+
import type { WorkflowAgentSessionIndex } from "./orchestration/workflow-agent-session-index.js";
|
|
13
14
|
export interface ParkedAgentMatch {
|
|
14
15
|
handle: string;
|
|
15
16
|
row: BackgroundAgentRecord;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* S-185 车CM —— **park 的出身判别式**(全仓唯一的那一个)。
|
|
20
|
+
*
|
|
21
|
+
* 一条 pending checkpoint 落到 `/decide` 时,它可能来自三种出身:①一只 parked 的**后台委派子代**
|
|
22
|
+
* ([ref] 的第一条腿)、②一只 parked 的 **workflow 子代**(core 7.10.0 [ref] 开的第二条车道)、③一条
|
|
23
|
+
* 普通的**任务级** suspend(legacy 腿)。三个消费点 —— decide 路由、SLA deny-sweep 的静默跳过臂、
|
|
24
|
+
* 将来的读面 —— 此前只认得出 ①,于是 ② 一律落到 ③ 上拿一个**响亮但错因**的 409
|
|
25
|
+
* `conflict.resume_context_unavailable`(getCtx(子会话) 恒 null),而 sweep 那条静默跳过臂对它整个失效。
|
|
26
|
+
*
|
|
27
|
+
* 修法**不是**再写一条 if 链,而是把「出身」抽成这一个判别式:它读两个源(bg 行店 / workflow 索引),
|
|
28
|
+
* 三个消费点都读它。加第三条车道时只改这里一处 —— 这条纪律就是本函数存在的全部理由。
|
|
29
|
+
*
|
|
30
|
+
* 两条臂共享同一条硬判据:**赎回键逐字相等**(bg 行的 `parkedCheckpointToken` / wa* 行的同名位 ⟺
|
|
31
|
+
* `cp.token`)。行 REOPENED 后在**新** token 下 re-park,旧 cp 的 decide 不得匹配到它;判别不出一律
|
|
32
|
+
* 诚实 miss(落 legacy 腿,它对已消失/不属于它的 pending cp 有自己的 404/409 处置)——「宁可不命中,
|
|
33
|
+
* 绝不错配」与收敛器那条铁则同源。
|
|
34
|
+
*
|
|
35
|
+
* 顺序 = bg 先、workflow 后:bg 臂是既有行为,零回归优先;两臂的键域(bg 行的 sessionId / wa* 行的
|
|
36
|
+
* sessionId)结构上互斥(一个子会话不会同时是两种子代),所以顺序只是确定性,不是优先级语义。
|
|
37
|
+
*/
|
|
38
|
+
export type ParkOrigin = {
|
|
39
|
+
kind: "background";
|
|
40
|
+
handle: string;
|
|
41
|
+
row: BackgroundAgentRecord;
|
|
42
|
+
}
|
|
43
|
+
/** `agent` 是**权威实体**(从 run 的 blob 里按赎回键复核出来的那一行);身份位一律从它读 —— 索引行
|
|
44
|
+
* 是可被改盘的派生投影,不做第二个身份写者(7.69.0 合并重扫 c1 同族收口)。 */
|
|
45
|
+
| {
|
|
46
|
+
kind: "workflow";
|
|
47
|
+
runId: string;
|
|
48
|
+
run: WorkflowRun;
|
|
49
|
+
agent: WorkflowAgentRun;
|
|
50
|
+
};
|
|
51
|
+
/** 判别式的两个源。各自可缺席(部署没接就是没接)—— 缺席 ⇒ 那条臂不判,不是失败。 */
|
|
52
|
+
export interface ParkOriginSources {
|
|
53
|
+
agentStore?: BackgroundAgentStore;
|
|
54
|
+
/** S-185:workflow 出身的**有界** join 索引(契约见 orchestration/workflow-agent-session-index.ts)。 */
|
|
55
|
+
workflowIndex?: WorkflowAgentSessionIndex;
|
|
56
|
+
/** 索引只回答「去哪条 run 找」,行本体(含赎回键)仍以 run 的 blob 为准 ⇒ 两件必须成对在场。 */
|
|
57
|
+
workflowRunStore?: Pick<WorkflowRunStore, "get">;
|
|
58
|
+
warn?: (event: string, fields: Record<string, unknown>) => void;
|
|
59
|
+
}
|
|
60
|
+
export declare function findParkOrigin(sources: ParkOriginSources, cp: {
|
|
61
|
+
token: string;
|
|
62
|
+
scope: string;
|
|
63
|
+
sessionId: string;
|
|
64
|
+
}): Promise<ParkOrigin | undefined>;
|
|
17
65
|
/**
|
|
18
66
|
* pending checkpoint → parked bg 行匹配。
|
|
19
67
|
*
|
|
@@ -101,13 +149,75 @@ export interface ParkedDecideResult {
|
|
|
101
149
|
status: number;
|
|
102
150
|
body: Record<string, unknown>;
|
|
103
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* 两条 park 车道(bg 赎回 / workflow 驱动)共用的 **`ResumeOutcome` 铸点**(S-185 车CM 单点化)。
|
|
154
|
+
*
|
|
155
|
+
* 判据一字未改,只是从 bg 腿里提出来:两条腿结算的是**同一种门**(core gateMatch 的 `policy_ask`),
|
|
156
|
+
* 各铸一份必然漂移,而漂的是「人到底批了什么」这条审计事实。
|
|
157
|
+
*
|
|
158
|
+
* · D-1 绑定:操作员在 GET 看到的 `boundCallId`/`boundInputHash` 原样透传,server **绝不重算**;
|
|
159
|
+
* 缺席时回落到行上 persisted 的那两位(core resume 侧做 fail-closed 等值校验)。
|
|
160
|
+
* · `reason` 是**存在性**判定不是真值判定 —— 显式空串("运维选择不写理由")与"没传"是两件事
|
|
161
|
+
* (姊妹 approval-hmac.ts `env.reason ?? null` 同判据)。
|
|
162
|
+
* · `hostDecision` = 结算**事实**逐字上 outcome;`gateOutcome` 一格 server **永不**填(core 对宿主
|
|
163
|
+
* 自铸的结算词是 pre-CAS 响亮拒)。
|
|
164
|
+
* · `answer` 原样透传([ref]:header 逐字匹配等语义校验全在 core,server 不预判)。
|
|
165
|
+
*/
|
|
166
|
+
export declare function buildParkedResumeOutcome(req: ParkedDecideRequest): ResumeOutcome;
|
|
167
|
+
/**
|
|
168
|
+
* S-185 车CM —— **第三条车道的计划**(纯函数;驱动本身归 resume 家族的那条腿,见下方「为什么不自建驱动」)。
|
|
169
|
+
*
|
|
170
|
+
* ## 这条车道为什么长这样(亲读 core 实现后的结论,不是从类型面推的)
|
|
171
|
+
* 任务书原案是「server 直接 `startWorkflow({resumeFromRunId, parkedResume})`」。亲读安装的 core
|
|
172
|
+
* `dist/orchestration/run-workflow-tool.js` 与 `dist/core/runner/prepare-caps-and-workflow.js` 证伪了它:
|
|
173
|
+
* · `startWorkflow(runner, fn, opts)` 的 `fn` 是**脚本函数**,由 `scriptRunner.run({scriptSource, primitives,
|
|
174
|
+
* scriptArgs})` 现编 —— server 手上只有持久化的 `WorkflowRun` 行,没有脚本源,也没有那一整套按 host
|
|
175
|
+
* 任务折出来的 governance/model/limits;自己拼一份 = 在 workflow 启动语义上多一个写者(本仓宪法明令禁止)。
|
|
176
|
+
* · core 给部署方留的**唯一**通道是 `RunInternals.workflowParkedResume`(d.ts 逐字:「A host that decided a
|
|
177
|
+
* workflow child's parked checkpoint **launches the run that re-invokes** `Workflow({resumeFromRunId})`
|
|
178
|
+
* with the decision here」),由 `prepare-caps-and-workflow` 折进 Workflow 工具的 `parkedResume` dep,
|
|
179
|
+
* 再进 `RunWorkflowOptions.parkedResume`。core 自己给模型的 park 回执也是这句:「once the approval is
|
|
180
|
+
* decided, the deployment resumes this run (resumeFromRunId: …)」。
|
|
181
|
+
* ⇒ 第三条车道 = **把决定挂在宿主会话的下一跑上**,而那一跑必须走 resume 家族**既有**的那条腿
|
|
182
|
+
* (`notify-wake.ts` 的成文纪律:wake 会烧模型,所以复用 `ctx.legs.resumeWake`,「而不是在本域自建第二条
|
|
183
|
+
* 驱动路径」)。本函数因此只出**计划**:宿主会话 + 要注入的消息 + 要随 internals 走的那一条决定。
|
|
184
|
+
*
|
|
185
|
+
* ## 诚实边界(登记,不假装没有)
|
|
186
|
+
* · **模型中介**:决定送达宿主之后,真正的赎回发生在宿主再调一次 `Workflow({resumeFromRunId})` 时。
|
|
187
|
+
* core 的 park 回执逐字告诉模型这件事,但它不是一条确定性的机器腿。宿主没照做 ⇒ 卡仍 pending、
|
|
188
|
+
* 人可再决(**不会**静默吃掉批准)。
|
|
189
|
+
* · **`inheritedGate` 不供**:那是「跨进程重供 checkpoint 的父约束」的席位,而 wa* 行不带父约束链;
|
|
190
|
+
* 同进程时 core 的内存 registry 自动重供,跨进程且该 cp 带 `requiresParentConstraint` 时 core 响亮拒
|
|
191
|
+
* (`resume.parent_constraint_missing`)⇒ 该 ordinal 再 park 一次,不会 fail-open。
|
|
192
|
+
*/
|
|
193
|
+
/**
|
|
194
|
+
* 🔴 拒臂就是**一条 `ParkedDecideResult`**(7.69.0 合并重扫 c2/c7)。此前它把 `status`/`body` 平铺在
|
|
195
|
+
* 判别对象上(`{ok:false, status, body}`),于是本文件里同时存在两种「HTTP 错误结果」的写法,而
|
|
196
|
+
* `test/error-code-catalog-live.test.ts` 的目录抽取器只认这一族既有的 `{ status: 4xx, body: {…} }` 形
|
|
197
|
+
* (它的臂③ 顶注逐字点名 parked-decide)——两条新码因此在 wire 上发得出、附录 A 查不到、机器门还看不见。
|
|
198
|
+
* 收法是让**返回形回到同一种**(本文件只留一个 HTTP 结果类型),不是给扫描器加第九条臂:
|
|
199
|
+
* 后者会让「换个包装就隐形」这条病继续存在,只是多一条特判去追它。
|
|
200
|
+
*/
|
|
201
|
+
export type WorkflowParkedDecidePlan = {
|
|
202
|
+
ok: true;
|
|
203
|
+
hostSessionId: string;
|
|
204
|
+
runId: string;
|
|
205
|
+
message: string;
|
|
206
|
+
outcome: ResumeOutcome;
|
|
207
|
+
} | {
|
|
208
|
+
ok: false;
|
|
209
|
+
deny: ParkedDecideResult;
|
|
210
|
+
};
|
|
211
|
+
export declare function planWorkflowParkedDecide(origin: Extract<ParkOrigin, {
|
|
212
|
+
kind: "workflow";
|
|
213
|
+
}>, req: ParkedDecideRequest): WorkflowParkedDecidePlan;
|
|
104
214
|
/**
|
|
105
215
|
* `/decide` parked 赎回腿([ref] 件3d server 半场;[ref]/[ref]/[ref] core 裁定形)。
|
|
106
216
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
217
|
+
* 调用方先经 {@link findParkOrigin} 判出 `kind:"background"` 再进来(S-185 车CM:判别只做一次);
|
|
218
|
+
* 返回的结果对象 = 赎回腿已受理/已拒,调用方直接回 HTTP。
|
|
109
219
|
*
|
|
110
|
-
*
|
|
220
|
+
* 链:`claimParkedAgent`(access = 行镜像双轴
|
|
111
221
|
* `{scope: row.scope, owner: row.owner}`,[ref] Q3)→ outcome 铸造(D-1 echo/persisted 回落)
|
|
112
222
|
* → 裸 `reviveTool.execute(args, ctx.reviveClaim)`。execute 一旦发起 drive,rollback 全在 core 内
|
|
113
223
|
* (driveParkedResume);server 只在「claim 成功但 drive 从未发起」窗口补 `rollbackParkedClaim`
|
|
@@ -116,5 +226,5 @@ export interface ParkedDecideResult {
|
|
|
116
226
|
* 200 是**受理**语义(drive 是 core 内 fire-and-forget):失败经行/notification 面 + REOPENED
|
|
117
227
|
* re-park 诚实可见,操作员可再决。
|
|
118
228
|
*/
|
|
119
|
-
export declare function decideParkedAgent(deps: ParkedDecideDeps, req: ParkedDecideRequest): Promise<ParkedDecideResult
|
|
229
|
+
export declare function decideParkedAgent(deps: ParkedDecideDeps, req: ParkedDecideRequest, match: ParkedAgentMatch): Promise<ParkedDecideResult>;
|
|
120
230
|
//# sourceMappingURL=parked-decide.d.ts.map
|