@sema-agent/server 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +74 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +391 -0
- package/dist/boot/budget-tracing.d.ts +48 -0
- package/dist/boot/budget-tracing.js +86 -0
- package/dist/boot/config-center.d.ts +62 -0
- package/dist/boot/config-center.js +1002 -0
- package/dist/boot/coordinators.d.ts +33 -0
- package/dist/boot/coordinators.js +97 -0
- package/dist/boot/execution-env.d.ts +26 -0
- package/dist/boot/execution-env.js +370 -0
- package/dist/boot/leader.d.ts +27 -0
- package/dist/boot/leader.js +81 -0
- package/dist/boot/reapers.d.ts +53 -0
- package/dist/boot/reapers.js +252 -0
- package/dist/boot/resolve-spec.d.ts +70 -0
- package/dist/boot/resolve-spec.js +1072 -0
- package/dist/boot/runner-deps.d.ts +101 -0
- package/dist/boot/runner-deps.js +343 -0
- package/dist/boot/runtime-caps.d.ts +21 -0
- package/dist/boot/runtime-caps.js +62 -0
- package/dist/boot/session-faces.d.ts +57 -0
- package/dist/boot/session-faces.js +157 -0
- package/dist/boot/shutdown.d.ts +50 -0
- package/dist/boot/shutdown.js +129 -0
- package/dist/boot/stores.d.ts +32 -0
- package/dist/boot/stores.js +361 -0
- package/dist/boot/workflow-orchestration.d.ts +46 -0
- package/dist/boot/workflow-orchestration.js +150 -0
- package/dist/capabilities/scenarios.d.ts +5 -3
- package/dist/capabilities/scenarios.js +5 -3
- package/dist/config-center/apply-effective.js +4 -3
- package/dist/config-lkg.d.ts +2 -1
- package/dist/config-lkg.js +2 -1
- package/dist/config-types.d.ts +47 -7
- package/dist/config.d.ts +30 -13
- package/dist/config.js +562 -387
- package/dist/hooks/hook-llm.js +9 -0
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/attachments.js +2 -2
- package/dist/http/routes/memory-policy.js +3 -3
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/session-sync.js +2 -2
- package/dist/http/routes/sessions.js +2 -2
- package/dist/http/routes/tasks.js +2 -2
- package/dist/http/routes/trace-usage.js +2 -2
- package/dist/http/routes/workflows.js +3 -1
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +25 -5
- package/dist/http/sse-log.js +1 -1
- package/dist/main.js +164 -3799
- package/dist/model-select.d.ts +1 -1
- package/dist/model-select.js +1 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -5
- package/dist/plugins/checkpoint-store-sql.js +10 -3
- package/dist/plugins/local-checkpoint-store.js +8 -2
- package/dist/plugins/remote-env-host.d.ts +2 -1
- package/dist/run-local.js +2 -1
- package/dist/session-titler.d.ts +3 -1
- package/dist/session-titler.js +2 -2
- package/dist/trace/project.js +4 -1
- package/package.json +5 -3
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { matchCatalogModel } from "../model-select.js";
|
|
1
2
|
export function createHookLlm(deps) {
|
|
2
3
|
const { config, metrics } = deps;
|
|
3
4
|
// anthropic 路由自己的 baseUrl(与 core createAnthropicBrain 同一解析链:model.baseUrl || config.baseUrl
|
|
@@ -40,6 +41,14 @@ export function createHookLlm(deps) {
|
|
|
40
41
|
}
|
|
41
42
|
if (config.models[requested])
|
|
42
43
|
return shape(requested);
|
|
44
|
+
// 复审 2026-07-29 D1:目录键是 **name** 形,而调用点会传 **provider id** 形——`http/routes/tasks.ts`
|
|
45
|
+
// 的 titler 腿([1992]②「标题跟 turn」)对对象形 spec 传的就是 `spec.model.id`。registry 热应用后
|
|
46
|
+
// name 与 id 常态分叉(目录键是运维起的短名,`Model.id` 是 provider 侧的真名),单键索引下这类请求全落到下面那条
|
|
47
|
+
// 「不在目录里」⇒ 用户每选一次非主模型,标题就 llm_failed(对话本身正常),与 [1992] 同族的病形。
|
|
48
|
+
// 双键索引沿用同仓先例 [865]② `matchCatalogModel`:name 优先、id 扫目录值、跳过 `default` 别名键。
|
|
49
|
+
const byId = matchCatalogModel(requested, config.models);
|
|
50
|
+
if (byId !== undefined)
|
|
51
|
+
return shape(byId);
|
|
43
52
|
if (requested === config.model.id)
|
|
44
53
|
return shapeFrom(requested, config.model, requested);
|
|
45
54
|
return { ok: false, error: `hook model "${requested}" is not in this deployment's catalog` };
|
|
@@ -746,7 +746,7 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
|
|
|
746
746
|
}
|
|
747
747
|
prev = cur;
|
|
748
748
|
if (Date.now() - start > MAX_MS) {
|
|
749
|
-
res.write(`event: error\ndata: ${JSON.stringify({ type: "error",
|
|
749
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "STREAM_MAX_DURATION", message: "approvals stream reached its 15-minute cap — reconnect to continue" })}\n\n`);
|
|
750
750
|
break;
|
|
751
751
|
}
|
|
752
752
|
if (Date.now() - lastBeat > 15_000) {
|
|
@@ -61,7 +61,7 @@ async function handleAttachmentsBody(req, res, url, ctx, miss) {
|
|
|
61
61
|
}
|
|
62
62
|
const allow = deps.config.attachmentMimeAllowlist;
|
|
63
63
|
if (allow && !allow.some((a) => a === mime || (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))))) {
|
|
64
|
-
sendError(res, 415, "attachment.mime_not_allowed", `mime '${mime}' is not in this deployment's allowlist`, {
|
|
64
|
+
sendError(res, 415, "attachment.mime_not_allowed", `mime '${mime}' is not in this deployment's allowlist`, { allowed: allow });
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
const max = deps.config.attachmentMaxBytes ?? 32 * 1024 * 1024;
|
|
@@ -71,7 +71,7 @@ async function handleAttachmentsBody(req, res, url, ctx, miss) {
|
|
|
71
71
|
}
|
|
72
72
|
catch (err) {
|
|
73
73
|
if (err instanceof HttpError && err.status === 413) {
|
|
74
|
-
sendError(res, 413, "attachment.too_large", `attachment exceeds the per-file limit (${max} bytes)`, {
|
|
74
|
+
sendError(res, 413, "attachment.too_large", `attachment exceeds the per-file limit (${max} bytes)`, { maxBytes: max });
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
throw err;
|
|
@@ -76,11 +76,11 @@ async function handleMemoryPolicyBody(req, res, url, ctx, miss) {
|
|
|
76
76
|
scope = decodeURIComponent(url.slice("/v1/memory/sync/".length));
|
|
77
77
|
}
|
|
78
78
|
catch {
|
|
79
|
-
sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope"
|
|
79
|
+
sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope");
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
if (scope.length === 0) {
|
|
83
|
-
sendError(res, 400, "memory_sync_invalid_scope", "missing :scope"
|
|
83
|
+
sendError(res, 400, "memory_sync_invalid_scope", "missing :scope");
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
@@ -90,7 +90,7 @@ async function handleMemoryPolicyBody(req, res, url, ctx, miss) {
|
|
|
90
90
|
const body = await readJson(req); // 413/400 (oversize / bad JSON) via the typed top-level catch — auth already done
|
|
91
91
|
const parsed = parseMemorySyncRequest(body, scope);
|
|
92
92
|
if (!parsed.ok) {
|
|
93
|
-
sendError(res, 422, "memory_sync_invalid_body", parsed.error
|
|
93
|
+
sendError(res, 422, "memory_sync_invalid_body", parsed.error);
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
try {
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -154,7 +154,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
154
154
|
const taskId = clientTaskId ?? uuidv7();
|
|
155
155
|
const created = await runStore.createRun(taskId, sessionId, prepared.auth?.principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
156
156
|
if (created.ok)
|
|
157
|
-
deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective); // fire-and-forget
|
|
157
|
+
deps.sessionTitler?.maybeTitle(sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget;model 跟 turn([1992]②)
|
|
158
158
|
if (!created.ok) {
|
|
159
159
|
// race twin: two concurrent same-taskId submits (gateway retry racing the original) — the loser's
|
|
160
160
|
// session claim collides with ITS OWN taskId → idempotent 202 replay, not a 409 (the run exists, once).
|
|
@@ -234,7 +234,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
234
234
|
if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
|
|
235
235
|
// 复审 D-F1/B-F2:归一漏网(多行体,旧门正则扫不到)——A6 迁移到 sendError 后此站点由
|
|
236
236
|
// errorCode 必填位置参数保结构(不再靠源级反射门兜底)。
|
|
237
|
-
sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits
|
|
237
|
+
sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`);
|
|
238
238
|
return;
|
|
239
239
|
}
|
|
240
240
|
if (createHash("sha256").update(body).digest("hex") !== hash) {
|
|
@@ -246,7 +246,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
246
246
|
// blob as durably stored when it isn't.
|
|
247
247
|
const put = await fs.putBlob(hash, new Uint8Array(body));
|
|
248
248
|
if (!put.ok) {
|
|
249
|
-
sendError(res, 502, put.error.code, "blob store write failed"
|
|
249
|
+
sendError(res, 502, put.error.code, "blob store write failed");
|
|
250
250
|
return;
|
|
251
251
|
}
|
|
252
252
|
res.writeHead(204).end(); // no body
|
|
@@ -796,7 +796,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
796
796
|
if (typeof fs.blobSizes === "function") {
|
|
797
797
|
const known = (await fs.blobSizes([hash])).get(hash);
|
|
798
798
|
if (known !== undefined && known > cap) {
|
|
799
|
-
sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", {
|
|
799
|
+
sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { sizeBytes: known, limit: cap });
|
|
800
800
|
return;
|
|
801
801
|
}
|
|
802
802
|
}
|
|
@@ -806,7 +806,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
806
806
|
return;
|
|
807
807
|
}
|
|
808
808
|
if (bytes.byteLength > cap) {
|
|
809
|
-
sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", {
|
|
809
|
+
sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { sizeBytes: bytes.byteLength, limit: cap });
|
|
810
810
|
return;
|
|
811
811
|
}
|
|
812
812
|
// [1894]③:content-type 按扩展名(保守表,未知=octet-stream)+ 二进制判别头(首 8KiB NUL/控制
|
|
@@ -214,7 +214,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
214
214
|
const tid = earlyDurableTid ?? uuidv7(); // reuse the id already emitted as the X-Task-Id header (defensive mint if absent)
|
|
215
215
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
216
216
|
if (created.ok)
|
|
217
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective); // fire-and-forget; in-titler dedupe
|
|
217
|
+
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]②)
|
|
218
218
|
if (!created.ok) {
|
|
219
219
|
const conflict = { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId };
|
|
220
220
|
// Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
|
|
@@ -966,7 +966,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
966
966
|
const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
|
|
967
967
|
if (!created.ok)
|
|
968
968
|
return { status: 409, body: { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", errorCode: "conflict.session_active_run", activeTaskId: created.activeTaskId } };
|
|
969
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective); // fire-and-forget
|
|
969
|
+
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]②)
|
|
970
970
|
durableTaskId = tid;
|
|
971
971
|
}
|
|
972
972
|
if (deps.checkpointStore && prepared.spec.sessionId) {
|
|
@@ -269,13 +269,13 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs) {
|
|
|
269
269
|
}
|
|
270
270
|
const stale = Date.now() - new Date(run.updatedAt).getTime() > staleMs;
|
|
271
271
|
if (stale && events.length === 0) {
|
|
272
|
-
res.write(`event: error\ndata: ${JSON.stringify({
|
|
272
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "WORKER_DOWN", message: "run stalled (instance lost?)" })}\n\n`);
|
|
273
273
|
break;
|
|
274
274
|
}
|
|
275
275
|
if (Date.now() - start > MAX_MS) {
|
|
276
276
|
// BL-16: the stream hit its 15-min cap but the RUN is still going — emit a terminal event so the client
|
|
277
277
|
// can tell "reconnect to continue" from "task completed" (a silent close looked like completion).
|
|
278
|
-
res.write(`event: error\ndata: ${JSON.stringify({
|
|
278
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "STREAM_MAX_DURATION", message: "trace stream reached its 15-minute cap — reconnect with Last-Event-ID to continue (the run is still active)" })}\n\n`);
|
|
279
279
|
break;
|
|
280
280
|
}
|
|
281
281
|
if (events.length === 0) {
|
|
@@ -485,8 +485,10 @@ async function streamWorkflowRun(req, res, runId, scope) {
|
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
487
|
catch {
|
|
488
|
+
// A1/A6/B:归一 error 帧形(`type` + 机器码)。此前只发人话 message ⇒ 消费端要么锚文案、要么把
|
|
489
|
+
// "流断了"和"流正常结束"混为一谈;`workflow.stream_error` 是本帧的铸码(附录 A `*.stream_error` 族)。
|
|
488
490
|
if (!closed && !res.writableEnded)
|
|
489
|
-
res.write(`event: error\ndata: ${JSON.stringify({ message: "workflow stream error" })}\n\n`);
|
|
491
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "workflow.stream_error", message: "workflow stream error" })}\n\n`);
|
|
490
492
|
}
|
|
491
493
|
finally {
|
|
492
494
|
clearInterval(hb);
|
package/dist/http/server.d.ts
CHANGED
|
@@ -197,7 +197,7 @@ export interface ServiceCoordinatorDeps {
|
|
|
197
197
|
leaderEndpoint?: LeaderEndpoint;
|
|
198
198
|
/** Fire-and-forget session auto-titler (first-submit hook; absent = feature dark). */
|
|
199
199
|
sessionTitler?: {
|
|
200
|
-
maybeTitle(sessionId: string, objective: string): void;
|
|
200
|
+
maybeTitle(sessionId: string, objective: string, model?: string): void;
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
/** **函数/构件缝**:main.ts 把跨多个子系统的动作编译成一个可调用面交给路由(路由不该自己
|
package/dist/http/server.js
CHANGED
|
@@ -281,7 +281,7 @@ export function createHttpServer(rawDeps) {
|
|
|
281
281
|
// 4xx, not a 500. Everything else is a genuine internal error: log the cause, never echo it (info disclosure).
|
|
282
282
|
if (err instanceof HttpError) {
|
|
283
283
|
if (!res.headersSent)
|
|
284
|
-
sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.
|
|
284
|
+
sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.extra ?? {}) });
|
|
285
285
|
else
|
|
286
286
|
res.end();
|
|
287
287
|
return;
|
|
@@ -1091,7 +1091,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1091
1091
|
if (err instanceof HttpError) {
|
|
1092
1092
|
// Typed rejection: echo the stable `code` + any structured extras (e.g. the scenario
|
|
1093
1093
|
// allowlist) beside the human message — machine code on the wire, prose stays with the shell/web.
|
|
1094
|
-
sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.
|
|
1094
|
+
sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.extra ?? {}) });
|
|
1095
1095
|
return null;
|
|
1096
1096
|
}
|
|
1097
1097
|
throw err;
|
|
@@ -1181,6 +1181,27 @@ export function createHttpServer(rawDeps) {
|
|
|
1181
1181
|
}
|
|
1182
1182
|
if (!cp)
|
|
1183
1183
|
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1184
|
+
// 🔴 GATE-KIND GUARD ([1995]①,`gate_not_resumable` / `gate_not_plan_review` 的**反方向镜像**):这条腿
|
|
1185
|
+
// 只解 TOOL 审批门(core gateMatch:`human` / `irreversible_ask` ↔ outcome.gate "policy_ask")。此前它无
|
|
1186
|
+
// 条件铸 outcome{gate:"policy_ask"} 交给 core —— 一个 plan_review / resource_limit / task_done park 被误
|
|
1187
|
+
// 路由到 /decide(而 /v1/approvals 把它们与工具审批**混在同一个队列**里、toolName 同为 null)就会深入到
|
|
1188
|
+
// core 的 `checkpoint.gate_mismatch`。那是 core 的 **pre-CAS** 拒绝(checkpoint 仍 pending),但 server 的
|
|
1189
|
+
// CheckpointError 分类表把 gate_mismatch 归为 TERMINAL ⇒ 行被 setTerminal 成 failed + 释放 task_active,
|
|
1190
|
+
// 于是「行说这活死了、park 却还活着」:claim 泄漏(同 session 可并发起第二个 run)、事后用正确的腿救能
|
|
1191
|
+
// 200 真跑完却拿不到 taskId、run 行永停 failed(账本与现实分叉)。这里 pre-CAS 纯拒绝:不动 checkpoint、
|
|
1192
|
+
// 不动 run 行、不烧模型腿,与另外两条腿的守卫同姿势(core 的 gate-match 仍是 fail-closed 兜底)。
|
|
1193
|
+
// ⚠️ 门放在 parked 赎回腿**之前**:赎回腿同样只铸 policy_ask outcome,错门 cp 在那条链上只会更深地炸。
|
|
1194
|
+
// D-D SLA deny-sweep 不受影响:它的取行查询本就只选 gate_kind IN ('human','irreversible_ask')。
|
|
1195
|
+
const decideGateKind = cp.gate?.kind;
|
|
1196
|
+
if (decideGateKind !== "human" && decideGateKind !== "irreversible_ask") {
|
|
1197
|
+
return {
|
|
1198
|
+
status: 409,
|
|
1199
|
+
body: {
|
|
1200
|
+
error: `task is suspended on a '${decideGateKind ?? "unknown"}' gate, not a tool approval — a plan_review gate is resolved via POST /v1/assistant/tasks/:id/plan_review, a resource_limit suspension via POST /v1/assistant/tasks/:id/resume`,
|
|
1201
|
+
errorCode: "gate_not_tool_approval",
|
|
1202
|
+
},
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1184
1205
|
// design/153 件3d parked 赎回腿([1574]/[1584]/[1588] core 裁定;src/parked-decide.ts):该 pending
|
|
1185
1206
|
// checkpoint 若属于一个 parked 后台子代,decide 必须走 claim→裸 execute(reviveClaim) 链——legacy 腿的
|
|
1186
1207
|
// resumeStream 会绕开 bg registry(无 consumeParkedFlip、行永 parked、生命周期分叉),故 parked-first。
|
|
@@ -1243,7 +1264,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1243
1264
|
if (e instanceof HttpError) {
|
|
1244
1265
|
return {
|
|
1245
1266
|
status: 409,
|
|
1246
|
-
body: { error: `resume blocked by an admission policy change: ${e.message}`,
|
|
1267
|
+
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
1247
1268
|
};
|
|
1248
1269
|
}
|
|
1249
1270
|
throw e;
|
|
@@ -1871,7 +1892,6 @@ export function createHttpServer(rawDeps) {
|
|
|
1871
1892
|
status,
|
|
1872
1893
|
body: {
|
|
1873
1894
|
error: e.message,
|
|
1874
|
-
code: e.code,
|
|
1875
1895
|
// invalid_outcome means DIFFERENT things per gate: for policy_ask it IS the D-1 action-binding
|
|
1876
1896
|
// mismatch; for plan_review/dry_run_review it is a content-sanitize reject (editedPlan/reason carried a
|
|
1877
1897
|
// forbidden tag) — give those a gate-appropriate code so the UI routes to the right recovery, not the
|
|
@@ -2210,7 +2230,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2210
2230
|
res.setHeader("retry-after", String(adm.retryAfterSec));
|
|
2211
2231
|
// E4 定稿:429 带 typed code + center 结构化字段 verbatim 透传(windowType/remaining/resetAt/
|
|
2212
2232
|
// isEstimate/pool)——人话文案归壳/web(契约如此),error 串保留旧消费者兼容。
|
|
2213
|
-
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", {
|
|
2233
|
+
sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
|
|
2214
2234
|
return true;
|
|
2215
2235
|
}
|
|
2216
2236
|
/** Apply the per-principal rate limit; responds 429 and returns true when the caller is over. */
|
package/dist/http/sse-log.js
CHANGED
|
@@ -55,7 +55,7 @@ export async function streamSseLog(req, res, provider, id, staleMs) {
|
|
|
55
55
|
if (Date.now() - start > MAX_MS) {
|
|
56
56
|
// BL-16: cap reached but the run continues — emit a terminal reconnect signal (not a silent close, which
|
|
57
57
|
// a client couldn't distinguish from a completed run). Distinct from the `failed`/`done` run events.
|
|
58
|
-
res.write(`event: error\ndata: ${JSON.stringify({ type: "error",
|
|
58
|
+
res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "STREAM_MAX_DURATION", message: "stream reached its 15-minute cap — reconnect with Last-Event-ID to continue (the run is still active)" })}\n\n`);
|
|
59
59
|
break;
|
|
60
60
|
}
|
|
61
61
|
if (events.length === 0) {
|