@sema-agent/server 7.144.0 → 7.145.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 +129 -0
- package/README.md +3 -0
- package/README.zh-CN.md +1 -0
- package/USAGE.md +16 -8
- package/dist/audit.d.ts +2 -21
- package/dist/audit.js +0 -9
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +3 -3
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/config-d-migration.d.ts +3 -2
- package/dist/fleet/fleet-bus.d.ts +13 -3
- package/dist/fleet/fleet-bus.js +17 -3
- package/dist/http/admission.js +2 -1
- package/dist/http/checkpoint-ctx.d.ts +1 -0
- package/dist/http/checkpoint-ctx.js +1 -0
- package/dist/http/durable-run-leg.js +7 -6
- package/dist/http/resume-legs.js +5 -3
- package/dist/http/route-ctx.d.ts +15 -0
- package/dist/http/routes/approvals-assistant.d.ts +20 -1
- package/dist/http/routes/approvals-assistant.js +27 -6
- package/dist/http/routes/rules.d.ts +1 -1
- package/dist/http/routes/rules.js +5 -2
- package/dist/http/routes/session-sync.js +12 -0
- package/dist/http/routes/tasks.js +12 -9
- package/dist/http/run-meta.d.ts +14 -7
- package/dist/http/run-meta.js +5 -2
- package/dist/http/server.d.ts +13 -5
- package/dist/http/server.js +3 -0
- package/dist/http/submit-ctx.d.ts +12 -0
- package/dist/http/submit-ctx.js +23 -0
- package/dist/observability/fail-open.d.ts +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/plugins/approval-ask-store-file.d.ts +16 -2
- package/dist/plugins/approval-ask-store-file.js +21 -1
- package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
- package/dist/plugins/approval-ask-store-memory.js +11 -8
- package/dist/plugins/caching-session-store.d.ts +3 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +29 -11
- package/dist/plugins/checkpoint-store-sql.js +38 -16
- package/dist/plugins/file-run-store.d.ts +13 -1
- package/dist/plugins/file-run-store.js +11 -1
- package/dist/plugins/local-checkpoint-store.d.ts +14 -3
- package/dist/plugins/local-checkpoint-store.js +23 -5
- package/dist/plugins/local-session-store.d.ts +52 -16
- package/dist/plugins/local-session-store.js +47 -14
- package/dist/plugins/memory-run-store.d.ts +2 -0
- package/dist/plugins/memory-run-store.js +6 -0
- package/dist/plugins/one-time-migrations.js +2 -2
- package/dist/plugins/run-store-sql.d.ts +4 -0
- package/dist/plugins/run-store-sql.js +3 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/prompt-screen.d.ts +21 -0
- package/dist/prompt-screen.js +8 -0
- package/dist/rules-consent.d.ts +63 -51
- package/dist/rules-consent.js +6 -10
- package/dist/run-acceptance.d.ts +40 -10
- package/dist/run-acceptance.js +38 -5
- package/dist/runs.js +1 -1
- package/dist/security.d.ts +10 -3
- package/dist/session-agent.d.ts +39 -0
- package/dist/session-agent.js +16 -0
- package/dist/session-error-wire.d.ts +4 -4
- package/dist/trace/model-segment.d.ts +3 -2
- package/dist/trace/model-segment.js +16 -8
- package/dist/trace/redact.d.ts +66 -2
- package/dist/trace/redact.js +220 -101
- package/dist/trace/self-minted-wire.d.ts +73 -1
- package/dist/trace/self-minted-wire.js +27 -0
- package/package.json +2 -2
- package/wire-changes/server@7.145.0.json +146 -0
|
@@ -352,9 +352,12 @@ export class FileRunStore {
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
|
|
355
|
+
writeRunFile(r) {
|
|
356
356
|
mkdirSync(this.runDir(r.taskId), { recursive: true });
|
|
357
357
|
atomicWriteFile(this.tmpDir, this.runJsonPath(r.taskId), `${JSON.stringify(this.toDisk(r))}\n`);
|
|
358
|
+
}
|
|
359
|
+
persist(r) {
|
|
360
|
+
this.writeRunFile(r);
|
|
358
361
|
this.runs.set(r.taskId, r);
|
|
359
362
|
}
|
|
360
363
|
eventLogFor(taskId) {
|
|
@@ -427,6 +430,13 @@ export class FileRunStore {
|
|
|
427
430
|
this.byIdem.set(idemKey, taskId);
|
|
428
431
|
return { ok: true };
|
|
429
432
|
}
|
|
433
|
+
async fillObjectivePreview(taskId, preview) {
|
|
434
|
+
const r = this.runs.get(taskId);
|
|
435
|
+
if (!r || r.objectivePreview !== null)
|
|
436
|
+
return;
|
|
437
|
+
this.writeRunFile({ ...r, objectivePreview: preview });
|
|
438
|
+
r.objectivePreview = preview;
|
|
439
|
+
}
|
|
430
440
|
async requestCancel(taskId, owner) {
|
|
431
441
|
const r = this.runs.get(taskId);
|
|
432
442
|
if (!r || !ownerEq(r.owner, owner) || r.status !== "running")
|
|
@@ -91,6 +91,15 @@ export declare class LocalCheckpointStore {
|
|
|
91
91
|
private ensureScopesRebuilt;
|
|
92
92
|
/** All still-pending checkpoints across the registered scopes (summary + full row via get). */
|
|
93
93
|
private pendings;
|
|
94
|
+
/**
|
|
95
|
+
* 一条会话的待决行 —— **只读这条会话自己的行**(本店停驻行读者的那一条律:一行的字节只坏它自己那一行;决断不许因为别的会话的一行坏行而失败)。
|
|
96
|
+
* 读口 = core 店的 `listBySession`(按会话取行,一行读不出的行答 `unreadable` 而不是整页失败、也不是跳过;它不经 scope 注册表、不投摘要);
|
|
97
|
+
* `unreadable` 的行经 `get(token)` 取原行(core 店的 `get` 原样拷出,不判形)—— 它仍是这条会话的一张待决行,选不选它由下游的门型绑定判
|
|
98
|
+
* ({@link pickPendingRow}:门型读不出 ⇒ 不命中,与 SQL 孪生 `gate_kind IN (…)` 对 NULL 同向),续不续由续跑的第一级(core 判官)答。
|
|
99
|
+
* 修前三个按会话的读口(`findPendingTokenBySession` / `peekPendingScope` / `deleteBySession`)走 {@link pendings} 的整店枚举,而那一路经 core 的
|
|
100
|
+
* `listByScope` 投摘要 —— 同库任何一行摘要读不动(缺 `state`)⇒ 整只枚举抛,别人那张好卡的决断也 500。
|
|
101
|
+
*/
|
|
102
|
+
private sessionPendings;
|
|
94
103
|
/** Operator approval queue — the same NON-secret card as the SQL twins (never the token). */
|
|
95
104
|
/** 每一只待决 checkpoint 的会话(去重;不按门型过滤 —— 理由与 SQL 孪生 `listPendingSessionIds` 逐字同,见那里)。 */
|
|
96
105
|
listPendingSessionIds(): Promise<string[]>;
|
|
@@ -157,9 +166,11 @@ export declare class LocalCheckpointStore {
|
|
|
157
166
|
* 语义与存在理由的唯一属主 = SQL 侧 `peekScopeByToken` 顶注。
|
|
158
167
|
*
|
|
159
168
|
* ⚠️ 形差如实记:SQL 孪生那口是真「轻读」(只 SELECT scope 一列),本形没有列可选 —— core 的 File 店
|
|
160
|
-
* 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()`
|
|
161
|
-
* 本引擎续不续由 core
|
|
162
|
-
* `
|
|
169
|
+
* 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级拒、也不判形(两只孪生 7.144.0 起都不按版本拒 ——
|
|
170
|
+
* 本引擎续不续由 core 判官在续跑时答),原样拷出一行。读不动的形有两层:整店级 = 账本重放抛(认不出的事件种类等 ⇒ 整只店装不起来);
|
|
171
|
+
* 单行级也存在 —— 按会话的读口({@link sessionPendings})走 core 店的 `listBySession`,摘要读不出的那一行答 `unreadable`(读口不抛,行经 `get(token)` 取原行)。
|
|
172
|
+
* 即便如此,「行体读得出而 scope 读不出」这一支在本形仍结构上不存在:`scope` 是账本重放认一行的门(`token` / `scope` / `status` 皆字符串)里的成员,
|
|
173
|
+
* 门不过的行重放时被跳过并披露、根本不进店;进了店的行,行体其余成员损成什么样,`get(token)?.scope` 都是字符串。哪天本形补了行级拒、或 core 放宽重放的那道门,这里要跟着改成不碰行体的读。
|
|
163
174
|
*/
|
|
164
175
|
peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
|
|
165
176
|
/** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null).
|
|
@@ -5,7 +5,7 @@ import { durableRowIdentity } from "../checkpoint-identity.js";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { existsSync, readFileSync, readdirSync, unlinkSync, mkdirSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
|
|
8
|
+
import { CHECKPOINT_LIST_LIMITS, FileCheckpointStore, atomicWriteFile, executionVerdict, sanitizePathComponent, } from "@sema-agent/core";
|
|
9
9
|
import { approvalDecisionOfWinner, approvalPayloadFingerprint, boundedRuleOffers, boundedToolInput, candidateIdentityOf, pendingRowPrecedes, queueRowGateKind, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
|
|
10
10
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
11
11
|
import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, readStringArrayColumn } from "./sql-json-column.js";
|
|
@@ -160,6 +160,24 @@ export class LocalCheckpointStore {
|
|
|
160
160
|
}
|
|
161
161
|
return out;
|
|
162
162
|
}
|
|
163
|
+
async sessionPendings(sessionId, scope) {
|
|
164
|
+
if (sessionId === "")
|
|
165
|
+
return [];
|
|
166
|
+
const out = [];
|
|
167
|
+
let after;
|
|
168
|
+
do {
|
|
169
|
+
const page = await this.inner.listBySession({ sessionId, status: "pending", ...(scope !== undefined ? { scope } : {}), ...(after !== undefined ? { after } : {}), limit: CHECKPOINT_LIST_LIMITS.session.max });
|
|
170
|
+
for (const row of page.rows) {
|
|
171
|
+
const token = row.kind === "row" ? row.checkpoint.token : row.token;
|
|
172
|
+
const cp = row.kind === "row" ? row.checkpoint : await this.inner.get(row.token);
|
|
173
|
+
if (cp === null || cp.status !== "pending" || (scope !== undefined && cp.scope !== scope))
|
|
174
|
+
continue;
|
|
175
|
+
out.push({ token, cp });
|
|
176
|
+
}
|
|
177
|
+
after = page.next;
|
|
178
|
+
} while (after !== undefined);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
163
181
|
async listPendingSessionIds() {
|
|
164
182
|
return [...new Set((await this.pendings()).map(({ cp }) => cp.sessionId))];
|
|
165
183
|
}
|
|
@@ -200,7 +218,7 @@ export class LocalCheckpointStore {
|
|
|
200
218
|
return rows.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
|
|
201
219
|
}
|
|
202
220
|
async findPendingTokenBySession(sessionId, scope, binding) {
|
|
203
|
-
return this.pickPendingRow(await this.
|
|
221
|
+
return this.pickPendingRow(await this.sessionPendings(sessionId, scope || undefined), sessionId, binding)?.token ?? null;
|
|
204
222
|
}
|
|
205
223
|
pickPendingRow(rows, sessionId, binding) {
|
|
206
224
|
let best;
|
|
@@ -292,7 +310,7 @@ export class LocalCheckpointStore {
|
|
|
292
310
|
return (await this.inner.get(token))?.scope;
|
|
293
311
|
}
|
|
294
312
|
async peekPendingScope(sessionId, binding) {
|
|
295
|
-
return this.pickPendingRow(await this.
|
|
313
|
+
return this.pickPendingRow(await this.sessionPendings(sessionId), sessionId, binding)?.cp.scope;
|
|
296
314
|
}
|
|
297
315
|
async reapExpired(cutoff) {
|
|
298
316
|
let n = 0;
|
|
@@ -415,8 +433,8 @@ export class LocalCheckpointStore {
|
|
|
415
433
|
return 0;
|
|
416
434
|
}
|
|
417
435
|
let n = 0;
|
|
418
|
-
for (const { token, cp } of await this.
|
|
419
|
-
if (
|
|
436
|
+
for (const { token, cp } of await this.sessionPendings(sessionId)) {
|
|
437
|
+
if (await this.inner.expire(token, cp.scope))
|
|
420
438
|
n++;
|
|
421
439
|
}
|
|
422
440
|
const p = join(this.ctxDir, ctxFileOf(sessionId));
|
|
@@ -348,8 +348,9 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
348
348
|
*/
|
|
349
349
|
headOf(sessionId: string): Promise<SessionHead | undefined>;
|
|
350
350
|
/**
|
|
351
|
-
* 引擎仓存档的头(core `SessionRepo.headOf`:日志首记录与尾,不重放)——
|
|
352
|
-
*
|
|
351
|
+
* 引擎仓存档的头(core `SessionRepo.headOf`:日志首记录与尾,不重放)—— 本店读存档头的**唯一**一处。存档头在本店只有三种读法:叶子(读内容的人要的那一格)、
|
|
352
|
+
* 行代号(换装 / 一次性迁移的 `expect.row`、迁移判「有没有、读不读得出」)与带戳判({@link assertArchiveAnonymous};读的是存档的**形** —— 头上有没有属主戳 —— 不是属主事实);
|
|
353
|
+
* 属主 / 建档时刻 / 谱系作为行事实从不从这里读(规则一,行事实只住边档)。
|
|
353
354
|
*/
|
|
354
355
|
private archiveHeadOf;
|
|
355
356
|
/**
|
|
@@ -490,14 +491,41 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
490
491
|
* 无主」)一个字节不动,没被记下的也不在这里替它记成无主:打开不是一个属主事实(7.119.0 前这一腿把「不知道」记成进程内存里的 null,重启即失)。 */
|
|
491
492
|
private openForItselfHeld;
|
|
492
493
|
/**
|
|
493
|
-
* 带 placement 的按 id 首建(锁内一形,调用方持这只会话的写锁、已判过存档不在且不是 fork 创建的格)。写序 =
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
494
|
+
* 带 placement 的按 id 首建(锁内一形,调用方持这只会话的写锁、已判过存档不在且不是 fork 创建的格)。写序 = **格先、存档后**(与登记先记属主再建存档、fork 先写目标格再拷存档、
|
|
495
|
+
* 导入先落格再导条目同一序;普通 acquire 的两条首建 —— 无 id 新建、按 id 缺档新建 —— 仍是存档先、格后:那两处写的属主是「无主」,崩在两笔之间留下的存档补铸后同样读作无主,
|
|
496
|
+
* 与「登记为无主」只在 M10 认领窗内有别,不在本条):
|
|
497
|
+
* 1. 会话元数据那一格:化身新铸;属主 = `placement.owner` 原样(core 7.62.0 [ref] 盒义务 7:委派树的属主是引擎判的,店只记、不推断;唯一读法 `placedSessionOwnerOf`,
|
|
498
|
+
* 坏值 ⇒ 响亮拒、一个字节不写;SQL 孪生把它与元组写进同一条 INSERT);建档时刻同一笔写(铸行点律);
|
|
499
|
+
* 2. 存档:元组 = `placement` 去掉属主(`SessionPlacementRecord` 没有属主 —— core 自己的店同样只把元组交给 repo),不交属主(引擎仓是匿名内容仓)。
|
|
500
|
+
* 崩在两笔之间 ⇒ 格在而存档不在 = 一次没建完的建行(与登记 / fork / 导入崩溃留下的同一族;不是 placed 行:不上列表、不进分区枚举、`requireExisting` ⇒ not_found、
|
|
501
|
+
* 同 id 再带 placement 建 ⇒ 下面那一判 `placement_refused`)—— 没有任何一刻盘上是「placed 存档在而格缺」(属主从没被记下)。保证到进程崩溃为止(那一格改名落定在建存档之前;
|
|
502
|
+
* 不另加 fsync 屏障,与另外三个建行点同)。
|
|
503
|
+
* 「placed 存档在而格缺」此后只剩三个来源,都照旧读作无主(补铸的格不写属主:属主没有事实可写),在具名树的停驻挂点被否决、响亮,匿名树 `null === null` 照放 ——
|
|
504
|
+
* ① 7.144.x 及更早崩在两笔之间留下的存量行(当年存档先建;不回填:行头属主格是空的,元组里那一份属主是本店当年漏进去的未声明成员,不作来源);
|
|
505
|
+
* ② `release` 删了格、没删存档就崩(判别位最后删;它的 agent 行已先删,没有复活腿会碰它,重发 release / 分区收割收完);③ 同一数据根上 run-local 腿经 core 店写下的 placed 存档
|
|
506
|
+
* (core 形:属主在行头,本店不读行头属主)。SQL 孪生的存量无主 placed 行由 M23 在窗内回填。
|
|
507
|
+
* 格已在而存档不在(登记先落了格、存档还没建 —— 一只普通会话的行;或本条崩在两笔之间留下的格)⇒ `placement_refused`(SQL 孪生:登记过的行在,带 placement 的建读回缺席元组,同一判)。
|
|
508
|
+
* 建后读回准入(镜像方向):别的写者在这一刻抢先建了同 id 的存档 ⇒ repo 返回的是那一只 ⇒ 元组缺席 / 异组 ⇒ 响亮拒。
|
|
509
|
+
* 第 2 步失败时那一格撤不撤,按「这只 id 上的存档是不是本次的」判,先认确证、说不准才问盘:
|
|
510
|
+
* · 确证不是本次的 ⇒ {@link withdrawCreatedCell} 撤(留着它 = 一只属主落在别人存档上的格):读回准入拒(读到的就是 create 交回那一只存档的元组)·
|
|
511
|
+
* core 的 create 抛 `conflict`(它判的「别的写者在存在判与写之间抢先落盘」);确证不被之后的读推翻;
|
|
512
|
+
* · 说不准(create 抛别的故障 —— 可能在行头已落盘之后,core 的 create 写完行头还要开追加日志;读元数据抛)⇒ 问盘({@link holdsThisPlacement}):本次元组下的 placed 存档
|
|
513
|
+
* ⇒ 那一格就是它的属主事实,留着(撤了正是「placed 存档在而格缺」);不在 / 普通 / 别的元组 ⇒ 撤;读不出 ⇒ 留着(残格惰性;撤了可能丢属主 —— 别的写者同一刻抢建同 id
|
|
514
|
+
* 又恰好不是 `conflict` 的那一形;数据根锁之下没有第二个进程,进程内同 id 的写者都排在这只会话的写锁上,这一形只有注入造得出,如实登记不另设臂)。
|
|
515
|
+
* 确证两臂同理是防线:这一节在写锁里、打开刚答「不在」,正常路上 create 交回的就是本次建的那一只。
|
|
516
|
+
* 撤完(或留下)之后上抛第 2 步的那一只故障。
|
|
499
517
|
*/
|
|
500
518
|
private createPlacedHeld;
|
|
519
|
+
/**
|
|
520
|
+
* 带 placement 的建行第 2 步说不准地失败之后,盘上这只 id 是不是本次元组下的 placed 存档 —— 判据就是读回准入那一只(`admitSessionPlacement`:缺席 / 普通 / 异组 ⇒ 不是;
|
|
521
|
+
* 它是纯比较,只抛 `placement_refused`),读的是 {@link placementProbe}(行头上的元组,唯一一读)。读不出(`corrupt_session` / `storage`)⇒ 答「是」(见调用方)。
|
|
522
|
+
*/
|
|
523
|
+
private holdsThisPlacement;
|
|
524
|
+
/**
|
|
525
|
+
* 格先、存档后的建行点(fork 的目标 · 带 placement 的建行)建存档那一步失败时,撤回第一步刚写的那一格(调用方持这只会话的写锁):撤得回 ⇒ 返回,调用方原样上抛那只故障;
|
|
526
|
+
* 撤不回 ⇒ 两只故障同句点名(句子由调用方给,带出路),原故障挂在 `cause` —— 留下的格就是崩在两步之间的那一形。
|
|
527
|
+
*/
|
|
528
|
+
private withdrawCreatedCell;
|
|
501
529
|
/**
|
|
502
530
|
* Idempotently record a session's owner — never overwrites a RECORDED owner (incl. a recorded `null`, "registered ownerless";
|
|
503
531
|
* mirrors TiDBSessionStore.register's first-write rule). TiDB parity: register INSERTs a session_meta row so the session EXISTS
|
|
@@ -648,7 +676,7 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
648
676
|
* ③ 内容,按写口的动词(唯一读法在下面这一只 switch;这里不读那一格、不判属主,只换**内容**;交给引擎仓的属主恒缺席 —— 规则一):
|
|
649
677
|
* · `create` / `judged`(导入):原地换锁里读到的那一份存档的日志(没有存档 ⇒ 导入新建一只,匿名),以锁里读到的存档头为 `expect.row`(**行代号**:
|
|
650
678
|
* 读与写之间存档换了一代 ⇒ 引擎仓拒 `conflict`;它判不出同一代里的追加)。匿名导入只换匿名存档的日志 —— 仍带属主戳的存档(更早的版本写下、一次性迁移 M33
|
|
651
|
-
* 改写不了的,或跳版升上来的)到不了这里:暂存提交在一切「谁赢」的判之前就以具名拒答了它({@link
|
|
679
|
+
* 改写不了的,或跳版升上来的)到不了这里:暂存提交在一切「谁赢」的判之前就以具名拒答了它({@link assertArchiveAnonymous});`create` 落在没有存档的名上,`replace` 先清存档。
|
|
652
680
|
* 读存档、校验、存档判与写在引擎仓里是一步,抛在哪一处内容都一个字节没动 ⇒ 撤回 ②({@link withdrawImportCell}:行原样 —— 净零写,SQL 孪生的事务回滚同义)再原样上抛;
|
|
653
681
|
* · `replace`(接口句「先清既有对话日志,再导入」):先清掉内容存档(不带 `expect` —— 带了就得先读它的头,读不出的存档就清不掉),再导入一只新存档。
|
|
654
682
|
* 清不可撤:清过之后导入再抛(I/O 故障),内容就没了,行留 ② 落下的 mirror(只读方向,下次推送补齐)—— 已发版本的 replace 同形(删后导入)。
|
|
@@ -695,16 +723,24 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
695
723
|
/** Drop a finished/aborted local staging handle from the registry (called by the handle on commit/abort). */
|
|
696
724
|
forgetStaging(stagingId: string): void;
|
|
697
725
|
/**
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
|
|
703
|
-
|
|
726
|
+
* **带戳存档拒绝判的唯一一读**(fork 与改日志的推送问「这份存档的头仍记着属主戳吗」只此一处,也是 {@link StampedSessionArchiveError} 在本店的唯一铸点;只读、零写;
|
|
727
|
+
* 一次性迁移 M33 为改写存档而读头 / 整行是窗内的另一件事,窗关即删):这份存档的头仍记着属主戳
|
|
728
|
+
* (更早的版本导入 / 换装写下的,一次性迁移 M33 改写不了的,或跳版升上来的)⇒ 抛那只具名拒。引擎的 fork 会把戳抄给子存档、引擎导入律不让匿名导入替换带戳存档,
|
|
729
|
+
* 本店不重写、不绕 —— 只让拒带对的出路(删掉会话后重推)。读的是存档的**形**(「这不是本店的存档形」),不是属主事实(属主读行那一格);读口是 {@link archiveHeadOf}
|
|
730
|
+
* (core `SessionRepo.headOf` 契约:与 `open()` 读到的头同一读)。没有存档的 id ⇒ 放行(没有存档就没有戳)。
|
|
731
|
+
* 三处问它,问的时刻不同、答案同一个:
|
|
732
|
+
* · {@link forkTo} 的源 —— 在写目标格之前(持源与目标两把写锁);
|
|
733
|
+
* · {@link commitStagedImport} —— 这一提交要换日志,排在一切「谁赢」的判之前(持这只会话的写锁):换不了日志的存档上谈不上同步冲突或载荷分叉,
|
|
734
|
+
* 那两条拒的出路(带同意重来)都是死路,唯一的出路在这一句里;
|
|
735
|
+
* · 会话同步导入的**第一阶段**(`http/routes/session-sync.ts`,经 `OwnerAwareSessionStore.assertArchiveAnonymous` 这一缝,不持锁):按 id 分类出 fork / stale 又没带同意时,
|
|
736
|
+
* 通用冲突指的出路同样是「带同意重来」⇒ 先问这一口,不等第二轮。不持锁的一问是早拒(此刻的答案),权威的一问是提交里持锁的那一问 —— 同一个事实的两次读。
|
|
737
|
+
* 只有 local 店有带戳存档这一形;SQL 孪生没有 ⇒ 不交这一口(调用方按「没有带戳存档」处置)。
|
|
738
|
+
*/
|
|
739
|
+
assertArchiveAnonymous(sessionId: string): Promise<void>;
|
|
704
740
|
/**
|
|
705
741
|
* {@link LocalStagingHandle.commit} 的整段 —— **一次持锁**(这只会话的写锁,{@link withSessionWriteLock}):属主 CAS(此刻的属主格 = 这次导入的门判过的那一格,
|
|
706
742
|
* 否则抛 {@link StagedCommitOwnerChangedError},一个字节不写;在判目的端日志之前判,SQL 孪生同序)→ 分区判 → 提交时重新分类(目的端此刻的日志 vs staged)→
|
|
707
|
-
* 这一提交要换日志(不是「id 与内容都相同」那一支)⇒ 带戳存档判({@link
|
|
743
|
+
* 这一提交要换日志(不是「id 与内容都相同」那一支)⇒ 带戳存档判({@link assertArchiveAnonymous};只有 local 店有带戳存档,SQL 孪生没有这一步)→
|
|
708
744
|
* 拒未经同意的 fork / stale / 载荷分叉(带戳判在它们之前:带戳存档上日志换不了,同步冲突与载荷分叉的出路 —— 带同意重来 —— 无从谈起)→ 本处写者拒 → 换装(导入写序 ①②③,落点 `judged` = 上面这一判;化身与 fork 键按换装律沿用)或 identical 且内容相同时日志不动、
|
|
709
745
|
* 行照样落(② 那一格:重戳属主 + mirror)→ 两支同记新近(推送即活动)→ **在同一次持锁里**
|
|
710
746
|
* 读出落定的会话面交回。锁放下之前谁也换不掉这一位住客,所以交回的面就是这次提交落下的那一位(SQL 孪生在提交事务里读同一行,同义);
|
|
@@ -1022,12 +1022,51 @@ export class LocalSessionStore {
|
|
|
1022
1022
|
if (this.metaCellOf(sessionId) !== undefined) {
|
|
1023
1023
|
throw new SessionError("placement_refused", `session ${sessionId} is already registered as an ordinary session — refusing to create a subagent transcript on its id`);
|
|
1024
1024
|
}
|
|
1025
|
-
const owner =
|
|
1026
|
-
const
|
|
1027
|
-
await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
|
|
1028
|
-
|
|
1025
|
+
const { owner: treeOwner, ...tuple } = placement;
|
|
1026
|
+
const owner = placedSessionOwnerOf(sessionId, { owner: treeOwner });
|
|
1027
|
+
const created = await this.writeMeta(sessionId, (before) => (before === undefined ? { incarnation: mintSessionIncarnation(), owner, createdAt: rowBornNow() } : undefined));
|
|
1028
|
+
if (!created)
|
|
1029
|
+
throw new SessionError("storage", `LocalSessionStore: session ${sessionId} gained a session-meta cell under its own write lock — refusing to guess`);
|
|
1030
|
+
const stuck = (cause, drop) => `LocalSessionStore: creating the subagent transcript ${sessionId} failed (${cause}) and its session-meta cell could not be withdrawn (${drop}) — the cell stays on disk and a placed create on this id is refused; delete the session to clear it`;
|
|
1031
|
+
const unsure = async (err) => {
|
|
1032
|
+
const foreign = err instanceof SessionError && err.code === "conflict";
|
|
1033
|
+
if (foreign || !(await this.holdsThisPlacement(sessionId, opts)))
|
|
1034
|
+
await this.withdrawCreatedCell(sessionId, err, stuck);
|
|
1035
|
+
throw err;
|
|
1036
|
+
};
|
|
1037
|
+
const session = await this.repo.create({ id: sessionId, placement: tuple }).catch(unsure);
|
|
1038
|
+
const persisted = await session.getMetadata().then((meta) => meta.placement, unsure);
|
|
1039
|
+
try {
|
|
1040
|
+
admitSessionPlacement(sessionId, persisted, opts, placementCapabilityOf(this));
|
|
1041
|
+
}
|
|
1042
|
+
catch (err) {
|
|
1043
|
+
await this.withdrawCreatedCell(sessionId, err, stuck);
|
|
1044
|
+
throw err;
|
|
1045
|
+
}
|
|
1029
1046
|
return this.handOut(sessionId, session);
|
|
1030
1047
|
}
|
|
1048
|
+
async holdsThisPlacement(sessionId, opts) {
|
|
1049
|
+
const probe = await this.placementProbe(sessionId);
|
|
1050
|
+
if (probe.state === "unusable")
|
|
1051
|
+
return true;
|
|
1052
|
+
try {
|
|
1053
|
+
admitSessionPlacement(sessionId, probe.state === "placed" ? probe.record : undefined, opts, placementCapabilityOf(this));
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
catch (e) {
|
|
1057
|
+
if (e instanceof SessionError && e.code === "placement_refused")
|
|
1058
|
+
return false;
|
|
1059
|
+
throw e;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
async withdrawCreatedCell(sessionId, cause, stuck) {
|
|
1063
|
+
try {
|
|
1064
|
+
await this.dropMeta(sessionId);
|
|
1065
|
+
}
|
|
1066
|
+
catch (dropErr) {
|
|
1067
|
+
throw new SessionError("storage", stuck(cause instanceof Error ? cause.message : String(cause), dropErr instanceof Error ? dropErr.message : String(dropErr)), cause instanceof Error ? cause : undefined);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1031
1070
|
async register(sessionId, owner) {
|
|
1032
1071
|
await this.withSessionWriteLock(sessionId, async () => {
|
|
1033
1072
|
await this.meta.ensureLoaded();
|
|
@@ -1171,8 +1210,7 @@ export class LocalSessionStore {
|
|
|
1171
1210
|
return null;
|
|
1172
1211
|
throw e;
|
|
1173
1212
|
}
|
|
1174
|
-
|
|
1175
|
-
throw new StampedSessionArchiveError(sourceId);
|
|
1213
|
+
await this.assertArchiveAnonymous(sourceId);
|
|
1176
1214
|
const forkOptions = await settledForkOptions(source);
|
|
1177
1215
|
const forkOwner = forkOwnerOf(owner, this.metaCellOf(sourceId)?.owner);
|
|
1178
1216
|
const existing = this.metaCellOf(target.id);
|
|
@@ -1188,12 +1226,7 @@ export class LocalSessionStore {
|
|
|
1188
1226
|
await copy();
|
|
1189
1227
|
}
|
|
1190
1228
|
catch (err) {
|
|
1191
|
-
|
|
1192
|
-
await this.dropMeta(target.id);
|
|
1193
|
-
}
|
|
1194
|
-
catch (dropErr) {
|
|
1195
|
-
throw new SessionError("storage", `LocalSessionStore: fork into ${target.id} failed while copying (${err instanceof Error ? err.message : String(err)}) and its target cell could not be withdrawn (${dropErr instanceof Error ? dropErr.message : String(dropErr)}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`, err instanceof Error ? err : undefined);
|
|
1196
|
-
}
|
|
1229
|
+
await this.withdrawCreatedCell(target.id, err, (cause, drop) => `LocalSessionStore: fork into ${target.id} failed while copying (${cause}) and its target cell could not be withdrawn (${drop}) — the target stays in the fork-in-progress state; retry the same fork or delete the session`);
|
|
1197
1230
|
throw err;
|
|
1198
1231
|
}
|
|
1199
1232
|
return { outcome: "created", id: target.id };
|
|
@@ -1338,7 +1371,7 @@ export class LocalSessionStore {
|
|
|
1338
1371
|
forgetStaging(stagingId) {
|
|
1339
1372
|
this.stagings.delete(stagingId);
|
|
1340
1373
|
}
|
|
1341
|
-
async
|
|
1374
|
+
async assertArchiveAnonymous(sessionId) {
|
|
1342
1375
|
if (((await this.archiveHeadOf(sessionId))?.owner ?? null) !== null)
|
|
1343
1376
|
throw new StampedSessionArchiveError(sessionId);
|
|
1344
1377
|
}
|
|
@@ -1354,7 +1387,7 @@ export class LocalSessionStore {
|
|
|
1354
1387
|
const rel = classifySyncRelationshipByIds(staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
|
|
1355
1388
|
const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(staged, dstEntries);
|
|
1356
1389
|
if (!contentEqual)
|
|
1357
|
-
await this.
|
|
1390
|
+
await this.assertArchiveAnonymous(sessionId);
|
|
1358
1391
|
if ((rel.relation === "fork" || rel.relation === "stale") && opts.resolution !== "overwrite-dst") {
|
|
1359
1392
|
throw new SyncConflictError(sessionId, rel);
|
|
1360
1393
|
}
|
|
@@ -57,6 +57,8 @@ export declare class MemoryRunStore implements InProcessClaimFence {
|
|
|
57
57
|
bindIdemBodyFingerprint(taskId: string, idem: RunIdemBinding): Promise<string | null>;
|
|
58
58
|
/** The run a submission's scoped `Idempotency-Key` names, if any. */
|
|
59
59
|
getRunByIdem(idemKey: string): Promise<RunRecord | undefined>;
|
|
60
|
+
/** run 行预览的写一次补写(SQL 孪生同义:列仍是 null 才写,建行写过 / 已补过的不动;行不在 ⇒ 无事)。 */
|
|
61
|
+
fillObjectivePreview(taskId: string, preview: string): Promise<void>;
|
|
60
62
|
requestCancel(taskId: string, owner: string | null): Promise<boolean>;
|
|
61
63
|
isCancelRequested(taskId: string, owner: string | null): Promise<boolean>;
|
|
62
64
|
requestPreempt(taskId: string, owner: string | null): Promise<boolean>;
|
|
@@ -109,6 +109,12 @@ export class MemoryRunStore {
|
|
|
109
109
|
const r = taskId !== undefined ? this.runs.get(taskId) : undefined;
|
|
110
110
|
return r ? this.toRecord(r) : undefined;
|
|
111
111
|
}
|
|
112
|
+
async fillObjectivePreview(taskId, preview) {
|
|
113
|
+
const r = this.runs.get(taskId);
|
|
114
|
+
if (!r || r.objectivePreview !== null)
|
|
115
|
+
return;
|
|
116
|
+
r.objectivePreview = preview;
|
|
117
|
+
}
|
|
112
118
|
async requestCancel(taskId, owner) {
|
|
113
119
|
const r = this.runs.get(taskId);
|
|
114
120
|
if (!r || !ownerEq(r.owner, owner) || r.status !== "running")
|
|
@@ -570,8 +570,8 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
570
570
|
"报告 / 收据 / `changedAnything` / `readingsSnapshot` / `readingFields` 里的 `localArchiveRowFactsBackfilled` / `localArchivesAnonymized` / `localArchivesKeptStamped` / `localArchivesUnreadable` / `localArchiveSnapshotsRecovered`、" +
|
|
571
571
|
"`local_archive_kept_stamped` / `local_archive_unreadable` 两行 warn、读数行 note 里的 M33 句与头注 M33 段;`test/local-archive-owner.test.ts` 里标题含 `P3` / `P4` / `P5` / `P6` / `F1` / `F2` / `F3` 的格与 `legacyRoot` / `writeArchive` / `boot` / `M33_ZERO` / `m33Of` 助手。" +
|
|
572
572
|
"**不随本条删**:行格的 `createdAt` / `forkedFrom` 两键(常设的数据形)、`LocalArchiveSnapshotsPresentError` / `assertNoArchiveSnapshots` / `LOCAL_ARCHIVE_SNAPSHOT_DIR` 与 `LocalBackend.ensureSchema` 里调它的那一句、" +
|
|
573
|
-
"`session-error-wire.ts` 的 `StampedSessionArchiveError` / `STAMPED_SESSION_ARCHIVE_SENTENCE` / `sessionErrorWireOf`
|
|
574
|
-
afterRemoval: "三只常设臂接住:① 数据根下的快照目录非空 ⇒ 拒启(`config.local_archive_snapshot_present`,点名文件与两条出路);② 跳过了带本条的版本直接升的数据根(或本条留着的存档),带属主戳的存档被 fork
|
|
573
|
+
"`session-error-wire.ts` 的 `StampedSessionArchiveError` / `STAMPED_SESSION_ARCHIVE_SENTENCE` / `sessionErrorWireOf` 与本店带戳判的唯一读口 `assertArchiveAnonymous`(fork 的源、改日志推送的落点、会话同步导入第一阶段的早拒三处问它)、行上缺建档时刻的 F 类读法(`server.local-session-row.created-at-absent`)",
|
|
574
|
+
afterRemoval: "三只常设臂接住:① 数据根下的快照目录非空 ⇒ 拒启(`config.local_archive_snapshot_present`,点名文件与两条出路);② 跳过了带本条的版本直接升的数据根(或本条留着的存档),带属主戳的存档被 fork、改日志的推送与会话同步导入第一阶段对 fork / stale 的早拒同一只具名拒(409 `conflict.session_archive_stamped` + 固定句,零写 / 行格净零写)," +
|
|
575
575
|
"读 / 续跑 / 拉取照常 —— 出路 = 删掉会话后重推,或升级时先过一次带本条的版本并在窗内起服;③ 行上没有建档时刻的格(同一类数据根)⇒ 列表 / 会话视图 / 交出的元数据读纪元(F 类 tag 计数)",
|
|
576
576
|
reportKeys: ["localArchiveRowFactsBackfilled", "localArchivesAnonymized", "localArchivesKeptStamped", "localArchivesUnreadable", "localArchiveSnapshotsRecovered"],
|
|
577
577
|
},
|
|
@@ -52,6 +52,10 @@ export declare class SqlRunStore {
|
|
|
52
52
|
* 续跑与 wake 复用 checkpoint 的会话 —— 所以零行只在「受理之后被删」这一种竞争里出现。
|
|
53
53
|
*/
|
|
54
54
|
createRun(taskId: string, sessionId: string, owner: string | null, instanceId: string, meta?: NewRunMeta): Promise<CreateRunOutcome>;
|
|
55
|
+
/** run 行预览的**写一次补写**(腿上装着 prompt 屏时建行不写预览,引擎交付可写的那一份之后补;律的属主 = `run-acceptance.ts`):
|
|
56
|
+
* 列仍是 NULL(没建过预览)才写 —— 建行写过的、已经补过的一律不动,所以补写永远盖不掉任何一份既有预览。不碰 `updated_at`(预览不是状态变化,
|
|
57
|
+
* 收割与列表的时序都不该因它移动)。行不在 ⇒ 0 行,无事。PG 臂过 `pgSanitizeText`(与建行写同一条:预览是模型可控标量)。 */
|
|
58
|
+
fillObjectivePreview(taskId: string, preview: string): Promise<void>;
|
|
55
59
|
/** Request cancel of a RUNNING run (durable, cross-replica). Sets the flag the owning instance's heartbeat
|
|
56
60
|
* tick polls; a same-replica caller additionally aborts the in-flight controller for instant effect. Only
|
|
57
61
|
* flags a `running` row (terminal/suspended → 0 affected = no-op). Returns whether a running row was flagged.
|
|
@@ -141,6 +141,9 @@ export class SqlRunStore {
|
|
|
141
141
|
conn.release();
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
async fillObjectivePreview(taskId, preview) {
|
|
145
|
+
await this.db.query(this.q("UPDATE task_run SET objective_preview = ? WHERE task_id = ? AND objective_preview IS NULL", "UPDATE task_run SET objective_preview = $1 WHERE task_id = $2 AND objective_preview IS NULL"), [this.db.dialect === "mysql" ? preview : pgSanitizeText(preview), taskId]);
|
|
146
|
+
}
|
|
144
147
|
async requestCancel(taskId, owner) {
|
|
145
148
|
const { affected } = await this.db.query(this.q("UPDATE task_run SET cancel_requested = 1 WHERE task_id = ? AND owner <=> ? AND status = 'running'", "UPDATE task_run SET cancel_requested = 1 WHERE task_id = $1 AND owner IS NOT DISTINCT FROM $2 AND status = 'running'"), [taskId, owner]);
|
|
146
149
|
return affected > 0;
|
|
@@ -48,7 +48,7 @@ export declare function ensureSessionDurable(hostStore: SessionStore, sessionId:
|
|
|
48
48
|
* durable 委派分区的部署上(stage-07:core `resolveSubagentTranscriptTier(...) === "full"`),那里 core 给每只新起的后台子代铸 placement、
|
|
49
49
|
* 转录生来就在 host 店 —— 所以这里**只验、不搬、不判属主**:属主是引擎判的(core 7.62.0 [ref] L1:没人派生的 run 读自己那一行、按化身绑一次,原样带给每一层子代;
|
|
50
50
|
* 复活的子代 = 它自己那一行的属主),placed 行建行那一笔原样记下了它(盒义务 7,三只持久会话店 + `session-ownership.ts` 的 `placedSessionOwnerOf`)。
|
|
51
|
-
* 挂点不认领、不推断、不收委派树根见证([ref] P-5;本仓的委派继承律在这一写点退役 —— 存量无主 placed 行:SQL 两店由一次性迁移 M23
|
|
51
|
+
* 挂点不认领、不推断、不收委派树根见证([ref] P-5;本仓的委派继承律在这一写点退役 —— 存量无主 placed 行:SQL 两店由一次性迁移 M23 在窗内回填,本机文件店没有回填臂;本机文件店的建行自 7.145.0 起格先、存档后,崩在两笔之间不再新生这一形,见 `local-session-store.ts` 头注)。
|
|
52
52
|
* 0. 宿主店读不出属主(没有 `headOf`,唯一判法 `headReadOf`)⇒ 响亮拒,一次读都不发。
|
|
53
53
|
* 1. 在不在、有没有真内容:{@link ensureSessionDurable}(`requireExisting` 打开 + 叶子非空 + forget 收尾)。任何失败(不在、空壳、读故障)⇒ 原样上抛 = 否决
|
|
54
54
|
* (读故障也否决:说不准转录在不在,就不让一次审批押在它上面)。不判它是不是 placed 行:复活的是旧行时 id 取行上的会话(升级前停驻、当年晋升进来的普通行),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「这条腿上有没有 prompt 屏」的**唯一读法** —— 提交时问一次,问的是这条腿两座注册**声明**的事件(部署座 `RunnerDeps.mods` + 任务座 `TaskSpec.mods`)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么问它
|
|
5
|
+
* prompt 屏(引擎的 `prompt.submit` 扩展站,底层 `classic.UserPromptSubmit`)可以在交付模型之前改写或拦下一条目标。屏装着的腿上,目标的哪一份
|
|
6
|
+
* 会被交付,只有屏判完才知道;本服务存 / 转目标的每张面(run 行预览、续跑输入、标题器取材)因此只能在交付之后写交付的那一份
|
|
7
|
+
* (律的属主 = `run-acceptance.ts`)。屏**不在**的腿上没有「屏前」:交付的就是请求原文,提交那一刻就知道 —— 与没有屏这回事之前逐字同。
|
|
8
|
+
*
|
|
9
|
+
* ## 判据(一条)
|
|
10
|
+
* 两座任一注册的 `uses.events` 覆盖屏听的任一站 ⇒ 有屏。`uses.events` 是注册**声明**的事件全集(名字或 `<前缀>.*`);引擎准入保证一张注册挂上的
|
|
11
|
+
* handler 不越出它声明的事件(越出 = 准入拒),所以「声明里没有」⇒ 这条腿的总线上**一定**没有屏 —— 判据只会多判有屏(声明了、`register`
|
|
12
|
+
* 时没挂),不会漏判。多判的代价是这条腿的那几张面等到交付才写(写的仍是交付的那一份),不是放行。
|
|
13
|
+
*
|
|
14
|
+
* ## 过渡(引擎包根今天没有导出这一判据)
|
|
15
|
+
* 屏听哪两站、`<前缀>.*` 怎么覆盖,读的是引擎自己的判据(引擎的 `promptScreenHasHandlers` 问的就是这两站;前缀形的展开同引擎准入的读法)。
|
|
16
|
+
* 引擎在包根导出「一组注册声明了 prompt 屏」的判据之后,{@link PROMPT_SCREEN_STATIONS} 与 {@link covers} 一起退役,本函数改调引擎那一只 —— 读口换、判据不变。
|
|
17
|
+
*/
|
|
18
|
+
import type { ModRegistration } from "@sema-agent/core";
|
|
19
|
+
/** 这条腿的两座(缺席的座 = 没有注册)里有没有一张注册声明了 prompt 屏。 */
|
|
20
|
+
export declare function promptScreenDeclared(seats: ReadonlyArray<ReadonlyArray<Pick<ModRegistration, "uses">> | undefined>): boolean;
|
|
21
|
+
//# sourceMappingURL=prompt-screen.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const PROMPT_SCREEN_STATIONS = ["prompt.submit", "classic.UserPromptSubmit"];
|
|
2
|
+
function covers(declared, station) {
|
|
3
|
+
return declared === station || (declared.endsWith(".*") && declared.length > 2 && station.startsWith(declared.slice(0, -1)));
|
|
4
|
+
}
|
|
5
|
+
export function promptScreenDeclared(seats) {
|
|
6
|
+
return seats.some((seat) => seat?.some((registration) => registration.uses.events.some((declared) => PROMPT_SCREEN_STATIONS.some((station) => covers(declared, station)))) === true);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=prompt-screen.js.map
|