@sema-agent/server 7.110.0 → 7.111.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.
Files changed (56) hide show
  1. package/MIGRATION.md +8 -0
  2. package/dist/approval-ask-audit-store.d.ts +38 -25
  3. package/dist/approval-ask-audit-store.js +26 -59
  4. package/dist/bench/s1/run-firm.d.ts +11 -0
  5. package/dist/bench/s1/run-firm.js +47 -32
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/session-faces.d.ts +4 -0
  8. package/dist/boot/session-faces.js +3 -1
  9. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  10. package/dist/boot/stage-07-capability-layer.js +4 -3
  11. package/dist/boot/stage-08-reapers.d.ts +1 -0
  12. package/dist/boot/stage-09-leader.d.ts +1 -0
  13. package/dist/boot/stage-09-leader.js +2 -1
  14. package/dist/boot/stage-10-http-server.d.ts +1 -0
  15. package/dist/boot/workflow-orchestration.js +1 -1
  16. package/dist/http/resume-legs.js +6 -3
  17. package/dist/http/routes/runs.js +14 -1
  18. package/dist/http/routes/tasks.js +8 -6
  19. package/dist/http/server.d.ts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/instruction-selection.d.ts +34 -0
  22. package/dist/instruction-selection.js +23 -0
  23. package/dist/observability/corrupt-read-seat.d.ts +18 -0
  24. package/dist/observability/corrupt-read-seat.js +18 -0
  25. package/dist/observability/fail-open.d.ts +15 -3
  26. package/dist/observability/fail-open.js +15 -3
  27. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -23
  28. package/dist/orchestration/workflow-agent-session-index.js +11 -50
  29. package/dist/orchestration/workflow-notify-journal.d.ts +11 -8
  30. package/dist/orchestration/workflow-notify-journal.js +20 -36
  31. package/dist/plugins/file-outcome-sink.d.ts +4 -1
  32. package/dist/plugins/file-outcome-sink.js +9 -3
  33. package/dist/plugins/file-run-store.js +3 -3
  34. package/dist/plugins/retention-store-sql.d.ts +1 -1
  35. package/dist/plugins/retention-store-sql.js +2 -0
  36. package/dist/plugins/store-backend.js +1 -0
  37. package/dist/plugins/store-contracts.d.ts +19 -2
  38. package/dist/plugins/store-contracts.js +9 -0
  39. package/dist/plugins/user-input-ledger-file.d.ts +4 -2
  40. package/dist/plugins/user-input-ledger-file.js +11 -1
  41. package/dist/plugins/user-input-ledger-sql.d.ts +14 -4
  42. package/dist/plugins/user-input-ledger-sql.js +46 -18
  43. package/dist/project-memory.d.ts +10 -2
  44. package/dist/project-memory.js +5 -2
  45. package/dist/runs.js +5 -3
  46. package/dist/trace/ledger-sink.d.ts +4 -0
  47. package/dist/trace/ledger-sink.js +1 -1
  48. package/dist/trace/project.d.ts +11 -3
  49. package/dist/trace/project.js +6 -5
  50. package/dist/trace/self-minted-wire.d.ts +26 -0
  51. package/dist/trace/self-minted-wire.js +9 -0
  52. package/dist/trace/wire-projection-faces.d.ts +3 -3
  53. package/dist/trace/wire-projection-faces.js +4 -2
  54. package/dist/user-input-ledger.d.ts +101 -48
  55. package/dist/user-input-ledger.js +52 -35
  56. package/package.json +2 -2
@@ -42,7 +42,7 @@ export const WIRE_PROJECTION_FACES = {
42
42
  coreType: `WiringManifest & ${arm("wiring_manifest")}`,
43
43
  excluded: ["governance", "configFingerprint", "autoConsolidation", "readDeny", "peerPresence", "permissionMode"],
44
44
  unwrapped: ["manifest"],
45
- minted: [],
45
+ minted: ["instructions"],
46
46
  },
47
47
  wiring_manifest_operator: {
48
48
  projector: "wiringManifestOperatorEventData",
@@ -51,7 +51,9 @@ export const WIRE_PROJECTION_FACES = {
51
51
  return withoutSections(WIRE_PROJECTION_FACES.wiring_manifest_tenant.excluded, WIRING_MANIFEST_OPERATOR_ONLY_SECTIONS);
52
52
  },
53
53
  unwrapped: ["manifest"],
54
- minted: [],
54
+ get minted() {
55
+ return WIRE_PROJECTION_FACES.wiring_manifest_tenant.minted;
56
+ },
55
57
  },
56
58
  engine_notice: { projector: "engineNoticeEventData", coreType: "EngineNotice", excluded: [], minted: ["ts"] },
57
59
  memory_import_report: { projector: "buildImportReportWire", coreType: "MemoryImportReport", excluded: [], minted: [] },
@@ -9,38 +9,60 @@
9
9
  * core 7.38.0 #1233 给了词:`detail.cause` 闭集 {@link USER_STEER_UNDRAINED_CAUSES} = `leg_end` / `crashed`,`crashed` 由**服务层**在
10
10
  * 该会话下一条 run 之前铸。本文件 = 那个服务层。
11
11
  *
12
- * ## 对象(一只账本,一类输入)
13
- * 「本服务以回执向调用方承诺已交给引擎、而消费只发生在进程内存里的用户输入」。今天只有 steer 一族(`kind:"steer"`)—— 两个入口同一个引擎动词
14
- * `TaskStream.steer`:`POST /v1/runs/:id/steer` 的 live 腿(200 applied)与 `POST /v1/runs/:id/interrupt` 的带 text 形(202 queued-immediate,档位 now;
15
- * 设计稿把后者划在病面外,车内亲核它进的是同一只进程内队列 ⇒ 同类同修,收车档登记);
16
- * follow-up 在本仓**没有**进程内受理口(`src/http/routes` 零 follow-up 入口;steer 路由不下传 `priority`,core 缺省 `next` = steer 车道)
17
- * ⇒ 闭集一词,不预留第二词。子代 steer(`/v1/runs/:id/subagents/:target/steer`)与 workflow agent steer 也回 200 applied,但它们投的是
18
- * **子代**的队列,core 的 `task.user_steer_undrained` 只记 run 自己的用户 steer 队列 —— 不同族,不进本账(收车档候裁)。
12
+ * ## 对象(一只账本,一类输入,一句规则)
13
+ * 「本服务以回执向调用方承诺已交给引擎、而消费只发生在进程内存里的用户输入」,**且承载它的那只 `TaskStream` 活在本服务的一条 run 腿里**(S-966)——
14
+ * 后半句是收敛判词成立的前提:证据面是那条 run 行(下文「收敛判据」)。行的 `taskId` = 回执上的 run、`sessionId` = 那条 run 的会话(人看的那一只),
15
+ * run 里是哪一只流(run 自己的 / 同步子代的)**不是**账本的事实:告知按 run 并帧(`steer` = 条数),读面行同形。今天只有 steer 一族(`kind:"steer"` =
16
+ * core 的车道词:引擎把这些输入记在 steer 车道),三个入口同一个引擎动词 `TaskStream.steer`:
17
+ * · `POST /v1/runs/:id/steer` 的 live 腿(200 applied);
18
+ * · `POST /v1/runs/:id/interrupt` 的带 text 形(202 queued-immediate,档位 now;设计稿把它划在病面外,车 SW 亲核它进的是同一只进程内队列 ⇒ 同类同修);
19
+ * · `POST /v1/runs/:id/subagents/:target/steer`(200 applied,S-966)—— 投进**同步子代**自己的流(core `createSteerHandle` → `stream.steer`)。子代由父腿
20
+ * await,父腿收场时 core 撤掉在飞的复活(`subagentRetain.disposeAll`),所以它活在父 run 的腿里;腿 = 子代句柄,`handle.settled` 兑现 ⇒ 结清。
21
+ * 规则的反面(不是例外表):workflow agent steer(`POST /v1/workflows/:id/agents/:label/steer`,200 applied)不在本账 —— workflow 是会话锚定的后台件
22
+ * (core `WorkflowTaskHandle.sessionScoped`),活得比发起它的 run 腿长,没有一条 run 行约束它,而 workflow run 行上没有持有者列 ⇒ 判不出写者死没死(另票);
23
+ * 子代 resume(`…/subagents/:target/resume`)不往在飞的队列里投,起的是一条新的子代腿;follow-up 在本仓**没有**进程内受理口(`src/http/routes` 零
24
+ * follow-up 入口;steer 路由不下传 `priority`,core 缺省 `next` = steer 车道)⇒ 闭集一词,不预留第二词。
19
25
  *
20
- * ## 四形行(append-only;行序 = 语义序;File 孪生的盘上形 = 下面的 {@link UserInputLedgerRecord},SQL 孪生 = 一行一受理 + CAS 列,同义)
26
+ * ## 五形行(append-only;行序 = 语义序;File 孪生的盘上形 = 下面的 {@link UserInputLedgerRecord},SQL 孪生 = 一行一受理 + CAS 列 + 删行,同义)
21
27
  * · `accepted` —— 受理点(`routes/runs.ts` 的 live 腿,**调 `TaskStream.steer` 之前**、回 200 之前)写;写不进 ⇒ 503
22
28
  * `steering.ledger_unavailable`,引擎一个字节都没收到(fail-closed:没账的 200 是空头承诺)。身份 = `acceptId`
23
29
  * (每次受理一枚;同 Idempotency-Key 的重放共享 `messageId`,但各是一次受理)。`writer` = 受理进程的 `instanceId`。
24
30
  * · `settled` —— 本进程对这次受理的守护**正常结束**:`leg_end`(承载它的腿在本进程收场 —— done / suspended / needs_review / failed
25
31
  * 一律,未消费的那一半 core 已用 `leg_end` 自己告知,本仓不重算「消费了没有」)或 `not_applied`(`TaskStream.steer`
26
- * 抛了,这次受理没有变成 200;路由随即落这一行)。
32
+ * 抛了,这次受理没有变成 200;路由随即落这一行)。⚠️ 子代腿的 `leg_end` 由 core 铸在**子代自己的**会话上(`taskId` / `sessionId` /
33
+ * `runId` 全是子代的),本服务没有那只会话的口 ⇒ 今天到不了人(S-966 core 请托;🚧 钉在 `test/engine-notice-wire.test.ts`)。
27
34
  * · `converged` —— 收敛器判「受理它的进程已经不在、而它从没 settle」⇒ `cause:"crashed"`(core 闭集里服务层那一臂)。
28
35
  * · `announced` —— 通告**落进新 run 的事件账之后**写(`taskId` = 告知它的那条**新** run;`EngineNoticeRouter.routeConfirmed` 等口的落账结果,
29
36
  * codex r1-F2):落账失败 ⇒ 不写,下一条 run 再说;重启再重启不重复。唯一的重复形 = 帧已落账而这一行没写成(写失败 / 进程死在两步之间)
30
37
  * —— run 事件账与本账是两只店、没有共同事务,两害取「多说一遍」,不取「声称说过」。
31
38
  * 转移只有四条(open → settled | converged;converged → announced),两只孪生都按 CAS 守它们(File 单进程 = 内存判;SQL = `WHERE state=…`)。
39
+ * · `erased` —— (S-965)**会话树删除**:该会话此前的每一次受理(不论状态)离开账本。行随会话走 —— 账本行的唯一归属键就是 `sessionId`,
40
+ * 所以两条删会话树的级联(在线删除协调器 `boot/session-faces.ts` 的 `purgeSession`、托管留存的事务化孪生
41
+ * `retention-store-sql.ts` 的 `purgeSessionTrees`)各有一臂删它,谓词同一只(`session_id` 单键,店动词 `deleteBySession`)。
42
+ * File 孪生的盘上形 = 一行会话级墓碑 `{ kind:"erased", sessionId, at }`(与 workflow 完成信箱 `drain{sessionId}` 同形),
43
+ * 只擦它**之前**的同会话行 —— 会话 id 删后可被重新登记,之后的受理属于下一位住客;SQL 孪生 = 真删行。被删受理上迟到的转移全输
44
+ * (行不在了,与「未知 acceptId」同一条守卫)。
32
45
  *
33
- * ## 收敛判据(一条:**受理它的进程已经不在,而它从没 settle**)
34
- * 「进程不在」的证据按车道各一句({@link WriterGoneEvidence},闭集穷举)—— 与 run 店自己的两臂同构:
35
- * · `exclusive_data_root`(local / File)—— 数据根的 `LOCK` 证明本进程之前的持有者已死(`FileRunStore.reclaimOrphanedAtBoot` 的同一条论证)
36
- * ⇒ 每一行 `writer ≠ 本进程` 的 open 受理都是前世的孤儿。
37
- * · `run_store_verdict`(SQL,多副本)—— 「本进程启动前写的」**不**证明写者已死(滚动发布 / 三副本时别的副本活着)。证据 = run 店**已经落盘**的
38
- * 死亡判决:承载它的 run 行被 `reapStale`(写入时按心跳判「实例丢了」)翻成 `failed` 并落款 {@link RUN_STALLED_ERROR},持有者列仍是那个写者
39
- * (codex r1-F1 / r3-F1:读到的旧心跳不算 —— 读后写者可能恢复;判决落盘之后才收,收敛与 reap 的先后不再承重)。维护 tick 里收敛排在
40
- * reapStale **之后**(同一条腿串行:本拍刚 reap 的 run 本拍就收)。
41
- * 同一份证据也说得出「腿已经正常收场」(行正常翻面 / 易手 / 不在)⇒ 那条没人 settle 的受理补落 `settled{leg_end}`(判词 `leg_over`,见 {@link OpenVerdict}),
42
- * open 集因此只装活腿上的受理。
43
- * 两臂都排除 `writer = 本进程` 的行(活着的进程的受理归它自己的腿 settle)。触发 = boot 一次 + 每个维护 tick 一次(local 臂在 boot 之后恒空)。
46
+ * ## 收敛判据(一条:**受理它的进程已经不在,而承载它的腿没有正常收场**)—— 两车道同一只判词({@link judgeOpen},S-969 L8 合一)
47
+ * 证据面只有一个:承载它的 **run 行**(run 店;行上的 `instanceId` = 当前持有者,createRun 铸、markResuming 接管)。
48
+ * · 行在、持有者是写者、仍 `running` ⇒ 腿没收场;写者在不在由车道答(下一段)—— 不在 ⇒ `crashed`,可能在 ⇒ `live`(不动,等 reaper)。
49
+ * · 行在、持有者是写者、`failed` 且落款是 run 店的**死亡判决**({@link isHolderDeathVerdict}:`reapStale` 的 `RUN_STALLED_ERROR` /
50
+ * File 车道孤儿回收与 claim 接管的 `RUN_ORPHANED_ERROR`)⇒ 写者死在腿收场之前 ⇒ `crashed`。判决落盘之后才收(codex r1-F1 / r3-F1:
51
+ * 读到的旧心跳不算 —— 读后写者可能恢复;收敛与 reap 的先后因此不承重)。维护 tick 里收敛排在 reapStale **之后**(同一条腿串行)。
52
+ * · 其余 —— 行正常翻面(完成 / 失败 / 取消)、已 park(suspended / needs_review:引擎把未消费的 steer 迁进检查点,续跑重投)、易手(续跑被别的
53
+ * 进程接走)⇒ 腿正常收场了,只是没人替它落 settle ⇒ 补落 `settled{leg_end}`(判词 `leg_over`,见 {@link OpenVerdict}),不告知。
54
+ * · 行**不在**(`getRun` 答 undefined)⇒ 由车道答(下一段 ②):local `crashed`、SQL `leg_over`。
55
+ * 车道只回答事实表 {@link LANE_FACTS} 的**两问**({@link WriterGoneEvidence}):① 外来写者仍持有的 `running` 行,写者可能还活着吗;
56
+ * ② run 行不在,这条腿怎么了(local ⇒ `crashed`:File 店藏读不出的行、写者已证死;SQL ⇒ `leg_over`:店不藏行,不在 = 真删了)—— ① 按车道:
57
+ * · `exclusive_data_root`(local / File):不可能。数据根 `LOCK` 证明本进程之前的每一个写者都已死(`FileRunStore.reclaimOrphanedAtBoot` 的同一条论证)。
58
+ * boot 序是先孤儿回收(把这样的行翻成 `failed` + `RUN_ORPHANED_ERROR`)再收敛,所以 local 上最常见的崩溃形读到的是落款;仍 `running`
59
+ * 的只剩回收因续跑中的检查点刻意跳过的行(写者死、腿没收场 ⇒ 同样 `crashed`)。
60
+ * · `run_store_verdict`(SQL,多副本):可能。「本进程启动前写的」不证明写者已死(滚动发布 / 三副本)⇒ 等 reaper 把判决落在行上。
61
+ * S-969 L8 之前 local 臂不读 run 店、一律 `crashed`:进程恰死在 park 已提交、收场 settle 之前 ⇒ 那次 steer 已迁进检查点、续跑会重投,却被按崩溃
62
+ * 告知一次;settle 写丢后再崩的正常收场同理。两车道读同一只判词之后,「正常收场 / 已 park ⇒ 不告知」不再随车道变。
63
+ * 本进程写的行不读 run 店(活着的进程的受理归它自己的腿 settle,见 judgeOpen 首行)。触发 = boot 一次 + 每个维护 tick 一次;local 上 boot 之后
64
+ * open 集里不再有外来写者的行(每一行要么收敛要么结清,恒不判 `live`),维护 tick 的 local 臂恒空。一趟收敛对同一 taskId 只读一次 run 店(S-969 L9,
65
+ * 趟内备忘:同一条 run 上的受理在同一趟里得同一个判词)。
44
66
  *
45
67
  * ## 两张脸
46
68
  * · **通告**(user 受众)—— 该会话**下一条 run 的首拍**(三条腿开通告口那一拍、模型调用之前;{@link UserInputLedger.announceUndrained}):
@@ -112,8 +134,8 @@ export interface UserInputAcceptance {
112
134
  /** `accept` 的入参(新受理恒 open、恒未告知)。 */
113
135
  export type NewUserInputAcceptance = Omit<UserInputAcceptance, "state" | "announced">;
114
136
  /**
115
- * 四形行(closed set,`kind` 判别)—— File 孪生的盘上形,也是本账语义的成文形。SQL 孪生把同一组转移落在一行的列上。
116
- * 键 `at` = 这件事发生的时刻(ms epoch,写者的墙钟)。
137
+ * 五形行(closed set,`kind` 判别)—— File 孪生的盘上形,也是本账语义的成文形。SQL 孪生把同一组转移落在一行的列上(`erased` = 删行)。
138
+ * 键 `at` = 这件事发生的时刻(ms epoch,写者的墙钟)。前四形以 `acceptId` 为键(一次受理);`erased` 以 `sessionId` 为键(一棵会话树)。
117
139
  */
118
140
  export type UserInputLedgerRecord = {
119
141
  readonly kind: "accepted";
@@ -139,22 +161,30 @@ export type UserInputLedgerRecord = {
139
161
  readonly acceptId: string;
140
162
  readonly taskId: string;
141
163
  readonly at: number;
164
+ } | {
165
+ readonly kind: "erased";
166
+ readonly sessionId: string;
167
+ readonly at: number;
142
168
  };
143
169
  /**
144
- * 四条转移的**唯一**成文实现(纯内存折叠)。File 孪生用它重放盘、也用它判运行期 CAS;Memory 孪生就是它本身。
145
- * SQL 孪生不调它,但它的 `WHERE state = …` 谓词逐条对应这里的四个守卫 —— 两边同一套断言见试剂盒格(`test/helpers/user-input-ledger-kit.ts`)。
170
+ * 四条转移 + 会话树删除的**唯一**成文实现(纯内存折叠)。File 孪生用它重放盘、也用它判运行期 CAS;Memory 孪生就是它本身。
171
+ * SQL 孪生不调它,但它的 `WHERE state = …` 谓词逐条对应这里的四个守卫、`DELETE … WHERE session_id = ?` 对应 `erased` —— 两边同一套断言见
172
+ * 试剂盒格(`test/helpers/user-input-ledger-kit.ts`)。
146
173
  */
147
174
  export declare class UserInputLedgerFold {
148
175
  private readonly byId;
149
176
  private readonly bySession;
150
177
  has(acceptId: string): boolean;
151
178
  get(acceptId: string): UserInputAcceptance | undefined;
179
+ /** 本会话此刻在账的受理条数(`erased` 删掉的就是这么多)。 */
180
+ sessionSize(sessionId: string): number;
152
181
  /** 这一行能不能落(守卫判;**不**改状态)。重放与运行期写同用 —— 不能落的行在运行期不写,在重放里被忽略。 */
153
182
  admits(rec: UserInputLedgerRecord): boolean;
154
183
  /** 落一行(守卫不过 ⇒ 返回 false、状态不动)。 */
155
184
  apply(rec: UserInputLedgerRecord): boolean;
156
- /** open 受理的键集分页:按 (受理时刻, acceptId) 升序,严格在 `after` 之后,至多 `limit` 条(SQL 腿同一个谓词)。 */
157
- listOpen(after: OpenScanCursor | undefined, limit: number): UserInputAcceptance[];
185
+ /** open 受理的键集分页:按 (受理时刻, acceptId) 升序,严格在 `after` 之后,至多 `limit` 条(SQL 腿同一个谓词);`next` 与 SQL 腿同一条规则
186
+ * (页满 ⇒ 页末行的键,否则缺席)。折叠里没有读不出的行(盘上坏行在重放时就跳过了),所以页末行就是原始末行。 */
187
+ listOpen(after: OpenScanCursor | undefined, limit: number): OpenScanPage;
158
188
  /** 一个会话里收敛过的受理:按受理时刻升序,至多**最新**的 `limit` 条(SQL 腿 `ORDER BY … DESC LIMIT` 后翻转,同义)—— 读面的窗。 */
159
189
  listCrashed(sessionId: string, limit: number): UserInputAcceptance[];
160
190
  /** 一个会话里收敛且**未告知**的受理:按受理时刻升序,从**最旧**起至多 `limit` 条(SQL 腿 `ORDER BY … ASC LIMIT`,同义)—— 通告的待办。
@@ -166,14 +196,25 @@ export declare class UserInputLedgerFold {
166
196
  export declare function assertPageLimit(op: string, limit: number): number;
167
197
  /** 两只孪生共用的排序(受理时刻,同刻按 acceptId)—— SQL 腿的 `ORDER BY accepted_at_ms, accept_id` 与它逐字同义。 */
168
198
  export declare function sortAcceptances(list: UserInputAcceptance[]): UserInputAcceptance[];
169
- /** open 受理的键集分页游标:上一页末行的 (受理时刻, acceptId);下一页从严格之后开始。 */
199
+ /** open 受理的键集分页游标:上一页**原始**末行的 (受理时刻, acceptId);下一页从严格之后开始。 */
170
200
  export interface OpenScanCursor {
171
201
  readonly acceptedAtMs: number;
172
202
  readonly acceptId: string;
173
203
  }
204
+ /**
205
+ * `listOpen` 的一页(S-969 L6):`open` = 这一页里**读得出**的 open 受理(读不出形的行已逐行跳过 + 计 F 类 tag);`next` = 续翻游标 =
206
+ * 这一页**原始**末行的键(被跳过的坏行也算 —— 断点不许卡在坏行上,也不许因为页里少了一行就被当成「扫到底」),缺席 = 原始行不满一页(扫到底)。
207
+ * 游标属于店(只有店见过原始行),收敛扫描只照着它翻,不从交回的行反推。
208
+ */
209
+ export interface OpenScanPage {
210
+ readonly open: readonly UserInputAcceptance[];
211
+ readonly next?: OpenScanCursor;
212
+ }
174
213
  /** 收敛扫描的页长(open = 活腿上的受理 + 等收敛的孤儿,量级是人的手速)。 */
175
214
  export declare const USER_INPUT_OPEN_SCAN_MAX = 1000;
176
- /** 一趟收敛至多翻多少页(预算:十万条 open 行;到顶 ⇒ 断点留下,下一趟从断点续翻、扫到底再回绕 —— codex r3-F2)。 */
215
+ /** 一趟收敛至多翻多少页(预算:十万条 open 行;到顶 ⇒ 断点留下,下一趟从断点续翻、扫到底再回绕 —— codex r3-F2)。
216
+ * 一趟的上界(S-969 L9 成文):至多 `PAGES` 次页读 × `OPEN_SCAN_MAX` 行;run 店读 = 外来写者行里**不同 taskId** 的个数(趟内备忘,本进程写的行不读);
217
+ * 转移写 = 判出 crashed / leg_over 的行数。boot 那一趟在 stage-07 被 await(协调面开服之前收完 —— 时机不变),所以它的墙钟也受这个上界约束。 */
177
218
  export declare const USER_INPUT_OPEN_SCAN_PAGES = 100;
178
219
  /** 一个会话的「收敛过的受理」读口上限(读面 / 通告都经它;有界 wire)。收敛行 = 崩溃丢失,量级远小于受理总数;超出时取最新的这么多条。 */
179
220
  export declare const USER_INPUT_SESSION_READ_MAX = 500;
@@ -186,28 +227,41 @@ export interface UserInputLedgerStore {
186
227
  settle(acceptId: string, how: SettleHow, atMs: number): Promise<boolean>;
187
228
  converge(acceptId: string, atMs: number): Promise<boolean>;
188
229
  announce(acceptId: string, taskId: string, atMs: number): Promise<boolean>;
189
- /** open 受理的一页 —— 收敛扫描的读口。键集分页:按 (受理时刻, acceptId) 升序,严格在 `after` 之后(缺席 = 从头),至多 `limit` 条
190
- * (缺席 = {@link USER_INPUT_OPEN_SCAN_MAX})。收敛逐页翻到底(codex r2-F2:固定最旧一页会让活腿占满窗口时后面的崩溃丢失永远轮不到)。 */
191
- listOpen(after?: OpenScanCursor, limit?: number): Promise<UserInputAcceptance[]>;
230
+ /** open 受理的一页 —— 收敛扫描的读口。键集分页:按 (受理时刻, acceptId) 升序,严格在 `after` 之后(缺席 = 从头),至多 `limit` 条**原始行**
231
+ * (缺席 = {@link USER_INPUT_OPEN_SCAN_MAX});读不出形的行逐行跳过、续翻游标按原始行推进({@link OpenScanPage})。收敛逐页翻到底
232
+ * (codex r2-F2:固定最旧一页会让活腿占满窗口时后面的崩溃丢失永远轮不到)。三只读口对读不出的行同一处置:跳过 + F 类 tag
233
+ * `server.user-input-ledger.corrupt-record-skipped`,不整页抛(S-969 L6)。 */
234
+ listOpen(after?: OpenScanCursor, limit?: number): Promise<OpenScanPage>;
192
235
  /** 一个会话里**收敛过**(崩溃丢失)的受理,含已告知的(按受理时刻升序,至多最新的 {@link USER_INPUT_SESSION_READ_MAX} 条)—— **读面**的读口。
193
236
  * 只读收敛行是刻意的:一个长会话可以有成千上万条正常收场的受理,读口若按「会话的全部受理」截断,新的丢失会被旧的正常行挤出窗外。 */
194
237
  listCrashed(sessionId: string): Promise<UserInputAcceptance[]>;
195
238
  /** 一个会话里收敛且**未告知**的受理,从最旧起至多 `limit` 条(按受理时刻升序)—— **通告**的待办读口([ref] gpt-6 腿 [medium]:
196
239
  * 通告若读读面那只「最新 N 条含已告知」的窗,已告知行占窗 ⇒ 窗外更旧的未告知行永远不被说;待办只看未告知,说过的让位,窗外的最终可达)。 */
197
240
  listUnannounced(sessionId: string, limit: number): Promise<UserInputAcceptance[]>;
241
+ /** S-965 —— **会话树删除**:删掉该会话**每一种状态**的受理,回删除条数(没有可删的 ⇒ 0、零写;幂等)。按 `session_id` 单键:行上没有属主列,
242
+ * 会话所有权由调用方证过(在线删除 = 路由的 DELETE 门;留存 = 候选集本身出自本域加锁读的 `session_meta`),与 anchors / exemptions /
243
+ * attachments 同律。调用方 = 在线删除协调器(`boot/session-faces.ts` `purgeSession` 的一腿);托管留存的事务化孪生不调店、在自己的事务里
244
+ * 发**同一只谓词**(`retention-store-sql.ts` `purgeSessionTrees` ④)。写不进 ⇒ 抛(协调器据此不删会话本体,幂等重试收敛)。 */
245
+ deleteBySession(sessionId: string): Promise<number>;
246
+ }
247
+ /** 收敛判词读的 run 店面(只读一只动词)。 */
248
+ export interface RunStoreReader {
249
+ getRun(taskId: string): Promise<RunRecord | null | undefined>;
198
250
  }
199
251
  /**
200
- * 「受理它的进程已经不在」的证据(闭集,穷举 switch;两臂各对应 run 店的一只死亡判据,见顶注「收敛判据」)。
252
+ * 「受理它的进程已经不在」的证据(S-969 L8 合一):两车道读**同一只** run 店判词(见顶注「收敛判据」),`kind` 只回答车道事实表
253
+ * {@link LANE_FACTS} 里的两问(闭集穷举,判词本身不随车道变):
254
+ * ① 外来写者仍持有的 `running` 行,写者可能还活着吗 —— `exclusive_data_root`(local):不可能(数据根 LOCK:本进程之前的写者都已死);
255
+ * `run_store_verdict`(SQL,多副本):可能(等 reaper 把死亡判决落在行上)。
256
+ * ② run 行不在(`getRun` 答 undefined),这条腿怎么了(7.111.0 [ref] gpt-6 腿 X2)—— local:File 店 `hydrate` 跳过坏 / 缺时间戳的
257
+ * `run.json`(S-582 F 座)也答不在 ⇒ 读不出腿怎么收场、写者又已证死 ⇒ `crashed`(告知;= v7.110.0);SQL:店不藏行(读失败是响的
258
+ * Error,走 read-failed),不在 = 真删了 ⇒ 腿既不可能还活着也不可能续跑、没有可对照的 run ⇒ `leg_over`(结清;= v7.110.0)。S-965 起
259
+ * 删会话 / 留存级联连账本行一起删,SQL 的这一形只剩级联半拍里的竞态与旧副本混版窗删的会话。
201
260
  */
202
- export type WriterGoneEvidence = {
203
- readonly kind: "exclusive_data_root";
204
- } | {
205
- /** SQL 车道:run 店**已经落盘**的死亡判决(reapStale 的落款)—— 见 {@link judgeOpen}。 */
206
- readonly kind: "run_store_verdict";
207
- readonly runs: {
208
- getRun(taskId: string): Promise<RunRecord | null | undefined>;
209
- };
210
- };
261
+ export interface WriterGoneEvidence {
262
+ readonly kind: "exclusive_data_root" | "run_store_verdict";
263
+ readonly runs: RunStoreReader;
264
+ }
211
265
  /** 受理回执(路由持有;`withdraw` 凭它撤)。 */
212
266
  export interface UserInputAcceptReceipt {
213
267
  readonly acceptId: string;
@@ -266,7 +320,7 @@ export declare function buildCrashedSteerNotice(input: {
266
320
  sessionId: string;
267
321
  };
268
322
  /**
269
- * `create*`:账本活对象。`writer` = 本进程 `instanceId`;`evidence` 由装配层按车道给(local ⇒ `exclusive_data_root`,SQL ⇒ `run_store_verdict`)。
323
+ * `create*`:账本活对象。`writer` = 本进程 `instanceId`;`evidence` 由装配层按车道给(run 店 + 车道词;local ⇒ `exclusive_data_root`,SQL ⇒ `run_store_verdict`)。
270
324
  * 腿 → 受理的追踪是**进程内**的(`WeakMap` 以活 `TaskStream` 为键):收场 settle 只需要本进程自己受理过的那些,崩溃时它随进程消失 —— 那正是收敛要接的那一半。
271
325
  */
272
326
  export declare function createUserInputLedger(opts: {
@@ -286,11 +340,10 @@ export declare function createUserInputLedgerAtBoot(opts: Parameters<typeof crea
286
340
  converged: number;
287
341
  }>;
288
342
  /**
289
- * 装配层按**车道**给证据(闭集穷举:`StoreBackend.kind` 加一个词而这里没跟 ⇒ 编译红)。
290
- * · `local` —— 数据根 LOCK 保证单进程 ⇒ `exclusive_data_root`;
291
- * · `mysql` / `pg` —— 多副本共享一张表,「我启动之前写的」证明不了写者已死 ⇒ `run_store_verdict`(run 店已落盘的 reap 判决)。
343
+ * 装配层按**车道**给证据(闭集穷举:`StoreBackend.kind` 加一个词而这里没跟 ⇒ 编译红)。两臂都带 run 店(S-969 L8:判词同一只),车道词只答
344
+ * 「外来写者仍持有的 running 行」那一问:
345
+ * · `local` —— 数据根 LOCK 保证单进程 ⇒ `exclusive_data_root`(外来写者都已死);
346
+ * · `mysql` / `pg` —— 多副本共享一张表,「我启动之前写的」证明不了写者已死 ⇒ `run_store_verdict`(等 reaper 落款)。
292
347
  */
293
- export declare function writerGoneEvidenceFor(backendKind: "mysql" | "pg" | "local", runs: {
294
- getRun(taskId: string): Promise<RunRecord | null | undefined>;
295
- }): WriterGoneEvidence;
348
+ export declare function writerGoneEvidenceFor(backendKind: "mysql" | "pg" | "local", runs: RunStoreReader): WriterGoneEvidence;
296
349
  //# sourceMappingURL=user-input-ledger.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { USER_STEER_UNDRAINED_CAUSES } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "./observability/fail-open.js";
3
- import { RUN_STALLED_ERROR } from "./plugins/store-contracts.js";
3
+ import { isHolderDeathVerdict } from "./plugins/store-contracts.js";
4
4
  import { defaultEngineNoticeRouter } from "./trace/engine-notice-wire.js";
5
5
  export const USER_INPUT_LEDGER_DIR = "user-input-ledger";
6
6
  export const USER_INPUT_KIND_WORDS = { steer: true };
@@ -21,16 +21,24 @@ export class UserInputLedgerFold {
21
21
  get(acceptId) {
22
22
  return this.byId.get(acceptId);
23
23
  }
24
+ sessionSize(sessionId) {
25
+ return this.bySession.get(sessionId)?.length ?? 0;
26
+ }
24
27
  admits(rec) {
25
- const cur = this.byId.get(rec.acceptId);
26
28
  switch (rec.kind) {
27
29
  case "accepted":
28
- return cur === undefined;
30
+ return !this.byId.has(rec.acceptId);
29
31
  case "settled":
30
- case "converged":
32
+ case "converged": {
33
+ const cur = this.byId.get(rec.acceptId);
31
34
  return cur !== undefined && cur.state.kind === "open";
32
- case "announced":
35
+ }
36
+ case "announced": {
37
+ const cur = this.byId.get(rec.acceptId);
33
38
  return cur !== undefined && cur.state.kind === "converged" && cur.announced === undefined;
39
+ }
40
+ case "erased":
41
+ return this.sessionSize(rec.sessionId) > 0;
34
42
  default: {
35
43
  const unreachable = rec;
36
44
  return unreachable;
@@ -74,6 +82,12 @@ export class UserInputLedgerFold {
74
82
  this.byId.set(rec.acceptId, { ...cur, announced: { atMs: rec.at, taskId: rec.taskId } });
75
83
  return true;
76
84
  }
85
+ case "erased": {
86
+ for (const acceptId of this.bySession.get(rec.sessionId))
87
+ this.byId.delete(acceptId);
88
+ this.bySession.delete(rec.sessionId);
89
+ return true;
90
+ }
77
91
  default: {
78
92
  const unreachable = rec;
79
93
  return unreachable;
@@ -84,7 +98,9 @@ export class UserInputLedgerFold {
84
98
  assertPageLimit("listOpen", limit);
85
99
  const open = sortAcceptances([...this.byId.values()].filter((a) => a.state.kind === "open"));
86
100
  const rest = after === undefined ? open : open.filter((a) => a.acceptedAtMs > after.acceptedAtMs || (a.acceptedAtMs === after.acceptedAtMs && a.acceptId > after.acceptId));
87
- return rest.slice(0, limit);
101
+ const page = rest.slice(0, limit);
102
+ const last = page.length === limit ? page[page.length - 1] : undefined;
103
+ return last === undefined ? { open: page } : { open: page, next: { acceptedAtMs: last.acceptedAtMs, acceptId: last.acceptId } };
88
104
  }
89
105
  listCrashed(sessionId, limit) {
90
106
  const ids = this.bySession.get(sessionId) ?? [];
@@ -112,33 +128,30 @@ const USER_STEER_UNDRAINED = "task.user_steer_undrained";
112
128
  export function buildCrashedSteerNotice(input) {
113
129
  return {
114
130
  code: USER_STEER_UNDRAINED,
115
- message: `${input.steer} user steer(s) accepted by the server for run ${input.taskId} (steer 200 applied / interrupt 202 queued-immediate) may not have reached the model — the server process running that run ended before the run settled, and whether the run consumed them before it ended is not known (the engine reports consumption only at leg end, not per steer). They are NOT redelivered; check that run's transcript and results before re-sending any of them against a live run.`,
131
+ message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.steer} user steer(s) for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the run settled; whether the run consumed them is not known. They are NOT redelivered.`,
116
132
  detail: { steer: input.steer, cause: CRASHED_CAUSE, taskId: input.taskId, sessionId: input.sessionId },
117
133
  sessionId: input.sessionId,
118
134
  };
119
135
  }
120
136
  const errText = (e) => (e instanceof Error ? e.message : String(e));
121
- async function judgeOpen(a, me, liveHere, evidence) {
137
+ const LANE_FACTS = {
138
+ exclusive_data_root: { foreignHolderMayBeAlive: false, absentRunRow: "crashed" },
139
+ run_store_verdict: { foreignHolderMayBeAlive: true, absentRunRow: "leg_over" },
140
+ };
141
+ async function judgeOpen(a, me, liveHere, kind, getRun) {
122
142
  if (a.writer === me)
123
143
  return liveHere.has(a.acceptId) ? "live" : "leg_over";
124
- switch (evidence.kind) {
125
- case "exclusive_data_root":
144
+ const lane = LANE_FACTS[kind];
145
+ const run = await getRun(a.taskId);
146
+ if (!run)
147
+ return lane.absentRunRow;
148
+ if (run.instanceId === a.writer) {
149
+ if (run.status === "running")
150
+ return lane.foreignHolderMayBeAlive ? "live" : "crashed";
151
+ if (isHolderDeathVerdict(run))
126
152
  return "crashed";
127
- case "run_store_verdict": {
128
- const run = await evidence.runs.getRun(a.taskId);
129
- if (run && run.instanceId === a.writer) {
130
- if (run.status === "running")
131
- return "live";
132
- if (run.status === "failed" && run.error === RUN_STALLED_ERROR)
133
- return "crashed";
134
- }
135
- return "leg_over";
136
- }
137
- default: {
138
- const unreachable = evidence;
139
- return unreachable;
140
- }
141
153
  }
154
+ return "leg_over";
142
155
  }
143
156
  export function createUserInputLedger(opts) {
144
157
  const { store, writer, evidence } = opts;
@@ -157,32 +170,36 @@ export function createUserInputLedger(opts) {
157
170
  let scanCursor;
158
171
  let inFlight;
159
172
  const convergePass = async () => {
173
+ const reads = new Map();
174
+ const getRun = (taskId) => {
175
+ let read = reads.get(taskId);
176
+ if (read === undefined)
177
+ reads.set(taskId, (read = evidence.runs.getRun(taskId)));
178
+ return read;
179
+ };
160
180
  let n = 0;
161
181
  for (let page = 0; page < maxPages; page++) {
162
- let open;
182
+ let scanned;
163
183
  try {
164
- open = await store.listOpen(scanCursor, pageSize);
184
+ scanned = await store.listOpen(scanCursor, pageSize);
165
185
  }
166
186
  catch (e) {
167
187
  recordFailOpen("server.user-input-ledger.read-failed", `op=converge ${errText(e)}`);
168
188
  return n;
169
189
  }
170
- n += await convergePage(open);
171
- if (open.length < pageSize) {
172
- scanCursor = undefined;
190
+ n += await convergePage(scanned.open, getRun);
191
+ scanCursor = scanned.next;
192
+ if (scanCursor === undefined)
173
193
  return n;
174
- }
175
- const last = open[open.length - 1];
176
- scanCursor = { acceptedAtMs: last.acceptedAtMs, acceptId: last.acceptId };
177
194
  }
178
195
  return n;
179
196
  };
180
- const convergePage = async (open) => {
197
+ const convergePage = async (open, getRun) => {
181
198
  let n = 0;
182
199
  for (const a of open) {
183
200
  let verdict;
184
201
  try {
185
- verdict = await judgeOpen(a, writer, liveHere, evidence);
202
+ verdict = await judgeOpen(a, writer, liveHere, evidence.kind, getRun);
186
203
  }
187
204
  catch (e) {
188
205
  recordFailOpen("server.user-input-ledger.read-failed", `op=judge ${errText(e)}`);
@@ -324,7 +341,7 @@ export async function createUserInputLedgerAtBoot(opts) {
324
341
  export function writerGoneEvidenceFor(backendKind, runs) {
325
342
  switch (backendKind) {
326
343
  case "local":
327
- return { kind: "exclusive_data_root" };
344
+ return { kind: "exclusive_data_root", runs };
328
345
  case "mysql":
329
346
  case "pg":
330
347
  return { kind: "run_store_verdict", runs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.110.0",
3
+ "version": "7.111.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",
@@ -61,7 +61,7 @@
61
61
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
62
62
  },
63
63
  "dependencies": {
64
- "@sema-agent/core": "7.38.0",
64
+ "@sema-agent/core": "7.38.2",
65
65
  "@sema-agent/settings-schema": "6.4.0",
66
66
  "e2b": "^2.28.0",
67
67
  "libsodium-wrappers": "^0.8.4",