@sema-agent/server 7.36.0 → 7.37.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 +9 -0
- package/dist/boot/resolve-spec.js +6 -1
- package/dist/boot/runner-deps.d.ts +21 -3
- package/dist/boot/runner-deps.js +31 -4
- package/dist/boot/session-faces.d.ts +3 -0
- package/dist/boot/session-faces.js +11 -2
- package/dist/boot/side-query-lane.d.ts +77 -54
- package/dist/boot/side-query-lane.js +102 -66
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +19 -1
- package/dist/boot/task-list-lane.d.ts +92 -0
- package/dist/boot/task-list-lane.js +63 -0
- package/dist/bounded-session-map.d.ts +3 -0
- package/dist/bounded-session-map.js +5 -0
- package/dist/capabilities/scenarios.d.ts +30 -2
- package/dist/capabilities/scenarios.js +16 -3
- package/dist/config-types.d.ts +21 -1
- package/dist/config.js +8 -1
- package/dist/hooks/branch-transcript.d.ts +3 -2
- package/dist/hooks/branch-transcript.js +7 -1
- package/dist/http/active-run-conflict.d.ts +43 -0
- package/dist/http/active-run-conflict.js +8 -0
- package/dist/http/route-ctx.d.ts +22 -1
- package/dist/http/routes/approvals-assistant.js +13 -1
- package/dist/http/routes/notify-wake.js +6 -0
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +30 -2
- package/dist/http/server.js +103 -7
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/main.js +11 -3
- package/dist/memory-posture.d.ts +12 -1
- package/dist/memory-posture.js +2 -0
- package/dist/observability/fail-open.d.ts +9 -1
- package/dist/observability/fail-open.js +9 -1
- package/dist/plugins/memory-engine-pg.js +42 -4
- package/dist/plugins/memory-engine-tidb.js +38 -4
- package/dist/plugins/memory-origin-law.d.ts +69 -0
- package/dist/plugins/memory-origin-law.js +98 -0
- package/dist/plugins/retention-store-sql.d.ts +7 -0
- package/dist/plugins/retention-store-sql.js +24 -0
- package/dist/plugins/task-list-store-sql.d.ts +36 -25
- package/dist/plugins/task-list-store-sql.js +102 -0
- package/dist/run-local.js +14 -5
- package/dist/runs.js +17 -0
- package/dist/trace/engine-notice-wire.d.ts +128 -0
- package/dist/trace/engine-notice-wire.js +256 -0
- package/dist/trace/ledger-events.d.ts +11 -1
- package/dist/trace/ledger-sink.d.ts +17 -0
- package/dist/trace/ledger-sink.js +25 -0
- package/package.json +2 -2
|
@@ -14,11 +14,12 @@ export declare const RENDERED_ENTRY_TYPES: readonly ["message", "compaction", "c
|
|
|
14
14
|
/**
|
|
15
15
|
* 跳过的理由逐型给(「纯控制型」不是一句话,是逐个核过模型看不见):
|
|
16
16
|
* thinking_level_change / model_change / label / session_info / leaf / prompt_epoch /
|
|
17
|
-
* announced_listing / workspace_state / git_announcement —— 控制面元数据,core 折
|
|
17
|
+
* announced_listing / workspace_state / git_announcement / reminder_mark —— 控制面元数据,core 折
|
|
18
|
+
* messages 时不产出内容;
|
|
18
19
|
* custom —— data-only(session.js 折 messages 只认 message/custom_message 两型),模型看不见它的
|
|
19
20
|
* data;把它渲出来反而是把模型没见过的东西冒充成证据,与 [2092] 是同一判据的反方向。
|
|
20
21
|
*/
|
|
21
|
-
export declare const SKIPPED_ENTRY_TYPES: readonly ["thinking_level_change", "model_change", "label", "session_info", "leaf", "prompt_epoch", "announced_listing", "workspace_state", "git_announcement", "custom"];
|
|
22
|
+
export declare const SKIPPED_ENTRY_TYPES: readonly ["thinking_level_change", "model_change", "label", "session_info", "leaf", "prompt_epoch", "announced_listing", "workspace_state", "git_announcement", "reminder_mark", "custom"];
|
|
22
23
|
/** 渲染结果。**判别式**:证据不足时调用方拿不到 `text`,想用也用不了(见顶注)。 */
|
|
23
24
|
export type BranchTranscript = {
|
|
24
25
|
sufficient: true;
|
|
@@ -37,7 +37,8 @@ export const RENDERED_ENTRY_TYPES = ["message", "compaction", "custom_message"];
|
|
|
37
37
|
/**
|
|
38
38
|
* 跳过的理由逐型给(「纯控制型」不是一句话,是逐个核过模型看不见):
|
|
39
39
|
* thinking_level_change / model_change / label / session_info / leaf / prompt_epoch /
|
|
40
|
-
* announced_listing / workspace_state / git_announcement —— 控制面元数据,core 折
|
|
40
|
+
* announced_listing / workspace_state / git_announcement / reminder_mark —— 控制面元数据,core 折
|
|
41
|
+
* messages 时不产出内容;
|
|
41
42
|
* custom —— data-only(session.js 折 messages 只认 message/custom_message 两型),模型看不见它的
|
|
42
43
|
* data;把它渲出来反而是把模型没见过的东西冒充成证据,与 [2092] 是同一判据的反方向。
|
|
43
44
|
*/
|
|
@@ -55,6 +56,11 @@ export const SKIPPED_ENTRY_TYPES = [
|
|
|
55
56
|
// 内容:真正被看见的那份帧正文骑在它 `entryId` 指的那条 `message` 上(已由渲染臂收下)。把这条快照渲进
|
|
56
57
|
// 证据面 = 把一串 sha256 摘要当成对话冒充给评估者,正是 [2092] 判据的反方向。
|
|
57
58
|
"git_announcement",
|
|
59
|
+
// core 5.46.0(design/319 A 件):会话的**提醒出处标记**条目(`{type:"reminder_mark", mark}`)。同族
|
|
60
|
+
// 控制面元数据 —— 它是「本会话的引擎提醒/系统提示声明用的是哪一枚 mark」的记号,模型看见的是**别处**
|
|
61
|
+
// 那些带 mark 的提醒正文本身(已由渲染臂按 message 收下),这条条目本身从不进 `convertToLlm`。
|
|
62
|
+
// 把一枚不透明 token 渲进证据面 = 拿模型没见过的东西冒充证据([2092] 判据的反方向)。
|
|
63
|
+
"reminder_mark",
|
|
58
64
|
"custom",
|
|
59
65
|
];
|
|
60
66
|
/** 单次评估送进模型的 transcript 字符上限。超出保留**尾部**(最近的对话对"条件是否已达成"更相关)。 */
|
|
@@ -95,6 +95,28 @@ export interface ActiveRunConflictBody {
|
|
|
95
95
|
* best-effort**:缺席 = 无法证明(跨副本/重启后),不是「不活」——诚实缺席,绝不铸 0。消费端
|
|
96
96
|
* (cli running 臂,[3779] C3)凭它区分「真忙的后台 run」与「久无进展的疑似僵局」,替代猜测文案。 */
|
|
97
97
|
msSinceLastActivity?: number;
|
|
98
|
+
/**
|
|
99
|
+
* #316(c)([4664]② 成文的洞 (c) 的修):**两面口径的对齐位**。
|
|
100
|
+
*
|
|
101
|
+
* 病:poll 面(`GET /v1/runs/:id`)对「`running` 且 `now - updatedAt > runStaleSec`」的行**折叠**成
|
|
102
|
+
* `status:"failed"`(+「run stalled (instance lost?)」),而本体把 run 行 `status` **原样**报 `running`。
|
|
103
|
+
* ⇒ 自愈窗内(心跳 30s 断供 + `REAP_RUN_STALE_SEC` 默认 120s + `REAP_INTERVAL_SEC` 默认 60s ⇒ SQL 车道
|
|
104
|
+
* 上界 ≈180s;LOCAL/file 车道无周期腿,上界 = ∞)同一条死 run 两面自相矛盾:409 说「还在跑,去
|
|
105
|
+
* steer/cancel」,poll 说「已经 failed」。壳只能靠猜。
|
|
106
|
+
*
|
|
107
|
+
* 🔴 **刻意不学 poll 面谎报终态**:409 的 `activeTaskStatus` 是 run 行的**真**状态,而且这条 run 的
|
|
108
|
+
* session claim **确实**还攥着(reaper 还没跑);把它折成 `failed` 会造出「行说 failed 但锁还在、
|
|
109
|
+
* 新提交照旧 409」的第二种自相矛盾。改用 additive 的判别位:`stale:true` 与既有 `activeTaskStatus` /
|
|
110
|
+
* `msSinceLastActivity` **并存**,壳凭它把两面对上(「poll 那边会说 failed = 这条大概率是死行」)。
|
|
111
|
+
*
|
|
112
|
+
* 🔴 **只在为真时在场**(与 `PendingGateMaterial.governanceForced` 同一条纪律):缺席 = 「没有陈旧的
|
|
113
|
+
* 证据」,**不是**「证明它活着」——判据只有 `updated_at` 一维,慢而活的 run 有误杀窗([4664]② 洞 (b)
|
|
114
|
+
* 原样有效)。⇒ 这一位是**分诊材料**,不是终态断言。
|
|
115
|
+
*
|
|
116
|
+
* 🔴 **只对 `running` 铸**:park 两态(suspended/needs_review)根本不在 `reapStale` 射程内([4664]②
|
|
117
|
+
* 洞 (a)),给它们标 stale 就是假材料。
|
|
118
|
+
*/
|
|
119
|
+
stale?: true;
|
|
98
120
|
pendingGate?: PendingGateMaterial;
|
|
99
121
|
}
|
|
100
122
|
/** gate.kind → 它的那一个 resume 入口(sessionId/taskId 寻址,无秘密)。 */
|
|
@@ -122,6 +144,20 @@ export interface ActiveRunConflictDoneResult {
|
|
|
122
144
|
activeTaskStatus?: string;
|
|
123
145
|
/** #245 S1:与 409 body 同键同语义(见 {@link ActiveRunConflictBody.msSinceLastActivity})。 */
|
|
124
146
|
msSinceLastActivity?: number;
|
|
147
|
+
/**
|
|
148
|
+
* #316(c):与 409 body 同键同语义(见 {@link ActiveRunConflictBody.stale})。
|
|
149
|
+
*
|
|
150
|
+
* 🔴 **跨仓欠账,如实登记**(codex 对抗复审 R1-[medium] 验真后的真形):sema-sdk 的
|
|
151
|
+
* `spec/openapi.yaml` 里 `ActiveRunConflictDoneResult` 是 `additionalProperties: false` 的**封闭**
|
|
152
|
+
* schema,今天只声明 `status/errorCode/errorMessage/activeTaskId/activeTaskStatus/pendingGate` 六键。
|
|
153
|
+
* ⚠️ 亲验后的关键修正:这**不是本键引入的新问题** —— `msSinceLastActivity`(#245 S1)早就在这条
|
|
154
|
+
* done 帧上,同样没进那张封闭表,即**同族违例已存在**且没有任何门看得见它(producer-contract 只校
|
|
155
|
+
* 409 体那条 op,不校这条 SSE 帧)。本批**刻意不**为了绕开它而让 done 帧与 409 体分家(「同材料禁
|
|
156
|
+
* 分家」是本文件自己的纪律,分家换来的是两面口径再次相左 —— 正是本件在修的病)。
|
|
157
|
+
* ⇒ 跟车件已上报:sema-sdk 侧把 `msSinceLastActivity` + `stale` 一并补进该 schema,并给
|
|
158
|
+
* producer-contract 加一条**真** SSE 冲突 done 帧的校验格(今天那条空白正是两键都能悄悄漂出去的原因)。
|
|
159
|
+
*/
|
|
160
|
+
stale?: true;
|
|
125
161
|
pendingGate?: PendingGateMaterial;
|
|
126
162
|
}
|
|
127
163
|
export declare function toDoneFrameResult(body: ActiveRunConflictBody): ActiveRunConflictDoneResult;
|
|
@@ -129,9 +165,12 @@ interface ConflictProbeDeps<TToken> {
|
|
|
129
165
|
/** 结构形入参,但 `status` 取 run 行的**闭词表**(合并码重扫):判 park 要走词表属主
|
|
130
166
|
* `isParkedRunStatus`,而它的入参是穷举联合 —— 这一格写成开放 `string` 就只能在调用点裸 cast,
|
|
131
167
|
* 绕开那道编译期执法(core 加一个 park 类成员时本文件不会红)。 */
|
|
168
|
+
/** `updatedAt`(#316(c)):stale 折叠的唯一判据维([4664]② 洞 (b):慢而活的 run 有误杀窗)。可选 —— 缺席
|
|
169
|
+
* 或不可解析 ⇒ `stale` 键诚实缺席(与本模块「材料是 best-effort 增强」同方向)。 */
|
|
132
170
|
runStore?: {
|
|
133
171
|
getRun?: (id: string) => Promise<{
|
|
134
172
|
status?: RunRecord["status"];
|
|
173
|
+
updatedAt?: string;
|
|
135
174
|
} | null | undefined>;
|
|
136
175
|
} | undefined;
|
|
137
176
|
checkpointStore?: {
|
|
@@ -153,6 +192,10 @@ interface ConflictProbeDeps<TToken> {
|
|
|
153
192
|
} | undefined;
|
|
154
193
|
/** #220:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席 —— 与 best-effort 同方向)。 */
|
|
155
194
|
governance?: GovernancePosture | undefined;
|
|
195
|
+
/** #316(c):stale 折叠的阈值(秒),生产装配 = `config.runStaleSec` —— 与 poll 面
|
|
196
|
+
* (`routes/runs.ts` 的 `deps.config.runStaleSec * 1000`)**同一个旋钮**,两面口径因此同源。
|
|
197
|
+
* 缺席/非正数 ⇒ 不折叠(键缺席),等价于「本部署没有配这条判据」。 */
|
|
198
|
+
runStaleSec?: number | undefined;
|
|
156
199
|
/** #245 S1:turn 活性读口(生产装配 = `readTurnActivityMs`,turn-activity.ts)。返回最后活性时刻
|
|
157
200
|
* (epoch ms)或 undefined。可选:未装配/读不到 ⇒ `msSinceLastActivity` 键缺席,best-effort 同方向。 */
|
|
158
201
|
turnActivity?: ((taskId: string) => number | undefined) | undefined;
|
|
@@ -140,6 +140,7 @@ export function toDoneFrameResult(body) {
|
|
|
140
140
|
activeTaskId: body.activeTaskId,
|
|
141
141
|
...(body.activeTaskStatus !== undefined ? { activeTaskStatus: body.activeTaskStatus } : {}),
|
|
142
142
|
...(body.msSinceLastActivity !== undefined ? { msSinceLastActivity: body.msSinceLastActivity } : {}),
|
|
143
|
+
...(body.stale !== undefined ? { stale: body.stale } : {}), // #316(c):SSE done 帧与 409 体同材料,禁分家
|
|
143
144
|
...(body.pendingGate !== undefined ? { pendingGate: body.pendingGate } : {}),
|
|
144
145
|
};
|
|
145
146
|
}
|
|
@@ -162,11 +163,18 @@ export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
|
|
|
162
163
|
if (status === "running") {
|
|
163
164
|
// #245 S1:活性证据只在 running 形铸(parked 行的状态本身就是证据);读口失败与缺席同路(best-effort)。
|
|
164
165
|
const lastAt = deps.turnActivity?.(activeTaskId);
|
|
166
|
+
// #316(c):与 poll 面**逐字同一条**判据(`run.status === "running" && now - updatedAt > runStaleSec*1000`,
|
|
167
|
+
// 见 routes/runs.ts)。差别只在**结论**:poll 面折成终态,本面加位不改状态(理由见 `stale` 顶注)。
|
|
168
|
+
// 阈值/时间戳任一缺席或不可解析 ⇒ 键缺席(best-effort,绝不猜)。
|
|
169
|
+
const updatedAtMs = row?.updatedAt !== undefined ? new Date(row.updatedAt).getTime() : Number.NaN;
|
|
170
|
+
const staleMs = (deps.runStaleSec ?? 0) * 1000;
|
|
171
|
+
const stale = staleMs > 0 && Number.isFinite(updatedAtMs) && Date.now() - updatedAtMs > staleMs;
|
|
165
172
|
return {
|
|
166
173
|
...base,
|
|
167
174
|
error: "session already has an active run — POST /v1/runs/{activeTaskId}/steer injects a message into the running turn (queued, applied at the next turn boundary), or POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)",
|
|
168
175
|
activeTaskStatus: status,
|
|
169
176
|
...(lastAt !== undefined ? { msSinceLastActivity: Math.max(0, Date.now() - lastAt) } : {}),
|
|
177
|
+
...(stale ? { stale: true } : {}),
|
|
170
178
|
};
|
|
171
179
|
}
|
|
172
180
|
// parked:找 pending checkpoint 的门型,指到它的那一个 resume 入口
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -118,6 +118,24 @@ export interface DriveResumeArgs {
|
|
|
118
118
|
mintFreshRun?: {
|
|
119
119
|
source: string;
|
|
120
120
|
};
|
|
121
|
+
/** #316([4660] Inkglow 案 → [4664]① 定谳):**200 受理语义**——调用方声明「我的消费方按 poll/SSE 跟
|
|
122
|
+
* 终态,别让 HTTP 等模型往返」。在场为 `true` 时本腿在**受理点**(`resumeStream` 解析之后)立刻把
|
|
123
|
+
* `200 {taskId, status:"resuming", bindingEnforced:true}` 交回调用方,模型腿(流迭代 + 行驱动)继续
|
|
124
|
+
* 在同一条 promise 链上跑完 —— 不是新写口,逐字还是那条 `createSerialLedgerAppend` 单写串行链。
|
|
125
|
+
*
|
|
126
|
+
* 🔴 **受理点为什么在那里**(判据,不是随手挑的):`resumeStream` 内部跑完 core 的 pre-CAS 守卫 + 原子
|
|
127
|
+
* CAS(本文件下游注与 core 的 E23 记账都逐字这么写),它一解析,这条 run 已经在执行、也已经没有任何
|
|
128
|
+
* 「会变成拒绝」的判定留在后面。⇒ 受理点之前的三类判定**全部保持同步**:lease admission(429)、
|
|
129
|
+
* markResuming CAS(409 `conflict.not_resumable`)、pre-CAS CheckpointError(绑定不符 409 /
|
|
130
|
+
* 卡不在 404)。异步化的只有模型往返段。
|
|
131
|
+
*
|
|
132
|
+
* ⚠️ **verify 腿(`verifyRounds` 在场)不受本位影响**:core 的 `resumeWithVerification` 是一次整包
|
|
133
|
+
* await,没有「CAS 已过、模型还没跑」这个缝可停 —— 在那条臂上提前受理就等于把绑定判别也异步化了
|
|
134
|
+
* (那是决策证明的一部分,不能丢)。故它维持同步长调用形,如实登记在 CHANGELOG/契约文档。
|
|
135
|
+
*
|
|
136
|
+
* 今天唯一传 `true` 的调用方 = legacy 任务级 `/v1/approvals/:sessionId/decide`(那条 60s 超时腿)。
|
|
137
|
+
* D-D SLA deny-sweep / preempt / plan_review / wake 四条同族腿**不设**此位(见各自路由处的 #316 登记注)。 */
|
|
138
|
+
acceptEarly?: boolean;
|
|
121
139
|
}
|
|
122
140
|
/** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
|
|
123
141
|
* run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
|
|
@@ -143,7 +161,10 @@ export interface RouteLegs {
|
|
|
143
161
|
* `settledBy` = **这次结算的出处**(core `ApprovalSettledBy`),必填:本腿有两个调用方(HTTP
|
|
144
162
|
* `/decide` = 人;内部 D-D SLA deny-sweep = 窗到期),而函数内部分辨不出谁在叫它 —— 必填参数把
|
|
145
163
|
* 「漏报出处」变成编译错,不是一条下游把「没人答」渲染成「有人拒」的静默假出处。 */
|
|
146
|
-
resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, settledBy: ApprovalSettledBy, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding, onResumeCommitted?: (overrideSessionId?: string) => Promise<void
|
|
164
|
+
resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, settledBy: ApprovalSettledBy, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding, onResumeCommitted?: (overrideSessionId?: string) => Promise<void>,
|
|
165
|
+
/** #316:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/decide` 腿传 `true`;
|
|
166
|
+
* 内部 D-D SLA deny-sweep 不传 —— 它没有在等响应的人,提前受理对它零收益而少一条同步失败面。 */
|
|
167
|
+
acceptEarly?: boolean): Promise<{
|
|
147
168
|
status: number;
|
|
148
169
|
body: object;
|
|
149
170
|
}>;
|
|
@@ -388,6 +388,11 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
388
388
|
sendError(res, 404, "not_found.run", "task not found");
|
|
389
389
|
return;
|
|
390
390
|
}
|
|
391
|
+
// 🕐 **#316 同族登记(本批不改行为)**:这条腿与 legacy `/decide` 同病 —— `driveResumeIntoRunLog`
|
|
392
|
+
// 在响应之前同步跑完整条 resume 流,响应时长 = 模型往返、无服务端上限。#316 只对 legacy decide 腿
|
|
393
|
+
// 做了 200 受理化(那是 [4660] 雪崩的实测首环),本腿维持终局形:**wire BREAKING 需要消费方先表态**
|
|
394
|
+
// (能力显式表态制,[3780] 裁 A)。候消费方认领后同形改造 —— 改法现成:`resumePreempted` 一路把
|
|
395
|
+
// `acceptEarly: true` 透到 `driveResumeIntoRunLog` 即可(受理点/判定边界全在 `DriveResumeArgs.acceptEarly`)。
|
|
391
396
|
const out = await resumePreempted(run.sessionId, req); // gate-kind guard + drive into the run log (shared)
|
|
392
397
|
sendJson(res, out.status, out.body);
|
|
393
398
|
return;
|
|
@@ -485,6 +490,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
485
490
|
sendError(res, 404, "not_found.run", "task not found");
|
|
486
491
|
return;
|
|
487
492
|
}
|
|
493
|
+
// 🕐 **#316 同族登记(本批不改行为)**:同 `/resume` 腿那条注 —— 本腿同样在响应前同步跑完整条
|
|
494
|
+
// resume 流(时长 = 模型往返、无上限)。改造形现成(`acceptEarly` 透传),候消费方表态后同形改。
|
|
488
495
|
const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req,
|
|
489
496
|
// [4277]/[4288] 件5:让四个 reason 的判别日志共用同一个 id 字段 + 同一个 principalPresent 口径。
|
|
490
497
|
{ taskId, principalPresent: principal !== undefined });
|
|
@@ -683,7 +690,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
683
690
|
}
|
|
684
691
|
: undefined;
|
|
685
692
|
// #204 件6①:出处 = `"human"` —— 这条腿是运维**亲自**在 HTTP 上给的终局(approve/deny 两向都是)。
|
|
686
|
-
|
|
693
|
+
// #316([4660] Inkglow 案 → [4664]① 定谳):末位 `true` = **200 受理语义**。这条腿此前在响应之前
|
|
694
|
+
// 同步跑完整条 resume 流(模型往返、无服务端上限)⇒ 壳侧 60s AbortSignal 必然超时,而重试打回来
|
|
695
|
+
// 又撞 CAS —— [4660] 雪崩的首环。改后 HTTP 立刻回 `{taskId, status:"resuming", bindingEnforced:true}`,
|
|
696
|
+
// 结果面走既有 `GET /v1/runs/:id`(poll)/ `/events`(SSE)。**受理不是免检**:429/409/404 三类判定
|
|
697
|
+
// 逐字仍在响应前给出(判据全文见 `DriveResumeArgs.acceptEarly`)。
|
|
698
|
+
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, "human", req, answer, binding, grantOnCommit, true);
|
|
687
699
|
if (remember && deps.approvalExemptionStore) {
|
|
688
700
|
sendJson(res, out.status, { ...out.body, rememberApplied });
|
|
689
701
|
return;
|
|
@@ -151,6 +151,12 @@ async function handleNotifyWakeBody(req, res, url, ctx, miss) {
|
|
|
151
151
|
sendError(res, 400, "request.field_invalid", "message must be a non-empty string when present");
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
+
// 🕐 **#316 同族登记(本批不改行为)**:wake 与 legacy `/decide` 走同一条 `driveResumeIntoRunLog`
|
|
155
|
+
// 模型腿 —— 响应之前同步跑完整条续跑,时长 = 模型往返、无服务端上限([4664]① 定谳的同族三腿之一)。
|
|
156
|
+
// 本批只把 legacy decide 改成 200 受理形(那是 [4660] 雪崩实测首环);本腿是 wire BREAKING,按能力
|
|
157
|
+
// 显式表态制([3780] 裁 A)候消费方认领后同形改造 —— 改法 = `resumeWake` 透传 `acceptEarly: true`。
|
|
158
|
+
// ⚠️ 本腿多一条判据要一起想:它走 `mintFreshRun`(铸新 run 行),受理体里的 `taskId` 因此是**新铸的**
|
|
159
|
+
// 那个 id —— 改造时受理形必须带上它,否则壳没有可 poll 的锚。
|
|
154
160
|
const out = await resumeWake(wakeSession, wakeBody.message, principal, req);
|
|
155
161
|
sendJson(res, out.status, out.body);
|
|
156
162
|
return;
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -2100,7 +2100,7 @@ idemKey) {
|
|
|
2100
2100
|
return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
|
|
2101
2101
|
}
|
|
2102
2102
|
// [2255]① 真出路材料:activeTaskStatus + parked 时 pendingGate{kind,decidePath}(best-effort,失败退旧形)
|
|
2103
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
2103
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
2104
2104
|
}
|
|
2105
2105
|
// Durable F4: persist the resume rebuild inputs (sessionId-keyed) so an operator can resume from any
|
|
2106
2106
|
// replica even after this worker is gone — core's checkpoint blob can't carry service scenario context.
|
|
@@ -5,6 +5,7 @@ import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTermi
|
|
|
5
5
|
import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-tail-bus.js";
|
|
6
6
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
|
|
7
7
|
import { createLedgerSink } from "../../trace/ledger-sink.js";
|
|
8
|
+
import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js"; // #310:通告 wire 腿(sync stream 半场)
|
|
8
9
|
import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
|
|
9
10
|
import { redactSecrets } from "../../trace/redact.js";
|
|
10
11
|
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
|
|
@@ -364,7 +365,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
364
365
|
if (created.ok)
|
|
365
366
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget; in-titler dedupe;model 跟 turn([1992]②)
|
|
366
367
|
if (!created.ok) {
|
|
367
|
-
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
|
|
368
|
+
const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
|
|
368
369
|
// Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
|
|
369
370
|
// the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
|
|
370
371
|
// done 帧 additive 携带同一份材料 —— 形状铸在 toDoneFrameResult(有名字有类型),不在这行内联挑键。
|
|
@@ -525,6 +526,32 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
525
526
|
settleFleet("failed"); // [1.211 codex M2] publishTask 先写行再 emit——subscriber throw 时行已在,不清即永久 running 幽灵行
|
|
526
527
|
throw e;
|
|
527
528
|
}
|
|
529
|
+
// #310 `engine_notice` wire 腿(设计稿 = 黑板 [4630],三腿之一:**sync stream**)。
|
|
530
|
+
// core 的 `RunnerDeps.onNotice` 是 deps 级单 sink,通告上没有 runId ⇒ 归属只能靠 `detail.sessionId`;
|
|
531
|
+
// 分流器(`createEngineNoticeForwarder`)按会话把白名单三码投到这里注册的口。两口分开注册:
|
|
532
|
+
// · live = 本连接的 SSE(断流按既有 fail-open 词表留痕,不静默丢);
|
|
533
|
+
// · durable = detach 车道的账本(非 detach 车道 ledgerSink 恒 undefined ⇒ 无 durable 口,与本腿
|
|
534
|
+
// 其它帧的账面姿态一致;断连补看的那半由 detach 车道 / bg 腿承担)。
|
|
535
|
+
// 账本写不 flush 缓冲文本 —— 与 `onForwardEvent` 同款 fire-and-forget 姿态(seq 在链步内同步分配,
|
|
536
|
+
// 不会与主循环的 await 写乱序);代价是通告行可能排在同一 turn 的 text 行之前,那是"带外行"的既有账面语义。
|
|
537
|
+
const noticeLedger = ledgerSink;
|
|
538
|
+
const unregisterEngineNotice = registerEngineNoticeLeg({
|
|
539
|
+
sessionId: prepared.spec.sessionId,
|
|
540
|
+
live: (row) => {
|
|
541
|
+
if (res.writableEnded || res.destroyed) {
|
|
542
|
+
recordFailOpen(failOpenTagForDroppedFrame("engine_notice"), "frame=engine_notice");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
sseData(res, { type: "engine_notice", ...row });
|
|
546
|
+
},
|
|
547
|
+
// 写失败留痕不裸吞(codex 对抗复审 R1-[medium]):`route` 的同步 try 观察不到异步 reject,
|
|
548
|
+
// 本腿丢的是**断连补看**那一份(live SSE 已投),仍必须让"系统性在漏"在遥测里显形。
|
|
549
|
+
...(noticeLedger
|
|
550
|
+
? {
|
|
551
|
+
durable: (row) => void noticeLedger.append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`)),
|
|
552
|
+
}
|
|
553
|
+
: {}),
|
|
554
|
+
});
|
|
528
555
|
// R8 rewind handle is now the X-Task-Id RESPONSE HEADER (set with earlyDurableTid above), not an
|
|
529
556
|
// out-of-contract `run_started` data frame — the shell reads it from the response headers even on an early
|
|
530
557
|
// disconnect, and the SDK's CLOSED AgentEvent stream stays unpolluted (workflow re-review #2 caught the drift).
|
|
@@ -1163,6 +1190,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1163
1190
|
}
|
|
1164
1191
|
finally {
|
|
1165
1192
|
syncLegLive = false; // notifications from here on take the durable-inbox path
|
|
1193
|
+
unregisterEngineNotice(); // #310:腿结束即注销 wire 口(留着 = 下一条同会话腿的通告投进一条死连接)
|
|
1166
1194
|
if (durableHeartbeat)
|
|
1167
1195
|
clearInterval(durableHeartbeat);
|
|
1168
1196
|
// 1.207: drop the live verb handle — IDENTITY-GUARDED (review LOW): a suspend→fast-resume can
|
|
@@ -1308,7 +1336,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1308
1336
|
if (created.ok)
|
|
1309
1337
|
clearTurnActivity(tid); // #245 S1(codex S1-F2):清同 taskId 旧世代活性残留(runs.ts 同款注)
|
|
1310
1338
|
if (!created.ok)
|
|
1311
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
|
|
1339
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
|
|
1312
1340
|
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget;model 跟 turn([1992]②)
|
|
1313
1341
|
durableTaskId = tid;
|
|
1314
1342
|
}
|
package/dist/http/server.js
CHANGED
|
@@ -27,7 +27,9 @@ import {} from "../observability/rate-limit.js";
|
|
|
27
27
|
import { withPrincipal } from "../observability/principal-context.js";
|
|
28
28
|
import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
|
|
29
29
|
import { redactSecrets } from "../trace/redact.js";
|
|
30
|
-
import { createLedgerSink } from "../trace/ledger-sink.js"; // #253:resume-verify 臂的账本写口 = 与 bg/sync
|
|
30
|
+
import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js"; // #253:resume-verify 臂的账本写口 = 与 bg/sync 两腿同一只投影件;#310:本腿账本写口的单写串行链
|
|
31
|
+
import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js"; // #310:通告 wire 腿(resume 半场)
|
|
32
|
+
import { recordFailOpen } from "../observability/fail-open.js"; // #310:通告 durable 写失败的留痕口
|
|
31
33
|
import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
|
|
32
34
|
export { scopedIdempotencyKey };
|
|
33
35
|
import { streamSseLog } from "./sse-log.js";
|
|
@@ -1421,7 +1423,10 @@ export function createHttpServer(rawDeps) {
|
|
|
1421
1423
|
// 任何事实的记录。今天唯一传非 `"human"` 的调用方(SLA sweep)恒 deny,故不冲突;将来若有一个
|
|
1422
1424
|
// 会 approve 的非人调用方,它该传的是**缺席**而不是一个自造的词。
|
|
1423
1425
|
settledBy, req, // absent on the internal D-D SLA deny-sweep (resolveSpec rebuilds auth from the checkpoint, not req)
|
|
1424
|
-
answer, binding, onResumeCommitted)
|
|
1426
|
+
answer, binding, onResumeCommitted, // codex M2: fires post-markResuming-CAS, pre-model-drive (decide leg's exemption grant); MUST NOT throw the resume down — callers swallow internally. overrideSessionId = parked 腿的 root-会话 grant 锚([1591] 候裁②),legacy 腿不传。
|
|
1427
|
+
// #316:200 受理语义(HTTP `/decide` 传 true;内部 D-D SLA deny-sweep 不传 —— 没有在等响应的人)。
|
|
1428
|
+
// 语义与判据的唯一属主 = `DriveResumeArgs.acceptEarly` 顶注。
|
|
1429
|
+
acceptEarly) {
|
|
1425
1430
|
const cs = deps.checkpointStore;
|
|
1426
1431
|
// No scope filter here BY DESIGN (council): an operator may view/decide ANY tenant's pending approval.
|
|
1427
1432
|
// This load-before-CAS only READS; the definitive multi-tenant guard is the
|
|
@@ -1704,7 +1709,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1704
1709
|
// heartbeat, the resumeStream / resumeWithVerification loop, CheckpointError classification, re-suspend vs
|
|
1705
1710
|
// terminal drive. This HITL /decide path supplies a `policy_ask` outcome; the scheduler preempt path supplies
|
|
1706
1711
|
// a `resource_limit`/continue one — sharing this prevents the two resume entries from drifting.
|
|
1707
|
-
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(onResumeCommitted ? { onResumeCommitted } : {}) });
|
|
1712
|
+
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(onResumeCommitted ? { onResumeCommitted } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}) });
|
|
1708
1713
|
}
|
|
1709
1714
|
/**
|
|
1710
1715
|
* The resume-drive machinery SHARED by `resumeCheckpoint` (operator /decide, `policy_ask`) and `resumePreempted`
|
|
@@ -1716,8 +1721,59 @@ export function createHttpServer(rawDeps) {
|
|
|
1716
1721
|
* FRESH cancel signal AND a FRESH preemptSignal (so a resumed leg is itself cancel-able + RE-preempt-able —
|
|
1717
1722
|
* never the consumed suspend-time signal), classifies a CheckpointError, handles a re-suspend
|
|
1718
1723
|
* (setSuspended, keep the lock) vs a terminal (setTerminal), and returns the HTTP {status, body}.
|
|
1724
|
+
*
|
|
1725
|
+
* #316:**200 受理形的分派器**。`args.acceptEarly !== true` ⇒ 逐字旧行为(整条腿 await 完再回)。
|
|
1726
|
+
* 传 `true` ⇒ 在受理点(`driveResumeLeg` 里 `resumeStream` 解析之后)先把受理体交回,模型腿继续在
|
|
1727
|
+
* 同一条 promise 链上跑完。判据(受理点为什么在那里、哪三类判定必须留在同步侧)见
|
|
1728
|
+
* {@link DriveResumeArgs.acceptEarly} 的顶注 —— 那是这条语义的唯一属主,别在这里第二次成文。
|
|
1729
|
+
*
|
|
1730
|
+
* 🔴 **受理之后的失败绝不静默**:HTTP 已经走了,行/账本是权威可见面(腿内部各分支照旧驱行终局),
|
|
1731
|
+
* 但那条链的**结局**在这里必须留痕 —— 否则「受理了然后呢」在遥测里没有任何线索。
|
|
1732
|
+
*
|
|
1733
|
+
* 🔴 **如实登记的一条能见度差额**(codex 对抗复审 R1-[high],验真后按真形收窄):受理之后**行驱动写**
|
|
1734
|
+
* (`setTerminal`/`setSuspended`,腿尾那段)如果自己炸了,调用方拿不到任何东西 —— 修前那一形是 500。
|
|
1735
|
+
* ⚠️ 但**持久状态的分歧是既存的、两形逐字相同**:同步形下 500 并不会修好任何东西(账本可能已落
|
|
1736
|
+
* `done` 而行仍 `running` 且攥着 session claim,靠 reapStale 收;LOCAL/file 车道无周期腿 ⇒ 上界 ∞,
|
|
1737
|
+
* [4664]② 洞 (d))。⇒ 本批的真差额 = **调用方能见度**,不是新增的数据分歧;补偿 = 下面这条 error 级
|
|
1738
|
+
* 留痕 + `resume_post_accept_failed_total` 计数(受理前的 4xx **不**计入,见 `accepted` 标志)。
|
|
1739
|
+
* 真正的根治(受理前durable 记一条可续的续跑意图 + 行驱动重试对账)是**另一批**的设计件,已上报。
|
|
1719
1740
|
*/
|
|
1720
1741
|
async function driveResumeIntoRunLog(args) {
|
|
1742
|
+
if (args.acceptEarly !== true)
|
|
1743
|
+
return await driveResumeLeg(args);
|
|
1744
|
+
let settleAccepted;
|
|
1745
|
+
// 🔴 codex 对抗复审 R1-[medium](验真后修):遥测必须能分辨「受理前的正常拒绝」与「受理后的脱手
|
|
1746
|
+
// 失败」。处理器挂在 race **之前**(它必须无条件挂:否则受理前就 reject 的腿会变成进程级
|
|
1747
|
+
// unhandledRejection),所以判别位不能靠「处理器有没有跑」——用受理回调**同步**置起的这个标志。
|
|
1748
|
+
// 没有它:每一次 429 admission 拒 / 409 CAS 输 / pre-CAS 绑定不符都会被记成「受理后失败」,把这条
|
|
1749
|
+
// 新信号淹掉(常态冲突远多于真脱手失败)。
|
|
1750
|
+
let wasAccepted = false;
|
|
1751
|
+
const accepted = new Promise((resolve) => { settleAccepted = resolve; });
|
|
1752
|
+
const leg = driveResumeLeg(args, (receipt) => { wasAccepted = true; settleAccepted?.(receipt); });
|
|
1753
|
+
void leg.then((out) => {
|
|
1754
|
+
// 受理前的 4xx = 正常的同步拒绝(它就是调用方拿到的那个响应),不是本信号的事。
|
|
1755
|
+
if (wasAccepted && out.status >= 400) {
|
|
1756
|
+
deps.logger?.warn?.("resume_leg_failed_after_accept", { sessionId: args.sessionId, status: out.status, body: out.body });
|
|
1757
|
+
deps.metrics?.inc("resume_post_accept_failed_total");
|
|
1758
|
+
}
|
|
1759
|
+
}, (err) => {
|
|
1760
|
+
// 腿内部对「普通异常」已经驱行终局(claimedRow 守卫那支),这里补的是**可观察性**那一半。
|
|
1761
|
+
// 受理**前**抛的(race 会把它原样传给调用方 ⇒ 500)同样不计入本信号——那条路的可观察性归
|
|
1762
|
+
// 路由层的错误面,重复计数只会让「脱手失败」这个读数失去判别力。
|
|
1763
|
+
if (!wasAccepted)
|
|
1764
|
+
return;
|
|
1765
|
+
deps.logger?.error?.("resume_leg_threw_after_accept", { sessionId: args.sessionId, err: err instanceof Error ? err.message : String(err) });
|
|
1766
|
+
deps.metrics?.inc("resume_post_accept_failed_total");
|
|
1767
|
+
});
|
|
1768
|
+
return await Promise.race([accepted, leg]);
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* {@link driveResumeIntoRunLog} 的**腿本体**(实现逐字未动,只多了一个受理回调)。
|
|
1772
|
+
*
|
|
1773
|
+
* `onAccepted` = #316 的受理点回调:在 `resumeStream` 解析之后、流迭代之前触发**一次**(有 run 行时
|
|
1774
|
+
* 才触发 —— 没有行就没有可 poll 的终态面,那时提前受理等于把结果扔了)。缺席 ⇒ 全同步旧形。
|
|
1775
|
+
*/
|
|
1776
|
+
async function driveResumeLeg(args, onAccepted) {
|
|
1721
1777
|
// #196:`runner` = 本次续跑的执行 Runner(场景 hands 判别位裁定,与首跑同源;见 DriveResumeArgs 注)。
|
|
1722
1778
|
const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
|
|
1723
1779
|
// ── #151 车3 刀 3b:resume 腿的 `legKey` 真值(设计稿 §3.2′)──────────────────────────────────
|
|
@@ -1774,7 +1830,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1774
1830
|
// 错误终局化对方的行、孤儿化对方仍 pending 的卡(checkpoint 表无 per-session pending 唯一约束,
|
|
1775
1831
|
// findPendingTokenBySession 又是无序 LIMIT 1,两条 pending 并存时旧 task_done park 完全可能被选中)。
|
|
1776
1832
|
if (args.mintFreshRun && taskId && deps.runStore) {
|
|
1777
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, turnActivity: readTurnActivityMs }, sessionId, taskId) };
|
|
1833
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, taskId) };
|
|
1778
1834
|
}
|
|
1779
1835
|
if (!taskId && args.mintFreshRun && deps.runStore) {
|
|
1780
1836
|
const freshId = uuidv7();
|
|
@@ -1783,7 +1839,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1783
1839
|
objectivePreview: resumeObjective ? redactSecrets(resumeObjective).slice(0, 120) : null,
|
|
1784
1840
|
});
|
|
1785
1841
|
if (!created.ok) {
|
|
1786
|
-
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
1842
|
+
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
1787
1843
|
}
|
|
1788
1844
|
taskId = freshId;
|
|
1789
1845
|
freshRunClaimed = true;
|
|
@@ -1917,6 +1973,9 @@ export function createHttpServer(rawDeps) {
|
|
|
1917
1973
|
// never reaches here. Awaited BEFORE the model drive so the resumed leg's first ask observes the side-effects.
|
|
1918
1974
|
if (args.onResumeCommitted)
|
|
1919
1975
|
await args.onResumeCommitted();
|
|
1976
|
+
// #310 `engine_notice` wire 腿(三腿之一:**resume**)。声明在 try 之外、赋值在 `append` 铸出之后
|
|
1977
|
+
// (账本写口在 withPrincipal 闭包里才成形),注销在下方同一个 finally —— 任何退出路径都过它。
|
|
1978
|
+
let unregisterEngineNotice;
|
|
1920
1979
|
try {
|
|
1921
1980
|
// Wrap in withPrincipal so the resumed leg's LLM spend is attributed to the tenant's CostQuota — parity
|
|
1922
1981
|
// with the sync/async paths (council: otherwise maxPrincipalCostUsd is silently bypassed on resume).
|
|
@@ -2136,11 +2195,33 @@ export function createHttpServer(rawDeps) {
|
|
|
2136
2195
|
// machinery, then create + drain the stream INSIDE runWithContext (emit = append → events tail). The owner is
|
|
2137
2196
|
// the ORIGINAL submitter (`principal` = the checkpoint scope), NOT the operator who /decided — they answer.
|
|
2138
2197
|
const rs = deps.runStore;
|
|
2139
|
-
|
|
2198
|
+
const startSeq = rs && taskId ? await rs.maxSeq(taskId) : 0;
|
|
2140
2199
|
let text = "";
|
|
2141
2200
|
let reasoning = "";
|
|
2142
2201
|
const persistThinking = deps.config.traceThinking;
|
|
2143
|
-
|
|
2202
|
+
// 🔴 **单写串行链**(#310 codex 对抗复审 R1-[high],验真后修)。此前本腿是裸写口
|
|
2203
|
+
// (`rs.appendEvent(taskId, ++seq, …)`,同步分配 seq、立刻发起 insert)—— 主循环 await 自己的写
|
|
2204
|
+
// 所以主循环内部有序,但本腿有**两族 fire-and-forget 写者**:子代 forward 帧(下方 `warnAppend`
|
|
2205
|
+
// 那三处,既有)与 #310 的通告口(新增)。它们的 N 号 insert 在池化 SQL 后端上可能晚于后发的
|
|
2206
|
+
// N+1 号提交,而 events tail 读 `getEvents(id, afterSeq)`:读到 N+1 就把游标推过 N,那一行
|
|
2207
|
+
// **此后永远不会被投递**(重连也不会,游标只前进)。bg / sync 两腿早就有这条链(`LedgerSink`
|
|
2208
|
+
// 顶注 [1.211 codex H2] 逐字记着同一个病),本腿是漏掉的第三条 —— 判据抽成共享件,三腿同源。
|
|
2209
|
+
const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
|
|
2210
|
+
const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
|
|
2211
|
+
// #310:本腿同样**没有 live SSE**(续跑的消费方走 `GET /v1/runs/:id/events` 的 durable tail),
|
|
2212
|
+
// 故只挂 durable 口 —— 与本腿的 elicit/question/approval 三族帧姿态逐字相同。无账本(无 rs/taskId)
|
|
2213
|
+
// 时 `append` 本就是 resolve() 空写,注册它不产生任何行(与本腿其它帧同一条退化路径)。
|
|
2214
|
+
unregisterEngineNotice = registerEngineNoticeLeg({
|
|
2215
|
+
sessionId,
|
|
2216
|
+
// 写失败留痕不裸吞(codex 对抗复审 R1-[medium]):本腿账本同样是通告的唯一用户可见终点,
|
|
2217
|
+
// 与下方子代 forward 写口的 `warnAppend` 同一条纪律(C2/C5 批2)。
|
|
2218
|
+
durable: (row) => void append("engine_notice", row).catch((err) => {
|
|
2219
|
+
recordFailOpen("server.engine-notice.durable-append-failed", `leg=resume code=${row.code} task=${taskId ?? ""}`);
|
|
2220
|
+
// 字段名是 `noticeCode` 不是 `code`:`error-code-key-gate` 按词法扫 http/ 全树的 `code:` 键
|
|
2221
|
+
// (3.0.0 起 wire 错误体只认 `errorCode`),一条日志行也不给它留歧义面。
|
|
2222
|
+
deps.logger?.warn?.("engine_notice_append_failed", { taskId, noticeCode: row.code, sessionId: row.sessionId, err: err instanceof Error ? err.message : String(err) });
|
|
2223
|
+
}),
|
|
2224
|
+
});
|
|
2144
2225
|
// (resumeNotifiedKeys — this leg's delivered-notification keys — is hoisted above the inbox drain, see top)
|
|
2145
2226
|
// E18: anchor POST-resume turns too (a resumed conversation's new messages are rewindable). Same capture as
|
|
2146
2227
|
// runInBackground via the SHARED TurnAnchorCapture (only a settled assistant-text turn is anchored — see its
|
|
@@ -2271,6 +2352,17 @@ export function createHttpServer(rawDeps) {
|
|
|
2271
2352
|
steerableRuns.set(taskId, stream); // the resumed leg is in-flight again → live-steerable (parity with the first-run plain-stream path; taskId-guarded like the finally)
|
|
2272
2353
|
if (!taskId || !rs)
|
|
2273
2354
|
return await stream.result(); // no durable log to persist into → just drain
|
|
2355
|
+
// ── #316 **受理点**([4660] Inkglow 60s 雪崩首环的修)──────────────────────────────────
|
|
2356
|
+
// 到这一行为止,所有「会变成拒绝」的判定都已经落定:lease admission(429)、markResuming CAS
|
|
2357
|
+
// (409)、以及 `resumeStream` 内部 core 的 pre-CAS 守卫 + 原子 CAS(绑定不符 409 / 卡不在 404
|
|
2358
|
+
// ——它们在 `await legRunner.resumeStream(...)` 处就 reject 了,根本到不了这里)。剩下的只有
|
|
2359
|
+
// **模型往返**,时长无上限。⇒ 声明了受理语义的调用方在此拿走 `200 {status:"resuming"}`,本腿
|
|
2360
|
+
// 继续在同一条 promise 链上跑到停点(账本写口逐字还是上面那条 `createSerialLedgerAppend`
|
|
2361
|
+
// 单写串行链 —— 本改动**不新增任何写口**,也就不引入 fire-and-forget 的低 seq 竞态)。
|
|
2362
|
+
// 🔴 `taskId && rs` 之后才受理(上一行的早退已经把无行形滤走):没有 durable 行就没有 poll/SSE
|
|
2363
|
+
// 可跟的终态面,那种部署形上提前受理等于把结果扔掉 —— 它维持同步形(受理回调不触发 ⇒ 分派器
|
|
2364
|
+
// 的 race 自然落到腿本身的返回值上)。
|
|
2365
|
+
onAccepted?.({ status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } });
|
|
2274
2366
|
for await (const ev of stream) {
|
|
2275
2367
|
recordTurnActivity(taskId); // codex F5:S1 活性打点网的 resume 腿(sync/bg 双腿在 ledger-sink onActivity,本腿 append 不走那条链)——漏打则 resume 后 msSinceLastActivity 冻在 suspend 前,活跑的 run 被读成僵死
|
|
2276
2368
|
fleetPub?.onEvent(ev); // MF-Fleet (#7): turn_end → tokens, tool_start → live activity, in-stream task_progress → child row (parity with the sync leg's onEvent)
|
|
@@ -2612,6 +2704,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2612
2704
|
throw e;
|
|
2613
2705
|
}
|
|
2614
2706
|
finally {
|
|
2707
|
+
unregisterEngineNotice?.(); // #310:腿结束即注销 wire 口
|
|
2615
2708
|
if (taskId) {
|
|
2616
2709
|
// [1.207 codex H3] identity-guarded like steerableRuns below — a retriable-CheckpointError re-park
|
|
2617
2710
|
// (setSuspended) opens a window where a fast sibling resume markResuming's + registers its OWN
|
|
@@ -3180,6 +3273,9 @@ export function createHttpServer(rawDeps) {
|
|
|
3180
3273
|
}
|
|
3181
3274
|
// #204 件6②:出处 = `"timeout"`(窗到期,**没有人**答)。此前这里与人为 /decide 共用一个不带
|
|
3182
3275
|
// 出处的构造,于是下游把一次自动拒渲染成「有人拒绝了你」。
|
|
3276
|
+
// 🕐 **#316 同族登记(本批刻意不改)**:末位 `acceptEarly` 不传 —— 本腿没有在等响应的 HTTP 客户端
|
|
3277
|
+
// (它是内部 SLA 车道),受理化对它零收益;而且这条 for 循环**靠 await 串行**来给每 tick 的重活
|
|
3278
|
+
// 限流,改成受理即返会让一个 tick 把整批过期卡的模型腿同时点着(下一个问题,不是本批的修)。
|
|
3183
3279
|
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", "timeout").then((r) => { if (r.status >= 400)
|
|
3184
3280
|
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, body: r.body }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: e instanceof Error ? e.message : String(e) }));
|
|
3185
3281
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export { GiteaClient, GitHubClient, createRepoClient, GIT_API_KINDS, isGitApiKin
|
|
|
16
16
|
export { nowTool } from "./capabilities/builtin-tools.js";
|
|
17
17
|
export { createCouncilTool } from "./capabilities/code-review-council.js";
|
|
18
18
|
export { loadSkills, skillsForScenario, type LoadedSkill } from "./capabilities/skills.js";
|
|
19
|
-
export { buildScenarios, selectScenario, type Scenario, type ScenarioBundle, type ScenarioDeps, type ScenarioRequest, } from "./capabilities/scenarios.js";
|
|
19
|
+
export { buildScenarios, selectScenario, type Scenario, type ScenarioBundle, type ScenarioContext, type ScenarioDeps, type ScenarioRequest, } from "./capabilities/scenarios.js";
|
|
20
|
+
export { createTaskListLane, taskListKeyFor, MAX_TASK_LIST_SESSIONS, type TaskListLane, type TaskListLaneBackend, type TaskListLaneOpts } from "./boot/task-list-lane.js";
|
|
21
|
+
export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task-list-store-sql.js";
|
|
20
22
|
export { createAuthorizer, principalFrom, HttpError, type AuthContext, type OwnerAwareSessionStore, } from "./security.js";
|
|
21
23
|
export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
|
|
22
24
|
export type { TaskRequestBody } from "./http/wire-types.js";
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,11 @@ export { nowTool } from "./capabilities/builtin-tools.js";
|
|
|
22
22
|
export { createCouncilTool } from "./capabilities/code-review-council.js";
|
|
23
23
|
export { loadSkills, skillsForScenario } from "./capabilities/skills.js";
|
|
24
24
|
export { buildScenarios, selectScenario, } from "./capabilities/scenarios.js";
|
|
25
|
+
// #318:会话级任务清单车道(嵌入式装配自己组 ScenarioDeps 时接 `taskListStoreFor` 的属主工厂)。
|
|
26
|
+
// 两条 `ensure*TaskListSchema` 一并导出:车道**不建表**(建表属主 = 调它的装配层,见车道顶注),
|
|
27
|
+
// 所以走 SQL 臂的嵌入式装配必须自己先 await 对应方言的 ensure —— 不导出等于把那一步藏起来。
|
|
28
|
+
export { createTaskListLane, taskListKeyFor, MAX_TASK_LIST_SESSIONS } from "./boot/task-list-lane.js";
|
|
29
|
+
export { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./plugins/task-list-store-sql.js";
|
|
25
30
|
export { createAuthorizer, principalFrom, HttpError, } from "./security.js";
|
|
26
31
|
export { memoryScopeFor, memoryEngineBackendFor } from "./memory-scope.js";
|
|
27
32
|
export { createHttpServer, } from "./http/server.js";
|
package/dist/main.js
CHANGED
|
@@ -182,7 +182,7 @@ async function main() {
|
|
|
182
182
|
await configCenter.applyLocalRemoteExec();
|
|
183
183
|
// design/158 A10:持久层装配搬到 src/boot/stores.ts(逐字)。⚠️ 该段就地归一 `config.sessionBackend`
|
|
184
184
|
// 且承载三条 fail-loud 拒启断言 —— 位置即契约,理由见该文件头注。
|
|
185
|
-
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, } = await openStores({ config, logger, metrics, localRoot });
|
|
185
|
+
const { backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore, taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState, usageWindowStore, memoryPosture, taskListLane, } = await openStores({ config, logger, metrics, localRoot });
|
|
186
186
|
// design/183 I6(server 同族):**每副本 boot 必查**收编日志的在飞行 —— 续跑或响亮留痕,禁静默跳过。
|
|
187
187
|
// 位置:store 开完之后(要 backend)、任何路由装配之前(半迁移状态绝不带进服务期)。判据与「为什么
|
|
188
188
|
// 是续跑而不是拒启」见 boot/adoption.ts 顶注。
|
|
@@ -306,7 +306,9 @@ async function main() {
|
|
|
306
306
|
// `taskAttachmentTable` 如实告知(车1 交接件③):它是「boot 真的 ensure 过那张表吗」的事实,
|
|
307
307
|
// 不是一个默认值 —— 无对象存储的部署上附件面整段不接线、表不建,一条盲发的 DELETE 会以
|
|
308
308
|
// unknown-table 打红整轮 sweep(理由逐字见 retention-store-sql.ts 的 `SqlRetentionStoreOptions`)。
|
|
309
|
-
|
|
309
|
+
// `taskListTables`(#318)同姿势但事实不同:`boot/task-list-lane.ts` 在**任何** SQL 后端上都无条件
|
|
310
|
+
// ensure 那两张表(不像附件那样挂在对象存储配置上),所以这里的如实答案就是「车道落在 SQL 臂上」。
|
|
311
|
+
const retentionExecutor = backend?.retention?.({ taskAttachmentTable: taskAttachmentStore !== undefined, taskListTables: taskListLane.backend !== "memory", logger });
|
|
310
312
|
const retentionLaneStore = backend?.retentionLane?.();
|
|
311
313
|
// lane **自持**的 boot 不变式(件④;codex F3)——与下面 core 的 `assertRetentionCapability` 是两道门,
|
|
312
314
|
// 各答各的问题,本道**不看 locked**。判据与三条拒启臂逐字见 `boot/retention-lane.ts`。
|
|
@@ -804,6 +806,9 @@ async function main() {
|
|
|
804
806
|
// 构造区上方 const)同实例;ensureChildSessionDurable 见上方构造闭包注释。
|
|
805
807
|
checkpointStore: checkpointStore ? checkpointStore : undefined,
|
|
806
808
|
ensureChildSessionDurable: ensureChildSessionDurable ? ensureChildSessionDurable : undefined,
|
|
809
|
+
// #318([4659] F2 → [4692] 定谳):会话级任务清单店解析器(boot/task-list-lane.ts 的产物,
|
|
810
|
+
// openStores 里按后端三态构造一次)。修前不接这一键 ⇒ core 每 turn 铸一只私有内存店。
|
|
811
|
+
taskListStoreFor: taskListLane.storeFor,
|
|
807
812
|
brandIdentity: config.configProvider === "local",
|
|
808
813
|
// core 1.287 extraTools seam ([802]③ server half): SendUserFile reaches the delegated-child roster. SAME
|
|
809
814
|
// existence gate as the top-level mount (the one sendUserFileToolSpec built above — lane/tenancy/config
|
|
@@ -827,6 +832,9 @@ async function main() {
|
|
|
827
832
|
// (必须在 center overlay 盖名之前取:赎回腿要的是与 spawn 同源的生产装配,subRunner/bg opts/
|
|
828
833
|
// checkpointStore 同实例天然成立;per-request 项如 webSearch 对 revive 无关——objective 在 core
|
|
829
834
|
// driveParkedResume 里被丢弃)。checkpointStore/agentStore 任一缺席 = park 本就不可达,分支不接线。
|
|
835
|
+
// #318:这条取用**刻意不带** ScenarioContext —— boot 期没有任何一条真会话,给它编一个假 sessionId
|
|
836
|
+
// 会在清单表里铸出一份无人认领的分区。它只取裸 Agent 工具,task-list 家族在这份 bundle 里根本不被
|
|
837
|
+
// 消费([4692]③ 已核),缺席 = 修前字节形。
|
|
830
838
|
const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === "Agent") : undefined;
|
|
831
839
|
// [1596]/[1597] 跨副本父约束重供席工厂:host 任务的 toolPolicy 解析槽按**当前**部署配置重建
|
|
832
840
|
// 「部署 ⊇ 操作员」两层完整链(design/181 件二收编;实现与全部理由在 boot/parked-revive-gate.ts,
|
|
@@ -933,7 +941,7 @@ async function main() {
|
|
|
933
941
|
const { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe } = createSessionFaces({
|
|
934
942
|
config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore,
|
|
935
943
|
resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore,
|
|
936
|
-
workflowCompletionInbox,
|
|
944
|
+
workflowCompletionInbox, taskListLane,
|
|
937
945
|
});
|
|
938
946
|
// design/158 A10:leader 段搬到 src/boot/leader.ts(逐字)。
|
|
939
947
|
const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend });
|