@sema-agent/server 7.14.0 → 7.15.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 +16 -1
- package/USAGE.md +2 -2
- package/dist/approval-ask-machine.d.ts +10 -0
- package/dist/approval-ask-machine.js +10 -0
- package/dist/boot/memory-boundary.d.ts +6 -0
- package/dist/boot/memory-boundary.js +10 -2
- package/dist/boot/stores.js +8 -7
- package/dist/capabilities/memory-notice.d.ts +13 -9
- package/dist/capabilities/memory-notice.js +35 -15
- package/dist/http/routes/capabilities.js +12 -0
- package/dist/http/routes/runs.js +23 -2
- package/dist/http/routes/trace-usage.js +38 -37
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +45 -0
- package/dist/observability/metrics.js +4 -1
- package/dist/tool-approval.d.ts +1 -0
- package/dist/tool-approval.js +69 -6
- package/package.json +2 -2
package/MIGRATION.md
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
> 版本策略:`@sema-agent/server` 1.x = 快速迭代期,行为破坏性变更可能落在 minor 版本
|
|
4
4
|
> (内部多 AI 协作节奏,当日黑板通告+实解)。**生产部署请锁精确版本**;GA 后 2.0 起严格 semver
|
|
5
|
-
> (BREAKING → major)
|
|
5
|
+
> (BREAKING → major)。本文件只记录会影响存量部署行为的变更。
|
|
6
|
+
>
|
|
7
|
+
> ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
|
|
8
|
+
> 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
|
|
9
|
+
|
|
10
|
+
## SQL 存储面 BREAKING(3.0.0 之后的三个窗)
|
|
11
|
+
|
|
12
|
+
**常设口径**:本仓**不出 `ALTER TABLE` 增量迁移**(成文裁定:预生产期零存量用户窗口,schema 变更
|
|
13
|
+
一律删表/删库重建);boot 对旧形 schema **拒启并带恢复动作文案**,不会静默跑在错形表上。只用
|
|
14
|
+
file/local 存储形(未配 SQL 后端)的部署不受本节任何条目影响。
|
|
15
|
+
|
|
16
|
+
| 窗 | 变更 | 升级动作 |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| **7.6.0**(2026-08-08) | SQL 双端归一化第一刀(#192):隔离键排序规则钉死(MySQL 腿表级 `COLLATE utf8mb4_bin`/PG 腿逐列 `COLLATE "C"`)+索引名 36 条+列名/宽度收窄 | **删库重建**(两方言) |
|
|
19
|
+
| **7.8.0**(2026-08-09) | SQL 命名三轴归一化第二刀(#192):9 张表名单数化、epoch 毫秒列补 `_ms` 后缀 5 列、approval 两表 `version→rev`;wire 面零变化 | **删库重建**(两方言) |
|
|
20
|
+
| **7.14.0**(2026-08-12) | `tool_result` 换代(core 5.26.0 #119):ref 四段单射形、主键 `VARCHAR(190)→518`、新增出处两列 `owner_session_id`/`owner_task_id` | 升级前两方言 **`DROP TABLE tool_result`**(不删=boot 拒启;offload 产物是可恢复窗缓存,转录内联预览不受影响) |
|
|
6
21
|
|
|
7
22
|
## server 3.0.0 —— BREAKING 四条(2026-07-31)
|
|
8
23
|
|
package/USAGE.md
CHANGED
|
@@ -154,8 +154,8 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
|
|
|
154
154
|
🔴 **`false` 自 core 5.27.0(7.14.0 提货)起还多一层——但只在 file 记忆引擎上**(`MEMORY_ENGINE_BACKEND` 未设 = 默认单机形):该会话是**受限会话**——materialize/搜索/harvest 一律按**已提交账**供给,记忆根下无事务背书的磁盘分歧既不收编也不供给,而是留盘 + 响亮点名(`restricted_divergence`,本服务把它计进 `memory_harvest_rejections_total{code}` 并 warn 一条),等下一次**非受限**会话走正常门收编。合法流程不受影响:用户手改、`git pull` 落下的删除照旧(延后,不销毁),并发的可写会话提交的变更算有事务背书。
|
|
155
155
|
⚠️ **`MEMORY_ENGINE_BACKEND=pg|tidb` 上没有这一层**(引擎的受限视图是 file 后端的实装):那两条腿的读侧本来就走库、盘上目录只是每任务的投影工作区,不存在「读磁盘即收编」的通道,所以也无洞可堵——`false` 在它们上仍然照常买到披露、写门与零收编 harvest 三件(与后端无关)。
|
|
156
156
|
⚠️ **口径别混**:「受限」是**会话级的这一个声明**;请求键 `memoryWrite:false` 铸出的只读**平面**(以及 org 层默认只读、双根非写面)**保持原样的 adopt-on-read**——它只是不 harvest,照旧看得见盘上的新内容。
|
|
157
|
-
⚠️ **core 5.26.0 起,remote 执行车道上的 `# Memory` 写指令被撤**(沙箱手带够不着 host 记忆根;读与注入不变)
|
|
158
|
-
另:记忆**没有写工具**,模型是用普通文件工具往记忆根写的——所以记忆根必须落在任务的 fs 授权边界内。结构性够不着时启动会 warn 并给出三条改法(
|
|
157
|
+
⚠️ **core 5.26.0 起,remote 执行车道上的 `# Memory` 写指令被撤**(沙箱手带够不着 host 记忆根;读与注入不变)。**判据是「`REMOTE_EXEC` 有没有设」,不是那个旗**——凡设了 `REMOTE_EXEC`(含 `host`)且这次任务真有记忆面,写指令就被撤。因此被打到的**不止**显式 `MEMORY_ENGINE_REMOTE_LANE=allow` 的部署,还包括:① `MEMORY_ENGINE_BACKEND=pg|tidb` 的 DB 记忆面(它不受那个旗管辖——旗只管 file 引擎,库是持久真身,任何车道上都照常点亮)= 最常见的云形;② `REMOTE_EXEC=host` + file 引擎(文件平面就在本机,但 core 的执行环境判决仍是 remote)。若该车道确实够得着记忆根,`MEMORY_PERSISTENCE_CAPABLE=true` 是官方恢复路径(启动日志 `memory_remote_lane_write_instruction_dropped` 会点名这一条);够不着就表 `false`,让只读状态对模型说明白。
|
|
158
|
+
另:记忆**没有写工具**,模型是用普通文件工具往记忆根写的——所以记忆根必须落在任务的 fs 授权边界内。结构性够不着时启动会 warn 并给出三条改法(把根挪进 workspace / 经 `additionalDirectories` 授权 / 声明 `MEMORY_PERSISTENCE_CAPABLE=false` 让披露诚实)。⚠️ **「挪根」用哪个旋钮按记忆后端分腿**:file 引擎是 `MEMORY_ENGINE_DIR`;`MEMORY_ENGINE_BACKEND=pg|tidb` 的根是 `<数据根>/memory-work`(数据根 = `LOCAL_DATA_ROOT`/`AGENT_DATA_DIR` 族),那两条腿**不读** `MEMORY_ENGINE_DIR`——启动 warn 的文案自己会按腿指对旋钮。
|
|
159
159
|
- **记忆检索的向量面(embedder,7.14.0 起)**:记忆检索有三档——`lexical`(词面 jaccard)/ `portable`(库内存向量,进程内算余弦)/ `native`(库侧向量算子)。⚠️ **没有档位旋钮**:档位是「记忆后端上限 × embedder 在不在场」**推断**出来的结果(显式选档只会造出「选了 native 却没 embedder」这类矛盾态)。**配上 embedder = 唯一的开法**,而且只在 `MEMORY_ENGINE_BACKEND=pg` 上有意义(tidb 记忆后端 v1 是词面档,file 引擎没有 embedder 接缝——两者配了 `MEMORY_EMBEDDER_*` 一律**拒启**点名)。不配 = 保持 `lexical`(默认姿态,零 warn)。
|
|
160
160
|
|
|
161
161
|
| env | 缺省 | 说明 |
|
|
@@ -4,6 +4,16 @@ export type BatchState = "OPEN" | "ROUTING_UNBOUND" | "ROUTING_BOUND" | "ABORTED
|
|
|
4
4
|
export declare const ASK_TERMINAL: ReadonlySet<AskState>;
|
|
5
5
|
/** 批侧终态集合(无出边的 2 态)。 */
|
|
6
6
|
export declare const BATCH_TERMINAL: ReadonlySet<BatchState>;
|
|
7
|
+
/**
|
|
8
|
+
* #229:回决理由(`AskRow.decisionNote` → `decision_note` 列)的字符上限,**两条回决腿共用一个常量**。
|
|
9
|
+
*
|
|
10
|
+
* 🔴 为什么住在这个模块:两个消费者分别是 durable 腿的 zod 体(`http/routes/runs.ts` 的
|
|
11
|
+
* `AskDecisionBodySchema`)与 live 腿的手写 parser(`tool-approval.ts` 的 `parseToolApprovalResponse`),
|
|
12
|
+
* 两处都在 ask 状态机的**同一张行**上写同一列。上限各写一份字面量 = 同一列上两个口径,而分歧只会在
|
|
13
|
+
* 「一条腿收下、另一条腿 400」的那天才被发现。本模块是两腿唯一的共同上游(askId/batchId 铸造与转移表
|
|
14
|
+
* 都在这里),放这里不引新依赖边。
|
|
15
|
+
*/
|
|
16
|
+
export declare const MAX_DECISION_NOTE_CHARS = 2048;
|
|
7
17
|
/** ask 转移合法性(表驱动,含拒绝同值自环——STREAM_PENDING→STREAM_PENDING 不在表里,故 false)。 */
|
|
8
18
|
export declare function canAskTransition(from: AskState, to: AskState): boolean;
|
|
9
19
|
/** 批转移合法性,同形。 */
|
|
@@ -28,6 +28,16 @@ import { createHash } from "node:crypto";
|
|
|
28
28
|
export const ASK_TERMINAL = new Set(["DECIDED", "PARKED", "DENIED", "VOID"]);
|
|
29
29
|
/** 批侧终态集合(无出边的 2 态)。 */
|
|
30
30
|
export const BATCH_TERMINAL = new Set(["ROUTING_BOUND", "ABORTED"]);
|
|
31
|
+
/**
|
|
32
|
+
* #229:回决理由(`AskRow.decisionNote` → `decision_note` 列)的字符上限,**两条回决腿共用一个常量**。
|
|
33
|
+
*
|
|
34
|
+
* 🔴 为什么住在这个模块:两个消费者分别是 durable 腿的 zod 体(`http/routes/runs.ts` 的
|
|
35
|
+
* `AskDecisionBodySchema`)与 live 腿的手写 parser(`tool-approval.ts` 的 `parseToolApprovalResponse`),
|
|
36
|
+
* 两处都在 ask 状态机的**同一张行**上写同一列。上限各写一份字面量 = 同一列上两个口径,而分歧只会在
|
|
37
|
+
* 「一条腿收下、另一条腿 400」的那天才被发现。本模块是两腿唯一的共同上游(askId/batchId 铸造与转移表
|
|
38
|
+
* 都在这里),放这里不引新依赖边。
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_DECISION_NOTE_CHARS = 2048;
|
|
31
41
|
const ASK_TRANSITIONS = {
|
|
32
42
|
STREAM_PENDING: ["DECIDED", "PARKING", "VOID"],
|
|
33
43
|
PARKING: ["PARKED", "DENIED", "VOID"],
|
|
@@ -56,6 +56,12 @@ export interface MemoryWriteBoundaryFacts {
|
|
|
56
56
|
coreRemoteExecutionEnv: boolean;
|
|
57
57
|
/** 部署声明 `MEMORY_PERSISTENCE_CAPABLE`(三态)。两极都让本模块闭嘴,但理由不同 —— 见各臂注释。 */
|
|
58
58
|
declaredCapable: boolean | undefined;
|
|
59
|
+
/** 本部署的记忆引擎后端(`config.memoryEngineBackend`)。**只进文案**,不参与任何判决 —— 它在这里的
|
|
60
|
+
* 唯一作用是把「怎么把根挪进围栏」这句话指向**在该腿上真的有效**的那个旋钮:`MEMORY_ENGINE_DIR` 只被
|
|
61
|
+
* file 腿读(memory-scope 的 `resolveMemoryEngineRoot`),pg/tidb 腿的根是
|
|
62
|
+
* `<LOCAL_DATA_ROOT|数据根>/memory-work`(boot/stores.ts 两支),那条腿上指 MEMORY_ENGINE_DIR 是让
|
|
63
|
+
* operator 去拧一个不接线的旋钮 —— 修不好还以为自己修了。 */
|
|
64
|
+
engineBackend: "file" | "pg" | "tidb";
|
|
59
65
|
/** boot 期算得出的文件工具围栏根。空数组 = 算不出(每任务临时目录 / 未配 workspace)⇒ 不判。 */
|
|
60
66
|
containmentRoots: readonly string[];
|
|
61
67
|
}
|
|
@@ -63,7 +63,7 @@ function isInside(child, root) {
|
|
|
63
63
|
* 当新闻报。
|
|
64
64
|
*/
|
|
65
65
|
export function buildMemoryWriteBoundaryAudit(facts) {
|
|
66
|
-
const { memoryRoot, lane, hostSemanticsLane, coreRemoteExecutionEnv, declaredCapable, containmentRoots } = facts;
|
|
66
|
+
const { memoryRoot, lane, hostSemanticsLane, coreRemoteExecutionEnv, declaredCapable, containmentRoots, engineBackend } = facts;
|
|
67
67
|
if (memoryRoot === undefined)
|
|
68
68
|
return { warnings: [] }; // 引擎未接
|
|
69
69
|
if (declaredCapable !== undefined)
|
|
@@ -93,6 +93,14 @@ export function buildMemoryWriteBoundaryAudit(facts) {
|
|
|
93
93
|
return { warnings: [] }; // 围栏根算不出 ⇒ 不判(见头注)
|
|
94
94
|
if (containmentRoots.some((root) => isInside(memoryRoot, root)))
|
|
95
95
|
return { warnings: [] };
|
|
96
|
+
// 第一条恢复路径按**后端腿**指对旋钮(2026-08-12 复扫 low:pg/tidb 腿上 MEMORY_ENGINE_DIR 不接线,
|
|
97
|
+
// 而「in-process + MEMORY_ENGINE_BACKEND=pg + 未配 LOCAL_DATA_ROOT」恰恰是这条 warn 的默认命中形 ——
|
|
98
|
+
// 照旧文案去拧只会一直修不好)。另外两条(additionalDirectories / MEMORY_PERSISTENCE_CAPABLE=false)
|
|
99
|
+
// 两条腿都真有效,不分腿。
|
|
100
|
+
const relocateRoot = engineBackend === "file"
|
|
101
|
+
? `point MEMORY_ENGINE_DIR at a path under the workspace`
|
|
102
|
+
: `point LOCAL_DATA_ROOT at a path under the workspace (on the "${engineBackend}" memory plane the root is ` +
|
|
103
|
+
`<data root>/memory-work and MEMORY_ENGINE_DIR is inert — only the file engine reads it)`;
|
|
96
104
|
return {
|
|
97
105
|
warnings: [
|
|
98
106
|
{
|
|
@@ -100,7 +108,7 @@ export function buildMemoryWriteBoundaryAudit(facts) {
|
|
|
100
108
|
detail: `the memory engine root (${memoryRoot}) is outside every file-tool containment root known at boot ` +
|
|
101
109
|
`(${containmentRoots.join(", ")}). Memory has no write TOOL — the model saves by writing files into that root — ` +
|
|
102
110
|
`so a "remember this" turn will be answered with a save the file tools structurally refuse (path_not_in_root). ` +
|
|
103
|
-
`Fix it in one of three ways:
|
|
111
|
+
`Fix it in one of three ways: ${relocateRoot}, hand the root to tasks via ` +
|
|
104
112
|
`additionalDirectories, or declare MEMORY_PERSISTENCE_CAPABLE=false so the model is told up front that this ` +
|
|
105
113
|
`deployment cannot save instead of receipting a write that never lands.`,
|
|
106
114
|
},
|
package/dist/boot/stores.js
CHANGED
|
@@ -29,7 +29,7 @@ import { LocalTaskAttachmentStore } from "../plugins/local-task-attachment-store
|
|
|
29
29
|
import { PgTaskAttachmentStore, TiDBTaskAttachmentStore, ensurePgTaskAttachmentSchema, ensureTiDBTaskAttachmentSchema } from "../plugins/task-attachment-store.js";
|
|
30
30
|
import { createSessionStore } from "../plugins/session-store.js";
|
|
31
31
|
import { assertCloudSnapshotBlobPosture, openStoreBackendWithFallback } from "../plugins/store-backend.js";
|
|
32
|
-
import { memoryEngineBackendFor, memoryEngineRemoteLanePosture } from "../memory-scope.js";
|
|
32
|
+
import { buildMemoryRemoteLaneWarn, memoryEngineBackendFor, memoryEngineRemoteLanePosture } from "../memory-scope.js";
|
|
33
33
|
import { assertToolResultProvenanceSchema } from "../plugins/tool-result-store-sql.js";
|
|
34
34
|
import { buildMemoryWriteBoundaryAudit } from "./memory-boundary.js";
|
|
35
35
|
export async function openStores(ctx) {
|
|
@@ -225,6 +225,7 @@ export async function openStores(ctx) {
|
|
|
225
225
|
coreRemoteExecutionEnv,
|
|
226
226
|
declaredCapable: config.memoryPersistenceCapable,
|
|
227
227
|
containmentRoots,
|
|
228
|
+
engineBackend: config.memoryEngineBackend, // 只进文案:哪个旋钮真能挪动这条腿的根
|
|
228
229
|
});
|
|
229
230
|
for (const w of audit.warnings)
|
|
230
231
|
logger.warn(w.tag, { detail: w.detail, lane: lane ?? "in-process", root: memoryEngine?.root ?? null });
|
|
@@ -395,12 +396,12 @@ export async function openStores(ctx) {
|
|
|
395
396
|
});
|
|
396
397
|
}
|
|
397
398
|
// 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
|
-
|
|
399
|
+
// "forced" so an operator override states what it depends on (harvest only sees the WORKER fs). 文案与分腿
|
|
400
|
+
// 判据在 memory-scope.buildMemoryRemoteLaneWarn(纯,可单测);这里只喂**装配结果**(引擎真接上了没有)——
|
|
401
|
+
// DB 腿的引擎在 :113 就点亮了,与车道姿态无关,照旧打 dark 文案会与上面的 memory_engine_enabled 对撞。
|
|
402
|
+
const laneWarn = buildMemoryRemoteLaneWarn(config, memoryEngine !== undefined);
|
|
403
|
+
if (laneWarn)
|
|
404
|
+
logger.warn("memory_engine_remote_lane", laneWarn);
|
|
404
405
|
// 142-S2.5-W1: TOC 同步 client 腿——只在 file memory 形态接线(loadConfig 已拒 DB backend
|
|
405
406
|
// 上的 MEMORY_SYNC_*,这条分支到不了)。boot 后 fire-and-forget 一轮(失败 warn 不阻断——纯本地现状
|
|
406
407
|
// 是安全降级面);之后 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
|
|
@@ -299,6 +299,18 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
299
299
|
backend: deps.backend,
|
|
300
300
|
parkFacility: deps.checkpointStore !== undefined,
|
|
301
301
|
}).active,
|
|
302
|
+
// #229(设计稿 233 稿B v2 §3):回决**理由**位在本二进制里存在的探测位 —— live
|
|
303
|
+
// `POST /v1/tool-approvals/:id/respond` 收 `note` + 回执带 `noteRecorded`,durable 回决口的三个回体
|
|
304
|
+
// 投 `decisionNote`。
|
|
305
|
+
// 🔴 **恒 true,且必须与 `noteRecorded` 同车**:老 server 对未知请求键**静默忽略 + 照回 200**
|
|
306
|
+
// (live 腿的 parser 是非 strict 的手写形),所以没有这一位时,「理由记上了」与「这台压根不认识
|
|
307
|
+
// 这个键」在 wire 上不可判别 —— 壳只能拿一个 200 猜。位在场 ⇒ 按 `noteRecorded` 逐次读结果;
|
|
308
|
+
// 位缺席 ⇒ 老 server,别渲这一格。
|
|
309
|
+
// 🔴 谓词**刻意不挂任何设施**(不是 `Boolean(deps.backend)` 之类):本位声明的是「这个二进制认识
|
|
310
|
+
// 这个键」这件版本事实,**不是**「你的理由一定会被记下」——后者是 per-call 的,由 `noteRecorded`
|
|
311
|
+
// 逐次如实回答(店缺席/店抖动时它就是 `false`)。把部署条件混进版本位,会让「says yes ⟺ 面真能用」
|
|
312
|
+
// 这句话在两个不同的问题上各说一半(与 `permissionRulesRevoke` 的存在性信号同族,见其注)。
|
|
313
|
+
approvalDecisionNote: true,
|
|
302
314
|
// [1469] POST /v1/side-query(core 1.361 Runner.sideQuery 包装):一次性 brain 路由问答,无 session
|
|
303
315
|
// 副作用。恒可用(runner 自带)——探测位供壳判「引擎腿在」而非 trial-by-404。
|
|
304
316
|
sideQuery: true,
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -14,6 +14,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
|
|
|
14
14
|
import { streamSseLog } from "../sse-log.js";
|
|
15
15
|
import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
|
|
16
16
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
17
|
+
import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
|
|
17
18
|
import { normalizeRunEventType } from "../../trace/project.js";
|
|
18
19
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
19
20
|
import { buildActiveRunConflict } from "../active-run-conflict.js";
|
|
@@ -89,7 +90,9 @@ const AskDecisionBodySchema = z
|
|
|
89
90
|
decision: z.enum(["approve", "deny"]),
|
|
90
91
|
/** 仅 approve 臂有意义;deny 带 = 宽收后忽略(与三条 respond 同姿势)。 */
|
|
91
92
|
updatedInput: z.unknown().optional(),
|
|
92
|
-
|
|
93
|
+
/** 上限走**共享常量**(#229):live 腿的手写 parser 写同一列,两处各写一份 `2048` 字面量 =
|
|
94
|
+
* 同一列上两个口径,而分歧只会在「一条腿收下、另一条腿 400」的那天才暴露。 */
|
|
95
|
+
note: z.string().max(MAX_DECISION_NOTE_CHARS).optional(),
|
|
93
96
|
idempotencyKey: z
|
|
94
97
|
.string()
|
|
95
98
|
.min(1)
|
|
@@ -1748,6 +1751,18 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1748
1751
|
deps.toolApproval?.notifyExternalDecision(row.askId, row.decision === "approve");
|
|
1749
1752
|
return true;
|
|
1750
1753
|
};
|
|
1754
|
+
/**
|
|
1755
|
+
* #229(设计稿 233 稿B v2 §2):回决理由的**读面**投影 —— 三个回体共用这一份,行上有才发。
|
|
1756
|
+
*
|
|
1757
|
+
* 🔴 **裁定翻面,原样记账**:本 handler 原先明写「`note` 不回显(以请求者可见权限为界的最窄安全形)」。
|
|
1758
|
+
* 那条最窄形的实测代价是 `decisionNote` 全仓**只写不读** —— 一个零读面的「审计位」对任何按审计面
|
|
1759
|
+
* 接它的消费端都是当场落空。现行裁定:三回体一律 additive 投,**从不发 null / 空键**(缺席 = 这条
|
|
1760
|
+
* 决议没留理由,与 `updatedInputForwarded` 的「从不发 false」同族)。
|
|
1761
|
+
* 读权前提没有放宽:能走到这条口的调用方本来就有权决这只 ask(属主门在本 handler 上游),而 409 支
|
|
1762
|
+
* 回显的**首决**理由与它同支已经在回显的 `decision`/`decidedAtMs`/`actor` 是同一份首决投影 ——
|
|
1763
|
+
* 多这一格不新开任何一条越权读路径。
|
|
1764
|
+
*/
|
|
1765
|
+
const decisionNoteEcho = (row) => (row.decisionNote === null ? {} : { decisionNote: row.decisionNote });
|
|
1751
1766
|
/** 200 回放形:与首决 200 同键集,只差 `updatedInputForwarded`(§12-A 明写回放不承诺字节等同)。
|
|
1752
1767
|
* 调用前提 = 已过 {@link syncLiveFromDecidedRow}(行合形且活体窗已同步)。 */
|
|
1753
1768
|
const replayDecided = (row) => {
|
|
@@ -1758,6 +1773,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1758
1773
|
decision: row.decision,
|
|
1759
1774
|
decidedAtMs: row.decidedAtMs,
|
|
1760
1775
|
...(storedActor ? { actor: storedActor } : {}),
|
|
1776
|
+
...decisionNoteEcho(row),
|
|
1761
1777
|
});
|
|
1762
1778
|
};
|
|
1763
1779
|
// 终局分派 —— **一律按传进来的这一行**投影,不重读(§12-A:`DecideResult.row` 已是新鲜行;
|
|
@@ -1775,11 +1791,13 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1775
1791
|
return;
|
|
1776
1792
|
}
|
|
1777
1793
|
const first = projectDecisionActor(row.decisionActor);
|
|
1778
|
-
//
|
|
1794
|
+
// 首决回显四件(#229 起含 `decisionNote`;翻面理由见 {@link decisionNoteEcho})——回显的恒是
|
|
1795
|
+
// **首决**那条理由,不是本次请求带来的那条(本次请求压根没落地)。
|
|
1779
1796
|
sendError(res, 409, "conflict.ask_decided", "this ask was already decided — the first decision stands and is echoed here", {
|
|
1780
1797
|
decision: row.decision,
|
|
1781
1798
|
decidedAtMs: row.decidedAtMs,
|
|
1782
1799
|
...(first ? { actor: first } : {}),
|
|
1800
|
+
...decisionNoteEcho(row),
|
|
1783
1801
|
});
|
|
1784
1802
|
return;
|
|
1785
1803
|
}
|
|
@@ -1935,6 +1953,9 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1935
1953
|
decidedAtMs: outcome.row.decidedAtMs,
|
|
1936
1954
|
actor,
|
|
1937
1955
|
...(forwarded ? { updatedInputForwarded: true } : {}),
|
|
1956
|
+
// #229:同上,**按行**投(不按 `body.note`)——赢者行才是这次回决的真相,而 `decideAsk` 的提交与
|
|
1957
|
+
// 回读之间那条行仍可能被版本化收敛改写(同一条「每个入参都取自行」的纪律)。
|
|
1958
|
+
...decisionNoteEcho(outcome.row),
|
|
1938
1959
|
});
|
|
1939
1960
|
return;
|
|
1940
1961
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { buildToolResultRef } from "@sema-agent/core";
|
|
2
1
|
import { projectEvents, taskSummary, mapTraceEvent } from "../../trace/project.js";
|
|
3
2
|
import { projectArtifacts } from "../../trace/artifacts.js";
|
|
4
3
|
import { usageSummary, usageSeries, usageBreakdown } from "../../usage-analytics.js";
|
|
@@ -235,7 +234,11 @@ async function handleTaskArtifacts(res, runStore, taskId) {
|
|
|
235
234
|
* 「坏 query + 已知 ref = 400」而「坏 query + 未知 ref = 404」,状态码之差立刻成了存在性 oracle。
|
|
236
235
|
* query 的合法性只取决于 query 自身,与调用方看不看得见这个 ref 无关,所以先判是安全的。
|
|
237
236
|
*
|
|
238
|
-
* ② **归属绑定 =
|
|
237
|
+
* ② **归属绑定 = 店里记的出处(`ownerOf`)与本 run 的 session/task 精确相等**(实现见 ②-a),不解析 ref、
|
|
238
|
+
* 不复制段转义规则,**且没有第二条腿**。
|
|
239
|
+
*
|
|
240
|
+
* 下面这一大段是这条判据的**病历**(前缀形 → 本 task 日志作证形 → 出处形),留着是因为「为什么不
|
|
241
|
+
* 照 ref 的字面去判」每隔一阵就被重新问一次;当前生效的判据只有 🟡 段之后的那一条。
|
|
239
242
|
*
|
|
240
243
|
* 先说为什么**不是**前缀判定([3321]② 的字面形、[3322] 的指定形):core 的 ref 形是
|
|
241
244
|
* `tr_<seg(session)>_<seg(toolCallId)>`,而 `_` 在两段里都合法 ⇒ session `team` 的前缀 `tr_team_`
|
|
@@ -247,7 +250,7 @@ async function handleTaskArtifacts(res, runStore, taskId) {
|
|
|
247
250
|
* 跨租户**拒绝**面(别人注册一个 `<你的 session>_toolu` 就能让你自己的 ref 永远 404)——R2 复审两条
|
|
248
251
|
* 中标,已撤回该启发式。
|
|
249
252
|
*
|
|
250
|
-
*
|
|
253
|
+
* 当年的改判据(**已被 ②-a 取代,见下**):ref **必须由本 task 自己的耐久事件日志作证**。日志里每条 tool_start/tool_end 都带
|
|
251
254
|
* `toolCallId`(`trace/project.ts` 的 toolStartEventData / toolEndEventData 是唯一铸造点),用 core
|
|
252
255
|
* 的**同一个** `buildToolResultRef` 把它们铸成 ref 集合,再要求 `ref` 是其中一员 —— 精确相等,
|
|
253
256
|
* 零解析、零转义规则复制,歧义面结构上不存在(A 的 taskId 只作证 A 自己的 toolCallId)。
|
|
@@ -255,10 +258,18 @@ async function handleTaskArtifacts(res, runStore, taskId) {
|
|
|
255
258
|
* trace 的 tool-result 块与本判据读的是同一份日志,所以「trace 上看得见 ⇒ 这里读得到」成立;
|
|
256
259
|
* 同 session 里**别的 task** 的 ref(或日志已过保留窗被逐出的)按 404 同形拒,请到那条 task 上读。
|
|
257
260
|
*
|
|
258
|
-
*
|
|
259
|
-
* `put` 带出处落库 + `ownerOf` 读回,读面改用出处判定(实现见 ②-a)
|
|
260
|
-
*
|
|
261
|
-
*
|
|
261
|
+
* 🟡 **两条残余的现状(7.14.0 合并窗复扫改口,下面原文保留作病历)**。core 按当年这里写的方向动了刀:
|
|
262
|
+
* `put` 带出处落库 + `ownerOf` 读回,读面改用出处判定(实现见 ②-a)。逐条对账:
|
|
263
|
+
* · (b) 拼接非单射的越权面**已彻底消失** —— ref 铸法换成 `~` 多段,`~` 不在段的合法字符集里。
|
|
264
|
+
* · (a) 合成 id 铸的 ref **只销了一半**:offload / budget / compaction 三个写点的出处只声明
|
|
265
|
+
* session ⇒ 现在读得到;而 `task-registry-monitor` 的 spill(`<stream>_seg<n>`)与
|
|
266
|
+
* `task-registry-agent` 的 spill(`c<cycle>`)在出处里**点名了 taskId,填的是 core TaskRegistry
|
|
267
|
+
* 的 handle id**(`b/w/a/m` 前缀 + 16 hex,见 core `task-registry.js`),与本路由段里的 server
|
|
268
|
+
* run id(uuidv7)是两个命名空间,恒不相等 ⇒ 这两族 ref 在本面**恒 404**(改前也 404,不是回归)。
|
|
269
|
+
* 而它们恰恰是 core 在成品文本里明写「call ReadToolResult with ref …」露给调用方的那批。
|
|
270
|
+
* 真解仍在 core 侧(出处该声明的是**调用方认得的** task 坐标,或干脆只声明 session);本仓不自行
|
|
271
|
+
* 放宽 taskId 那条腿 —— 那是单方面改判定语义,归属判据会与上游各漂一份。
|
|
272
|
+
* 日志重铸那条腿**已删**(见 ②-b:它对任何真实行都不可达,且每次未命中还要付一次全量日志读)。
|
|
262
273
|
*
|
|
263
274
|
* ⚠️ 两处**已知残余**,都要 core 侧动刀才能消,写在明处而不是让下一个人自己撞(codex 复审 R3):
|
|
264
275
|
*
|
|
@@ -276,7 +287,7 @@ async function handleTaskArtifacts(res, runStore, taskId) {
|
|
|
276
287
|
* 铸出那一枚精确的 toolCallId(provider 铸,本服务不控也不产)。同样只有 core 换成单射编码才真消。
|
|
277
288
|
* 这里**不留特征化测试**去钉这条通路 —— 把缺陷钉成契约是另一种病。
|
|
278
289
|
*
|
|
279
|
-
* ③ **同形 404**(unknown ref /
|
|
290
|
+
* ③ **同形 404**(unknown ref / 出处判不过(含无属主行)/ 本部署没有 durable store)。任何可区分的响应都是
|
|
280
291
|
* 缺陷:前两者之差会告诉调用方「这枚 ref 存在,只是不是你的」;第三者若回 501 则会把部署形告诉一个
|
|
281
292
|
* 连自己的 ref 都读不到的 caller。文案恒定且**不回显 ref**(回显=把输入原样反射进错误面)。
|
|
282
293
|
* 残余面(诚实写明):响应字节同形,**时序**并不同形 —— 三条臂到达的后端调用数不同。要抹平时序得让
|
|
@@ -322,10 +333,12 @@ ref, query) {
|
|
|
322
333
|
return notFound();
|
|
323
334
|
// ②-a 出处判定(#119,core 5.26.0 —— 上面 (a)/(b) 两条残余的**正解到货**)。
|
|
324
335
|
// 店里现在自己记着「这枚 ref 属于谁」(`put` 的第三参落库,`ownerOf` 读回),读面拿**自己已知的字段**
|
|
325
|
-
//
|
|
336
|
+
// 比对即可:零解析、零段转义规则复制,拼接歧义那条残余彻底消失,合成 id 那条**消了一半**
|
|
337
|
+
// (monitor/agent spill 两族的出处点名的 taskId 是 core handle id,与本函数收到的 server run id 不同
|
|
338
|
+
// 命名空间 ⇒ 恒判假;详见头注 🟡 段)。
|
|
326
339
|
//
|
|
327
|
-
// 🔴 为什么必须改而不是"锦上添花":5.26.0 起 offload ref
|
|
328
|
-
// 没有内容 ⇒
|
|
340
|
+
// 🔴 为什么必须改而不是"锦上添花":5.26.0 起 offload ref 是多段单射形(内容坐标进 ref),服务端手里
|
|
341
|
+
// 没有内容 ⇒ 原来那条重铸判据**重铸不出来** ⇒ 每一枚真 offload ref 都会 404。读面整死。
|
|
329
342
|
//
|
|
330
343
|
// ⚠️ 辖域(有意的口径变化,写在明处):出处的粒度由 core 定 —— offload/budget/projection 三个写点只声明
|
|
331
344
|
// session,把 taskId 钉在那里会把「一个 session 的两条 task 共用一枚 ref」这个**设计内**的共享形变成拒绝
|
|
@@ -334,32 +347,20 @@ ref, query) {
|
|
|
334
347
|
// 之前已过,session 的属主就是这个 caller),换来的是这条读面在 5.26.0 上还活着。
|
|
335
348
|
const owner = store?.ownerOf ? await store.ownerOf(ref) : undefined;
|
|
336
349
|
const ownedByThisCaller = owner !== undefined && owner.sessionId === run.sessionId && (owner.taskId === undefined || owner.taskId === taskId);
|
|
337
|
-
// ②-b
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (!attested)
|
|
361
|
-
return notFound();
|
|
362
|
-
}
|
|
350
|
+
// ②-b **没有**第二条腿:出处判不过就是 404(unowned 行也在内 —— core 的口径就是「读面对 unowned
|
|
351
|
+
// fail-closed」)。这里曾有一条「用本 task 耐久日志作证过的 toolCallId 经 `buildToolResultRef` 重铸再
|
|
352
|
+
// 精确相等」的兜底,7.14.0 合并窗复扫判定为**结构性死支**并整条删除,证据链写在明处:
|
|
353
|
+
// · 重铸只喂得起两个参数(服务端手里没有内容)⇒ 铸出的是**两段**形 `tr_<sid>~<callId>`;
|
|
354
|
+
// · core 5.27.0 的**每一个**落库写点铸的都是 ≥3 段(offload/budget/compaction 三段带内容摘要、
|
|
355
|
+
// detail 卸载四段、monitor/agent spill 三段带 `<stream>_seg<n>`/`c<cycle>`),而 `~` 不在段的合法
|
|
356
|
+
// 字符集里 ⇒ **段数不同的 ref 恒不相等** ⇒ 该判据对库里任何真实行都不可能成立;
|
|
357
|
+
// · 旧引擎(≤5.25.0)的 `tr_<sid>_<call>` 同样重铸不出来(分隔符换了),而按旧规则手写一个 legacy
|
|
358
|
+
// 编码器 = 复制上游规则(本文件头注自己禁掉的那件事);存量行的真处置是**删表重建**(见 CHANGELOG)。
|
|
359
|
+
// 删的另一半理由是**代价**:走到这条腿的前提是「出处缺席」,而任意乱写的未知 ref 恰好也落在这里,
|
|
360
|
+
// 于是每个必然 404 的请求都要付一次整份耐久事件日志读(已鉴权调用方可用随机 ref 放大后端读),
|
|
361
|
+
// 换不到任何放行。行为面只收窄不放宽:能放行的集合原本就是空集。
|
|
362
|
+
if (!ownedByThisCaller)
|
|
363
|
+
return notFound();
|
|
363
364
|
if (!store)
|
|
364
365
|
return notFound(); // durable store 缺席:404 同形,不 501(不泄露部署形)
|
|
365
366
|
const slice = await store.get(ref, { ...(offset !== undefined ? { offset } : {}), ...(limit !== undefined ? { limit } : {}) });
|
package/dist/memory-scope.d.ts
CHANGED
|
@@ -44,6 +44,26 @@ export declare function memoryEngineRemoteLanePosture(config: ServiceConfig): {
|
|
|
44
44
|
lane: string;
|
|
45
45
|
posture: "dark" | "forced";
|
|
46
46
|
} | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* N0 启动告警的**文案**(纯,可单测;boot/stores.ts 的装配点只负责喂「引擎这一腿真的接上了吗」并打日志)。
|
|
49
|
+
*
|
|
50
|
+
* 为什么要按引擎真身分腿(2026-08-12 复扫,已核真):{@link memoryEngineRemoteLanePosture} 的 `dark` 只对
|
|
51
|
+
* **file** 引擎腿有裁决权 —— 车道门的唯一消费者是 {@link memoryEngineBackendFor}。`MEMORY_ENGINE_BACKEND=pg|tidb`
|
|
52
|
+
* 的两条腿在 boot/stores.ts 里**先于**任何车道判断就被点亮(库是持久真身,与手的文件平面无关),于是同一次
|
|
53
|
+
* 启动会先打 `memory_engine_enabled {enabled:true, backend:"pg"}`,几行之后再打一句「memory dark
|
|
54
|
+
* (fail-closed) … Set MEMORY_ENGINE_REMOTE_LANE=allow」—— 两句直接对撞,而且把运维指向一个在该腿上**不接线**
|
|
55
|
+
* 的旋钮(`memoryEngineRemoteLaneAllowed` 的读者只有本文件与那条日志)。日志是运维唯一能看见的部署事实,
|
|
56
|
+
* 一句谎比没有这句更贵。
|
|
57
|
+
*
|
|
58
|
+
* 返回 `undefined` = 本部署没什么可说的(无平面分裂车道 / 记忆整体关 / 多租户 file 腿的 dark 成因是租户隔离,
|
|
59
|
+
* 已由 `memory_engine_enabled` 的 reason 位报因,不在这里重复)。
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildMemoryRemoteLaneWarn(config: ServiceConfig,
|
|
62
|
+
/** 记忆引擎这一腿**真的**接上了吗(`memoryEngine !== undefined`)——装配结果,不在本函数里重算。 */
|
|
63
|
+
engineWired: boolean): {
|
|
64
|
+
lane: string;
|
|
65
|
+
effect: string;
|
|
66
|
+
} | undefined;
|
|
47
67
|
/**
|
|
48
68
|
* design/138 S1 wiring gate (pure — unit-testable without booting main): build the file-based memory-engine
|
|
49
69
|
* backend for this deployment, or `undefined` when memory must stay dark.
|
package/dist/memory-scope.js
CHANGED
|
@@ -98,6 +98,51 @@ export function memoryEngineRemoteLanePosture(config) {
|
|
|
98
98
|
return undefined; // hands work THIS machine's fs — same plane
|
|
99
99
|
return { lane, posture: config.memoryEngineRemoteLaneAllowed === true ? "forced" : "dark" };
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* N0 启动告警的**文案**(纯,可单测;boot/stores.ts 的装配点只负责喂「引擎这一腿真的接上了吗」并打日志)。
|
|
103
|
+
*
|
|
104
|
+
* 为什么要按引擎真身分腿(2026-08-12 复扫,已核真):{@link memoryEngineRemoteLanePosture} 的 `dark` 只对
|
|
105
|
+
* **file** 引擎腿有裁决权 —— 车道门的唯一消费者是 {@link memoryEngineBackendFor}。`MEMORY_ENGINE_BACKEND=pg|tidb`
|
|
106
|
+
* 的两条腿在 boot/stores.ts 里**先于**任何车道判断就被点亮(库是持久真身,与手的文件平面无关),于是同一次
|
|
107
|
+
* 启动会先打 `memory_engine_enabled {enabled:true, backend:"pg"}`,几行之后再打一句「memory dark
|
|
108
|
+
* (fail-closed) … Set MEMORY_ENGINE_REMOTE_LANE=allow」—— 两句直接对撞,而且把运维指向一个在该腿上**不接线**
|
|
109
|
+
* 的旋钮(`memoryEngineRemoteLaneAllowed` 的读者只有本文件与那条日志)。日志是运维唯一能看见的部署事实,
|
|
110
|
+
* 一句谎比没有这句更贵。
|
|
111
|
+
*
|
|
112
|
+
* 返回 `undefined` = 本部署没什么可说的(无平面分裂车道 / 记忆整体关 / 多租户 file 腿的 dark 成因是租户隔离,
|
|
113
|
+
* 已由 `memory_engine_enabled` 的 reason 位报因,不在这里重复)。
|
|
114
|
+
*/
|
|
115
|
+
export function buildMemoryRemoteLaneWarn(config,
|
|
116
|
+
/** 记忆引擎这一腿**真的**接上了吗(`memoryEngine !== undefined`)——装配结果,不在本函数里重算。 */
|
|
117
|
+
engineWired) {
|
|
118
|
+
const posture = memoryEngineRemoteLanePosture(config);
|
|
119
|
+
if (posture === undefined || !config.memoryEngineEnabled)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (config.memoryEngineBackend !== "file") {
|
|
122
|
+
if (!engineWired)
|
|
123
|
+
return undefined; // DB 腿没接上只可能是拒启路径(半配 fail-loud),不在这里猜
|
|
124
|
+
return {
|
|
125
|
+
lane: posture.lane,
|
|
126
|
+
effect: `memory ON over a remote lane: the "${config.memoryEngineBackend}" memory plane is durable in the DB and is NOT gated by ` +
|
|
127
|
+
`MEMORY_ENGINE_REMOTE_LANE (that knob gates the FILE engine only). What IS split here is the file plane — the ` +
|
|
128
|
+
`engine's materialization dir lives on the WORKER fs while this lane routes the model's file tools to the sandbox fs, ` +
|
|
129
|
+
`so harvest only sees what lands on the worker. Declare MEMORY_PERSISTENCE_CAPABLE=true|false to state which one it is.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (config.requirePrincipal === true)
|
|
133
|
+
return undefined; // 多租户 file 腿:dark 的成因是隔离,不是车道
|
|
134
|
+
return posture.posture === "dark"
|
|
135
|
+
? {
|
|
136
|
+
lane: posture.lane,
|
|
137
|
+
effect: "memory dark (fail-closed): the file engine works the worker's local fs while this lane routes model file tools " +
|
|
138
|
+
"to the sandbox fs — sandbox writes are never harvested. Set MEMORY_ENGINE_REMOTE_LANE=allow ONLY if both are one fs.",
|
|
139
|
+
}
|
|
140
|
+
: {
|
|
141
|
+
lane: posture.lane,
|
|
142
|
+
effect: "MEMORY_ENGINE_REMOTE_LANE=allow: memory engine ON over a remote lane — harvest only sees files landing on the " +
|
|
143
|
+
"WORKER fs; verify the lane really shares it.",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
101
146
|
/**
|
|
102
147
|
* design/138 S1 wiring gate (pure — unit-testable without booting main): build the file-based memory-engine
|
|
103
148
|
* backend for this deployment, or `undefined` when memory must stay dark.
|
|
@@ -320,7 +320,10 @@ export function createMetrics() {
|
|
|
320
320
|
// S21 — which backend snapshot blobs actually land on (minio|sql). Partial MINIO_* config silently falls to sql.
|
|
321
321
|
m.gauge("snapshot_blob_backend", "1 on the active snapshot-blob backend series (S21), by backend (minio|sql)");
|
|
322
322
|
// S22 — memory embed/KNN runtime failures (boot said vector; runtime silently degrades to lexical).
|
|
323
|
-
|
|
323
|
+
// HELP 文案按**唯一活发射点**写(#228 接线后的复扫):stores.ts 的 onFailure 腿打 `{ backend: dialect }`,
|
|
324
|
+
// 从没有过 `where` 维度(那是退役的 MEMORY_BACKEND/EMBEDDING_* store 面的形)—— 运维照旧文案写
|
|
325
|
+
// `sum by (where)` 得到的是空序列。USAGE 的 `memory_embed_failed_total{backend}` 才是对的那一份。
|
|
326
|
+
m.counter("memory_embed_failed_total", "Memory embedding calls that failed at runtime (S22), by backend (the memory-engine dialect that owns the embedder)");
|
|
324
327
|
m.counter("memory_knn_query_failed_total", "Memory KNN searchScored queries that failed at runtime (S22)");
|
|
325
328
|
// LOW — dedup fold vs new insert (a fold UPDATEs the existing row; previously indistinguishable), and the
|
|
326
329
|
// dedup probe failing over to a plain INSERT.
|
package/dist/tool-approval.d.ts
CHANGED
package/dist/tool-approval.js
CHANGED
|
@@ -47,7 +47,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
47
47
|
import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
48
48
|
import { redactDeep, redactSecrets } from "./trace/redact.js";
|
|
49
49
|
import { createLogger } from "./observability/logger.js";
|
|
50
|
-
import { deriveAskId, deriveBatchId } from "./approval-ask-machine.js";
|
|
50
|
+
import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
51
51
|
import { ApprovalCardEnvelopeSchema, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, } from "./approval-card.js";
|
|
52
52
|
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
53
53
|
/** #151 车2:本模块自有的日志出口——同 config-provider.ts/runtime-caps-resolver.ts 先例(协调器不走
|
|
@@ -159,7 +159,27 @@ export function parseToolApprovalResponse(body) {
|
|
|
159
159
|
}
|
|
160
160
|
persistRule = rule;
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
// #229(设计稿 233 稿B):回决**理由**。与 durable 腿(`http/routes/runs.ts` 的 `AskDecisionBodySchema.note`)
|
|
163
|
+
// **同词同源同上限**({@link MAX_DECISION_NOTE_CHARS}),落的也是同一列 `decision_note` —— 不造第三口径。
|
|
164
|
+
// 🔴 **任何 decision 都可带**(deny 也算),与 durable 腿的无条件记账形对齐:「为什么拒」正是审计面上
|
|
165
|
+
// 最值钱的那一条,做成 allow-only 等于把它扔掉。⚠️ `allow_session` 是 wire 上的三值之一,落到行上是
|
|
166
|
+
// `approve`(三值映二值)—— 它的 note 因此记在**那条 approve 行**上,不另开一行、也不丢。
|
|
167
|
+
// 坏形(非串 / 超上限)是**响亮 400**,不静默截断:一条被悄悄砍半的审计理由比没有理由更坏。
|
|
168
|
+
const rawNote = body.note;
|
|
169
|
+
let note;
|
|
170
|
+
if (rawNote !== undefined) {
|
|
171
|
+
if (typeof rawNote !== "string" || rawNote.length > MAX_DECISION_NOTE_CHARS) {
|
|
172
|
+
return { ok: false, error: `note must be a string of at most ${MAX_DECISION_NOTE_CHARS} characters` };
|
|
173
|
+
}
|
|
174
|
+
note = rawNote;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
value: d,
|
|
179
|
+
...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
|
|
180
|
+
...(persistRule !== undefined ? { persistRule } : {}),
|
|
181
|
+
...(note !== undefined ? { note } : {}),
|
|
182
|
+
};
|
|
163
183
|
}
|
|
164
184
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
165
185
|
}
|
|
@@ -1664,14 +1684,28 @@ export class ToolApprovalCoordinator {
|
|
|
1664
1684
|
* 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */
|
|
1665
1685
|
async respondWithCas(askStore, askId, batchId, id, entry, parsed) {
|
|
1666
1686
|
let decidedWon = false;
|
|
1687
|
+
// 🔴 #229 + codex 对抗复审 round1 [high](验真后修):`noteRecorded` 的判据比 `decidedWon` **更严**,
|
|
1688
|
+
// 所以是**两个**变量而不是一个。`decidedWon` 回答「这条决议是不是终局」(单赢者 CAS 的语义,足够支撑
|
|
1689
|
+
// 200/404 的分派与 `notifyExternalDecision`);`noteLanded` 回答「**这一次请求**的那段文本在不在行上」。
|
|
1690
|
+
// 两者在**提交歧义**臂上会分岔(见下方 catch 里的理由),而回执上那句「你的理由记上了」只能由后者答。
|
|
1691
|
+
let noteLanded = false;
|
|
1667
1692
|
try {
|
|
1668
|
-
const decided = await this.withStoreDeadline(
|
|
1693
|
+
const decided = await this.withStoreDeadline(
|
|
1694
|
+
// #229:`decisionNote` 是**店缝上既有**的键(`DecideAskInput.decisionNote`,SQL twin 与 memory twin
|
|
1695
|
+
// 都已实装)⇒ 零新 SQL、零新列,live 腿与 durable 腿写同一条 UPDATE 的同一列。
|
|
1696
|
+
askStore.decideAsk(askId, batchId, {
|
|
1697
|
+
decision: parsed.value === "deny" ? "deny" : "approve",
|
|
1698
|
+
...(parsed.note !== undefined ? { decisionNote: parsed.note } : {}),
|
|
1699
|
+
}), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
1669
1700
|
// R3-1 迟到成功:HTTP 响应早已发出(这次调用报的是超时),但**决议真落了盘** —— 同 askId 下
|
|
1670
1701
|
// 还在悬挂的本地条目必须按真决议结清,否则它们只能等自己的窗/取消再绕一圈。
|
|
1671
1702
|
if (late.ok)
|
|
1672
1703
|
this.notifyExternalDecision(askId, parsed.value !== "deny", parsed.updatedInput);
|
|
1673
1704
|
});
|
|
1674
1705
|
decidedWon = decided.ok;
|
|
1706
|
+
// 干净赢下 CAS ⇒ note 与决议是**同一条 UPDATE** 写的,赢即落,不必再读一次。
|
|
1707
|
+
if (decided.ok)
|
|
1708
|
+
noteLanded = true;
|
|
1675
1709
|
if (!decided.ok) {
|
|
1676
1710
|
// CAS 输——respond 绝不覆盖赢家(D2)。逐字复用现行 404 形(409/410 分化留给车4)。
|
|
1677
1711
|
return { status: 404, body: { error: "no pending tool approval for this id (settled, expired, or not on this replica)", errorCode: "tool_approval.not_pending" } };
|
|
@@ -1687,8 +1721,18 @@ export class ToolApprovalCoordinator {
|
|
|
1687
1721
|
// 那本就该回放成功)。复核读本身失败/超时 ⇒ 维持「不知道」,守卫照旧 404(不许把未知说成成功)。
|
|
1688
1722
|
try {
|
|
1689
1723
|
const row = await this.withStoreDeadline(askStore.getAsk(askId), "getAsk(decideAsk-confirm)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
1690
|
-
if (row?.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve"))
|
|
1724
|
+
if (row?.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
|
|
1691
1725
|
decidedWon = true;
|
|
1726
|
+
// 🔴 codex 对抗复审 round1 [high](真 finding,红先复现):**note 的判据不能沿用决议的判据**。
|
|
1727
|
+
// 上一段那句「别人不可能写出同一条决议再让我们看见」对**决议**成立(单赢者 CAS),对 `note`
|
|
1728
|
+
// **不成立** —— 两路并发 respond 完全可以带**同决议、不同 note**:一路干净赢下 CAS(写进去的是
|
|
1729
|
+
// 它的 note),另一路撞上提交歧义,在这次确认读里看见那条 DECIDED 行。只比决议的话,输的那一路
|
|
1730
|
+
// 会把别人的胜利认成自己的,回一句「你的理由记上了」,而行上是**别人**的理由 —— 恰好是本布尔
|
|
1731
|
+
// 存在的意义被反过来用。所以这里逐字比**行上的 note**:相等才算落地(文本真的相同就不是谎,
|
|
1732
|
+
// 哪怕是别人写的);行上没有 note、或与本次提交不同 ⇒ 如实 `false`。
|
|
1733
|
+
// 决议侧的 `decidedWon` 保持原样(200/404 的分派与 `notifyExternalDecision` 的判据不动)。
|
|
1734
|
+
noteLanded = parsed.note === undefined || row.decisionNote === parsed.note;
|
|
1735
|
+
}
|
|
1692
1736
|
}
|
|
1693
1737
|
catch (confirmErr) {
|
|
1694
1738
|
this.noteStoreError(confirmErr, "getAsk(decideAsk-confirm)");
|
|
@@ -1726,9 +1770,12 @@ export class ToolApprovalCoordinator {
|
|
|
1726
1770
|
// session 记忆照记(grant 谈的是**将来**的 ask,与这次投递是否由我完成无关)。
|
|
1727
1771
|
// 但 `updatedInput` **没有**随那次结算送达闭包(R2-4)⇒ 显式声明未投递,回显里不许出现
|
|
1728
1772
|
// `updatedInputForwarded`。
|
|
1729
|
-
|
|
1773
|
+
// #229:`noteRecorded` 取的是**店的真实结果**(`noteLanded`,比 `decidedWon` 更严——见其声明处),
|
|
1774
|
+
// 与 `updatedInputForwarded` 的判据刻意分家:那一格问「闭包收到编辑没有」(本支恒否),
|
|
1775
|
+
// 这一格问「行上记下的是不是**这次**的理由」。两件事,不共用一个布尔。
|
|
1776
|
+
return this.finishRespond(id, entry, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded });
|
|
1730
1777
|
}
|
|
1731
|
-
const result = this.finishRespond(id, entry, parsed);
|
|
1778
|
+
const result = this.finishRespond(id, entry, parsed, { noteRecorded: noteLanded });
|
|
1732
1779
|
// round5(pendingByAskId 顶注):回决赢下 CAS 时同样要收尾「同 askId 重复本地注册」那一支——与
|
|
1733
1780
|
// windowExpired/runCancel/emitAllP 三条竞争者的赢家路径同精神(那三处已经这么做)。此处 entry 已经
|
|
1734
1781
|
// 经 `finishRespond` 自行 settle 并从 pendingByAskId 的 Set 里摘除自己,故这里天然只会清算真正
|
|
@@ -1768,6 +1815,21 @@ export class ToolApprovalCoordinator {
|
|
|
1768
1815
|
// 命令行);此时还回 `updatedInputForwarded: true` 等于告诉壳「你的编辑生效了」,是最不该撒的那种谎。
|
|
1769
1816
|
// 调用方在 stale-entry 分支传 `updatedInputDelivered: false`,回显里这个键就整个缺席(壳按未透传处理)。
|
|
1770
1817
|
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|
|
1818
|
+
// #229(设计稿 233 稿B v2 §1):`noteRecorded` = 这次回决的**理由到底有没有落进持久行**。
|
|
1819
|
+
//
|
|
1820
|
+
// 🔴 形照 `rulePersisted`(always-emit 布尔,只在请求真带了 `note` 时在场),值照**店的真实结果**
|
|
1821
|
+
// (调用方传进来的 `decidedWon`,含 CAS 抛错后那次确认读的改判),不是「askStore 在不在」:
|
|
1822
|
+
// · 本方法被 `respond()` **直接**调到 = store 缺席,或 store 在场但 `ensureAsk` 失败已清掉坐标 ——
|
|
1823
|
+
// 两种都是「压根没打过那条 UPDATE」,缺省 `false` 正是这一支的真相;
|
|
1824
|
+
// · `respondWithCas` 的 D5 fail-open 支(店抖动、确认读也没读到赢)同样 `false` —— **不知道不许
|
|
1825
|
+
// 说成成功**(与 `updatedInputForwarded` 从不发 `false`、只在真投递时发 `true` 是同一条纪律的两面:
|
|
1826
|
+
// 那一格靠缺席表达否定,这一格是三态里的一态,必须显式说 `false`,否则壳分不出「这台不支持」);
|
|
1827
|
+
// · 提交歧义臂上**赢了决议但行上是别人的 note**(同决议不同 note 的并发)同样 `false` —— 判据是
|
|
1828
|
+
// 「行上那段文本是不是这次提交的」,不是「这条决议是不是终局」(codex round1 [high],见调用方
|
|
1829
|
+
// `respondWithCas` 里 `noteLanded` 的推导)。
|
|
1830
|
+
// 能力位 `capabilities.approvalDecisionNote` 与本格**必须同车**:老服务对未知键静默丢 + 200,少了
|
|
1831
|
+
// 能力位,「记上了」与「这台不认识 note」在 wire 上不可判别。
|
|
1832
|
+
const noteRecorded = opts?.noteRecorded ?? false;
|
|
1771
1833
|
return {
|
|
1772
1834
|
status: 200,
|
|
1773
1835
|
body: {
|
|
@@ -1776,6 +1838,7 @@ export class ToolApprovalCoordinator {
|
|
|
1776
1838
|
decision: parsed.value,
|
|
1777
1839
|
...(rememberApplied !== undefined ? { rememberApplied } : {}),
|
|
1778
1840
|
...(parsed.updatedInput !== undefined && allowed && updatedInputDelivered ? { updatedInputForwarded: true } : {}),
|
|
1841
|
+
...(parsed.note !== undefined ? { noteRecorded } : {}),
|
|
1779
1842
|
},
|
|
1780
1843
|
};
|
|
1781
1844
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.15.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@sema-agent/sdk": "^6.
|
|
72
|
+
"@sema-agent/sdk": "^6.16.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|