@sema-agent/server 7.14.0 → 7.16.0-rc.1
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 +16 -1
- package/USAGE.md +38 -4
- package/dist/approval-ask-machine.d.ts +10 -0
- package/dist/approval-ask-machine.js +10 -0
- package/dist/approval-card.d.ts +65 -0
- package/dist/approval-card.js +54 -6
- package/dist/approval-reconciler.d.ts +17 -1
- package/dist/boot/memory-boundary.d.ts +6 -0
- package/dist/boot/memory-boundary.js +10 -2
- package/dist/boot/resolve-spec.js +31 -4
- package/dist/boot/runner-deps.d.ts +41 -2
- package/dist/boot/runner-deps.js +43 -0
- package/dist/boot/session-faces.js +26 -4
- package/dist/boot/shutdown.js +17 -0
- package/dist/boot/stores.js +28 -7
- package/dist/capabilities/memory-notice.d.ts +13 -9
- package/dist/capabilities/memory-notice.js +35 -15
- package/dist/config-center/apply-effective.d.ts +14 -0
- package/dist/config-center/apply-effective.js +81 -1
- package/dist/config-types.d.ts +28 -2
- package/dist/config.js +38 -2
- package/dist/fleet/fleet-terminal-window.d.ts +12 -0
- package/dist/fleet/fleet-terminal-window.js +27 -4
- package/dist/http/active-run-conflict.d.ts +41 -1
- package/dist/http/active-run-conflict.js +24 -10
- package/dist/http/routes/approvals-assistant.d.ts +11 -3
- package/dist/http/routes/approvals-assistant.js +97 -20
- package/dist/http/routes/capabilities.js +20 -1
- package/dist/http/routes/diagnostics.d.ts +18 -0
- package/dist/http/routes/diagnostics.js +26 -0
- package/dist/http/routes/runs.js +86 -19
- package/dist/http/routes/side-query.js +15 -1
- package/dist/http/routes/tasks.js +32 -3
- package/dist/http/routes/trace-usage.js +38 -37
- package/dist/http/server.js +67 -13
- package/dist/leader/fanout.d.ts +18 -0
- package/dist/leader/fanout.js +34 -1
- package/dist/leader/leader.js +9 -5
- package/dist/leader/wire.js +16 -5
- package/dist/main.js +1 -0
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +45 -0
- package/dist/model-select.d.ts +43 -1
- package/dist/model-select.js +70 -2
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +7 -1
- package/dist/parent-watch.d.ts +57 -0
- package/dist/parent-watch.js +108 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +133 -7
- package/dist/plugins/local-checkpoint-store.js +11 -1
- package/dist/plugins/memory-embedder-fingerprint.d.ts +119 -0
- package/dist/plugins/memory-embedder-fingerprint.js +280 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +0 -3
- package/dist/plugins/permission-rule-store-sql.js +1 -7
- package/dist/plugins/pg-pool.js +3 -0
- package/dist/plugins/store-backend.d.ts +5 -6
- package/dist/plugins/store-backend.js +4 -1
- package/dist/plugins/store-contracts.d.ts +17 -0
- package/dist/plugins/store-contracts.js +33 -0
- package/dist/plugins/tidb-pool.js +7 -0
- package/dist/plugins/tool-result-store-sql.d.ts +18 -13
- package/dist/plugins/tool-result-store-sql.js +50 -29
- package/dist/run-local.js +1 -0
- package/dist/tool-approval.d.ts +10 -0
- package/dist/tool-approval.js +143 -9
- package/dist/trace/project.js +8 -0
- package/dist/trace/redact.d.ts +14 -1
- package/dist/trace/redact.js +14 -2
- package/package.json +3 -3
package/dist/boot/shutdown.js
CHANGED
|
@@ -15,15 +15,20 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Runner, defaultTaskRegistry } from "@sema-agent/core";
|
|
17
17
|
import { createSighupIdleHandler } from "../sighup-idle.js";
|
|
18
|
+
import { createParentWatch } from "../parent-watch.js";
|
|
18
19
|
/** 注册 SIGTERM/SIGINT/SIGHUP 收尾链。**必须在 listen 之后调用**(见文件头「位置即契约」)。 */
|
|
19
20
|
export function installShutdownHandlers(ctx) {
|
|
20
21
|
const { config, logger, server, reaper, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, } = ctx;
|
|
21
22
|
let closing = false;
|
|
23
|
+
/** #219:parent 监视腿(装配在本函数尾,`config.parentPid` 在场才建)。声明提前只为让 hardShutdown
|
|
24
|
+
* 能把它一并停掉 —— 文件头契约 3 同族(收尾期不再有后台 tick)。 */
|
|
25
|
+
let parentWatch;
|
|
22
26
|
const hardShutdown = () => {
|
|
23
27
|
if (closing)
|
|
24
28
|
return;
|
|
25
29
|
closing = true;
|
|
26
30
|
clearInterval(reaper);
|
|
31
|
+
parentWatch?.stop(); // #219:契约 3 同族——收尾期不再探父进程
|
|
27
32
|
storeLiveProbe?.stop(); // #131-2:契约 3 同族——收尾期不再有探针 tick 打向正在关闭的池
|
|
28
33
|
configCenter?.stopRefreshLoop(); // #131-2:停机中途不再热应用配置
|
|
29
34
|
otelExporter?.stop();
|
|
@@ -127,5 +132,17 @@ export function installShutdownHandlers(ctx) {
|
|
|
127
132
|
idleGraceMs: config.sighupIdleGraceMs,
|
|
128
133
|
log: (event, fields) => logger.info(event, fields),
|
|
129
134
|
}));
|
|
135
|
+
// #219(黑板 [3617]):壳被 SIGKILL 后引擎被 reparent 到 init 而继续活着(孤儿引擎持端口/持库连接,
|
|
136
|
+
// 且再没有人会给它发 SIGTERM)。`SEMA_PARENT_PID` 设了才装配 —— 连续两拍 ESRCH 判死,出口**就是**
|
|
137
|
+
// 上面 SIGTERM 用的这条 `drainThenShutdown`(结算/park 语义与人工停机逐字一致,不是裸 exit)。
|
|
138
|
+
// 位置:与三个 process.on 同段、在它们之后 —— 它和信号腿共享 closing/draining 两个 latch。
|
|
139
|
+
if (config.parentPid !== undefined) {
|
|
140
|
+
parentWatch = createParentWatch({
|
|
141
|
+
pid: config.parentPid,
|
|
142
|
+
isStopped: () => closing || draining,
|
|
143
|
+
drain: drainThenShutdown,
|
|
144
|
+
log: (event, fields) => logger.info(event, fields),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
130
147
|
}
|
|
131
148
|
//# sourceMappingURL=shutdown.js.map
|
package/dist/boot/stores.js
CHANGED
|
@@ -16,6 +16,7 @@ import { join } from "node:path";
|
|
|
16
16
|
import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore, FileUsageWindowStore, InMemoryUsageWindowStore } from "@sema-agent/core";
|
|
17
17
|
import { createMemorySyncRunner, createMemorySyncTransport } from "../memory-sync-client.js";
|
|
18
18
|
import { memoryEmbedderFor } from "../plugins/memory-embedder.js";
|
|
19
|
+
import { ensurePgMemoryEmbedderMetaSchema, reconcileEmbedderFingerprint } from "../plugins/memory-embedder-fingerprint.js";
|
|
19
20
|
import { PgMemoryEngineBackend, ensurePgMemoryEngineSchema } from "../plugins/memory-engine-pg.js";
|
|
20
21
|
import { TiDBMemoryEngineBackend, ensureTiDBMemoryEngineSchema } from "../plugins/memory-engine-tidb.js";
|
|
21
22
|
import { PgMemoryHistoryStore, PgMemorySyncStore, ensurePgMemoryHistorySchema, ensurePgMemorySyncSchema } from "../plugins/memory-sync-store-pg.js";
|
|
@@ -29,7 +30,7 @@ import { LocalTaskAttachmentStore } from "../plugins/local-task-attachment-store
|
|
|
29
30
|
import { PgTaskAttachmentStore, TiDBTaskAttachmentStore, ensurePgTaskAttachmentSchema, ensureTiDBTaskAttachmentSchema } from "../plugins/task-attachment-store.js";
|
|
30
31
|
import { createSessionStore } from "../plugins/session-store.js";
|
|
31
32
|
import { assertCloudSnapshotBlobPosture, openStoreBackendWithFallback } from "../plugins/store-backend.js";
|
|
32
|
-
import { memoryEngineBackendFor, memoryEngineRemoteLanePosture } from "../memory-scope.js";
|
|
33
|
+
import { buildMemoryRemoteLaneWarn, memoryEngineBackendFor, memoryEngineRemoteLanePosture } from "../memory-scope.js";
|
|
33
34
|
import { assertToolResultProvenanceSchema } from "../plugins/tool-result-store-sql.js";
|
|
34
35
|
import { buildMemoryWriteBoundaryAudit } from "./memory-boundary.js";
|
|
35
36
|
export async function openStores(ctx) {
|
|
@@ -150,6 +151,25 @@ export async function openStores(ctx) {
|
|
|
150
151
|
});
|
|
151
152
|
},
|
|
152
153
|
});
|
|
154
|
+
// design/234 embedder 指纹门(clay 终裁 [3647]⑤ 方向=清空重建):同维度换模型是**静默**混空间形
|
|
155
|
+
// ——旧空间与新空间的向量在同一列里做 cosine,排序变噪音,没有任何一层会说出来。指纹
|
|
156
|
+
// `{model,dimensions}` 明文存单行元表,boot 一次比对:不等/无主/不可解析 ⇒ 先清 `embedding` 列、
|
|
157
|
+
// 后写元表(次序即正确性,反序中途崩溃=新 identity 已记而旧向量还在=正是被修的病),写后 CAS
|
|
158
|
+
// 重读收敛并发副本。⚠️ 位置:ensure*Schema 之后、`new PgMemoryEngineBackend` **之前** —— 比对跑在
|
|
159
|
+
// backend 构造之后 = 服务已经能用旧空间向量检索了。门自身失败一律抛(拒启)。
|
|
160
|
+
// A 态(embedder 缺席)门整个不装:连 ensure 都不调,元表不动(旧 identity 保留,配置回归同
|
|
161
|
+
// identity 时存量向量直接续用)。⚠️ ③b 扫的是**源码文本**,调用写在 if 内照样入册(K1)。
|
|
162
|
+
if (embedder !== undefined && config.memoryEmbedder !== undefined) {
|
|
163
|
+
await ensurePgMemoryEmbedderMetaSchema(q);
|
|
164
|
+
await reconcileEmbedderFingerprint(q, { model: config.memoryEmbedder.model, dimensions: config.memoryEmbedder.dimensions }, { log: logger });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
logger.info("memory_embedder_identity_absent", {
|
|
168
|
+
note: "no MEMORY_EMBEDDER_* configured — the vector fingerprint gate is not wired and any recorded identity is left as-is; " +
|
|
169
|
+
"note that rows rewritten while the embedder is absent get their embedding written NULL anyway, so re-configuring the " +
|
|
170
|
+
"SAME identity later reuses vectors only for rows that have not been touched in the meantime",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
153
173
|
const pgMem = new PgMemoryEngineBackend(q, {
|
|
154
174
|
history: countedHistorySink(new PgMemoryHistoryStore(q)),
|
|
155
175
|
...(embedder !== undefined ? { embedder } : {}),
|
|
@@ -225,6 +245,7 @@ export async function openStores(ctx) {
|
|
|
225
245
|
coreRemoteExecutionEnv,
|
|
226
246
|
declaredCapable: config.memoryPersistenceCapable,
|
|
227
247
|
containmentRoots,
|
|
248
|
+
engineBackend: config.memoryEngineBackend, // 只进文案:哪个旋钮真能挪动这条腿的根
|
|
228
249
|
});
|
|
229
250
|
for (const w of audit.warnings)
|
|
230
251
|
logger.warn(w.tag, { detail: w.detail, lane: lane ?? "in-process", root: memoryEngine?.root ?? null });
|
|
@@ -395,12 +416,12 @@ export async function openStores(ctx) {
|
|
|
395
416
|
});
|
|
396
417
|
}
|
|
397
418
|
// N0 boot warn: loud in BOTH postures — "dark" so an upgrade that silently turns memory off is visible,
|
|
398
|
-
// "forced" so an operator override states what it depends on (harvest only sees the WORKER fs).
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
419
|
+
// "forced" so an operator override states what it depends on (harvest only sees the WORKER fs). 文案与分腿
|
|
420
|
+
// 判据在 memory-scope.buildMemoryRemoteLaneWarn(纯,可单测);这里只喂**装配结果**(引擎真接上了没有)——
|
|
421
|
+
// DB 腿的引擎在 :113 就点亮了,与车道姿态无关,照旧打 dark 文案会与上面的 memory_engine_enabled 对撞。
|
|
422
|
+
const laneWarn = buildMemoryRemoteLaneWarn(config, memoryEngine !== undefined);
|
|
423
|
+
if (laneWarn)
|
|
424
|
+
logger.warn("memory_engine_remote_lane", laneWarn);
|
|
404
425
|
// 142-S2.5-W1: TOC 同步 client 腿——只在 file memory 形态接线(loadConfig 已拒 DB backend
|
|
405
426
|
// 上的 MEMORY_SYNC_*,这条分支到不了)。boot 后 fire-and-forget 一轮(失败 warn 不阻断——纯本地现状
|
|
406
427
|
// 是安全降级面);之后 harvest 真有 patch 落地时再触发(onMemoryHarvestReport 站点,inflight 节流)。
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* · **engine 在场就不是我们的座位**。哪怕 `memoryWrite:false`(writeScope null)——那恰恰是引擎自己
|
|
15
15
|
* 挂只读披露的状态,server 再叠一句口径不同的「你没有持久记忆」= 同一件事两种说法同框。
|
|
16
16
|
* · **推断到「有落盘路」就闭嘴**。判别式与 core 的 `rosterCanPersist` 同口径(已知落盘路 = 挂载且未
|
|
17
|
-
* 排除的文件写工具 / 可写 shell)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* 排除的文件写工具 / 可写 shell)。⚠️ 名单确实要读(codex 复审 [medium] 之后加的 `excludeTools` 一位:
|
|
18
|
+
* roster 把落盘路**整批**卸掉时,手带在场也等于没有写通道),但负控仍然成立:必须**全部**已知落盘路
|
|
19
|
+
* 都不在才算断链 —— `excludeTools:["Write"]` 不算(Bash/Edit 还在),请求面摘一个工具名改不动结论。
|
|
20
20
|
*/
|
|
21
21
|
import { type PromptProvider, type TaskSpec } from "@sema-agent/core";
|
|
22
22
|
import type { ScenarioHands } from "./hands-lane.js";
|
|
@@ -71,13 +71,17 @@ export interface MemoryFaceFacts {
|
|
|
71
71
|
export declare function shouldDiscloseNoPersistentMemory(facts: MemoryFaceFacts): boolean;
|
|
72
72
|
/**
|
|
73
73
|
* 把披露段组进一个 prompt provider,**按 provider 自己的钩子形分腿**(换形会改段身份与 append 座位):
|
|
74
|
-
* · typed(`stableBlocks`)⇒
|
|
74
|
+
* · typed(`stableBlocks`)⇒ {@link mountNotice}(普通形追加 behavior 声明;已组装 identity 形接在其文本后)。
|
|
75
75
|
* · string(`stableSystem`)⇒ 接在 role 层文本之后(落进 `core/role.base` 段)。
|
|
76
|
-
* · 两个钩子都没有(= core 会用 `defaultPromptProvider`)⇒
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
76
|
+
* · 两个钩子都没有(= core 会用 `defaultPromptProvider`)⇒ **只发一条 typed 声明**,role 层留给 core 自己铸。
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ 最后一腿为什么不是「自铸 role 层」(2026-08-12 复扫 medium,已实测):自铸形
|
|
79
|
+
* `stableSystem: (ctx) => ${ctx.userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT}\n\n${段落}` 把**调用方的**
|
|
80
|
+
* systemPrompt 逐字变成了 provider 的输出。调用方传一份已组装 prompt(集成方/center 的常见形)时,装配器的
|
|
81
|
+
* 迁移卫就在 provider 输出上探到三锚 ⇒ pass-through 臂 ⇒ 保留集无 `core/role.append` ⇒ 已被 append 门放行的
|
|
82
|
+
* `appendSystemPrompt`/`settings.outputStyle` 静默蒸发(门在包装**之前**按未包装的 provider 判,两边分家)。
|
|
83
|
+
* 走 typed 声明腿就没有这条耦合:role 层仍由 core 按 `userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT` 自己铸,
|
|
84
|
+
* 我方只贡献一条 behavior 段,`providerDropsAppend` 的判决包装前后逐格相同。
|
|
81
85
|
*/
|
|
82
86
|
export declare function withNoPersistentMemoryNotice(provider: PromptProvider | undefined): PromptProvider;
|
|
83
87
|
//# sourceMappingURL=memory-notice.d.ts.map
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
* · **engine 在场就不是我们的座位**。哪怕 `memoryWrite:false`(writeScope null)——那恰恰是引擎自己
|
|
15
15
|
* 挂只读披露的状态,server 再叠一句口径不同的「你没有持久记忆」= 同一件事两种说法同框。
|
|
16
16
|
* · **推断到「有落盘路」就闭嘴**。判别式与 core 的 `rosterCanPersist` 同口径(已知落盘路 = 挂载且未
|
|
17
|
-
* 排除的文件写工具 / 可写 shell)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* 排除的文件写工具 / 可写 shell)。⚠️ 名单确实要读(codex 复审 [medium] 之后加的 `excludeTools` 一位:
|
|
18
|
+
* roster 把落盘路**整批**卸掉时,手带在场也等于没有写通道),但负控仍然成立:必须**全部**已知落盘路
|
|
19
|
+
* 都不在才算断链 —— `excludeTools:["Write"]` 不算(Bash/Edit 还在),请求面摘一个工具名改不动结论。
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
21
|
+
import { NO_PERSISTENT_MEMORY_NOTICE } from "@sema-agent/core";
|
|
22
|
+
import { hasConstitutionAnchors } from "../task-settings.js";
|
|
22
23
|
/**
|
|
23
24
|
* 「已知落盘路」的工具名闭集 —— 与 core 的 `rosterCanPersist` 同口径:**文件写工具**(Write/Edit/
|
|
24
25
|
* NotebookEdit)或**可写 shell**(Bash)。刻意**不是** `HAND_TOOL_EFFECTS` 里 effect=write 的全集:
|
|
@@ -63,28 +64,47 @@ export function shouldDiscloseNoPersistentMemory(facts) {
|
|
|
63
64
|
// `Write` 不算无写通道(Bash/Edit 还在)—— 必须**全部**已知落盘路都不在,才算这条链断了。
|
|
64
65
|
return !anyPersistenceToolSurvives(facts.excludeTools);
|
|
65
66
|
}
|
|
67
|
+
/** 披露段作为独立 `behavior` 声明的形。**不能**用 `identity`:那个 slot 会替换 role base。 */
|
|
68
|
+
const NOTICE_BLOCK = { id: NO_PERSISTENT_MEMORY_SECTION_ID, slot: "behavior", text: NO_PERSISTENT_MEMORY_NOTICE };
|
|
69
|
+
/**
|
|
70
|
+
* 把披露段挂进一组 typed 声明。两条腿,分界线是 core 装配器会走哪条臂:
|
|
71
|
+
* · 普通形 ⇒ 追加一条 `behavior` 声明(装配器正常 compose,段落各就各位);
|
|
72
|
+
* · 声明里有**已组装 identity**(三锚齐全)⇒ 装配器走 pass-through 臂:`roleBase = 该 identity 的文本`,
|
|
73
|
+
* 其余声明**一条不挂**(core 只发一条「N 条声明未挂载」的告警)。此时追加 behavior 段等于什么都没做 ——
|
|
74
|
+
* 披露判为真、模型侧一个字都收不到,正是 #148 要消灭的那种沉默。所以这一形改成把披露段接在**该 identity
|
|
75
|
+
* 文本之后**(pass-through 臂原样把它当 `core/role.base` 发出去),文字真的进 prompt。
|
|
76
|
+
* ⚠️ `contentHash` 一并摘掉:文本被追加过就不再是 center 那份发布物的摘要,留着 = 一条对不上的对账锚
|
|
77
|
+
* (core 会为不匹配的摘要再发一条告警)。原文一个字节不改,只在后面接。
|
|
78
|
+
*/
|
|
79
|
+
function mountNotice(decls) {
|
|
80
|
+
const assembledIdentity = decls.find((d) => d.slot === "identity" && hasConstitutionAnchors(d.text));
|
|
81
|
+
if (!assembledIdentity)
|
|
82
|
+
return [...decls, NOTICE_BLOCK];
|
|
83
|
+
return decls.map((d) => (d === assembledIdentity ? { id: d.id, slot: d.slot, text: `${d.text}\n\n${NO_PERSISTENT_MEMORY_NOTICE}` } : d));
|
|
84
|
+
}
|
|
66
85
|
/**
|
|
67
86
|
* 把披露段组进一个 prompt provider,**按 provider 自己的钩子形分腿**(换形会改段身份与 append 座位):
|
|
68
|
-
* · typed(`stableBlocks`)⇒
|
|
87
|
+
* · typed(`stableBlocks`)⇒ {@link mountNotice}(普通形追加 behavior 声明;已组装 identity 形接在其文本后)。
|
|
69
88
|
* · string(`stableSystem`)⇒ 接在 role 层文本之后(落进 `core/role.base` 段)。
|
|
70
|
-
* · 两个钩子都没有(= core 会用 `defaultPromptProvider`)⇒
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
89
|
+
* · 两个钩子都没有(= core 会用 `defaultPromptProvider`)⇒ **只发一条 typed 声明**,role 层留给 core 自己铸。
|
|
90
|
+
*
|
|
91
|
+
* ⚠️ 最后一腿为什么不是「自铸 role 层」(2026-08-12 复扫 medium,已实测):自铸形
|
|
92
|
+
* `stableSystem: (ctx) => ${ctx.userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT}\n\n${段落}` 把**调用方的**
|
|
93
|
+
* systemPrompt 逐字变成了 provider 的输出。调用方传一份已组装 prompt(集成方/center 的常见形)时,装配器的
|
|
94
|
+
* 迁移卫就在 provider 输出上探到三锚 ⇒ pass-through 臂 ⇒ 保留集无 `core/role.append` ⇒ 已被 append 门放行的
|
|
95
|
+
* `appendSystemPrompt`/`settings.outputStyle` 静默蒸发(门在包装**之前**按未包装的 provider 判,两边分家)。
|
|
96
|
+
* 走 typed 声明腿就没有这条耦合:role 层仍由 core 按 `userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT` 自己铸,
|
|
97
|
+
* 我方只贡献一条 behavior 段,`providerDropsAppend` 的判决包装前后逐格相同。
|
|
75
98
|
*/
|
|
76
99
|
export function withNoPersistentMemoryNotice(provider) {
|
|
77
100
|
if (provider?.stableBlocks) {
|
|
78
101
|
const blocks = provider.stableBlocks.bind(provider);
|
|
79
|
-
return {
|
|
80
|
-
...provider,
|
|
81
|
-
stableBlocks: (ctx) => [...blocks(ctx), { id: NO_PERSISTENT_MEMORY_SECTION_ID, slot: "behavior", text: NO_PERSISTENT_MEMORY_NOTICE }],
|
|
82
|
-
};
|
|
102
|
+
return { ...provider, stableBlocks: (ctx) => mountNotice(blocks(ctx)) };
|
|
83
103
|
}
|
|
84
104
|
if (provider?.stableSystem) {
|
|
85
105
|
const stable = provider.stableSystem.bind(provider);
|
|
86
106
|
return { ...provider, stableSystem: (ctx) => `${stable(ctx)}\n\n${NO_PERSISTENT_MEMORY_NOTICE}` };
|
|
87
107
|
}
|
|
88
|
-
return {
|
|
108
|
+
return { stableBlocks: () => [NOTICE_BLOCK] };
|
|
89
109
|
}
|
|
90
110
|
//# sourceMappingURL=memory-notice.js.map
|
|
@@ -8,6 +8,20 @@ import type { EffectiveConfig } from "./types.js";
|
|
|
8
8
|
* `this.deps.models/roles/pricing` LIVE per-task and `/v1/models` reads `config.models` — both share the
|
|
9
9
|
* reference captured at boot, so mutating it (vs reassigning) updates both with no split, no Runner rebuild. */
|
|
10
10
|
export declare function mutateInPlace<V>(target: Record<string, V>, source: Record<string, V>): void;
|
|
11
|
+
/**
|
|
12
|
+
* #233 / A-002.8 —— 无法解读的名单条目的**代表元**。
|
|
13
|
+
*
|
|
14
|
+
* 为什么需要它(codex 复审 R1 真 finding):把坏条目**丢掉**会让 `[7, null]` 这种「在场但一条都读不
|
|
15
|
+
* 懂」的名单塌成**空数组**,而空数组在消费端的语义恰恰是「治理关闭 = 全放行」—— 一个表达"我要限制"
|
|
16
|
+
* 的配置,因为写错了类型,反而把门整个打开。方向必须反过来:名单在场 = operator 意图限制;读不懂的
|
|
17
|
+
* 条目 ⇒ 换成一个**永不可能是 catalog name** 的代表元(前导 NUL,registry-core 的 name 词法不可能产
|
|
18
|
+
* 出),于是它在归一时解不到任何目录条目 ⇒ 谁都准不了。
|
|
19
|
+
*
|
|
20
|
+
* 后果是**有界的**:被拒的只有「用户逐 turn 显式点模型」这一件事;`default` / roles / 档位表照常,
|
|
21
|
+
* 任务照跑。写错配置的代价是"点不了模型"而不是"服务不可用",且 warn 当场点名成因。
|
|
22
|
+
* 🔴 只活在内存判定面:不进 wire(`/v1/models` 只发布尔)、不进日志(warn 打的是原始坏值的类型标记)。
|
|
23
|
+
*/
|
|
24
|
+
export declare const UNPARSABLE_AT_MODEL_ALLOWLIST_ENTRY = "\0<unparsable-at-model-allowlist-entry>";
|
|
11
25
|
/** 返回值=是否 COMMIT(false 仅在 [2283]③ 世代序 CAS 拒绝时)。caller 收到 false 必须把**同一世代的
|
|
12
26
|
* 旁路消费**(prompts 采用/pricing/keyResolver/etag 推进/LKG 落盘)一并跳过——否则 applyEffective 拒了
|
|
13
27
|
* 主面、旁路却半应用同一个被拒世代,混合世代从侧门回来。首次 apply(live 未登记)恒 true。 */
|
|
@@ -87,6 +87,73 @@ const appliedGeneration = new WeakMap();
|
|
|
87
87
|
* 反自评等),且旧守卫 `length>0` 粘住的不是 env 底而是**上一个 center 世代**(applyRuntimeHot 头注
|
|
88
88
|
* NON-STICKY 论证同族,roles 曾漏在纪律外——cli [2311] 实测「用户撤销被静默吞掉」即此)。 */
|
|
89
89
|
const envRolesBaseline = new WeakMap();
|
|
90
|
+
/**
|
|
91
|
+
* #233 / A-002.8 —— 无法解读的名单条目的**代表元**。
|
|
92
|
+
*
|
|
93
|
+
* 为什么需要它(codex 复审 R1 真 finding):把坏条目**丢掉**会让 `[7, null]` 这种「在场但一条都读不
|
|
94
|
+
* 懂」的名单塌成**空数组**,而空数组在消费端的语义恰恰是「治理关闭 = 全放行」—— 一个表达"我要限制"
|
|
95
|
+
* 的配置,因为写错了类型,反而把门整个打开。方向必须反过来:名单在场 = operator 意图限制;读不懂的
|
|
96
|
+
* 条目 ⇒ 换成一个**永不可能是 catalog name** 的代表元(前导 NUL,registry-core 的 name 词法不可能产
|
|
97
|
+
* 出),于是它在归一时解不到任何目录条目 ⇒ 谁都准不了。
|
|
98
|
+
*
|
|
99
|
+
* 后果是**有界的**:被拒的只有「用户逐 turn 显式点模型」这一件事;`default` / roles / 档位表照常,
|
|
100
|
+
* 任务照跑。写错配置的代价是"点不了模型"而不是"服务不可用",且 warn 当场点名成因。
|
|
101
|
+
* 🔴 只活在内存判定面:不进 wire(`/v1/models` 只发布尔)、不进日志(warn 打的是原始坏值的类型标记)。
|
|
102
|
+
*/
|
|
103
|
+
export const UNPARSABLE_AT_MODEL_ALLOWLIST_ENTRY = "\u0000<unparsable-at-model-allowlist-entry>";
|
|
104
|
+
/**
|
|
105
|
+
* #233 / A-002.8 —— `models.atModelAllowlist` 的 STAGE 侧归一(纯函数,可抛的事都不做)。
|
|
106
|
+
*
|
|
107
|
+
* 语义(registry-core `ModelsDomain.atModelAllowlist` 的 JSDoc 同源):**缺席/空 = 全放行**。返回值恒是
|
|
108
|
+
* 一个新数组,调用点每次 apply 整表重算 ⇒ **非粘**。
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ 入参刻意收 `unknown`:`EffectiveConfig` 把它声明成 `string[]`,但 **local lane 的 eff 来自
|
|
111
|
+
* `FileConfigStore` 的原始 JSON**(不是 zod 输出),类型在那条腿上不是运行时保证。
|
|
112
|
+
*
|
|
113
|
+
* 坏形处置(不静默,三条各有一句;共同的方向 = **在场即意图限制,读不懂就更严不更松**):
|
|
114
|
+
* - 字段在场但**不是数组** ⇒ 响亮 warn + 落**一条代表元**(见上)= 用户逐 turn 选择全拒。刻意不按
|
|
115
|
+
* "缺席"处置:那会把一个写坏了的**限制**声明翻译成"不限制",正是治理轴上最不该有的失败方向。
|
|
116
|
+
* 也刻意不整代拒绝:那会把整个模型面(models/roles/tiers/密钥)一起拖下水,爆炸半径不成比例。
|
|
117
|
+
* - 数组里的**非串/空串**成员 ⇒ **逐条换成代表元**(不是丢弃)+ warn:名单只会更窄,不会更宽。
|
|
118
|
+
* - 成员名**不在本世代启用目录里**(悬空 ref)⇒ warn(照 `sema_registry_default_dangling` 先例)。
|
|
119
|
+
* 保留原串:悬空成员在消费侧本就归一不到任何条目;留声必须有——名单整条悬空时"谁都点不了"是
|
|
120
|
+
* fail-closed 的真后果,运维要能一眼看见成因。
|
|
121
|
+
*/
|
|
122
|
+
function stageAtModelAllowlist(raw, models, logger) {
|
|
123
|
+
if (raw === undefined || raw === null)
|
|
124
|
+
return [];
|
|
125
|
+
if (!Array.isArray(raw)) {
|
|
126
|
+
logger?.warn("sema_registry_at_model_allowlist_invalid", {
|
|
127
|
+
type: typeof raw,
|
|
128
|
+
hint: "models.atModelAllowlist must be an array of catalog model names — an unreadable allowlist is treated as ALLOWING NOTHING (fail-closed: a broken restriction must not read as 'no restriction'); tasks still run on the default/role models",
|
|
129
|
+
});
|
|
130
|
+
return [UNPARSABLE_AT_MODEL_ALLOWLIST_ENTRY];
|
|
131
|
+
}
|
|
132
|
+
const names = [];
|
|
133
|
+
const bad = [];
|
|
134
|
+
for (const entry of raw) {
|
|
135
|
+
if (typeof entry === "string" && entry.length > 0)
|
|
136
|
+
names.push(entry);
|
|
137
|
+
else {
|
|
138
|
+
bad.push(typeof entry === "string" ? "<empty-string>" : `<non-string:${typeof entry}>`);
|
|
139
|
+
names.push(UNPARSABLE_AT_MODEL_ALLOWLIST_ENTRY);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (bad.length > 0) {
|
|
143
|
+
logger?.warn("sema_registry_at_model_allowlist_bad_entries", {
|
|
144
|
+
bad,
|
|
145
|
+
hint: "non-string / empty entries in models.atModelAllowlist allow NOTHING (they are kept as unresolvable placeholders, never dropped — dropping them would silently WIDEN the allowlist, and an all-bad list would collapse to the empty = allow-everything form)",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const dangling = names.filter((n) => n !== UNPARSABLE_AT_MODEL_ALLOWLIST_ENTRY && models[n] === undefined);
|
|
149
|
+
if (dangling.length > 0) {
|
|
150
|
+
logger?.warn("sema_registry_at_model_allowlist_dangling", {
|
|
151
|
+
dangling,
|
|
152
|
+
hint: "these @-mention allowlist entries name a model that is not in the enabled catalog — they allow nothing; an allowlist that is ENTIRELY dangling refuses every user model pick",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return names;
|
|
156
|
+
}
|
|
90
157
|
/** 返回值=是否 COMMIT(false 仅在 [2283]③ 世代序 CAS 拒绝时)。caller 收到 false 必须把**同一世代的
|
|
91
158
|
* 旁路消费**(prompts 采用/pricing/keyResolver/etag 推进/LKG 落盘)一并跳过——否则 applyEffective 拒了
|
|
92
159
|
* 主面、旁路却半应用同一个被拒世代,混合世代从侧门回来。首次 apply(live 未登记)恒 true。 */
|
|
@@ -130,6 +197,14 @@ function commitStaged(config, staged) {
|
|
|
130
197
|
config.model = config.models.default;
|
|
131
198
|
mutateInPlace(config.roles, mp.roles); // IN PLACE: Runner reads this.deps.roles live([2314] 裁①:staged roles 已是 env 底+center 逐键 merge 的整表,无条件落——旧 `length>0` 守卫=空表粘住上一 center 世代)
|
|
132
199
|
mutateInPlace(config.tiers, mp.activeTiers);
|
|
200
|
+
// #233:整表重赋值(不是 mutateInPlace)—— 本键的全部消费点(resolveSpec / side-query / GET /v1/models /
|
|
201
|
+
// 未知模型 400 的 available)都是**逐请求**从 `config` 上读,没有任何一处在 boot 期捕获这个数组本身
|
|
202
|
+
// (与 `config.modelApiKeyEnv` 的 "reassign ok" 同判据)。非粘由 stage 侧整表重算保证。
|
|
203
|
+
// 🔴 刻意与整个模型面**同生共死**(在 `if (mp)` 里,不在外面):名单说的是"这本目录里哪些可点",
|
|
204
|
+
// 所以它必须与它描述的那本目录同世代。plane 被跳过时(deferModelPlane 的 tier-frozen 窗口 /
|
|
205
|
+
// 本世代零启用模型 ⇒ 目录仍是上一世代或 env 底),名单跟着一起停在同一世代——把新名单配到旧目录上
|
|
206
|
+
// 才是真正的错配(按新名字判旧目录 = 全员悬空 = 谁都点不了)。
|
|
207
|
+
config.atModelAllowlist = mp.atModelAllowlist;
|
|
133
208
|
}
|
|
134
209
|
if (staged.gateAssignments) {
|
|
135
210
|
for (const [k, v] of staged.gateAssignments)
|
|
@@ -300,6 +375,10 @@ function stageEffective(config, eff, logger, opts) {
|
|
|
300
375
|
// wouldDefaultModel 共用同一只,报数与真 apply 永不撕裂——codex M2)。
|
|
301
376
|
const picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, enabled[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", { source, name, hint: "explicit default names a model that is not in the enabled catalog — falling to the next source" }));
|
|
302
377
|
models.default = models[picked.name];
|
|
378
|
+
// #233 / A-002.8 `@`-mention 允许名单:与 roles 同族的**非粘整表重算**——每次 modelPlane apply 都从
|
|
379
|
+
// 本世代的 eff 重算,center 撤名单/发空表 ⇒ 本表清空 ⇒ 回「全放行」。粘写法(`length>0` 才写)会让
|
|
380
|
+
// 上一个 center 世代的名单常驻 = **收不回的治理锁**(roles 那条 [2314] 裁①同族病,不重犯)。
|
|
381
|
+
const atModelAllowlist = stageAtModelAllowlist(eff.models?.atModelAllowlist, models, logger);
|
|
303
382
|
modelPlane = {
|
|
304
383
|
models,
|
|
305
384
|
modelApiKeyEnv,
|
|
@@ -307,7 +386,8 @@ function stageEffective(config, eff, logger, opts) {
|
|
|
307
386
|
quotaWeights,
|
|
308
387
|
roles,
|
|
309
388
|
activeTiers,
|
|
310
|
-
|
|
389
|
+
atModelAllowlist,
|
|
390
|
+
infoLine: { count: enabled.length, default: models.default.id, defaultSource: picked.source, roles: Object.keys(roles), tiers: Object.keys(activeTiers), atModelAllowlist: atModelAllowlist.length, sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length },
|
|
311
391
|
};
|
|
312
392
|
}
|
|
313
393
|
// Runtime governance/limit gates (phase-2): STARTUP only (restart-to-apply) — the live RateLimiter / CostQuota
|
package/dist/config-types.d.ts
CHANGED
|
@@ -213,6 +213,21 @@ export interface ServiceConfigFlat {
|
|
|
213
213
|
/** Role → model map (1.24): `subagent`/`summarize` → the cheap model when `MODEL_CHEAP_ID` is set
|
|
214
214
|
* (council lenses + compaction get cheap), `default`/`team`/`synthesize` stay on the main model. */
|
|
215
215
|
roles: ModelRoles;
|
|
216
|
+
/** #233 / A-002.8 —— **`@`-mention 允许名单**(registry-core `models.atModelAllowlist` 的服务端消费面)。
|
|
217
|
+
* 语义(registry-core `ModelsDomain.atModelAllowlist` 的 JSDoc 同源):**缺席/空 = 全部启用模型都可被用户点选**;非空 = 用户
|
|
218
|
+
* 逐 turn 选择通道只能落在名单内的 catalog name 上。
|
|
219
|
+
*
|
|
220
|
+
* 辖域(设计稿 233 v2 终裁,施工侧成文):
|
|
221
|
+
* - **受限**=用户通道:`body.model` / `settings.model`(同属请求体,untrusted) / objective 内联
|
|
222
|
+
* `@name` / `body.compactionModel` / `POST /v1/side-query` 的 `body.model`;
|
|
223
|
+
* - **不受限**=operator 配置通道:`models.roles` / `models.default` / 档位绑定表 / side-query 的
|
|
224
|
+
* `modelRole`(角色表选路)——名单管「用户可点什么」,不管「operator 配什么」。
|
|
225
|
+
* - **提交时门**:core 运行时路由(degrade / cascade / roles 解析)**不在射程**——那些是引擎按配置
|
|
226
|
+
* 自己走的路,不是一次用户选择(诚实声明,非运行时执法)。
|
|
227
|
+
*
|
|
228
|
+
* NO env source —— env lane 恒空(INERT=全放行,ship-dark 缺省=旧行为);只由 applyEffective 从
|
|
229
|
+
* `models.atModelAllowlist` 整表重算填充(**非粘**:center 撤名单 ⇒ 本表清空 ⇒ 回全放行)。 */
|
|
230
|
+
atModelAllowlist: readonly string[];
|
|
216
231
|
/**
|
|
217
232
|
* Session backend: in-memory (default), tidb (durable L1 event-log center), or `auto` — probe a
|
|
218
233
|
* configured DB at startup and fall back to in-memory if it's unreachable (for local/intranet binaries:
|
|
@@ -919,6 +934,17 @@ export interface ServiceConfigFlat {
|
|
|
919
934
|
* T11 stands, just bounded instead of immediate. A SECOND SIGHUP (or SIGTERM) escalates to the normal drain
|
|
920
935
|
* path. Default 120s, floor 5s. Env: SIGHUP_IDLE_GRACE_MS. */
|
|
921
936
|
sighupIdleGraceMs: number;
|
|
937
|
+
/** #219(黑板 [3617]):引擎 **parent 监视**。壳自 spawn 引擎的同机形里,壳被 SIGKILL(崩溃/强杀,
|
|
938
|
+
* 没有任何清理钩子)后引擎被 reparent 到 init 而**继续活着** —— 孤儿引擎占端口、占库连接、占模型
|
|
939
|
+
* 配额,而且再没有人会给它发 SIGTERM(T11 的进程组腿只覆盖信号送得到的形)。设了本键 = 引擎每 15s
|
|
940
|
+
* 用 `process.kill(pid, 0)` 探一拍这个 pid,**连续两拍** ESRCH(父不在)才判死;判死后走与 SIGTERM
|
|
941
|
+
* **逐字同一条** graceful drain 腿(in-flight run 的结算 / park 语义与人工停机一致),不是裸
|
|
942
|
+
* `process.exit`。`EPERM` = 进程在、只是本进程无权给它发信号 ⇒ **算活着**。
|
|
943
|
+
* **缺席 ⇒ 整件不装配**(opt-in;存量部署逐字零变化);**非正整数 ⇒ 拒启**(带指路文案)。
|
|
944
|
+
* 拍频固定 15s,不开 interval 旋钮(有真需求再议,防旋钮增殖)。
|
|
945
|
+
* 已知残余(成文,不收窄):两拍窗口内 pid 被系统复用 ⇒ 探活看到的是**新**进程,监视判"父还活着"
|
|
946
|
+
* 而不自退。监视是**尽力自愈**、不是强一致的父子生命周期绑定。 Env: `SEMA_PARENT_PID`。 */
|
|
947
|
+
parentPid?: number;
|
|
922
948
|
/** E23 (shell-host contract): inbound MCP elicitation HITL. When enabled, the service mounts `RunnerDeps.onElicit` + the
|
|
923
949
|
* `POST /v1/elicitations/:id/respond` route so an opted-in MCP server (`McpServerSpec.elicitation`, itself default
|
|
924
950
|
* OFF) can ask the END USER for input mid-tool-call. LIVE-ONLY (no durable suspend; lost on crash/replica-change).
|
|
@@ -1118,7 +1144,7 @@ export interface ServiceConfigFlat {
|
|
|
1118
1144
|
/** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
|
|
1119
1145
|
export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "rewindSnapshotMaxMb" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "sendUserFile">;
|
|
1120
1146
|
/** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
|
|
1121
|
-
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "cascadeLadder" | "degrade">;
|
|
1147
|
+
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
|
|
1122
1148
|
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
|
1123
1149
|
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
1124
1150
|
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
@@ -1130,7 +1156,7 @@ export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authToken
|
|
|
1130
1156
|
/** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
|
|
1131
1157
|
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "rosterRetentionMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes">;
|
|
1132
1158
|
/** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
|
|
1133
|
-
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
1159
|
+
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "parentPid" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
1134
1160
|
/** 组:observability(可观测)。 */
|
|
1135
1161
|
export type ServiceObservabilityConfig = Pick<ServiceConfigFlat, "metricsToken" | "traceToken" | "toolTrace" | "traceThinking" | "logLevel" | "otel">;
|
|
1136
1162
|
/** 组:integrations(外部集成)。`mcpServers` 由 sema-registry 适配器填(无 env 腿),归本组。 */
|
package/dist/config.js
CHANGED
|
@@ -967,6 +967,11 @@ function parseModelDomain() {
|
|
|
967
967
|
tiers: {}, // 档位表:env lane 恒空(INERT);applyEffective 从 models.tierGroups 解析填充(保引用)
|
|
968
968
|
projects: {}, // 142-S4 项目登记簿:env lane 恒空(INERT);applyEffective 从 projects 域填充(保引用)
|
|
969
969
|
roles,
|
|
970
|
+
// #233 / A-002.8 `@`-mention 允许名单:env lane 恒空(INERT = 全放行,ship-dark 缺省 = 旧行为);
|
|
971
|
+
// applyEffective 从 models.atModelAllowlist 整表重算填充(非粘,见 config-types.ts 键注)。
|
|
972
|
+
// 刻意**没有** env 腿:名单是 center 治理面的字段(registry-core 侧还有 name⊆catalog 的 cross-domain
|
|
973
|
+
// 校验),给它开一条 env 后门 = 两个写者一个语义面,而 env 侧没有那道校验。
|
|
974
|
+
atModelAllowlist: [],
|
|
970
975
|
cascadeLadder: csv("MODEL_CASCADE_LADDER"),
|
|
971
976
|
degrade: process.env.MODEL_DEGRADE_TO
|
|
972
977
|
? {
|
|
@@ -1304,6 +1309,36 @@ function syncEntryCap(name, field) {
|
|
|
1304
1309
|
throw new Error(`env ${name}="${raw}" must be an integer >= 1 (it is an entries-per-batch cap; an ignored cap means NO cap)`);
|
|
1305
1310
|
return { [field]: n };
|
|
1306
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* #219(黑板 [3617]):`SEMA_PARENT_PID` —— 引擎 parent 监视的**唯一**开关(壳侧 cli 已确认这个名字)。
|
|
1314
|
+
*
|
|
1315
|
+
* 三态,与 `syncEntryCap` 同族(#210 A 档「坏值拒启」):
|
|
1316
|
+
* · 未设 / 空串 ⇒ **键缺席**,整件不装配(opt-in;存量部署逐字零变化,零 warn);
|
|
1317
|
+
* · 正整数 ⇒ 原样进配置;
|
|
1318
|
+
* · 其余(0 / 负数 / 小数 / 非数字 / 带单位后缀)⇒ **拒启**并指路。
|
|
1319
|
+
*
|
|
1320
|
+
* 为什么坏值不静默回默认:本键的"默认"就是**整件不装配**,于是静默回默认 = operator 设了监视、进程
|
|
1321
|
+
* 照常起、监视根本不在 —— 恰恰是这个键要防的那类"孤儿引擎"在无人知晓的情况下继续发生。而且坏值的
|
|
1322
|
+
* 另一半后果更隐蔽:`0` 在 `process.kill(0, 0)` 里的语义是**当前进程组**(探活恒成功=监视永不触发),
|
|
1323
|
+
* 负数是**进程组** id(探到的根本不是父进程);这两个值不是"差一点对",是语义完全不同的另一件事。
|
|
1324
|
+
*/
|
|
1325
|
+
function parentPidEnv() {
|
|
1326
|
+
const raw = process.env.SEMA_PARENT_PID;
|
|
1327
|
+
if (raw === undefined || raw.trim() === "")
|
|
1328
|
+
return {};
|
|
1329
|
+
const n = Number(raw);
|
|
1330
|
+
// 上界 = int32 最大值:`process.kill` 的 pid 形参是 int32,`2147483648` 抛的是 `ERR_INVALID_ARG_TYPE`
|
|
1331
|
+
// 而**不是** ESRCH(codex 复审 M,实测证实)。落在探活的"未知 errno ⇒ 保守判活"支上,后果是:配置
|
|
1332
|
+
// 过了校验、`parent_watch_armed` 照打、之后每一拍都被当成"父还活着" —— 监视永久失效。校验面必须只
|
|
1333
|
+
// 放行 process.kill 真吃得下的范围,才对得起"设了就一定在监视"这句承诺。
|
|
1334
|
+
if (!Number.isInteger(n) || n < 1 || n > 2_147_483_647) {
|
|
1335
|
+
throw new Error(`env SEMA_PARENT_PID="${raw}" must be a POSITIVE INTEGER process id in [1, 2147483647] — it is the pid this ` +
|
|
1336
|
+
`engine watches (the shell/supervisor that spawned it); 0 and negatives mean "process GROUP" to ` +
|
|
1337
|
+
`process.kill, and anything past 2147483647 is not a pid process.kill can even be asked about. ` +
|
|
1338
|
+
`UNSET it to turn the parent watch off (absent = not wired at all).`);
|
|
1339
|
+
}
|
|
1340
|
+
return { parentPid: n };
|
|
1341
|
+
}
|
|
1307
1342
|
/**
|
|
1308
1343
|
* #228(黑板 [3590]/[3606] 两裁):`MEMORY_EMBEDDER_*` 族 —— 记忆检索向量面的**唯一**开关。
|
|
1309
1344
|
*
|
|
@@ -1849,6 +1884,7 @@ function parseLimitsHttpDomain() {
|
|
|
1849
1884
|
},
|
|
1850
1885
|
drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), 10_000), // SIGTERM drain window, default 10min, floor 10s(#210 B 档:把 600 当成"秒"填进来的手滑最常见,夹取后必须点名)
|
|
1851
1886
|
sighupIdleGraceMs: clampEnvWithWarn("SIGHUP_IDLE_GRACE_MS", String(120_000), 5_000), // B3: SIGHUP idle-shutdown window, default 2min, floor 5s
|
|
1887
|
+
...parentPidEnv(), // #219:壳 pid 存活监视,缺席 ⇒ 键缺席 ⇒ 整件不装配
|
|
1852
1888
|
// BL-9: numEnv (not bare Number) — a non-numeric typo FAILS at startup instead of becoming NaN, which would
|
|
1853
1889
|
// silently DISABLE the operator cost/rate ceiling (every `> ceiling` comparison against NaN is false).
|
|
1854
1890
|
rateLimitPerMin: numEnv("RATE_LIMIT_RPM", "0"),
|
|
@@ -1956,7 +1992,7 @@ const STORE_GROUP_KEYS = [
|
|
|
1956
1992
|
];
|
|
1957
1993
|
const MODEL_PLANE_GROUP_KEYS = [
|
|
1958
1994
|
"gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "models",
|
|
1959
|
-
"modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "tiers", "projects", "roles", "cascadeLadder", "degrade",
|
|
1995
|
+
"modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "tiers", "projects", "roles", "atModelAllowlist", "cascadeLadder", "degrade",
|
|
1960
1996
|
];
|
|
1961
1997
|
const APPROVAL_GROUP_KEYS = [
|
|
1962
1998
|
"approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
|
|
@@ -1986,7 +2022,7 @@ const ORCHESTRATION_GROUP_KEYS = [
|
|
|
1986
2022
|
];
|
|
1987
2023
|
const LIMITS_HTTP_GROUP_KEYS = [
|
|
1988
2024
|
"port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
|
|
1989
|
-
"attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "sighupIdleGraceMs", "rateLimitPerMin", "maxTaskCostUsd",
|
|
2025
|
+
"attachmentUnboundTtlMs", "infraCostRates", "drainGraceMs", "sighupIdleGraceMs", "parentPid", "rateLimitPerMin", "maxTaskCostUsd",
|
|
1990
2026
|
"maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec", "reapIntervalSec", "runStaleSec", "toolResultTtlSec",
|
|
1991
2027
|
];
|
|
1992
2028
|
const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
|
|
@@ -95,4 +95,16 @@ export declare function recentTerminalWorkflowRows(store: WorkflowRunStore | und
|
|
|
95
95
|
export declare function clearTerminalWindowCooldownForTest(store: WorkflowRunStore, scope: string): void;
|
|
96
96
|
/** test-only:这个 store 目前还记着几个 scope(codex R4-M2 的空闲销号有没有真的销)。 */
|
|
97
97
|
export declare function trackedTerminalWindowScopesForTest(store: WorkflowRunStore): number;
|
|
98
|
+
/**
|
|
99
|
+
* test-only:下一次开流会不会**合流到**当前这代在飞的读(与 {@link sharedRead} 同一份判据,不另抄常数)。
|
|
100
|
+
*
|
|
101
|
+
* 为什么需要它:挂死的读永不 settle,那一代会一直留在表上、只被复用窗按龄挡掉。测试要断言"后端恢复后
|
|
102
|
+
* 的下一次开流读得到行",前置条件恰恰是"毒代已过复用窗"——那条前置条件必须**等出来**(轮询本谓词),
|
|
103
|
+
* 不能靠"上一条连接自己吃满预算 + 一次客户端往返"凑出的那几毫秒墙钟余量(实测 8~28ms,既无因果保证
|
|
104
|
+
* 也无下界 ⇒ 负载敏感偶发红)。读到的是快照:表里没有这一 scope 就是"不会合流"。
|
|
105
|
+
*
|
|
106
|
+
* ⚠️ 刻意**不走** `scopeState`(它会顺手建表项)——观测口不许改被观测的状态(`trackedTerminalWindow
|
|
107
|
+
* ScopesForTest` 钉的空闲销号计数会被它污染)。
|
|
108
|
+
*/
|
|
109
|
+
export declare function terminalWindowJoinsInFlightForTest(store: WorkflowRunStore, scope: string): boolean;
|
|
98
110
|
//# sourceMappingURL=fleet-terminal-window.d.ts.map
|
|
@@ -216,8 +216,12 @@ export async function recentTerminalWorkflowRows(store, scope, pullScope) {
|
|
|
216
216
|
// 再发就是加压"。两个反例都被这条判据排除:① 我等的那代其实已经完成/失败并销号(快失败的店不累积
|
|
217
217
|
// 在飞的活,没有加压问题,不该罚 10 秒);② 我超时时表上挂的已经是**别人**开的新一代(我这代早settle
|
|
218
218
|
// 了),拿它当证据会让一个已经健康的 scope 被上一代的迟到超时按住。
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
// 🔴 只**查表**、不铸造(2026-08-12 合并码重扫 confirmed):快失败路径上 probe 的 .finally 先跑
|
|
220
|
+
// (清 inFlight + evictIfIdle 销号),这里若用 scopeState() 会**重建**一个新空条目——它 inFlight
|
|
221
|
+
// 恒 undefined ⇒ 冷却不开、又没人再销,每个快失败的 scope 就永久留一条(R4-M2 那条只增内存线
|
|
222
|
+
// 换了个入口)。观测口 terminalWindowJoinsInFlightForTest 的头注同判据:检查者不许创建它要检查的状态。
|
|
223
|
+
const st = stateByStore.get(store)?.get(scopeForPull);
|
|
224
|
+
if (st !== undefined && st.inFlight === probe)
|
|
221
225
|
openCooldown(store, scopeForPull, st);
|
|
222
226
|
// detail 用 `err.message` 而不是 `String(err)`:后者会调用抛出值自己的 toString,而那本身可能再抛
|
|
223
227
|
// (逃出本兜底,把降级变成故障)。非 Error 抛出值就如实说"不是 Error",不去问它任何问题。
|
|
@@ -243,12 +247,31 @@ export function clearTerminalWindowCooldownForTest(store, scope) {
|
|
|
243
247
|
export function trackedTerminalWindowScopesForTest(store) {
|
|
244
248
|
return stateByStore.get(store)?.size ?? 0;
|
|
245
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* test-only:下一次开流会不会**合流到**当前这代在飞的读(与 {@link sharedRead} 同一份判据,不另抄常数)。
|
|
252
|
+
*
|
|
253
|
+
* 为什么需要它:挂死的读永不 settle,那一代会一直留在表上、只被复用窗按龄挡掉。测试要断言"后端恢复后
|
|
254
|
+
* 的下一次开流读得到行",前置条件恰恰是"毒代已过复用窗"——那条前置条件必须**等出来**(轮询本谓词),
|
|
255
|
+
* 不能靠"上一条连接自己吃满预算 + 一次客户端往返"凑出的那几毫秒墙钟余量(实测 8~28ms,既无因果保证
|
|
256
|
+
* 也无下界 ⇒ 负载敏感偶发红)。读到的是快照:表里没有这一 scope 就是"不会合流"。
|
|
257
|
+
*
|
|
258
|
+
* ⚠️ 刻意**不走** `scopeState`(它会顺手建表项)——观测口不许改被观测的状态(`trackedTerminalWindow
|
|
259
|
+
* ScopesForTest` 钉的空闲销号计数会被它污染)。
|
|
260
|
+
*/
|
|
261
|
+
export function terminalWindowJoinsInFlightForTest(store, scope) {
|
|
262
|
+
return joinsInFlight(stateByStore.get(store)?.get(scope)?.inFlight, Date.now());
|
|
263
|
+
}
|
|
264
|
+
/** 后来者是否加入这一代在飞的读:仍在自己的预算之内才加入(见上方取舍段);更老的那代已被放弃,不加入。
|
|
265
|
+
* 抽成具名谓词的唯一理由是**单一属主**——测试的观测口({@link terminalWindowJoinsInFlightForTest})与
|
|
266
|
+
* 生产的合流决策必须由同一行代码回答,否则测试里那份常数复制件会在实现改判据的那天悄悄失效。 */
|
|
267
|
+
function joinsInFlight(probe, now) {
|
|
268
|
+
return probe !== undefined && now - probe.startedAt < FLEET_SNAPSHOT_TERMINAL_READ_BUDGET_MS;
|
|
269
|
+
}
|
|
246
270
|
/** 合流层:同 (store, scope) 上**同时只有一代**在飞的读;后来者加入仍在预算内的那一代,settle 即销号。 */
|
|
247
271
|
function sharedRead(store, scope) {
|
|
248
272
|
const st = scopeState(store, scope);
|
|
249
273
|
const existing = st.inFlight;
|
|
250
|
-
|
|
251
|
-
if (existing && Date.now() - existing.startedAt < FLEET_SNAPSHOT_TERMINAL_READ_BUDGET_MS)
|
|
274
|
+
if (joinsInFlight(existing, Date.now()))
|
|
252
275
|
return existing;
|
|
253
276
|
const entry = {
|
|
254
277
|
startedAt: Date.now(),
|