@sema-agent/server 7.13.0 → 7.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +21 -0
- package/dist/boot/coordinators.js +2 -1
- package/dist/boot/memory-boundary.d.ts +84 -0
- package/dist/boot/memory-boundary.js +110 -0
- package/dist/boot/resolve-spec.js +31 -0
- package/dist/boot/runner-deps.js +19 -0
- package/dist/boot/stores.js +93 -3
- package/dist/capabilities/memory-notice.d.ts +83 -0
- package/dist/capabilities/memory-notice.js +90 -0
- package/dist/config-types.d.ts +86 -9
- package/dist/config.d.ts +1 -1
- package/dist/config.js +111 -1
- package/dist/governance-ask-marks.js +2 -1
- package/dist/http/routes/capabilities.js +10 -4
- package/dist/http/routes/trace-usage.js +43 -14
- package/dist/http/server.d.ts +9 -1
- package/dist/http/server.js +9 -1
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +2 -1
- package/dist/plugins/approval-ask-store-sql.js +2 -1
- package/dist/plugins/memory-embedder.d.ts +44 -0
- package/dist/plugins/memory-embedder.js +173 -0
- package/dist/plugins/store-backend.d.ts +3 -1
- package/dist/plugins/tidb-pool.js +11 -4
- package/dist/plugins/tool-result-store-sql.d.ts +35 -2
- package/dist/plugins/tool-result-store-sql.js +127 -11
- package/dist/plugins/web-search.d.ts +3 -1
- package/dist/plugins/web-search.js +3 -1
- package/dist/security.js +3 -1
- package/dist/tool-approval.js +19 -0
- package/package.json +3 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #217(板 [3499]②/[3521]④ 两次点名的 server 接线半场)—— 零记忆面部署形的**持久记忆披露**。
|
|
3
|
+
*
|
|
4
|
+
* 病(core [3479] 实测,#148 立案):一台没有任何持久记忆面的部署,用户说「请记住 X」,模型回一句
|
|
5
|
+
* 「已经记住了」——落盘处并不存在。用户带着「这条事实已入库」的信念离开,下一次会话才发现是空的。
|
|
6
|
+
*
|
|
7
|
+
* core 5.26.0 的分工:引擎**能自证**只读的两态(engine 侧 `writeScope:null` / runner 侧 `handsReadOnly`)
|
|
8
|
+
* 由引擎自动挂 `MEMORY_READONLY_NOTICE`;而「整台部署根本没有记忆面」这一形 core 只导出文本
|
|
9
|
+
* (`NO_PERSISTENT_MEMORY_NOTICE`)、**不注入** —— 逐字原文:「A deployment that composes its own prompt
|
|
10
|
+
* should include this block exactly when no memory face is mounted」。组装归宿主,与 `MEMORY_SAFETY`
|
|
11
|
+
* 同姿势。本模块就是那半场。
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ 两条边界,越界即双重注入/假话:
|
|
14
|
+
* · **engine 在场就不是我们的座位**。哪怕 `memoryWrite:false`(writeScope null)——那恰恰是引擎自己
|
|
15
|
+
* 挂只读披露的状态,server 再叠一句口径不同的「你没有持久记忆」= 同一件事两种说法同框。
|
|
16
|
+
* · **推断到「有落盘路」就闭嘴**。判别式与 core 的 `rosterCanPersist` 同口径(已知落盘路 = 挂载且未
|
|
17
|
+
* 排除的文件写工具 / 可写 shell)。`excludeTools:["Write"]` **不算**无写通道(Bash/Edit 仍可写)—— 本
|
|
18
|
+
* 判别式压根不读工具名单,读的是**手带表态**(`hands`/`handsReadOnly`),所以这条负控是构造性成立的:
|
|
19
|
+
* 请求面摘一个工具名改不动部署级的手带事实。
|
|
20
|
+
*/
|
|
21
|
+
import { DEFAULT_SYSTEM_PROMPT, NO_PERSISTENT_MEMORY_NOTICE } from "@sema-agent/core";
|
|
22
|
+
/**
|
|
23
|
+
* 「已知落盘路」的工具名闭集 —— 与 core 的 `rosterCanPersist` 同口径:**文件写工具**(Write/Edit/
|
|
24
|
+
* NotebookEdit)或**可写 shell**(Bash)。刻意**不是** `HAND_TOOL_EFFECTS` 里 effect=write 的全集:
|
|
25
|
+
* core 逐字裁过「a generic write-effect tool does NOT count — a mail sender's side effect is not a
|
|
26
|
+
* memory store」,`TaskStop` 就是那种(有写副作用、不是落盘路)。
|
|
27
|
+
*
|
|
28
|
+
* 名字会漂,所以配一道防漂移钉(test/no-persistent-memory-notice.test.ts):每个名字必须在 core 的
|
|
29
|
+
* `HAND_TOOL_EFFECTS` 里且 effect 为 `write` —— core 改名/改档位当场红,而不是让这里静默失配成
|
|
30
|
+
* 「没有一个写工具被排除」(fail-open 方向)。
|
|
31
|
+
*/
|
|
32
|
+
export const KNOWN_PERSISTENCE_TOOL_NAMES = ["Write", "Edit", "NotebookEdit", "Bash"];
|
|
33
|
+
/** 披露段在 `prompt.assembled` 清单里的段 id(`core/` 命名空间是 core 保留的,部署段一律自家前缀)。 */
|
|
34
|
+
export const NO_PERSISTENT_MEMORY_SECTION_ID = "sema/memory.no-persistent";
|
|
35
|
+
/** 排除后还剩不剩已知落盘路。空/缺席的排除名单 ⇒ 一定还剩(最常见的路径,提前短路)。 */
|
|
36
|
+
function anyPersistenceToolSurvives(excludeTools) {
|
|
37
|
+
if (excludeTools === undefined || excludeTools.length === 0)
|
|
38
|
+
return true;
|
|
39
|
+
const excluded = new Set(excludeTools);
|
|
40
|
+
return KNOWN_PERSISTENCE_TOOL_NAMES.some((name) => !excluded.has(name));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 该不该把 `NO_PERSISTENT_MEMORY_NOTICE` 组进本次任务的 system prompt。
|
|
44
|
+
*
|
|
45
|
+
* 优先序(每一条都有独立理由,不是同一条判据的三种写法):
|
|
46
|
+
* ① engine 在场 ⇒ **否**(引擎的座位,见文件头注);
|
|
47
|
+
* ② 部署显式声明 ⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator 的部署事实压过我们的推断,
|
|
48
|
+
* 与 core 对 `memoryPersistenceCapable` 的处置同向;
|
|
49
|
+
* ③ 否则推断:有已知落盘路(手带在场且不是只读)⇒ 否;没有 ⇒ 是。
|
|
50
|
+
*/
|
|
51
|
+
export function shouldDiscloseNoPersistentMemory(facts) {
|
|
52
|
+
if (facts.memory !== undefined)
|
|
53
|
+
return false;
|
|
54
|
+
if (facts.declaredCapable !== undefined)
|
|
55
|
+
return !facts.declaredCapable;
|
|
56
|
+
if (facts.hands === "none" || facts.handsReadOnly)
|
|
57
|
+
return true;
|
|
58
|
+
// remote 执行环境:手带写的是沙箱盘(每任务即焚),够不着任何 host 侧持久面 —— core 的
|
|
59
|
+
// `rosterCanPersist` 在这一形下把四只写工具**全部**不算,这里逐字同口径。
|
|
60
|
+
if (facts.remoteExecutionEnv)
|
|
61
|
+
return true;
|
|
62
|
+
// 手带在场,但 roster 可能把落盘路整批卸掉了(`excludeTools` 是真卸载)。⚠️ 负控仍成立:排掉**一个**
|
|
63
|
+
// `Write` 不算无写通道(Bash/Edit 还在)—— 必须**全部**已知落盘路都不在,才算这条链断了。
|
|
64
|
+
return !anyPersistenceToolSurvives(facts.excludeTools);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 把披露段组进一个 prompt provider,**按 provider 自己的钩子形分腿**(换形会改段身份与 append 座位):
|
|
68
|
+
* · typed(`stableBlocks`)⇒ 追加一条 `behavior` 声明。**不能**用 `identity`:那个 slot 会替换 role base。
|
|
69
|
+
* · string(`stableSystem`)⇒ 接在 role 层文本之后(落进 `core/role.base` 段)。
|
|
70
|
+
* · 两个钩子都没有(= core 会用 `defaultPromptProvider`)⇒ **自铸 role 层**。这里绝不能去包
|
|
71
|
+
* `defaultPromptProvider.stableSystem` —— 它返回的是 `composeConstitution(...)` 的**整装** prompt,
|
|
72
|
+
* 装配器的迁移卫探到三锚(CYBER_RISK/URL_SAFETY/`# Harness`)就走 pass-through 臂,那条臂的段子集
|
|
73
|
+
* 没有 `core/role.append`,调用方的 append rider 会被静默丢掉。自铸的是 core 自己那一句
|
|
74
|
+
* `userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT`(引用常量,不复制字节)。
|
|
75
|
+
*/
|
|
76
|
+
export function withNoPersistentMemoryNotice(provider) {
|
|
77
|
+
if (provider?.stableBlocks) {
|
|
78
|
+
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
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (provider?.stableSystem) {
|
|
85
|
+
const stable = provider.stableSystem.bind(provider);
|
|
86
|
+
return { ...provider, stableSystem: (ctx) => `${stable(ctx)}\n\n${NO_PERSISTENT_MEMORY_NOTICE}` };
|
|
87
|
+
}
|
|
88
|
+
return { stableSystem: (ctx) => `${ctx.userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT}\n\n${NO_PERSISTENT_MEMORY_NOTICE}` };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=memory-notice.js.map
|
package/dist/config-types.d.ts
CHANGED
|
@@ -13,6 +13,22 @@ import type { QuestionThrottle } from "./question.js";
|
|
|
13
13
|
import type { InfraCostRates } from "./observability/cost-taxonomy.js";
|
|
14
14
|
import type { Autonomy, CommandRule } from "./runtime-governance.js";
|
|
15
15
|
import type { GitApiKind } from "./git-api-kind.js";
|
|
16
|
+
/** #228:记忆向量面的 embedder 坐标(env `MEMORY_EMBEDDER_*` 解析结果;单一属主 = 本类型,
|
|
17
|
+
* `plugins/memory-embedder.ts` 的工厂直接消费它,不复制形状)。语义与三问见
|
|
18
|
+
* {@link ServiceConfigFlat.memoryEmbedder}。纯数据 —— 端点原文照存(拼 `/embeddings` 是 transport
|
|
19
|
+
* 的活,与 `memorySync.url` 的尾斜杠口径一致)。 */
|
|
20
|
+
export interface MemoryEmbedderConfig {
|
|
21
|
+
/** OpenAI 兼容服务的基址或完整 `/embeddings` 端点(两种写法都收,拼接在工厂里归一)。 */
|
|
22
|
+
endpoint: string;
|
|
23
|
+
/** 模型名,原样进请求体 `{ model, input }`。 */
|
|
24
|
+
model: string;
|
|
25
|
+
/** 声明维度(正整数)。响应向量长度与它不等 = 抛,绝不截断/补零。 */
|
|
26
|
+
dimensions: number;
|
|
27
|
+
/** 单次 embed 的活性上限(ms)。`MEMORY_EMBEDDER_TIMEOUT_MS`,默认 30s。 */
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
/** 可选 Bearer(自托管 TEI/vllm/ollama 通常不需要;公有云端点需要)。 */
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
}
|
|
16
32
|
/** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
|
|
17
33
|
* scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */
|
|
18
34
|
export interface ScopedMcpServer {
|
|
@@ -23,7 +39,8 @@ export interface ScopedMcpServer {
|
|
|
23
39
|
* off (the control plane never mounts). The runner-facing facts (the runner principal, the build-host CACHE_BASE,
|
|
24
40
|
* the default base ref) live here so image-api injects only VETTED, server-fixed values into the build.sh argv —
|
|
25
41
|
* operator free-text never reaches the runner. */
|
|
26
|
-
/** #151 车3(design/172 流内审批协议)的配置面。总开关默认 **
|
|
42
|
+
/** #151 车3(design/172 流内审批协议)的配置面。总开关默认 **true**(clay 裁 2026-08-08,#164 翻真验证后;
|
|
43
|
+
* 树上已生效,发布线落在 7.4.0 之后的下一个发布);四个从属旋钮只在开关为真时生效
|
|
27
44
|
* (语义与「谁需要 / 谁被伤 / 什么补偿」三问见 `ServiceConfigFlat.streamApproval` 的注)。 */
|
|
28
45
|
export interface StreamApprovalConfig {
|
|
29
46
|
/** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **true**(clay 裁 2026-08-08,#164 翻真验证后;7.5.0 起)。显式 false ⇒ 全链逐字 7.3.0 前行为(唯一干净还原键)。
|
|
@@ -233,6 +250,37 @@ export interface ServiceConfigFlat {
|
|
|
233
250
|
/** The single-user deployment's one memory scope name (env MEMORY_SCOPE; default "local" under
|
|
234
251
|
* single-user). Feeds `spec.memory.scope` — the new engine consumes the same normalized scopes. */
|
|
235
252
|
memoryScope?: string;
|
|
253
|
+
/** #217(core 5.26.0 `TaskSpec.memoryPersistenceCapable`)—— 本部署对「这台机器上的任务**能不能**把
|
|
254
|
+
* 用户说的『记住 X』落到持久处」的**自我声明**,env `MEMORY_PERSISTENCE_CAPABLE`,三态:
|
|
255
|
+
* · 缺席(默认)⇒ 键不上 TaskSpec,core 按自己的 roster 推断(已知落盘路 = 挂载且未排除的文件写工具
|
|
256
|
+
* 或可写 shell;remote 执行环境的手带写的是沙箱盘,不算够得着 host 记忆根);
|
|
257
|
+
* · `true` ⇒ 部署自证有推断**看不见**的持久通道(自定义 writer / MCP 记忆服务 / remote lane 与记忆根
|
|
258
|
+
* 共享挂载)。它撤回引擎的只读披露,并在 remote lane 上**恢复** `# Memory` 写指令(core 5.26.0 r13/r14
|
|
259
|
+
* 收窄的官方恢复路径);
|
|
260
|
+
* · `false` ⇒ 强制披露只读/无持久面,并关掉文件工具往记忆根的写通道(披露不会被存储事实打脸)。
|
|
261
|
+
*
|
|
262
|
+
* 🔴 **`false` 自 core 5.27.0 起还多一层**([3612] F2,#231 提货):declared-false 的会话是**受限会话**
|
|
263
|
+
* —— materialize/search/harvest 一律按**已提交账**(ledger + shadow)供给,磁盘上无事务背书的分歧
|
|
264
|
+
* 既不收编也不供给,而是留盘 + 响亮点名(`restricted_divergence`,`HarvestRejectionCode` 新成员),
|
|
265
|
+
* 等下一次**非受限**会话走正常门收编。这补上了 5.26.0 自己登记的那条残余(读侧入站同步收编磁盘分歧
|
|
266
|
+
* **与会话意图无关**)——在那之前,这里写的「披露不会被存储事实打脸」只是半真话:披露说「本机存不下」,
|
|
267
|
+
* 引擎却仍可能把别处落进记忆根的字节收进这次会话的账。
|
|
268
|
+
* ⚠️ **这一层的射程 = file 记忆引擎**(`MEMORY_ENGINE_BACKEND` 未设的默认单机形):受限视图是
|
|
269
|
+
* `FileMemoryEngineBackend.restrictedAdoptionView` 的实装,而 `engine.materialize` 取它走的是
|
|
270
|
+
* `?? this.backend` 的**可选** seam。我方 pg/tidb 两只记忆后端没有实装、也不需要——它们的读侧走库,
|
|
271
|
+
* 盘上目录只是每任务的投影工作区,没有「读磁盘即收编」这条通道。`false` 在 DB 后端上仍照常买到
|
|
272
|
+
* 披露 / 写门 / 零收编 harvest 三件(引擎级,与后端无关)。
|
|
273
|
+
*
|
|
274
|
+
* 与请求键 `memoryWrite`(MF-30)**两轴正交**,不是同一件事的两种写法:`memoryWrite` 是**本次运行**的
|
|
275
|
+
* 客户端表态(per-run 暂停写,writeScope:null),`memoryPersistenceCapable` 是**部署**声明(operator 面,
|
|
276
|
+
* 与请求无关)。两轴任一取「否」方向即只读方向 —— 客户端能收紧本次运行,永远不能靠表态把部署声明的
|
|
277
|
+
* `false` 撑开(operator 旋钮不挂客户端表态派生腿)。
|
|
278
|
+
* ⚠️ **别把两轴的「只读」当成同一件事**([3612] F2 修后口径,逐字对齐 core):**受限**是**会话级的
|
|
279
|
+
* verdict 单键**(= 这里的 declared-false),而 `writeScope:null` 的只读**平面**(`memoryWrite:false` 的
|
|
280
|
+
* per-run 暂停 / org 层默认只读 / 双根非写面)**保持 adopt-on-read 原样** —— 受限是会话的**声明**,
|
|
281
|
+
* 永远不是平面的**结构**。所以:一次 `memoryWrite:false` 的运行照常收编磁盘上的新内容(用户手改、
|
|
282
|
+
* git pull 落下的文件都还认),它只是不 harvest;而 declared-false 的部署连收编都不做。 */
|
|
283
|
+
memoryPersistenceCapable?: boolean;
|
|
236
284
|
/** 142-S2.5-W1: TOC 同步 client 腿 —— file memory 形态把本地记忆盘与中心(DB 权威,
|
|
237
285
|
* POST /v1/memory/sync/:scope)做一轮一 RTT 双向同步。env 三键:`MEMORY_SYNC_URL`(设了=开;缺省
|
|
238
286
|
* undefined=纯本地现状零变化)/`MEMORY_SYNC_TOKEN`(Bearer)/`MEMORY_SYNC_SCOPE`(可选,缺省=
|
|
@@ -246,6 +294,25 @@ export interface ServiceConfigFlat {
|
|
|
246
294
|
maxPushEntries?: number;
|
|
247
295
|
maxPullEntries?: number;
|
|
248
296
|
};
|
|
297
|
+
/** #228(黑板 [3590]/[3606] 两裁):记忆检索的**向量面**接线 —— OpenAI 兼容 `/embeddings` 端点。
|
|
298
|
+
* env 四键(+ 可选 key):`MEMORY_EMBEDDER_ENDPOINT` / `MEMORY_EMBEDDER_MODEL` / `MEMORY_EMBEDDER_DIM`
|
|
299
|
+
* (+ `MEMORY_EMBEDDER_TIMEOUT_MS` / `MEMORY_EMBEDDER_API_KEY`)。三问:
|
|
300
|
+
*
|
|
301
|
+
* ① **谁需要**:pg 记忆后端的部署。core 的记忆引擎三档(`vectorMode = lexical|portable|native`)不是
|
|
302
|
+
* 档位旋钮,而是「后端上限 × embedder 在场」的**推断结果**([3590] 裁①:不开档位 env —— 显式选档
|
|
303
|
+
* 只会造出「选了 native 却没 embedder」这类矛盾态)。本仓在 #228 之前从未注入过 embedder,所以
|
|
304
|
+
* pg 部署的检索**恒 lexical**(词面 jaccard),向量列空转。配上这几键 = 该部署真正拿到语义检索。
|
|
305
|
+
* ② **半配为何拒启**:三键里少任何一件,「向量面」都无从组装。静默忽略的后果不是功能缺席,而是
|
|
306
|
+
* **operator 以为开了**——检索照常返回结果(词面档),质量差异只在最终答案里,任何日志都不报。
|
|
307
|
+
* 所以照 `MEMORY_SYNC_*` / `REQUIRED_ENVS_BY_LANE` 的同款姿势整拒并点名缺哪几键。
|
|
308
|
+
* ③ **维度守卫为何是拒启不是 warn**:向量列写错维度是**静默毒库**形([3606] 裁③)——错长度的向量
|
|
309
|
+
* 既不会让 DB 报错(本仓 pg 记忆表的 embedding 是 `jsonb`,无维度约束),也不会让检索报错(维度
|
|
310
|
+
* 不匹配的行只是悄悄退回词面档),只会让「向量面开着」这句话变成谎。所以坏 DIM(非正整数)在
|
|
311
|
+
* boot 期拒;运行期真维度不符(模型换了 / 端点指错)由 embedder 自己抛,绝不截断补零。
|
|
312
|
+
*
|
|
313
|
+
* ⚠️ 后端门:只有 `MEMORY_ENGINE_BACKEND=pg` 收这几键 —— tidb 记忆后端 v1 无向量档(传 embedder
|
|
314
|
+
* 它自己就 fail-loud 拒),file 引擎压根没有 embedder 接缝。配错后端 = 拒启点名。 */
|
|
315
|
+
memoryEmbedder?: MemoryEmbedderConfig;
|
|
249
316
|
/** design/170 件A(clay 三裁 [2687]):org 记忆准入模式。`enforce`(默认)=判决即结果;`audit`=判决
|
|
250
317
|
* 照算但纯观察零行为变化(不整拒、不窄化 writeScope,只记 would-deny/would-narrow)——运维诊断位,
|
|
251
318
|
* 非发布步骤(audit-first 灰度步按裁2 免除)。env `MEMORY_ORG_ADMISSION_MODE`(enumEnv 二值)。 */
|
|
@@ -908,16 +975,26 @@ export interface ServiceConfigFlat {
|
|
|
908
975
|
* 旋钮本身缺省对现行为零影响(D1)。`STREAM_ASK_WINDOW_MARGIN_MS`,默认 10000(10s)。ttl 概念沿用既有
|
|
909
976
|
* `approvalTimeoutSec` 族,本旋钮不新增第二个 TTL 概念。 */
|
|
910
977
|
streamAskWindowMarginMs: number;
|
|
911
|
-
/** #151 车3(design/172 流内审批协议)总开关 +
|
|
912
|
-
*
|
|
913
|
-
* (
|
|
914
|
-
*
|
|
915
|
-
*
|
|
978
|
+
/** #151 车3(design/172 流内审批协议)总开关 + 四个从属旋钮。
|
|
979
|
+
*
|
|
980
|
+
* **现行默认(clay 裁 2026-08-08 走 (a),#164 翻真验证后)**:`STREAM_APPROVAL_ENABLED` 默认 **true**
|
|
981
|
+
* = 协议整体默认 **ON**;`STREAM_ASK_WINDOW_MS` 默认 **300000**(5min,与 sync 腿既有活卡窗、与
|
|
982
|
+
* `DEFAULT_APPROVAL_TTL_MS` 对齐——「开协议」不再顺带把窗砍短)。⚠️ 版本坐标:翻转**已在树上生效**,
|
|
983
|
+
* 发布线上落在 7.4.0 之后的下一个发布,所以一台自报 7.4.0 的**已发布** worker 仍是 OFF,别只按
|
|
984
|
+
* `/health` 的版本号推默认。显式 `STREAM_APPROVAL_ENABLED=false` 是**唯一干净还原键** ⇒ 全链逐字回到
|
|
985
|
+
* 翻转前行为:不发 `approval_request` 帧、不落 `approval_ask` 行、不起收敛器腿、窗 = 既有
|
|
986
|
+
* `DEFAULT_APPROVAL_TTL_MS`(5min)、`askStore` 不注入协调器。
|
|
987
|
+
*
|
|
988
|
+
* **旋钮为什么存在(2026-08-08 翻转前的成文前提,已被上面的裁定取代,留作沿革)**:那一版默认 false +
|
|
989
|
+
* 60s 窗,三问记的是那个世界——
|
|
990
|
+
* - 谁需要 60s 窗:opt-in 开了协议的部署——approver 就盯着流、秒级应答,窗短 ⇒ 少占一条活腿、更快落到
|
|
991
|
+
* 可恢复的 park 面。
|
|
916
992
|
* - 谁被伤:①未 opt-in 的存量部署(把 5min 砍成 60s = 人离开工位 90 秒回来卡已经没了);②**park 设施
|
|
917
993
|
* 不在场**的部署——窗到期后 `"unavailable"` 没有降级目的地,core fail-closed deny,60s 会把
|
|
918
994
|
* 「人 90 秒后批准 = 放行」变成「拒绝」,是真行为回归。
|
|
919
|
-
* - 补偿:把 60s 绑在**新旋钮 + 新开关**上,一个字不动 `DEFAULT_APPROVAL_TTL_MS
|
|
920
|
-
*
|
|
995
|
+
* - 补偿:把 60s 绑在**新旋钮 + 新开关**上,一个字不动 `DEFAULT_APPROVAL_TTL_MS`。
|
|
996
|
+
* ⇒ 翻真时「窗变短」这一项被从翻转里摘出(窗默认抬回 300000),被伤面①②因此消失;60s 只在运维显式设
|
|
997
|
+
* `STREAM_ASK_WINDOW_MS=60000` 时才回来。
|
|
921
998
|
*
|
|
922
999
|
* `STREAM_APPROVAL_ENABLED` / `STREAM_ASK_WINDOW_MS` / `STREAM_APPROVAL_REPLAY_MAX` /
|
|
923
1000
|
* `STREAM_APPROVAL_ADMIT_MAX_PER_TASK` / `STREAM_APPROVAL_ADMIT_MAX_PER_OWNER` /
|
|
@@ -1046,7 +1123,7 @@ export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" |
|
|
|
1046
1123
|
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
1047
1124
|
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">;
|
|
1048
1125
|
/** 组:memory(记忆面 + TOC 同步腿)。 */
|
|
1049
|
-
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
1126
|
+
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memoryPersistenceCapable" | "memorySync" | "memoryEmbedder" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
1050
1127
|
/** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
|
|
1051
1128
|
* 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。 */
|
|
1052
1129
|
export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
|
package/dist/config.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./co
|
|
|
4
4
|
/** design/158 A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
|
|
5
5
|
* every existing `from "./config.js"` importer compiles unchanged (pure-type consumers should prefer
|
|
6
6
|
* importing config-types.js directly — a type-only leaf, no loader baggage). */
|
|
7
|
-
export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat, ServiceConfigGroups, ServiceStoreConfig, ServiceModelPlaneConfig, ServiceApprovalConfig, ServiceMemoryConfig, ServiceAuthConfig, ServiceOrchestrationConfig, ServiceLimitsHttpConfig, ServiceObservabilityConfig, ServiceIntegrationsConfig, } from "./config-types.js";
|
|
7
|
+
export type { ServiceConfig, ScopedMcpServer, ImageBakeConfig, ServiceConfigFlat, ServiceConfigGroups, ServiceStoreConfig, ServiceModelPlaneConfig, ServiceApprovalConfig, ServiceMemoryConfig, ServiceAuthConfig, ServiceOrchestrationConfig, ServiceLimitsHttpConfig, ServiceObservabilityConfig, ServiceIntegrationsConfig, MemoryEmbedderConfig, } from "./config-types.js";
|
|
8
8
|
/** Parse the AUTONOMY env into a validated autonomy mode. Unset/empty → undefined (unmanaged → no extra
|
|
9
9
|
* tightening). An UNKNOWN value FAILS at startup rather than silently becoming a no-op (a typo'd `AUTONOMY=readonly`
|
|
10
10
|
* must not silently leave a deployment ungoverned — fail-loud, same discipline as numEnv). Exported so the HOT
|
package/dist/config.js
CHANGED
|
@@ -1257,6 +1257,32 @@ function parseMemoryEngineWord(word) {
|
|
|
1257
1257
|
`(engine ON, the default) or ${MEMORY_ENGINE_OFF_WORDS.join(" | ")} (engine OFF). ` +
|
|
1258
1258
|
`An unrecognized value used to read as "on", so a misspelled kill-switch silently kept memory injection running.`);
|
|
1259
1259
|
}
|
|
1260
|
+
/**
|
|
1261
|
+
* #217:`MEMORY_PERSISTENCE_CAPABLE` 的**三态**读取(core 5.26.0 `TaskSpec.memoryPersistenceCapable`)。
|
|
1262
|
+
*
|
|
1263
|
+
* 与 {@link parseMemoryEngineWord} 共用同一份双族词表(operator 面写法一致,别让同一台机器上的两个记忆
|
|
1264
|
+
* 旋钮各认一套词),但**多一个态**:缺席/空串 ⇒ `undefined` ⇒ TaskSpec 上整键缺席 ⇒ core 走自己的推断
|
|
1265
|
+
* (三态语义的 absent 腿,零迁移)。空串等同未设,与 MEMORY_ENGINE 同理由(compose 的 `${X:-}` 模板天天产它)。
|
|
1266
|
+
*
|
|
1267
|
+
* 不认得的词 **拒启**而不是回落:这个键是「本部署能不能替用户记住事情」的真值来源,静默倒向任何一侧都
|
|
1268
|
+
* 是猜——倒向 `true` 会让一台其实存不下的部署继续给幻觉式回执(#148 原病),倒向 `false` 会让一台真有
|
|
1269
|
+
* 自定义持久通道的部署对用户说假话。
|
|
1270
|
+
*/
|
|
1271
|
+
function parseMemoryPersistenceCapable(raw) {
|
|
1272
|
+
if (raw === undefined)
|
|
1273
|
+
return undefined;
|
|
1274
|
+
const w = raw.trim().toLowerCase();
|
|
1275
|
+
if (w === "")
|
|
1276
|
+
return undefined;
|
|
1277
|
+
if (MEMORY_ENGINE_ON_WORDS.includes(w))
|
|
1278
|
+
return true;
|
|
1279
|
+
if (MEMORY_ENGINE_OFF_WORDS.includes(w))
|
|
1280
|
+
return false;
|
|
1281
|
+
throw new Error(`env MEMORY_PERSISTENCE_CAPABLE="${raw}" is not a known on/off word — use one of: ${MEMORY_ENGINE_ON_WORDS.join(" | ")} ` +
|
|
1282
|
+
`(this deployment CAN persist "remember X" — declares a channel the roster inference cannot see, and restores the ` +
|
|
1283
|
+
`\`# Memory\` write instruction on a remote execution lane) or ${MEMORY_ENGINE_OFF_WORDS.join(" | ")} (it CANNOT — ` +
|
|
1284
|
+
`forces the read-only disclosure). Leave it UNSET to let the engine infer from the mounted roster.`);
|
|
1285
|
+
}
|
|
1260
1286
|
/** #210:SANDBOX_PKG_SOURCE 的闭集校验(词表 = README/MIGRATION 成文的四词)。`none` 由调用点的三元
|
|
1261
1287
|
* 链先摘走(字段整个缺席),留在这里的只可能是前三词或一个手滑值。 */
|
|
1262
1288
|
const SANDBOX_PKG_SOURCES = ["global", "cn", "custom", "none"];
|
|
@@ -1278,6 +1304,85 @@ function syncEntryCap(name, field) {
|
|
|
1278
1304
|
throw new Error(`env ${name}="${raw}" must be an integer >= 1 (it is an entries-per-batch cap; an ignored cap means NO cap)`);
|
|
1279
1305
|
return { [field]: n };
|
|
1280
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* #228(黑板 [3590]/[3606] 两裁):`MEMORY_EMBEDDER_*` 族 —— 记忆检索向量面的**唯一**开关。
|
|
1309
|
+
*
|
|
1310
|
+
* 三态(与 `MEMORY_SYNC_*` 半配拒启同族):
|
|
1311
|
+
* · 三键全缺席 ⇒ `undefined`,不注入 embedder,现状零变化(检索恒 lexical 词面档);
|
|
1312
|
+
* · 半配(1~2 键)⇒ **拒启并点名缺哪几键**(REQUIRED_ENVS_BY_LANE 的报文形)。静默忽略的后果不是
|
|
1313
|
+
* 功能缺席,而是 operator 以为向量面开着——检索照常出结果,差异只在答案质量里,零日志;
|
|
1314
|
+
* · 坏值(URL 不合法 / 非 http(s) / DIM 非正整数 / TIMEOUT 非法或越界)⇒ 拒启带指路(#210 A 档)。
|
|
1315
|
+
*
|
|
1316
|
+
* 另两条门(都是「配了但永远不生效」的死旋钮形,一律拒启而非 warn):
|
|
1317
|
+
* · `MEMORY_ENGINE_BACKEND` 必须是 `pg` —— tidb 记忆后端 v1 无向量档(传 embedder 它自己 fail-loud
|
|
1318
|
+
* 拒),file 引擎没有 embedder 接缝;
|
|
1319
|
+
* · 记忆引擎本身必须开着(`MEMORY_ENGINE != off`)。
|
|
1320
|
+
*
|
|
1321
|
+
* 维度守卫的**位置**(诚实记档):本仓建 pg 记忆表时不传 `memoryVector`(见 boot/stores.ts 的
|
|
1322
|
+
* `ensurePgMemoryEngineSchema(q)`),即 embedding 列是 `jsonb`、schema 侧**没有可对表的维度常量**;
|
|
1323
|
+
* 所以 boot 期能做的只有「DIM 是正整数」,真正的维度守卫在写入路径两处:① 本仓 embedder 拿到长度
|
|
1324
|
+
* 不符的响应直接抛(绝不截断/补零);② `PgMemoryEngineBackend.embeddingParam` 对长度不符的向量返回
|
|
1325
|
+
* null(该行退回词面档,不写坏向量)。
|
|
1326
|
+
*/
|
|
1327
|
+
function parseMemoryEmbedder(ctx) {
|
|
1328
|
+
// 空串 = 未设(compose `${X:-}` 模板天天产它,本仓一律同判)
|
|
1329
|
+
const endpoint = process.env.MEMORY_EMBEDDER_ENDPOINT || undefined;
|
|
1330
|
+
const model = process.env.MEMORY_EMBEDDER_MODEL || undefined;
|
|
1331
|
+
const dimRaw = process.env.MEMORY_EMBEDDER_DIM || undefined;
|
|
1332
|
+
const apiKey = process.env.MEMORY_EMBEDDER_API_KEY || undefined;
|
|
1333
|
+
const timeoutRaw = process.env.MEMORY_EMBEDDER_TIMEOUT_MS || undefined;
|
|
1334
|
+
const trio = [
|
|
1335
|
+
["MEMORY_EMBEDDER_ENDPOINT", endpoint],
|
|
1336
|
+
["MEMORY_EMBEDDER_MODEL", model],
|
|
1337
|
+
["MEMORY_EMBEDDER_DIM", dimRaw],
|
|
1338
|
+
];
|
|
1339
|
+
const missing = trio.filter(([, v]) => v === undefined).map(([name]) => name);
|
|
1340
|
+
if (missing.length === 3) {
|
|
1341
|
+
// 三键全缺席 = 文档化的默认姿态(不注入)。但**从属**两键单独在场 = 同款半配陷阱:operator 调了
|
|
1342
|
+
// 超时/配了 key,却一个向量都不会算(MEMORY_SYNC_TOKEN/SCOPE 无 URL 对称拒启的同款先例)。
|
|
1343
|
+
if (timeoutRaw !== undefined || apiKey !== undefined) {
|
|
1344
|
+
throw new Error(`MEMORY_EMBEDDER_TIMEOUT_MS/MEMORY_EMBEDDER_API_KEY are set but the embedder itself is not configured — refusing to start half-configured (set MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL and MEMORY_EMBEDDER_DIM to enable the memory vector plane, or unset the other MEMORY_EMBEDDER_* keys)`);
|
|
1345
|
+
}
|
|
1346
|
+
return undefined;
|
|
1347
|
+
}
|
|
1348
|
+
// 三键缺任一(TS 也靠这一支收窄,不需要非空断言)
|
|
1349
|
+
if (endpoint === undefined || model === undefined || dimRaw === undefined) {
|
|
1350
|
+
throw new Error(`MEMORY_EMBEDDER_* is half-configured — missing ${missing.join(", ")}. The memory vector plane needs all three of MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL, MEMORY_EMBEDDER_DIM; refusing to start rather than silently running lexical-only retrieval while the deployment believes vectors are on (#228).`);
|
|
1351
|
+
}
|
|
1352
|
+
if (!URL.canParse(endpoint)) {
|
|
1353
|
+
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${endpoint}" is not a valid URL (expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint)`);
|
|
1354
|
+
}
|
|
1355
|
+
const parsedEndpoint = new URL(endpoint);
|
|
1356
|
+
const proto = parsedEndpoint.protocol;
|
|
1357
|
+
if (proto !== "http:" && proto !== "https:") {
|
|
1358
|
+
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${endpoint}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
|
|
1359
|
+
}
|
|
1360
|
+
// URL 里的 userinfo 一律拒(codex 二轮 F1):`https://user:pass@host/v1` 形把凭据种进一个会被
|
|
1361
|
+
// 传输层错误、日志、PatchReport 冲突文案反复复制的字符串里 —— 脱敏只能事后擦,拒收才是源头。
|
|
1362
|
+
// 需要 auth 就走 MEMORY_EMBEDDER_API_KEY(Bearer 头,不进 URL)。
|
|
1363
|
+
if (parsedEndpoint.username !== "" || parsedEndpoint.password !== "") {
|
|
1364
|
+
throw new Error("MEMORY_EMBEDDER_ENDPOINT must not carry credentials in the URL (user:password@host) — URL userinfo leaks into transport errors, logs and memory conflict reports. Use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.");
|
|
1365
|
+
}
|
|
1366
|
+
const dimensions = Number(dimRaw);
|
|
1367
|
+
if (!Number.isSafeInteger(dimensions) || dimensions < 1) {
|
|
1368
|
+
throw new Error(`MEMORY_EMBEDDER_DIM="${dimRaw}" must be a positive integer (the embedding dimension, e.g. 1024). A wrong dimension writes wrong-length vectors into the memory embedding column, which neither the DB nor retrieval reports — the vector plane just silently degrades to the lexical floor (#228).`);
|
|
1369
|
+
}
|
|
1370
|
+
if (ctx.memoryEngineBackend !== "pg") {
|
|
1371
|
+
throw new Error(`MEMORY_EMBEDDER_* is set but MEMORY_ENGINE_BACKEND=${ctx.memoryEngineBackend} — the vector plane exists only on the pg memory backend (the tidb backend is lexical-only in v1 and refuses an embedder; the file engine has no embedder seam). Set MEMORY_ENGINE_BACKEND=pg, or unset MEMORY_EMBEDDER_*.`);
|
|
1372
|
+
}
|
|
1373
|
+
if (!ctx.memoryEngineEnabled) {
|
|
1374
|
+
throw new Error("MEMORY_EMBEDDER_* is set but MEMORY_ENGINE=off — the memory engine is not wired at all, so the embedder would never be called; unset one of the two (#228)");
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
endpoint,
|
|
1378
|
+
model,
|
|
1379
|
+
dimensions,
|
|
1380
|
+
// 活性保护:挂死的 embedder 会把整条记忆写入路径吊住(undici 默认 headers 超时 300s 太钝)。
|
|
1381
|
+
// 下界 1s(比这更短的向量调用只会全轮超时);上界 5min(再长就不是超时保护了)。
|
|
1382
|
+
timeoutMs: numEnvBounded("MEMORY_EMBEDDER_TIMEOUT_MS", "30000", 1000, 300_000),
|
|
1383
|
+
...(apiKey !== undefined ? { apiKey } : {}),
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1281
1386
|
/** 域:memory(记忆面 + TOC 同步腿)—— 记忆引擎开关/方言/根目录、scope 缺省、同步 client 三键、项目记忆。 */
|
|
1282
1387
|
function parseMemoryDomain(ctx) {
|
|
1283
1388
|
const { requirePrincipal } = ctx; // 跨域入参①:多租户下记忆面 DARK(scope 缺省不铸)
|
|
@@ -1376,7 +1481,12 @@ function parseMemoryDomain(ctx) {
|
|
|
1376
1481
|
// memoryScopeFor returns undefined there). An explicit MEMORY_SCOPE always wins.
|
|
1377
1482
|
memoryEngineBackend,
|
|
1378
1483
|
memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
|
|
1484
|
+
// #217:三态 ⇒ 只有真表态才上键(缺席腿必须是**整键缺席**,不是 present-as-undefined —— 后者会被
|
|
1485
|
+
// resolve-spec 原样搬上 TaskSpec,把 core 的「absent = 各自推断」读成一个显式表态)。
|
|
1486
|
+
...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(parseMemoryPersistenceCapable(process.env.MEMORY_PERSISTENCE_CAPABLE)),
|
|
1379
1487
|
...(memorySync ? { memorySync } : {}),
|
|
1488
|
+
// #228:向量面接线(三态解析 + 后端/引擎两门,全在 parseMemoryEmbedder 里 fail-loud)
|
|
1489
|
+
...((v) => (v !== undefined ? { memoryEmbedder: v } : {}))(parseMemoryEmbedder({ memoryEngineBackend, memoryEngineEnabled })),
|
|
1380
1490
|
memoryOrgAdmissionMode,
|
|
1381
1491
|
...(memoryOrgDirectoryJson !== undefined ? { memoryOrgDirectoryJson } : {}),
|
|
1382
1492
|
memoryOrgGrantTtlMs,
|
|
@@ -1856,7 +1966,7 @@ const APPROVAL_GROUP_KEYS = [
|
|
|
1856
1966
|
];
|
|
1857
1967
|
const MEMORY_GROUP_KEYS = [
|
|
1858
1968
|
"memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
|
|
1859
|
-
"memorySync", "memoryOrgAdmissionMode", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
|
|
1969
|
+
"memoryPersistenceCapable", "memorySync", "memoryEmbedder", "memoryOrgAdmissionMode", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
|
|
1860
1970
|
"projectMemoryEnabled", "syncImportLeaseStaleSec",
|
|
1861
1971
|
];
|
|
1862
1972
|
const AUTH_GROUP_KEYS = [
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
*
|
|
41
41
|
* 🔴 codex 交叉复审 round3 [medium](验真):`toolCallId` **不是**全局唯一的 —— 它多数情况下是**提供方**
|
|
42
42
|
* 逐字给的 id(core 的 brain 层不另铸),本仓自己的代码就是这么假设的:core 把 tool-result 引用命名成
|
|
43
|
-
* `tr_<sessionId
|
|
43
|
+
* `tr_<sessionId>~<toolCallId>~<contentSeg>`(core 5.26.0 #119 起的四段单射形;见 `plugins/tool-result-store-sql.ts`
|
|
44
|
+
* 的 `deleteBySession` 顶注),
|
|
44
45
|
* `deriveAskId` 也把 `sourceTaskId`/`runId`/`legKey` 一并入哈希。一张不分格的表会**跨任务/跨租户串味**:
|
|
45
46
|
* 低熵 id(本地/自建模型服务常见的 `call_1` 之类)下,A 任务的治理标会被 B 任务的普通 ask 读走(假出处),
|
|
46
47
|
* 或被 B 的一次治理放行反手清掉(丢真出处)。
|
|
@@ -159,8 +159,11 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
159
159
|
// served this task — the RESOLVED `Model.id`, not the requested `TaskSpec.model` ref … Lets a UI echo
|
|
160
160
|
// 'served by X'". A grep for the identifier `effectiveModel` returns zero hits in core and that was mistaken
|
|
161
161
|
// for "core doesn't have it" (search by SEMANTICS, not by identifier name). It already reaches the wire:
|
|
162
|
-
// `POST /v1/tasks` returns the TaskResult verbatim (
|
|
163
|
-
//
|
|
162
|
+
// `POST /v1/tasks` returns the TaskResult verbatim (`routes/tasks.ts` `sendSubmitResult` — the sync submit
|
|
163
|
+
// leg's one response mouth; design/158 A9 moved the whole sync-submit surface OUT of `http/server.ts`, so the
|
|
164
|
+
// old `~L1852` anchor into that file is dead), the SDK declares `TaskResult.model`, and this server already
|
|
165
|
+
// consumes the resolved id internally for cache-family accounting (`budget.ts` `cacheFamilyOfMirror` + its
|
|
166
|
+
// caller in `routes/side-query.ts`, NOT `http/server.ts`). Pinned by
|
|
164
167
|
// `test/effective-model-echo-wire.test.ts` (real HTTP: resolved id echoed, absent key stays absent) so a
|
|
165
168
|
// future projection/whitelist on this leg can't silently drop it — the `toolEndEventData`-label bug class.
|
|
166
169
|
// Nothing is advertised as a capability BIT here because an additive result field needs no gate
|
|
@@ -354,9 +357,12 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
354
357
|
// `workflowsCapability(deps)` (hardened script runner ∧ governance), boot-computed in main.ts. Falls back
|
|
355
358
|
// to the `Boolean(workflowRunStore)` proxy when the boot flag isn't wired (older callers / tests).
|
|
356
359
|
// • `workflowsList` = can durably LIST workflow runs = `Boolean(workflowRunStore)` (GET /v1/workflows* 501s
|
|
357
|
-
// without it — list AND detail/stream, see the store gate
|
|
360
|
+
// without it — list AND detail/stream, see the store gate in `routes/workflows.ts`, the
|
|
361
|
+
// `if (!deps.workflowRunStore)` → 501 `capability.self_orchestration_required` arm).
|
|
358
362
|
// ⚠️ In the CURRENT service wiring these two COINCIDE: `workflowRunStore` is built IFF self-orchestration is on
|
|
359
|
-
// (InMemoryWorkflowRunStore when WORKFLOW_RUN_STORE=memory, else File —
|
|
363
|
+
// (InMemoryWorkflowRunStore when WORKFLOW_RUN_STORE=memory, else File — built in
|
|
364
|
+
// `boot/workflow-orchestration.ts`, which design/158 A10 split out of main.ts; main.ts itself no longer
|
|
365
|
+
// mentions InMemoryWorkflowRunStore at all, so the old `main.ts ~L527` anchor is dead), the SAME gate that
|
|
360
366
|
// wires the safe script runner `workflowsCapability` checks → `Boolean(workflowRunStore)` ≡ `workflowsCapability`
|
|
361
367
|
// always today. So this split is core's FORWARD-LOOKING orthogonal-axes CONTRACT, NOT a fix for a live
|
|
362
368
|
// divergence (the earlier "memory backend → no store → divergence" framing was WRONG — InMemory IS a store;
|
|
@@ -255,6 +255,11 @@ async function handleTaskArtifacts(res, runStore, taskId) {
|
|
|
255
255
|
* trace 的 tool-result 块与本判据读的是同一份日志,所以「trace 上看得见 ⇒ 这里读得到」成立;
|
|
256
256
|
* 同 session 里**别的 task** 的 ref(或日志已过保留窗被逐出的)按 404 同形拒,请到那条 task 上读。
|
|
257
257
|
*
|
|
258
|
+
* 🟢 **两条残余已随 core 5.26.0 #119 销账**(下面原文保留作病历)。core 按当年这里写的方向动了刀:
|
|
259
|
+
* `put` 带出处落库 + `ownerOf` 读回,读面改用出处判定(实现见 ②-a),于是 (a) 合成 id 铸的 ref 不再
|
|
260
|
+
* 读不到、(b) 拼接非单射的越权面结构上消失(ref 铸法本身也换成 `~` 四段单射)。日志重铸只剩**存量
|
|
261
|
+
* 兜底**一条腿(无出处的旧行),新写入一律经出处。
|
|
262
|
+
*
|
|
258
263
|
* ⚠️ 两处**已知残余**,都要 core 侧动刀才能消,写在明处而不是让下一个人自己撞(codex 复审 R3):
|
|
259
264
|
*
|
|
260
265
|
* (a) **覆盖面**:core 除了「按工具调用 id 卸载」这条主路,还从**合成 id** 铸 ref ——
|
|
@@ -315,22 +320,46 @@ ref, query) {
|
|
|
315
320
|
const run = ownedRun ?? (await runStore.getRun(taskId));
|
|
316
321
|
if (!run)
|
|
317
322
|
return notFound();
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
323
|
+
// ②-a 出处判定(#119,core 5.26.0 —— 上面 (a)/(b) 两条残余的**正解到货**)。
|
|
324
|
+
// 店里现在自己记着「这枚 ref 属于谁」(`put` 的第三参落库,`ownerOf` 读回),读面拿**自己已知的字段**
|
|
325
|
+
// 比对即可:零解析、零段转义规则复制,合成 id 铸的 ref 与拼接歧义两条残余同时消失。
|
|
326
|
+
//
|
|
327
|
+
// 🔴 为什么必须改而不是"锦上添花":5.26.0 起 offload ref 是四段单射形(内容坐标进 ref),服务端手里
|
|
328
|
+
// 没有内容 ⇒ 下面那条重铸判据**重铸不出来** ⇒ 每一枚真 offload ref 都会 404。读面整死。
|
|
329
|
+
//
|
|
330
|
+
// ⚠️ 辖域(有意的口径变化,写在明处):出处的粒度由 core 定 —— offload/budget/projection 三个写点只声明
|
|
331
|
+
// session,把 taskId 钉在那里会把「一个 session 的两条 task 共用一枚 ref」这个**设计内**的共享形变成拒绝
|
|
332
|
+
// (core d.ts 逐字裁过)。所以有出处时的判据是「同 session,且出处**若点名了** task 则必须是本 task」——
|
|
333
|
+
// 相对旧的「本 task 日志作证过」是**放宽到同 session**。放宽的边界仍在 owner 之内(run 归属门在本函数
|
|
334
|
+
// 之前已过,session 的属主就是这个 caller),换来的是这条读面在 5.26.0 上还活着。
|
|
335
|
+
const owner = store?.ownerOf ? await store.ownerOf(ref) : undefined;
|
|
336
|
+
const ownedByThisCaller = owner !== undefined && owner.sessionId === run.sessionId && (owner.taskId === undefined || owner.taskId === taskId);
|
|
337
|
+
// ②-b 无出处兜底:**没声明属主**的行按老判据走 —— 本 task 耐久日志作证过的 toolCallId,经 core 同一个
|
|
338
|
+
// 铸造点变成 ref 再精确相等。unowned 本身不是授权(core:读面对 unowned 一律 fail-closed),日志作证才是。
|
|
339
|
+
//
|
|
340
|
+
// ⚠️ 这条**不是**「读得到升级前的存量行」(codex 复审 medium,已核真并改口):5.26.0 的
|
|
341
|
+
// `buildToolResultRef` 铸的是 `~` 分隔形,重铸不出旧引擎的 `tr_<sid>_<call>`,而按旧规则手写一个 legacy
|
|
342
|
+
// 编码器 = 复制上游规则(本文件头注自己禁掉的那件事)。真正的处置在别处 —— 本批要求**删表重建**
|
|
343
|
+
// `tool_result`(见 CHANGELOG),所以升级后库里根本没有旧行。这一支覆盖的是「5.26.0 引擎写下、但写点
|
|
344
|
+
// 没声明出处」的行(core 自家的 offload 写点都带出处,所以现实里几乎为空),留着是纵深不是承诺。
|
|
345
|
+
// 成本口径:整份日志一读一扫,与同域的 `/turns`、`/artifacts` 同款;有出处时**根本不读日志**。
|
|
346
|
+
if (!ownedByThisCaller) {
|
|
347
|
+
if (owner !== undefined)
|
|
348
|
+
return notFound(); // 出处在,但不是这位 —— 不给第二次机会(日志重铸绕不过出处)
|
|
349
|
+
const events = await runStore.getEvents(taskId, 0);
|
|
350
|
+
let attested = false;
|
|
351
|
+
for (const ev of events) {
|
|
352
|
+
// 取键姿势与同域的 `trace/artifacts.ts` 逐字同款(同一份日志、同一个字段,读法不该有第二种)。
|
|
353
|
+
const { toolCallId: id } = (ev.data ?? {});
|
|
354
|
+
// 逐条比对而不是先建全集:命中即停,长日志上不必把整份集合物化。
|
|
355
|
+
if (typeof id === "string" && id.length > 0 && buildToolResultRef(run.sessionId, id) === ref) {
|
|
356
|
+
attested = true;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
330
359
|
}
|
|
360
|
+
if (!attested)
|
|
361
|
+
return notFound();
|
|
331
362
|
}
|
|
332
|
-
if (!attested)
|
|
333
|
-
return notFound();
|
|
334
363
|
if (!store)
|
|
335
364
|
return notFound(); // durable store 缺席:404 同形,不 501(不泄露部署形)
|
|
336
365
|
const slice = await store.get(ref, { ...(offset !== undefined ? { offset } : {}), ...(limit !== undefined ? { limit } : {}) });
|
package/dist/http/server.d.ts
CHANGED
|
@@ -511,7 +511,15 @@ export declare function validateUserSkills(skills: unknown): string | null;
|
|
|
511
511
|
* POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1)
|
|
512
512
|
* GET /v1/runs/:id → run status + result (poll)
|
|
513
513
|
* GET /v1/runs/:id/events → SSE replay from Last-Event-ID, then tail to terminal (S2)
|
|
514
|
-
* POST /v1/sessions/:id/
|
|
514
|
+
* POST /v1/sessions/:id/notify → inject an out-of-session event (design/144 §2): live stream ⇒ core
|
|
515
|
+
* `TaskStream.notify()`; idle ⇒ parked in the session inbox, drained
|
|
516
|
+
* as a `task_notification` on the next stream open
|
|
517
|
+
* POST /v1/sessions/:id/wake → wake a `task_done` pure park with a message and resume it
|
|
518
|
+
* (design/144 §3). NOT a gate decision — a pending gate is refused
|
|
519
|
+
* (`wake.gate_pending`). Goes through `resumeWake`, which reuses the
|
|
520
|
+
* resume family's lease/CAS/run-log leg, so it BURNS MODEL TOKENS and
|
|
521
|
+
* is subject to the billable-submit gates (`SESSION_WAKE_RE`).
|
|
522
|
+
* (both live in `http/routes/notify-wake.ts` — design/158 A9 domain split)
|
|
515
523
|
*/
|
|
516
524
|
export declare function createHttpServer(rawDeps: ServiceDeps): http.Server & {
|
|
517
525
|
denyExpiredApprovals: (now: number) => Promise<void>;
|
package/dist/http/server.js
CHANGED
|
@@ -180,7 +180,15 @@ void ROUTE_DOMAINS;
|
|
|
180
180
|
* POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1)
|
|
181
181
|
* GET /v1/runs/:id → run status + result (poll)
|
|
182
182
|
* GET /v1/runs/:id/events → SSE replay from Last-Event-ID, then tail to terminal (S2)
|
|
183
|
-
* POST /v1/sessions/:id/
|
|
183
|
+
* POST /v1/sessions/:id/notify → inject an out-of-session event (design/144 §2): live stream ⇒ core
|
|
184
|
+
* `TaskStream.notify()`; idle ⇒ parked in the session inbox, drained
|
|
185
|
+
* as a `task_notification` on the next stream open
|
|
186
|
+
* POST /v1/sessions/:id/wake → wake a `task_done` pure park with a message and resume it
|
|
187
|
+
* (design/144 §3). NOT a gate decision — a pending gate is refused
|
|
188
|
+
* (`wake.gate_pending`). Goes through `resumeWake`, which reuses the
|
|
189
|
+
* resume family's lease/CAS/run-log leg, so it BURNS MODEL TOKENS and
|
|
190
|
+
* is subject to the billable-submit gates (`SESSION_WAKE_RE`).
|
|
191
|
+
* (both live in `http/routes/notify-wake.ts` — design/158 A9 domain split)
|
|
184
192
|
*/
|
|
185
193
|
export function createHttpServer(rawDeps) {
|
|
186
194
|
// design/158 A8:分组装配 → 平铺视图。**分组是装配面的形,不是消费面的形**——下面 ~700 处 `deps.x`
|
|
@@ -287,7 +287,12 @@ export interface TaskRequestBody {
|
|
|
287
287
|
/** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
|
|
288
288
|
* read-only over long-term memory (`TaskSpec.memory.writeScope:null` — the design/138 memory ENGINE materializes/
|
|
289
289
|
* reads but its harvest commits nothing). Absent/`true` ⇒ normal read+write. The shell's `/memory` pause carries
|
|
290
|
-
* this per request (no stored per-session flag). Re-applies on resume (rides in the persisted body).
|
|
290
|
+
* this per request (no stored per-session flag). Re-applies on resume (rides in the persisted body).
|
|
291
|
+
* ⚠️ **NOT the same axis as the deployment's `MEMORY_PERSISTENCE_CAPABLE:false`** (core 5.27.0 / [3612] F2):
|
|
292
|
+
* that one declares the SESSION restricted — materialize/search/harvest serve the committed account and
|
|
293
|
+
* unbacked disk divergence is refused (`restricted_divergence`). A `writeScope:null` PLANE keeps its ordinary
|
|
294
|
+
* adopt-on-read semantics: a paused run still sees the user's hand-edits and git-pull drops, it just commits
|
|
295
|
+
* nothing. Restriction is the session's DECLARATION, never the plane's structure. */
|
|
291
296
|
memoryWrite?: boolean;
|
|
292
297
|
/** 142-S4 projectId 线程化(design/142 §2):这个 run 归属的项目(center 登记簿键,generic lowercase
|
|
293
298
|
* UUID — core S1 `resolveProjectId` marker 或 S3 无仓 mint 的产物;客户端只透传,零铸造权威)。是「哪个
|
|
@@ -308,7 +308,8 @@ export declare const APPROVAL_BATCH_TABLE = "approval_batch";
|
|
|
308
308
|
* `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
|
|
309
309
|
* · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
|
|
310
310
|
* 与 `session_meta`/`task_run` 同宽。
|
|
311
|
-
* · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts
|
|
311
|
+
* · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts` 的同名导出常量,
|
|
312
|
+
* 刻意不钉行号 —— 旧注写的 `security.ts:350` 已随该文件编辑漂走)硬拒更长者;
|
|
312
313
|
* 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
|
|
313
314
|
* · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
|
|
314
315
|
* 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
|
|
@@ -82,7 +82,8 @@ export const APPROVAL_BATCH_TABLE = "approval_batch";
|
|
|
82
82
|
* `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
|
|
83
83
|
* · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
|
|
84
84
|
* 与 `session_meta`/`task_run` 同宽。
|
|
85
|
-
* · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts
|
|
85
|
+
* · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts` 的同名导出常量,
|
|
86
|
+
* 刻意不钉行号 —— 旧注写的 `security.ts:350` 已随该文件编辑漂走)硬拒更长者;
|
|
86
87
|
* 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
|
|
87
88
|
* · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
|
|
88
89
|
* 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
|