@sema-agent/server 2.0.1 → 3.1.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 +395 -0
- package/dist/boot/config-center.d.ts +2 -1
- package/dist/boot/config-center.js +28 -7
- 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-center/restart-signal.d.ts +10 -2
- package/dist/config-center/restart-signal.js +28 -4
- package/dist/config-lkg.d.ts +2 -1
- package/dist/config-lkg.js +2 -1
- package/dist/config-types.d.ts +7 -5
- package/dist/config.d.ts +16 -11
- package/dist/config.js +30 -33
- 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 +1 -1
- 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 +34 -15
- package/dist/plugins/remote-env-host.js +30 -10
- package/dist/plugins/remote-env-local-docker.d.ts +14 -0
- package/dist/plugins/remote-env-local-docker.js +10 -3
- package/dist/plugins/run-store-sql.d.ts +8 -3
- package/dist/plugins/run-store-sql.js +8 -3
- package/dist/run-local.js +2 -1
- package/dist/sema-registry.d.ts +18 -4
- package/dist/sema-registry.js +17 -3
- 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 +6 -4
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) {
|
package/dist/main.js
CHANGED
|
@@ -237,7 +237,7 @@ async function main() {
|
|
|
237
237
|
setTitleIfNull: titleCapable.setTitleIfNull.bind(sessionStore),
|
|
238
238
|
probeTitle: titleCapable.probeTitle.bind(sessionStore),
|
|
239
239
|
redact: redactSecrets, // audit 2026-07-13: scrub a model-echoed secret before persist/list/log
|
|
240
|
-
llm: (o) => hookLlm({ prompt: o.prompt, timeoutMs: o.timeoutMs }),
|
|
240
|
+
llm: (o) => hookLlm({ prompt: o.prompt, timeoutMs: o.timeoutMs, model: o.model }),
|
|
241
241
|
logger,
|
|
242
242
|
metrics,
|
|
243
243
|
})
|
package/dist/model-select.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* untrusted body/text can pick WHICH configured model but can never inject a baseUrl/apiKey (parseModelMention
|
|
11
11
|
* only ever returns an allow-listed catalog name).
|
|
12
12
|
*
|
|
13
|
-
* [865]②(
|
|
13
|
+
* [865]②(生产实锤:面板上选了 A 模型,实际跑的是目录里的另一个):
|
|
14
14
|
* - explicit ref 走 name+id 双键索引({@link matchCatalogModel})——壳 boot 回读的 mainLoopModel 是 **id 形**,
|
|
15
15
|
* 目录键是 **name 形**,单键索引把 id 形请求当「未知」静默落 default = session 中途静默换模型(最恶性的
|
|
16
16
|
* 上下文污染路径)。
|
package/dist/model-select.js
CHANGED
|
@@ -11,7 +11,7 @@ import { parseModelMention } from "@sema-agent/core";
|
|
|
11
11
|
* untrusted body/text can pick WHICH configured model but can never inject a baseUrl/apiKey (parseModelMention
|
|
12
12
|
* only ever returns an allow-listed catalog name).
|
|
13
13
|
*
|
|
14
|
-
* [865]②(
|
|
14
|
+
* [865]②(生产实锤:面板上选了 A 模型,实际跑的是目录里的另一个):
|
|
15
15
|
* - explicit ref 走 name+id 双键索引({@link matchCatalogModel})——壳 boot 回读的 mainLoopModel 是 **id 形**,
|
|
16
16
|
* 目录键是 **name 形**,单键索引把 id 形请求当「未知」静默落 default = session 中途静默换模型(最恶性的
|
|
17
17
|
* 上下文污染路径)。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
-
import { type Checkpoint, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
|
|
3
|
+
import { type Checkpoint, type CheckpointGate, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
5
|
/**
|
|
6
6
|
* design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
|
|
@@ -38,10 +38,18 @@ export interface PendingCheckpoint {
|
|
|
38
38
|
* resume binds to the EXACT action they saw (a stale view is then rejected fail-closed PRE-CAS). `boundCallId`
|
|
39
39
|
* === `toolCallId` (the pending tool call); `boundInputHash` is the server-minted opaque hash of the shown
|
|
40
40
|
* input (NEVER recomputed by the portal — echoed verbatim). Surfacing them here is what makes the D-1 TOCTOU
|
|
41
|
-
* guard REACHABLE by the portal (without it the portal can only do the unbound legacy fallback).
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
* guard REACHABLE by the portal (without it the portal can only do the unbound legacy fallback).
|
|
42
|
+
* 🔴 [1995]②: ABSENT (key omitted, never `null`) on a park with no tool action (plan_review /
|
|
43
|
+
* resource_limit / task_done) or a pre-D-1 legacy row ⇒ the consumer falls back to the unbound resolve.
|
|
44
|
+
* The wire contract (§1) is "an absent nullable field is OMITTED, not null" — a `null` here invited a
|
|
45
|
+
* consumer to treat the key's PRESENCE as "there is a binding" and echo `null` back into /decide. */
|
|
46
|
+
boundCallId?: string;
|
|
47
|
+
boundInputHash?: string;
|
|
48
|
+
/** 🔴 [1995]③ (additive): the checkpoint's gate kind — the operator queue mixes tool approvals with
|
|
49
|
+
* plan_review / resource_limit / task_done parks (all of which carry `toolName: null`), so a durable-recovery
|
|
50
|
+
* consumer needs this to ROUTE a row to the right wire (`/decide` vs `/plan_review` vs `/resume`). Absent
|
|
51
|
+
* only for a legacy SQL row suspended before the `gate_kind` column existed. */
|
|
52
|
+
gateKind?: CheckpointGate["kind"];
|
|
45
53
|
/** The suspended run's taskId (task_active join) — the key to the run/trace ("view task context"). */
|
|
46
54
|
taskId: string | null;
|
|
47
55
|
/** The pending tool call's args (post-hook), redacted + bounded — for tool_approval the write payload, for a
|
|
@@ -305,13 +305,15 @@ export class SqlCheckpointStore {
|
|
|
305
305
|
* joined from task_active (the JOIN KEY to the run/trace — a suspended run KEEPS its
|
|
306
306
|
* session claim, so the join is live for every pending row; null only in pathological windows). */
|
|
307
307
|
async listPending(scope) {
|
|
308
|
-
const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, ta.task_id " +
|
|
308
|
+
const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, c.gate_kind, ta.task_id " +
|
|
309
309
|
"FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
|
|
310
310
|
const { rows } = scope
|
|
311
311
|
? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at ASC`, [scope])
|
|
312
312
|
: await this.db.query(`${base} ORDER BY c.created_at ASC`);
|
|
313
313
|
const out = rows.map((r) => {
|
|
314
314
|
const toolCallId = r.tool_call_id ?? null;
|
|
315
|
+
const boundInputHash = r.bound_input_hash ?? null;
|
|
316
|
+
const gateKind = r.gate_kind ?? null;
|
|
315
317
|
return {
|
|
316
318
|
sessionId: String(r.session_id),
|
|
317
319
|
scope: String(r.scope),
|
|
@@ -319,8 +321,13 @@ export class SqlCheckpointStore {
|
|
|
319
321
|
toolCallId,
|
|
320
322
|
// D-1: boundCallId === the pending tool call id; boundInputHash is the server-minted opaque hash. The
|
|
321
323
|
// portal echoes BOTH on /decide so the resume binds to the exact action shown (TOCTOU guard reachable).
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
// [1995]②: a park with no tool action (plan_review / resource_limit / task_done — both columns NULL)
|
|
325
|
+
// OMITS the keys entirely; the wire contract says absent, never `null` (LOCAL twin does the same).
|
|
326
|
+
...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
|
|
327
|
+
...(boundInputHash !== null ? { boundInputHash } : {}),
|
|
328
|
+
// [1995]③ additive: the gate kind the row is parked on (NULL only on a pre-`gate_kind` legacy row) —
|
|
329
|
+
// lets a durable-recovery consumer route plan / tool / resource off this ONE queue.
|
|
330
|
+
...(gateKind !== null ? { gateKind } : {}),
|
|
324
331
|
taskId: r.task_id ?? null,
|
|
325
332
|
// Both drivers return the JSON column already parsed; null for pre-migration rows. PG 库内恒干净
|
|
326
333
|
// (拒绝式)——直读即审阅面=执行面。
|
|
@@ -192,8 +192,14 @@ export class LocalCheckpointStore {
|
|
|
192
192
|
scope: cp.scope,
|
|
193
193
|
toolName: pa?.toolName ?? null,
|
|
194
194
|
toolCallId,
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
// [1995]②: the D-1 binding fields are OMITTED when the park has no tool action (plan_review /
|
|
196
|
+
// resource_limit / task_done) — the wire contract says an absent nullable field is ABSENT, not `null`
|
|
197
|
+
// (a consumer branching on "key present ⇒ there is a binding" would echo `null` back into /decide).
|
|
198
|
+
...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
|
|
199
|
+
...(pa?.boundInputHash !== undefined ? { boundInputHash: pa.boundInputHash } : {}),
|
|
200
|
+
// [1995]③ additive: which gate this row is parked on, so a durable-recovery consumer can route
|
|
201
|
+
// plan / tool / resource off the SAME queue (toolName is null for every non-tool gate).
|
|
202
|
+
...(gate?.kind !== undefined ? { gateKind: gate.kind } : {}),
|
|
197
203
|
taskId: (await this.opts.getActiveTaskId?.(cp.sessionId)) ?? null,
|
|
198
204
|
input: boundedToolInput(pa?.args),
|
|
199
205
|
createdAt: cp.createdAt,
|
|
@@ -43,7 +43,8 @@ export interface HostEnvConfig {
|
|
|
43
43
|
* reports `supported:false` → INERT (no scheduler tools). Injected at boot only on the single-user host lane. */
|
|
44
44
|
scheduler?: SchedulerCapability;
|
|
45
45
|
/** design/103 background shell: `false` → INERT (multi-tenant host lane gate). Default (undefined) = on for the
|
|
46
|
-
* single-user TOC host lane. `HOST_BG_ENABLED=false` is an additional global kill switch(旧名
|
|
46
|
+
* single-user TOC host lane. `HOST_BG_ENABLED=false` is an additional global kill switch(🪦 旧名
|
|
47
|
+
* `HOST_BG_DISABLED` 自 server 3.0.0 起是 fail-loud 墓碑:设了即拒启并指路新名,不再被读取)。 */
|
|
47
48
|
backgroundShell?: boolean;
|
|
48
49
|
}
|
|
49
50
|
export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, SchedulerCapability, BackgroundShellCapability {
|
|
@@ -279,22 +280,40 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
|
|
|
279
280
|
* `ctx.sessionId` into the workspace dir id so two concurrent tasks never share a working directory.
|
|
280
281
|
*/
|
|
281
282
|
export declare function hostExecutionEnvFactory(config?: HostEnvConfig): ExecutionEnvFactory;
|
|
282
|
-
export { localDockerExecutionEnvFactory, type LocalDockerEnvConfig, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
283
|
+
export { localDockerExecutionEnvFactory, type LocalDockerEnvConfig, type LocalDockerEnvDeps, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
284
|
+
import { type LocalDockerEnvConfig, type LocalDockerEnvDeps } from "./remote-env-local-docker.js";
|
|
285
|
+
/**
|
|
286
|
+
* Config for the `remote-docker` provider: run each task in a container on a REMOTE docker daemon
|
|
287
|
+
* (DUAL-MODE-DESIGN §5 — "a user-specified REMOTE host's docker"; registry-core `RemoteExecSpec`).
|
|
288
|
+
*
|
|
289
|
+
* It is {@link LocalDockerEnvConfig} with the endpoint made REQUIRED — that is the whole difference, by design:
|
|
290
|
+
* the container adapter is already daemon-agnostic (workspace `mkdir`'d INSIDE the container, never a host
|
|
291
|
+
* bind-mount; bytes over `docker cp` / `docker exec cat`, which stream through the daemon API), and the endpoint
|
|
292
|
+
* rides as `-H <dockerHost>` on every invocation. So every local-docker knob (memory/cpus/pidsLimit/dropAllCaps/
|
|
293
|
+
* network/timeouts/env/dockerPath) applies unchanged to the remote lane.
|
|
294
|
+
*/
|
|
295
|
+
export interface RemoteDockerEnvConfig extends Omit<LocalDockerEnvConfig, "dockerHost" | "providerLabel"> {
|
|
296
|
+
/** Remote docker daemon endpoint (e.g. `tcp://host:2376` or `ssh://user@host`). REQUIRED for remote-docker. */
|
|
291
297
|
dockerHost: string;
|
|
292
|
-
/** Out-of-band env injected into the container (secrets resolved by the control plane). */
|
|
293
|
-
env?: Record<string, string>;
|
|
294
298
|
}
|
|
295
299
|
/**
|
|
296
|
-
* `
|
|
297
|
-
*
|
|
300
|
+
* `ExecutionEnvFactory` for the TOC `remote-docker` backend (DUAL-MODE-DESIGN §5): a per-task container on
|
|
301
|
+
* SOMEONE ELSE'S docker daemon (offload — the operator's box stays free; isolation:true, suspendable:false).
|
|
302
|
+
* Same lifetime contract as local-docker: unconnected/lazy, one container per task, `destroy()` = `docker rm -f`.
|
|
303
|
+
*
|
|
304
|
+
* 🔴 **Endpoint credentials are the docker CLI's own** — this adapter deliberately carries none. The docker CLI
|
|
305
|
+
* reads `DOCKER_CERT_PATH` + `DOCKER_TLS_VERIFY` from the worker's environment (inherited by every spawn) for a
|
|
306
|
+
* `tcp://` + TLS daemon, and uses `ssh-agent` / `~/.ssh/config` for an `ssh://` one. That keeps key material out
|
|
307
|
+
* of this process's config objects entirely. Consequence to know: `tcp://…:2376` WITHOUT those env vars fails
|
|
308
|
+
* loud at connect (the daemon refuses plaintext) — it does not silently downgrade; a plaintext `tcp://…:2375`
|
|
309
|
+
* daemon is unauthenticated and remains the operator's decision, exactly as in the registry-core schema.
|
|
310
|
+
* (The registry-core `remote-docker` arm additionally models `tlsCertEnv`/`tlsKeyEnv`/`tlsCaEnv`/`sshKeyEnv` =
|
|
311
|
+
* env-NAMEs the control plane resolves to PEM BYTES. Materializing those bytes to short-lived 0600 files and
|
|
312
|
+
* passing `--tlsverify --tlscert/--tlskey/--tlscacert` is the remaining config-surface follow-up, together with
|
|
313
|
+
* the `provider:"remote-docker"` arm on `AppConfig.remoteExec` — see the note on that union in config-types.ts.)
|
|
314
|
+
*
|
|
315
|
+
* Throws at factory-BUILD time on a missing/blank endpoint: a "remote" lane with no endpoint would otherwise
|
|
316
|
+
* quietly become the LOCAL daemon — an isolation-topology surprise, so it fails loud instead.
|
|
298
317
|
*/
|
|
299
|
-
export declare function remoteDockerExecutionEnvFactory(
|
|
318
|
+
export declare function remoteDockerExecutionEnvFactory(config: RemoteDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
|
|
300
319
|
//# sourceMappingURL=remote-env-host.d.ts.map
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
* (`{ ok:true, exitCode }`), never an error.
|
|
29
29
|
*
|
|
30
30
|
* Platforms: macOS + Linux (POSIX shell). Forward-compatible with the `remoteExec` superset: this file also
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* hosts the `local-docker` / `remote-docker` providers' factory entry points — both now IMPLEMENTED by the
|
|
32
|
+
* container adapter in remote-env-local-docker.ts (the two providers differ only in whether the `-H` daemon
|
|
33
|
+
* endpoint is off-box and required), re-exported/wrapped here so the discriminator has one canonical site.
|
|
33
34
|
*/
|
|
34
35
|
import { spawn } from "node:child_process";
|
|
35
36
|
import os from "node:os";
|
|
@@ -1647,19 +1648,38 @@ export function hostExecutionEnvFactory(config = {}) {
|
|
|
1647
1648
|
// ─────────────────────────────── remoteExec superset: docker providers ───────────────────────────────
|
|
1648
1649
|
//
|
|
1649
1650
|
// DUAL-MODE-DESIGN §5 defines a `remoteExec.provider` superset. `host` (above) is implemented; `local-docker`
|
|
1650
|
-
//
|
|
1651
|
-
// re-exported here so the discriminator entry
|
|
1652
|
-
//
|
|
1653
|
-
// implemented" so selecting it fails loud at wiring time instead of silently degrading isolation. NOT faked green.
|
|
1651
|
+
// (the isolation lane) and `remote-docker` (that same container lane aimed at a REMOTE daemon) are BOTH
|
|
1652
|
+
// implemented by remote-env-local-docker.ts and re-exported / wrapped here so the discriminator entry points +
|
|
1653
|
+
// the historical import sites stay stable.
|
|
1654
1654
|
// `local-docker` (the isolation lane): real implementation moved to remote-env-local-docker.ts. Re-export the
|
|
1655
1655
|
// factory + config from here so existing wiring (`import { localDockerExecutionEnvFactory } from "./remote-env-host"`)
|
|
1656
1656
|
// keeps working and the provider discriminator has one canonical entry point.
|
|
1657
1657
|
export { localDockerExecutionEnvFactory, RemoteLocalDockerExecutionEnv } from "./remote-env-local-docker.js";
|
|
1658
|
+
import { localDockerExecutionEnvFactory } from "./remote-env-local-docker.js";
|
|
1658
1659
|
/**
|
|
1659
|
-
* `
|
|
1660
|
-
*
|
|
1660
|
+
* `ExecutionEnvFactory` for the TOC `remote-docker` backend (DUAL-MODE-DESIGN §5): a per-task container on
|
|
1661
|
+
* SOMEONE ELSE'S docker daemon (offload — the operator's box stays free; isolation:true, suspendable:false).
|
|
1662
|
+
* Same lifetime contract as local-docker: unconnected/lazy, one container per task, `destroy()` = `docker rm -f`.
|
|
1663
|
+
*
|
|
1664
|
+
* 🔴 **Endpoint credentials are the docker CLI's own** — this adapter deliberately carries none. The docker CLI
|
|
1665
|
+
* reads `DOCKER_CERT_PATH` + `DOCKER_TLS_VERIFY` from the worker's environment (inherited by every spawn) for a
|
|
1666
|
+
* `tcp://` + TLS daemon, and uses `ssh-agent` / `~/.ssh/config` for an `ssh://` one. That keeps key material out
|
|
1667
|
+
* of this process's config objects entirely. Consequence to know: `tcp://…:2376` WITHOUT those env vars fails
|
|
1668
|
+
* loud at connect (the daemon refuses plaintext) — it does not silently downgrade; a plaintext `tcp://…:2375`
|
|
1669
|
+
* daemon is unauthenticated and remains the operator's decision, exactly as in the registry-core schema.
|
|
1670
|
+
* (The registry-core `remote-docker` arm additionally models `tlsCertEnv`/`tlsKeyEnv`/`tlsCaEnv`/`sshKeyEnv` =
|
|
1671
|
+
* env-NAMEs the control plane resolves to PEM BYTES. Materializing those bytes to short-lived 0600 files and
|
|
1672
|
+
* passing `--tlsverify --tlscert/--tlskey/--tlscacert` is the remaining config-surface follow-up, together with
|
|
1673
|
+
* the `provider:"remote-docker"` arm on `AppConfig.remoteExec` — see the note on that union in config-types.ts.)
|
|
1674
|
+
*
|
|
1675
|
+
* Throws at factory-BUILD time on a missing/blank endpoint: a "remote" lane with no endpoint would otherwise
|
|
1676
|
+
* quietly become the LOCAL daemon — an isolation-topology surprise, so it fails loud instead.
|
|
1661
1677
|
*/
|
|
1662
|
-
export function remoteDockerExecutionEnvFactory(
|
|
1663
|
-
|
|
1678
|
+
export function remoteDockerExecutionEnvFactory(config, deps = {}) {
|
|
1679
|
+
const dockerHost = config.dockerHost?.trim();
|
|
1680
|
+
if (!dockerHost) {
|
|
1681
|
+
throw new RemoteExecutionError("connect_failed", "remoteExec provider 'remote-docker' requires a non-empty dockerHost (the remote daemon endpoint, e.g. tcp://host:2376 or ssh://user@host) — use provider 'local-docker' for THIS machine's daemon");
|
|
1682
|
+
}
|
|
1683
|
+
return localDockerExecutionEnvFactory({ ...config, dockerHost, providerLabel: "remote-docker" }, deps);
|
|
1664
1684
|
}
|
|
1665
1685
|
//# sourceMappingURL=remote-env-host.js.map
|
|
@@ -17,8 +17,20 @@ export interface LocalDockerEnvConfig {
|
|
|
17
17
|
/**
|
|
18
18
|
* DOCKER_HOST override for the docker CLI (e.g. a non-default socket). Passed as `-H <value>` on EVERY docker
|
|
19
19
|
* invocation (not the process env) so it can't leak / drift. Absent = the daemon DOCKER_HOST points at.
|
|
20
|
+
*
|
|
21
|
+
* Pointing this OFF-BOX (`tcp://host:2376`, `ssh://user@host`) is exactly the DUAL-MODE-DESIGN §5
|
|
22
|
+
* **`remote-docker`** provider — see {@link remoteDockerExecutionEnvFactory} (remote-env-host.ts), which is
|
|
23
|
+
* this adapter with the endpoint required. Nothing else about the class is host-local: the workspace is
|
|
24
|
+
* `mkdir`'d INSIDE the container (never a bind-mount of a host path) and bytes move by `docker cp` /
|
|
25
|
+
* `docker exec cat`, which stream over the daemon API.
|
|
20
26
|
*/
|
|
21
27
|
dockerHost?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Provider NAME reported on the {@link WorkspaceHandle}. Default `"local-docker"`; the `remote-docker` arm
|
|
30
|
+
* passes `"remote-docker"` so a handle never mislabels which lane it is (the CLASS is shared — `remote-docker`
|
|
31
|
+
* IS this adapter aimed at a remote daemon). Cosmetic/observability only: it selects no behavior.
|
|
32
|
+
*/
|
|
33
|
+
providerLabel?: string;
|
|
22
34
|
/** Container memory limit (docker `--memory`, e.g. "2g"). Absent = no limit. */
|
|
23
35
|
memory?: string;
|
|
24
36
|
/** Container CPU limit (docker `--cpus`, e.g. 1.5). Absent = no limit. */
|
|
@@ -65,6 +77,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
65
77
|
private readonly spawnFn;
|
|
66
78
|
/** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
|
|
67
79
|
private readonly containerName;
|
|
80
|
+
/** Provider name put on the WorkspaceHandle ("local-docker" | "remote-docker"). Observability only. */
|
|
81
|
+
private readonly providerName;
|
|
68
82
|
/** Set once the container is up + the workspace dir created. */
|
|
69
83
|
private containerId?;
|
|
70
84
|
private handle?;
|
|
@@ -66,6 +66,8 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
66
66
|
spawnFn;
|
|
67
67
|
/** Stable container NAME (also our handle id). Created on connect; targeted by every docker exec/cp/rm. */
|
|
68
68
|
containerName;
|
|
69
|
+
/** Provider name put on the WorkspaceHandle ("local-docker" | "remote-docker"). Observability only. */
|
|
70
|
+
providerName;
|
|
69
71
|
/** Set once the container is up + the workspace dir created. */
|
|
70
72
|
containerId;
|
|
71
73
|
handle;
|
|
@@ -92,6 +94,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
92
94
|
const id = config.id ?? randomBytes(6).toString("hex");
|
|
93
95
|
const suffix = randomBytes(4).toString("hex"); // always-unique tail so a reused id never collides
|
|
94
96
|
this.containerName = `sema-docker-${sanitizeId(id)}-${suffix}`;
|
|
97
|
+
this.providerName = config.providerLabel?.trim() || PROVIDER;
|
|
95
98
|
}
|
|
96
99
|
resolve(p) {
|
|
97
100
|
return path.posix.isAbsolute(p) ? path.posix.normalize(p) : path.posix.normalize(path.posix.join(this.cwd, p));
|
|
@@ -165,7 +168,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
165
168
|
return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
|
|
166
169
|
}
|
|
167
170
|
this.containerId = run.value.stdout.trim() || this.containerName;
|
|
168
|
-
this.handle = { sandboxId: this.containerName, provider:
|
|
171
|
+
this.handle = { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
169
172
|
if (config?.secrets?.length) {
|
|
170
173
|
// Secrets are injected per-command via cfg.env (resolved by the control plane), not at connect — record only.
|
|
171
174
|
}
|
|
@@ -197,7 +200,7 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
197
200
|
// re-run `docker run --name <same>` → name conflict → the failure path rm -f's the very container we just
|
|
198
201
|
// re-attached to (destroying the running workspace). The name IS our exec/handle target.
|
|
199
202
|
this.containerId = this.containerName;
|
|
200
|
-
this.handle ??= { sandboxId: this.containerName, provider:
|
|
203
|
+
this.handle ??= { sandboxId: this.containerName, provider: this.providerName, mountPath: this.cfg.mountPath, sessionToken: this.containerName };
|
|
201
204
|
return ok(this.handle);
|
|
202
205
|
}
|
|
203
206
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `container '${this.containerName}' is not running (gone or stopped) — no managed snapshot to resume`) };
|
|
@@ -609,7 +612,11 @@ export class RemoteLocalDockerExecutionEnv {
|
|
|
609
612
|
return;
|
|
610
613
|
this.containerShellProbed = true;
|
|
611
614
|
try {
|
|
612
|
-
|
|
615
|
+
// 🔴 NO withHostFlag here — `docker()` already prepends it. A second `-H` is not a harmless repeat: the
|
|
616
|
+
// docker CLI rejects the whole command (`invalid argument … specify only one -H`), so on every
|
|
617
|
+
// REMOTE-daemon container (`dockerHost` set — always, on the `remote-docker` arm) the probe used to fail
|
|
618
|
+
// and silently degrade the user shell to /bin/sh for a reason unrelated to the image.
|
|
619
|
+
const r = await this.docker(["exec", this.containerName, "/bin/sh", "-c", "command -v bash"], { timeoutMs: this.cfg.controlTimeoutMs });
|
|
613
620
|
if (r.ok && r.value.exitCode === 0 && r.value.stdout.trim().length > 0)
|
|
614
621
|
this.containerShell = "bash";
|
|
615
622
|
}
|
|
@@ -126,9 +126,14 @@ export declare class SqlRunStore {
|
|
|
126
126
|
* suspend, an INTENTIONAL durable pause" (design/76 §2.5) — the service MUST park it the same way, else the run is
|
|
127
127
|
* terminalized + its lock released and the plan_review/dry_run_review resume can never run (it gets
|
|
128
128
|
* taskId=undefined and drives the model unprotected on an unlocked session). CAS on `running`/`needs_review`
|
|
129
|
-
* (reaper-revert guard, mirrors setSuspended).
|
|
130
|
-
*
|
|
131
|
-
*
|
|
129
|
+
* (reaper-revert guard, mirrors setSuspended). The park→resume→claim invariant is pinned on BOTH REAL engines by
|
|
130
|
+
* `test/run-store-reaper-integration.test.ts` (`inv#1` row ownership across park→resume→handover, `inv#2` two
|
|
131
|
+
* replicas racing one session ⇒ exactly one claim wins, `inv#3`/`inv#3'` stale-claim takeover vs fresh/parked
|
|
132
|
+
* claims), closing the former required-before-GA gap. Keep it that way: the P2 lease-SQL bug and this
|
|
133
|
+
* needs_review-park bug were both born from store SQL that mocked unit tests pass — measured, not asserted, when
|
|
134
|
+
* the invariant set was built: an upserting (non-exclusive) claim reddened all four real-DB cases while all 111
|
|
135
|
+
* mocked run-store unit tests stayed green. A mock cannot observe a unique-key claim race, a rolled-back loser,
|
|
136
|
+
* or a multi-table reaper DELETE.
|
|
132
137
|
*/
|
|
133
138
|
setNeedsReview(taskId: string): Promise<void>;
|
|
134
139
|
/**
|
|
@@ -206,9 +206,14 @@ export class SqlRunStore {
|
|
|
206
206
|
* suspend, an INTENTIONAL durable pause" (design/76 §2.5) — the service MUST park it the same way, else the run is
|
|
207
207
|
* terminalized + its lock released and the plan_review/dry_run_review resume can never run (it gets
|
|
208
208
|
* taskId=undefined and drives the model unprotected on an unlocked session). CAS on `running`/`needs_review`
|
|
209
|
-
* (reaper-revert guard, mirrors setSuspended).
|
|
210
|
-
*
|
|
211
|
-
*
|
|
209
|
+
* (reaper-revert guard, mirrors setSuspended). The park→resume→claim invariant is pinned on BOTH REAL engines by
|
|
210
|
+
* `test/run-store-reaper-integration.test.ts` (`inv#1` row ownership across park→resume→handover, `inv#2` two
|
|
211
|
+
* replicas racing one session ⇒ exactly one claim wins, `inv#3`/`inv#3'` stale-claim takeover vs fresh/parked
|
|
212
|
+
* claims), closing the former required-before-GA gap. Keep it that way: the P2 lease-SQL bug and this
|
|
213
|
+
* needs_review-park bug were both born from store SQL that mocked unit tests pass — measured, not asserted, when
|
|
214
|
+
* the invariant set was built: an upserting (non-exclusive) claim reddened all four real-DB cases while all 111
|
|
215
|
+
* mocked run-store unit tests stayed green. A mock cannot observe a unique-key claim race, a rolled-back loser,
|
|
216
|
+
* or a multi-table reaper DELETE.
|
|
212
217
|
*/
|
|
213
218
|
async setNeedsReview(taskId) {
|
|
214
219
|
await this.db.query(this.q("UPDATE task_run SET status = 'needs_review', updated_at = ? WHERE task_id = ? AND status IN ('running','needs_review')", "UPDATE task_run SET status = 'needs_review', updated_at = $1 WHERE task_id = $2 AND status IN ('running','needs_review')"), [new Date(), taskId]);
|
package/dist/run-local.js
CHANGED
|
@@ -291,7 +291,8 @@ export async function runLocal(argv, deps = {}) {
|
|
|
291
291
|
// TOC local LSP (core 1.190): run-local IS the pure-local host lane, so mount core's `NodeLspManager` (local
|
|
292
292
|
// child_process language servers over stdio, CC `services/lsp` parity) on the host lane. Default ON (degrades
|
|
293
293
|
// gracefully to grep/read if a server binary is absent); `LSP_HOST_ENABLED=false` opts out (design/158 B4 split
|
|
294
|
-
// this lane off `LSP_ENABLED
|
|
294
|
+
// this lane off `LSP_ENABLED`; 🪦 server 3.0.0 起 `LSP_ENABLED=false` 是 fail-loud 墓碑,拒启指路新名,
|
|
295
|
+
// 不再是可用的 opt-out)。`requirePrincipal !== true`
|
|
295
296
|
// keeps it uniform with main.ts (a no-op here — run-local is single-user — but never relies on that). Both runners.
|
|
296
297
|
const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) }) : undefined;
|
|
297
298
|
// Long-term memory (design/138 S1): run-local IS the single-user lane, so the file-based memory ENGINE is
|
package/dist/sema-registry.d.ts
CHANGED
|
@@ -10,9 +10,23 @@
|
|
|
10
10
|
* (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
|
|
11
11
|
*
|
|
12
12
|
* Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
|
|
13
|
-
* core brain honors model.baseUrl, absent = "" = boot-env endpoint).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* core brain honors model.baseUrl, absent = "" = boot-env endpoint).
|
|
14
|
+
*
|
|
15
|
+
* Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
|
|
16
|
+
* TODO, which went stale when `mutateInPlace` landed):
|
|
17
|
+
* - models/roles/roster/teams/projects/autonomy — **HOT**. `applyEffective` mutates `config.models`/
|
|
18
|
+
* `config.roles` IN PLACE, and core's Runner reads `this.deps.models/roles` per task off that very
|
|
19
|
+
* reference. Both brains re-resolve `model.baseUrl || config.baseUrl` inside `buildRequest()` on every
|
|
20
|
+
* call (core 2.1.0 `brain/openai.js` + `brain/anthropic.js`), so a moved gateway takes effect on the
|
|
21
|
+
* next task with no restart.
|
|
22
|
+
* - the same plane under a **tier-frozen** Runner — deferred, not hot: core expands a PRIVATE catalog copy
|
|
23
|
+
* at construction, so `main` defers the whole plane and the `models-tiers` restart slice signals.
|
|
24
|
+
* - **`degrade-route`** — the one genuinely boot-frozen catalog consumer left: reactive degrade
|
|
25
|
+
* (`MODEL_DEGRADE_REACTIVE`) bakes the target Model + its gateway + its key into the brain composition
|
|
26
|
+
* at boot. It cannot be hot-applied without rebuilding the brain, so it is registered on the existing
|
|
27
|
+
* restartRequired /health contract instead (see `restart-signal.ts`). Fail-loud beats serving a stale
|
|
28
|
+
* gateway silently on the rate-limit path.
|
|
29
|
+
* - skills/mcp/scenarios/runtime-gates — restart-to-apply by construction (baked into the boot wiring).
|
|
16
30
|
*
|
|
17
31
|
* (design/158 A13, internal-lossless) This module is now a FACADE: the implementation lives in
|
|
18
32
|
* `src/config-center/` split by responsibility group (HTTP client / EffectiveConfig application /
|
|
@@ -21,7 +35,7 @@
|
|
|
21
35
|
*/
|
|
22
36
|
export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./config-center/http-client.js";
|
|
23
37
|
export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./config-center/apply-effective.js";
|
|
24
|
-
export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSignal, } from "./config-center/restart-signal.js";
|
|
38
|
+
export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./config-center/restart-signal.js";
|
|
25
39
|
export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./config-center/skills-mcp.js";
|
|
26
40
|
export type { CenterSkillManifest, CenterMcpServer, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./config-center/types.js";
|
|
27
41
|
//# sourceMappingURL=sema-registry.d.ts.map
|
package/dist/sema-registry.js
CHANGED
|
@@ -10,9 +10,23 @@
|
|
|
10
10
|
* (core 1.45), so each model/cascade-rung authenticates with its own upstream key.
|
|
11
11
|
*
|
|
12
12
|
* Per-model `baseUrl` IS transported (a catalog model may live on a different gateway;
|
|
13
|
-
* core brain honors model.baseUrl, absent = "" = boot-env endpoint).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* core brain honors model.baseUrl, absent = "" = boot-env endpoint).
|
|
14
|
+
*
|
|
15
|
+
* Hot-reload status (复审 2026-07-29 P1-11 — 亲读判定,取代此处旧的 "models/roles are restart-to-apply"
|
|
16
|
+
* TODO, which went stale when `mutateInPlace` landed):
|
|
17
|
+
* - models/roles/roster/teams/projects/autonomy — **HOT**. `applyEffective` mutates `config.models`/
|
|
18
|
+
* `config.roles` IN PLACE, and core's Runner reads `this.deps.models/roles` per task off that very
|
|
19
|
+
* reference. Both brains re-resolve `model.baseUrl || config.baseUrl` inside `buildRequest()` on every
|
|
20
|
+
* call (core 2.1.0 `brain/openai.js` + `brain/anthropic.js`), so a moved gateway takes effect on the
|
|
21
|
+
* next task with no restart.
|
|
22
|
+
* - the same plane under a **tier-frozen** Runner — deferred, not hot: core expands a PRIVATE catalog copy
|
|
23
|
+
* at construction, so `main` defers the whole plane and the `models-tiers` restart slice signals.
|
|
24
|
+
* - **`degrade-route`** — the one genuinely boot-frozen catalog consumer left: reactive degrade
|
|
25
|
+
* (`MODEL_DEGRADE_REACTIVE`) bakes the target Model + its gateway + its key into the brain composition
|
|
26
|
+
* at boot. It cannot be hot-applied without rebuilding the brain, so it is registered on the existing
|
|
27
|
+
* restartRequired /health contract instead (see `restart-signal.ts`). Fail-loud beats serving a stale
|
|
28
|
+
* gateway silently on the rate-limit path.
|
|
29
|
+
* - skills/mcp/scenarios/runtime-gates — restart-to-apply by construction (baked into the boot wiring).
|
|
16
30
|
*
|
|
17
31
|
* (design/158 A13, internal-lossless) This module is now a FACADE: the implementation lives in
|
|
18
32
|
* `src/config-center/` split by responsibility group (HTTP client / EffectiveConfig application /
|
package/dist/session-titler.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface SessionTitlerDeps {
|
|
|
29
29
|
llm(opts: {
|
|
30
30
|
prompt: string;
|
|
31
31
|
timeoutMs: number;
|
|
32
|
+
model?: string;
|
|
32
33
|
}): Promise<{
|
|
33
34
|
ok: true;
|
|
34
35
|
text: string;
|
|
@@ -47,7 +48,8 @@ export interface SessionTitlerDeps {
|
|
|
47
48
|
}
|
|
48
49
|
export interface SessionTitler {
|
|
49
50
|
/** Fire-and-forget: maybe generate + persist a title for this session. Never throws, never blocks the caller. */
|
|
50
|
-
|
|
51
|
+
/** `model`([1992]②)= 本 turn 真实驱动的模型——带上则标题 hook 同链路,不再裸落 env 缺省。 */
|
|
52
|
+
maybeTitle(sessionId: string, objective: string, model?: string): void;
|
|
51
53
|
}
|
|
52
54
|
export declare const TITLE_MAX_CHARS = 80;
|
|
53
55
|
/** Single line, control chars stripped, hard cap; surrounding quotes (a common LLM tic) trimmed. */
|
package/dist/session-titler.js
CHANGED
|
@@ -65,7 +65,7 @@ export function createSessionTitler(deps) {
|
|
|
65
65
|
deps.logger?.warn?.("session_title_failed", { ...meta, note: "同 cause 后续降为 debug 级" });
|
|
66
66
|
};
|
|
67
67
|
return {
|
|
68
|
-
maybeTitle(sessionId, objective) {
|
|
68
|
+
maybeTitle(sessionId, objective, model) {
|
|
69
69
|
if (!sessionId || !objective.trim())
|
|
70
70
|
return;
|
|
71
71
|
if (seen.has(sessionId))
|
|
@@ -93,7 +93,7 @@ export function createSessionTitler(deps) {
|
|
|
93
93
|
deps.metrics?.inc("session_title_total", { outcome: "already_titled" });
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
-
const r = await deps.llm({ prompt: buildTitlePrompt(objective), timeoutMs: LLM_TIMEOUT_MS });
|
|
96
|
+
const r = await deps.llm({ prompt: buildTitlePrompt(objective), timeoutMs: LLM_TIMEOUT_MS, model });
|
|
97
97
|
if (!r.ok) {
|
|
98
98
|
deps.metrics?.inc("session_title_total", { outcome: "llm_failed" });
|
|
99
99
|
logFailure(sessionId, r.error);
|
package/dist/trace/project.js
CHANGED
|
@@ -510,7 +510,10 @@ export function mapTraceEvent(type, seq, data) {
|
|
|
510
510
|
case "failed":
|
|
511
511
|
// BL-13: redact at the read boundary too (defense-in-depth) — covers any errorMessage that reached the
|
|
512
512
|
// log un-redacted via another write path; idempotent on the BL-12-redacted catch-path message.
|
|
513
|
-
|
|
513
|
+
// A1/A6/B(复审 2026-07-29):error 帧统一为 `{ type:"error", errorCode, message }` —— 与 sse-log /
|
|
514
|
+
// approvals 的帧同形。这一处是 3.0.0 摘 legacy `code` 键时的漏网:帧在 src/trace/ 铸,而当时的门只扫
|
|
515
|
+
// src/http/,于是"SSE error 帧机器键统一为 errorCode"的公开声明与 wire 上跑的东西不一致(门是绿的)。
|
|
516
|
+
return { event: "error", data: { type: "error", errorCode: "WORKER_DOWN", message: redactSecrets(String(data.errorMessage ?? "run failed")) } };
|
|
514
517
|
default:
|
|
515
518
|
return null; // compacted etc. — not a client trace event
|
|
516
519
|
}
|