@sema-agent/server 3.6.0 → 3.6.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.
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import type { SessionTreeEntry } from "@sema-agent/core";
|
|
2
|
+
/**
|
|
3
|
+
* ── [2092] 渲染臂的判据:**证据面与模型所见同进同出** ─────────────────────────────────────────────
|
|
4
|
+
* core 的 `SessionTreeEntry` 是 12 型联合,曾经这里只渲 message/compaction 两支 ⇒ 10 型静默丢弃。
|
|
5
|
+
* 其中 `custom_message` 正是非标准入口用户内容(steering)的落点:core `convertToLlm` 把它按
|
|
6
|
+
* `role:"user"` 送模型,而证据面渲没了 ⇒ 评估者如实判「证据不足」⇒ 拦停死循环直到 CAP(cli 带
|
|
7
|
+
* 最小复现的真缺陷)。所以分类规则不是「渲会话内容」,而是:**模型看得见的必须在,看不见的必须不在**。
|
|
8
|
+
*
|
|
9
|
+
* 两张表 = 对 core 型全集的**显式全分类**;test/branch-transcript.test.ts 里的闭合钉从 core d.ts
|
|
10
|
+
* 逐字取联合成员核对「型全集 = 渲染臂 ∪ 跳过表」—— core 加第 13 型 ⇒ 红,逼一次显式分类,
|
|
11
|
+
* 默认可见(下面循环的 default 臂做通用文本抽取)而不是默认失踪。
|
|
12
|
+
*/
|
|
13
|
+
export declare const RENDERED_ENTRY_TYPES: readonly ["message", "compaction", "custom_message"];
|
|
14
|
+
/**
|
|
15
|
+
* 跳过的理由逐型给(「纯控制型」不是一句话,是逐个核过模型看不见):
|
|
16
|
+
* thinking_level_change / model_change / label / session_info / leaf / prompt_epoch /
|
|
17
|
+
* announced_listing / workspace_state —— 控制面元数据,core 折 messages 时不产出内容;
|
|
18
|
+
* custom —— data-only(session.js 折 messages 只认 message/custom_message 两型),模型看不见它的
|
|
19
|
+
* data;把它渲出来反而是把模型没见过的东西冒充成证据,与 [2092] 是同一判据的反方向。
|
|
20
|
+
*/
|
|
21
|
+
export declare const SKIPPED_ENTRY_TYPES: readonly ["thinking_level_change", "model_change", "label", "session_info", "leaf", "prompt_epoch", "announced_listing", "workspace_state", "custom"];
|
|
2
22
|
/** 渲染结果。**判别式**:证据不足时调用方拿不到 `text`,想用也用不了(见顶注)。 */
|
|
3
23
|
export type BranchTranscript = {
|
|
4
24
|
sufficient: true;
|
|
@@ -22,14 +22,40 @@
|
|
|
22
22
|
* ⇒ 截断时必须在文本里**明说截了**,让模型知道自己看到的是一个后缀,而不是全部。
|
|
23
23
|
*/
|
|
24
24
|
import { redactSecrets } from "../trace/redact.js";
|
|
25
|
+
/**
|
|
26
|
+
* ── [2092] 渲染臂的判据:**证据面与模型所见同进同出** ─────────────────────────────────────────────
|
|
27
|
+
* core 的 `SessionTreeEntry` 是 12 型联合,曾经这里只渲 message/compaction 两支 ⇒ 10 型静默丢弃。
|
|
28
|
+
* 其中 `custom_message` 正是非标准入口用户内容(steering)的落点:core `convertToLlm` 把它按
|
|
29
|
+
* `role:"user"` 送模型,而证据面渲没了 ⇒ 评估者如实判「证据不足」⇒ 拦停死循环直到 CAP(cli 带
|
|
30
|
+
* 最小复现的真缺陷)。所以分类规则不是「渲会话内容」,而是:**模型看得见的必须在,看不见的必须不在**。
|
|
31
|
+
*
|
|
32
|
+
* 两张表 = 对 core 型全集的**显式全分类**;test/branch-transcript.test.ts 里的闭合钉从 core d.ts
|
|
33
|
+
* 逐字取联合成员核对「型全集 = 渲染臂 ∪ 跳过表」—— core 加第 13 型 ⇒ 红,逼一次显式分类,
|
|
34
|
+
* 默认可见(下面循环的 default 臂做通用文本抽取)而不是默认失踪。
|
|
35
|
+
*/
|
|
36
|
+
export const RENDERED_ENTRY_TYPES = ["message", "compaction", "custom_message"];
|
|
37
|
+
/**
|
|
38
|
+
* 跳过的理由逐型给(「纯控制型」不是一句话,是逐个核过模型看不见):
|
|
39
|
+
* thinking_level_change / model_change / label / session_info / leaf / prompt_epoch /
|
|
40
|
+
* announced_listing / workspace_state —— 控制面元数据,core 折 messages 时不产出内容;
|
|
41
|
+
* custom —— data-only(session.js 折 messages 只认 message/custom_message 两型),模型看不见它的
|
|
42
|
+
* data;把它渲出来反而是把模型没见过的东西冒充成证据,与 [2092] 是同一判据的反方向。
|
|
43
|
+
*/
|
|
44
|
+
export const SKIPPED_ENTRY_TYPES = [
|
|
45
|
+
"thinking_level_change",
|
|
46
|
+
"model_change",
|
|
47
|
+
"label",
|
|
48
|
+
"session_info",
|
|
49
|
+
"leaf",
|
|
50
|
+
"prompt_epoch",
|
|
51
|
+
"announced_listing",
|
|
52
|
+
"workspace_state",
|
|
53
|
+
"custom",
|
|
54
|
+
];
|
|
25
55
|
/** 单次评估送进模型的 transcript 字符上限。超出保留**尾部**(最近的对话对"条件是否已达成"更相关)。 */
|
|
26
56
|
export const BRANCH_TRANSCRIPT_MAX_CHARS = 60_000;
|
|
27
|
-
/**
|
|
28
|
-
function
|
|
29
|
-
const m = entry.message;
|
|
30
|
-
if (!m || typeof m.role !== "string")
|
|
31
|
-
return undefined;
|
|
32
|
-
const c = m.content;
|
|
57
|
+
/** content(string | 片段数组)→ 可读文本。只取 text 片段;image/工具结果等非文本片段不进 transcript。 */
|
|
58
|
+
function contentText(c) {
|
|
33
59
|
const parts = [];
|
|
34
60
|
if (typeof c === "string")
|
|
35
61
|
parts.push(c);
|
|
@@ -40,7 +66,36 @@ function messageText(entry) {
|
|
|
40
66
|
parts.push(t);
|
|
41
67
|
}
|
|
42
68
|
}
|
|
43
|
-
|
|
69
|
+
return parts.join("\n").trim();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `role:"bashExecution"` 消息(`!` 命令)→ 模型所见文本。镜像 core `bashExecutionToText`
|
|
73
|
+
* (dist/engine/harness/messages.js —— core 根不导出,本地镜像,锚在闭合钉+本注上):
|
|
74
|
+
* convertToLlm 把它按 `role:"user"` 送模型,除非 `excludeFromContext`(那时模型看不见 ⇒ 证据面也不该有)。
|
|
75
|
+
*/
|
|
76
|
+
function bashExecutionText(m) {
|
|
77
|
+
if (m["excludeFromContext"])
|
|
78
|
+
return undefined;
|
|
79
|
+
let text = `Ran \`${String(m["command"] ?? "")}\`\n`;
|
|
80
|
+
const out = m["output"];
|
|
81
|
+
text += typeof out === "string" && out ? `\`\`\`\n${out}\n\`\`\`` : "(no output)";
|
|
82
|
+
if (m["cancelled"])
|
|
83
|
+
text += "\n\n(command cancelled)";
|
|
84
|
+
else if (m["exitCode"] !== null && m["exitCode"] !== undefined && m["exitCode"] !== 0)
|
|
85
|
+
text += `\n\nCommand exited with code ${String(m["exitCode"])}`;
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
/** 从一条 message entry 里抽人类可读文本。 */
|
|
89
|
+
function messageText(entry) {
|
|
90
|
+
const m = entry.message;
|
|
91
|
+
if (!m || typeof m.role !== "string")
|
|
92
|
+
return undefined;
|
|
93
|
+
if (m.role === "bashExecution") {
|
|
94
|
+
const text = bashExecutionText(m)?.trim();
|
|
95
|
+
// convertToLlm 对 bashExecution 的送模型 role 是 "user" —— 证据面同款,别让评估者猜生僻 role。
|
|
96
|
+
return text ? { role: "user", text } : undefined;
|
|
97
|
+
}
|
|
98
|
+
const text = contentText(m.content);
|
|
44
99
|
return text ? { role: m.role, text } : undefined;
|
|
45
100
|
}
|
|
46
101
|
/**
|
|
@@ -64,7 +119,23 @@ export function renderBranchTranscript(entries, opts) {
|
|
|
64
119
|
const sum = e.summary;
|
|
65
120
|
if (typeof sum === "string" && sum.trim())
|
|
66
121
|
lines.push(`[earlier conversation, summarized] ${sum.trim()}`);
|
|
122
|
+
continue;
|
|
67
123
|
}
|
|
124
|
+
if (e.type === "custom_message") {
|
|
125
|
+
// [2092] 真缺陷的落点:steering 等非标准入口的用户内容。core convertToLlm 把它按 role:"user"
|
|
126
|
+
// 送模型(display 只管 UI,不改模型可见性)—— 证据面同进同出,渲成 user 行。
|
|
127
|
+
const text = contentText(e.content);
|
|
128
|
+
if (text)
|
|
129
|
+
lines.push(`user: ${text}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (SKIPPED_ENTRY_TYPES.includes(e.type))
|
|
133
|
+
continue;
|
|
134
|
+
// default 臂:比我们的分类表更新的型(core 加了第 13 型、部署窗里还没重新分类)——
|
|
135
|
+
// **默认可见而不是默认失踪**:通用文本抽取,能抽出来就带型标渲出;闭合钉会在 CI 红,逼显式分类。
|
|
136
|
+
const generic = contentText(e.content ?? e.text);
|
|
137
|
+
if (generic)
|
|
138
|
+
lines.push(`[${e.type}] ${generic}`);
|
|
68
139
|
}
|
|
69
140
|
if (lines.length === 0)
|
|
70
141
|
return { sufficient: false, reason: "nothing-renderable" };
|
|
@@ -65,6 +65,26 @@ function isWithin(root, child) {
|
|
|
65
65
|
// (or absolute, on Windows drive change) path for an outside/sibling target.
|
|
66
66
|
return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* SERIALIZE git worktree ADMIN ops per repo (MODULE-level, keyed by normalized repoRoot). `git worktree
|
|
70
|
+
* add`/`remove`/`prune` all mutate the same repo's `.git/worktrees` registry; running any two CONCURRENTLY
|
|
71
|
+
* against one repo races. Two callers share this registry:
|
|
72
|
+
* - the isolation wrapper's mint (`add`) and teardown (`remove`) legs;
|
|
73
|
+
* - the service reaper's `prune` leg (#62 svc3 race: prune used to bypass the wrapper's closure-local
|
|
74
|
+
* lock entirely — the adverse arm is prune observing a mid-mint "registered, dir not yet populated"
|
|
75
|
+
* window and DEREGISTERING the half-minted worktree, handing the task a dir git no longer tracks and
|
|
76
|
+
* making its eventual `git worktree remove` fail loud on a healthy run).
|
|
77
|
+
* A promise-chain mutex per repoRoot: each op runs after the prior one SETTLES (success OR failure — never
|
|
78
|
+
* deadlock). Git ops are ~ms and serial-safe; agents still run fully parallel. The map is bounded by the
|
|
79
|
+
* number of distinct configured repoRoots (operator config, typically 1) — no eviction needed.
|
|
80
|
+
*/
|
|
81
|
+
const gitAdminLocks = new Map();
|
|
82
|
+
function serializeGitAdmin(repoRoot, fn) {
|
|
83
|
+
const key = norm(repoRoot);
|
|
84
|
+
const run = (gitAdminLocks.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
85
|
+
gitAdminLocks.set(key, run.then(() => undefined, () => undefined));
|
|
86
|
+
return run;
|
|
87
|
+
}
|
|
68
88
|
/**
|
|
69
89
|
* Wrap a deployment's `executionEnvFactory` so an agent with `ctx.isolation === "worktree"` runs in its own
|
|
70
90
|
* detached git worktree. See the file header for the full contract + trust gate + caveats.
|
|
@@ -80,19 +100,7 @@ export function withWorktreeIsolation(baseFactory, opts) {
|
|
|
80
100
|
if (!allowedRoots.some((root) => isWithin(root, repoRoot))) {
|
|
81
101
|
throw new Error(`withWorktreeIsolation: repoRoot ${repoRoot} is not within any allowedRoots [${allowedRoots.join(", ")}] — refusing to git-worktree an untrusted path`);
|
|
82
102
|
}
|
|
83
|
-
|
|
84
|
-
// `.git/worktrees` registry under a repo-level lock; running them CONCURRENTLY against one repo races (a
|
|
85
|
-
// parallel fan-out finishing together → the Runner calls several `destroy()` at once → some
|
|
86
|
-
// `git worktree remove` lose the lock and leave the worktree dir+registration behind — an orphan the
|
|
87
|
-
// dir-only `pruneWorktrees` can't reap). A per-wrapper (= per-repoRoot) promise-chain mutex chains every
|
|
88
|
-
// add/remove so they apply one at a time (git ops are fast + serial-safe; the agents themselves still run
|
|
89
|
-
// fully in parallel — only the ~ms git bookkeeping serializes).
|
|
90
|
-
let gitLock = Promise.resolve();
|
|
91
|
-
const serialize = (fn) => {
|
|
92
|
-
const run = gitLock.then(fn, fn); // run after the prior op SETTLES (success OR failure — never deadlock)
|
|
93
|
-
gitLock = run.then(() => undefined, () => undefined);
|
|
94
|
-
return run;
|
|
95
|
-
};
|
|
103
|
+
const serialize = (fn) => serializeGitAdmin(repoRoot, fn);
|
|
96
104
|
return async (ctx) => {
|
|
97
105
|
// TRUST GATE: the ONLY trigger is core's RunInternals-sourced ctx.isolation. We read nothing else from ctx
|
|
98
106
|
// (and never anything from a TaskSpec) to decide isolation — see file header.
|
|
@@ -162,7 +170,9 @@ export function withWorktreeIsolation(baseFactory, opts) {
|
|
|
162
170
|
*/
|
|
163
171
|
export async function reapOrphanWorktrees(baseEnvForGit, repoRoot, logger) {
|
|
164
172
|
try {
|
|
165
|
-
|
|
173
|
+
// #62:prune 动的是与 add/remove 同一张 `.git/worktrees` 注册表 ⇒ 必须过同一把 per-repoRoot 锁
|
|
174
|
+
// (曾绕锁直跑;恶劣臂见 serializeGitAdmin 注)。
|
|
175
|
+
await serializeGitAdmin(repoRoot, () => pruneWorktrees(baseEnvForGit, norm(repoRoot)));
|
|
166
176
|
}
|
|
167
177
|
catch (e) {
|
|
168
178
|
logger?.warn?.("worktree_prune_failed", { repoRoot: norm(repoRoot), error: e instanceof Error ? e.message : String(e) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.1",
|
|
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",
|