@sema-agent/server 7.55.0 → 7.56.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 (46) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +12 -6
  3. package/README.zh-CN.md +9 -5
  4. package/USAGE.md +108 -52
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-content-kind.d.ts +22 -0
  7. package/dist/approval-content-kind.js +5 -0
  8. package/dist/boot/leader.d.ts +21 -0
  9. package/dist/boot/leader.js +6 -0
  10. package/dist/boot/runtime-caps.d.ts +2 -1
  11. package/dist/boot/runtime-caps.js +3 -1
  12. package/dist/config-center/types.d.ts +2 -1
  13. package/dist/config-provider.js +1 -0
  14. package/dist/http/route-ctx.d.ts +15 -3
  15. package/dist/http/routes/approvals-assistant.js +2 -2
  16. package/dist/http/routes/leader.js +2 -2
  17. package/dist/http/routes/workflows.js +1 -1
  18. package/dist/http/server.js +17 -3
  19. package/dist/leader/endpoint.js +5 -4
  20. package/dist/leader/wire.d.ts +50 -0
  21. package/dist/leader/wire.js +19 -7
  22. package/dist/main.js +1 -1
  23. package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
  24. package/dist/plugins/checkpoint-store-sql.js +4 -1
  25. package/dist/plugins/file-run-store.js +2 -0
  26. package/dist/plugins/local-checkpoint-store.js +2 -0
  27. package/dist/plugins/local-session-store.d.ts +9 -9
  28. package/dist/plugins/local-session-store.js +5 -3
  29. package/dist/plugins/memory-run-store.js +2 -0
  30. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  31. package/dist/plugins/permission-rule-store-file.js +17 -8
  32. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  33. package/dist/plugins/permission-rule-store-sql.js +23 -13
  34. package/dist/plugins/pg-session-storage.js +17 -13
  35. package/dist/plugins/run-store-sql.js +6 -4
  36. package/dist/plugins/store-contracts.d.ts +4 -0
  37. package/dist/plugins/tidb-session-store.js +18 -14
  38. package/dist/rules-consent.d.ts +5 -4
  39. package/dist/rules-consent.js +5 -33
  40. package/dist/runtime-caps-resolver.js +3 -1
  41. package/dist/security.d.ts +7 -0
  42. package/dist/tool-approval.d.ts +12 -60
  43. package/dist/tool-approval.js +6 -61
  44. package/package.json +2 -2
  45. package/skills/find-skills.md +1 -1
  46. package/skills/loop.md +1 -1
@@ -230,27 +230,6 @@ export function createRuleConsentLane(stores, opts) {
230
230
  const monotonic = opts?.monotonic ?? (() => performance.now());
231
231
  const sleep = opts?.sleep ?? (async (ms) => new Promise((resolve) => setTimeout(resolve, ms)));
232
232
  const localImportLogger = createLogger();
233
- const anchors = new Map();
234
- const ANCHOR_CAP = 4096;
235
- const pruneAnchors = (nowMono) => {
236
- for (const [id, a] of anchors) {
237
- if (nowMono - a.mintedAtMono > ticketTtlMs)
238
- anchors.delete(id);
239
- }
240
- if (anchors.size <= ANCHOR_CAP)
241
- return;
242
- const excess = anchors.size - ANCHOR_CAP;
243
- let dropped = 0;
244
- for (const id of anchors.keys()) {
245
- if (dropped++ >= excess)
246
- break;
247
- anchors.delete(id);
248
- }
249
- };
250
- const decisionClock = (ticketId) => {
251
- const a = anchors.get(ticketId);
252
- return a === undefined ? undefined : { decisionNowMs: Math.ceil(a.mintedAtWall + (monotonic() - a.mintedAtMono)) };
253
- };
254
233
  return {
255
234
  async persistCardRule(input) {
256
235
  let prepared;
@@ -360,14 +339,10 @@ export function createRuleConsentLane(stores, opts) {
360
339
  }
361
340
  return { ok: false, reason: "too-many-candidates", candidates: preview.candidates.length };
362
341
  }
363
- const mintedAtMono = monotonic();
364
342
  const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "cc-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
365
- pruneAnchors(mintedAtMono);
366
- anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
367
343
  return { ok: true, preview, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
368
344
  },
369
345
  async redeemImport(principal, ticket) {
370
- const clock = decisionClock(ticket);
371
346
  const consumeOutcomeAfterThrow = async (err) => {
372
347
  localImportLogger.warn("cc_import_claim_write_failed", {
373
348
  err: err instanceof Error ? err.message : String(err),
@@ -398,7 +373,7 @@ export function createRuleConsentLane(stores, opts) {
398
373
  };
399
374
  let consumed;
400
375
  try {
401
- consumed = await stores.tickets.consume(ticket, principal, "cc-import", clock);
376
+ consumed = await stores.tickets.consume(ticket, principal, "cc-import");
402
377
  }
403
378
  catch (err) {
404
379
  return await consumeOutcomeAfterThrow(err);
@@ -407,7 +382,7 @@ export function createRuleConsentLane(stores, opts) {
407
382
  return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
408
383
  const releaseClaim = async () => {
409
384
  try {
410
- return await stores.tickets.release(ticket, principal, consumed.claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000, decisionClock(ticket));
385
+ return await stores.tickets.release(ticket, principal, consumed.claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
411
386
  }
412
387
  catch (err) {
413
388
  recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
@@ -496,10 +471,7 @@ export function createRuleConsentLane(stores, opts) {
496
471
  }
497
472
  if (approvalId === undefined)
498
473
  return { ok: true, preview: { members } };
499
- const mintedAtMono = monotonic();
500
474
  const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "local-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
501
- pruneAnchors(mintedAtMono);
502
- anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
503
475
  return { ok: true, preview: { members }, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
504
476
  },
505
477
  async redeemLocalImport(principal, ticket) {
@@ -530,7 +502,7 @@ export function createRuleConsentLane(stores, opts) {
530
502
  const driveAndSettle = async (approvalId, payloadHash, claimId) => {
531
503
  const tryRelease = async () => {
532
504
  try {
533
- return await stores.tickets.release(ticket, principal, claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000, decisionClock(ticket));
505
+ return await stores.tickets.release(ticket, principal, claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000);
534
506
  }
535
507
  catch (err) {
536
508
  recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
@@ -638,7 +610,7 @@ export function createRuleConsentLane(stores, opts) {
638
610
  };
639
611
  let consumed;
640
612
  try {
641
- consumed = await stores.tickets.consume(ticket, principal, "local-import", decisionClock(ticket));
613
+ consumed = await stores.tickets.consume(ticket, principal, "local-import");
642
614
  }
643
615
  catch (err) {
644
616
  return await claimOutcomeAfterThrow(err, "consume");
@@ -660,7 +632,7 @@ export function createRuleConsentLane(stores, opts) {
660
632
  case "minted": {
661
633
  let again;
662
634
  try {
663
- again = await stores.tickets.consume(ticket, principal, "local-import", decisionClock(ticket));
635
+ again = await stores.tickets.consume(ticket, principal, "local-import");
664
636
  }
665
637
  catch (err) {
666
638
  return await claimOutcomeAfterThrow(err, "consume");
@@ -31,7 +31,7 @@ export function toCoreRuntimeCaps(ent) {
31
31
  return undefined;
32
32
  const autoMode = ent.autoMode;
33
33
  const badCap = (v) => v !== undefined && typeof v !== "boolean";
34
- if (badCap(ent.allowWorkflows) || badCap(ent.forceDurableGate) || badCap(ent.allowFork) || badCap(ent.allowObservers) || badCap(autoMode)) {
34
+ if (badCap(ent.allowWorkflows) || badCap(ent.forceDurableGate) || badCap(ent.allowFork) || badCap(ent.allowObservers) || badCap(autoMode) || badCap(ent.allowMemoryOptOut)) {
35
35
  throw new Error("runtimeCaps from center carries a non-boolean cap (contract breach — fail closed)");
36
36
  }
37
37
  const caps = {};
@@ -45,6 +45,8 @@ export function toCoreRuntimeCaps(ent) {
45
45
  caps.allowObservers = ent.allowObservers;
46
46
  if (typeof autoMode === "boolean")
47
47
  caps.autoMode = autoMode;
48
+ if (typeof ent.allowMemoryOptOut === "boolean")
49
+ caps.allowMemoryOptOut = ent.allowMemoryOptOut;
48
50
  return Object.keys(caps).length ? caps : undefined;
49
51
  }
50
52
  export function centerEntitlementSourceWired(config) {
@@ -15,6 +15,13 @@ export interface SessionListItem {
15
15
  lastStatus: string;
16
16
  /** K-5c (shell §K): the latest run's task_id — the shell's one-hop live-tail re-attach anchor on resume. */
17
17
  lastRunId: string | null;
18
+ /** [ref]([ref]/[ref]):the LATEST run's **engine run id**(`TaskResult.runId`)when known —— OMIT-when-absent,
19
+ * 恒不铸 null。runs-ledger listers 从 latest run 的终局 `result` JSON 提取(⇒ 在跑 latest 键诚实缺席);
20
+ * local lister 走 `noteTaskRun` 席实时记录(⇒ 在跑也在场)——两面缺席集合不完全相等,读法一致:
21
+ * 缺席 = 「此行无已知 engine runId」,禁读成其它含义。⚠️ 与 `lastRunId`(= latest run 的 **task_id**)
22
+ * 是两条身份轴,命名对齐 core `SessionStoreSummary.lastTaskRunId`。wire:additive 键零投影透传
23
+ * (sessions-list.ts);sema-sdk spec `SessionSummary` 闭集候 SDK 半场开键([ref]/.82 班车)。 */
24
+ lastTaskRunId?: string;
18
25
  /** Auto-generated session title (null = untitled / lister face has no session_meta). */
19
26
  title: string | null;
20
27
  }
@@ -22,10 +22,10 @@ export declare const APPROVAL_GATE_KINDS_SQL_IN: string;
22
22
  * 因此续跑」。反面(真被消费)不铸键 —— additive 只记真,与 `updatedInputForwarded`/`decisionNote`
23
23
  * 的先例逐字同族。**缺席禁读作「有消费者」的证明**:老版本 server 从不发这个键。
24
24
  *
25
- * 成因三形([ref] 复审件3 补第三形):①持有悬挂 promise 的是**另一个副本**;②本副本重启后本地窗已空;
26
- * ③**弃单墓碑守卫把素批准翻成 park**({@link ToolApprovalCoordinator.guardRowDerivedApprove}:本地腿
27
- * 在场,但没有一条把这次 approve 当**放行**消费 —— 它们全部收成 park,人须经 durable 门再兑)。
28
- * 三形的处置同一句:壳读 run 面判续跑没有,绝不把 200 无键当成「run 已续跑」的证明。
25
+ * 成因两形([ref] 起;[ref] 复审件3 曾补的第三形 = 弃单墓碑守卫翻 park,随守卫整删退役。进程内仍可
26
+ * 短暂出现「腿在场而零交付」:编辑在飞让路 —— 标记放开后的下一拍按行兑现):①持有悬挂 promise 的是
27
+ * **另一个副本**;②本副本重启后本地窗已空。
28
+ * 两形的处置同一句:壳读 run 面判续跑没有,绝不把 200 无键当成「run 已续跑」的证明。
29
29
  *
30
30
  * 为什么不是 409/410(施工时的二选一,裁定写在这里):409/410 说的是「你的请求没有生效」,而这次决议
31
31
  * **确实生效了**(行是 DECIDED,幂等回放读得到、审计面记得住)——把它渲成错误会让壳把一次成功的
@@ -323,6 +323,10 @@ export interface LivePendingRow {
323
323
  * 子代 ask)上恒等成立。缺席 = 顶层 ask。`fromSubagent` 保留不动(byte-compat:先于本键的消费方仍按
324
324
  * 它判别)。 */
325
325
  originTaskId?: string;
326
+ /** [ref]([ref]②):内容问句分型 —— `"content_ask"` 当 `toolName` 为 AskUserQuestion(词属主
327
+ * `approval-content-kind.ts`,与 durable 两读面 / core summarize 面同词,OMIT 契约同族)。
328
+ * 缺席 = 普通工具 ask。展示/分诊分型用,永不参与决议路由。 */
329
+ contentKind?: "content_ask";
326
330
  }
327
331
  export interface ToolApprovalRunContext {
328
332
  taskId: string;
@@ -670,10 +674,6 @@ export declare class ToolApprovalCoordinator {
670
674
  /** [ref] + codex R3-[critical]:askId → **此刻正带着 `updatedInput` 决它的请求数**(见
671
675
  * {@link markEditedDecisionInFlight} 顶注)。只在 `decideAsk` 那一次调用期间非空,故天然有界。 */
672
676
  private readonly editedDecisionsInFlight;
673
- /** [ref] + codex R6-[critical]:askId → **弃单墓碑**到期时刻。看门狗放弃一枚在飞编辑标记时落一条,
674
- * 此后行派生的 approve 对这只 ask 改走 park 路由(理由见 {@link notifyExternalDecision} 里那段)。
675
- * 有界两道:条目自带 TTL(惰性过期)+ 表大小 FIFO 上限,同 park 墓碑表的纪律。 */
676
- private readonly abandonedEditedDecisions;
677
677
  /** [ref] 车2(D5 一次性 warn 节流):同实例只报第一次,后续只计数(避免 store 抖动期间刷屏)。 */
678
678
  private storeErrorWarned;
679
679
  private storeErrorTally;
@@ -911,8 +911,7 @@ export declare class ToolApprovalCoordinator {
911
911
  * 争终局与读真相并成店的一次原子 claim,分类器只剩纯函数半场。[ref](行派生 approve 丢编辑)已随
912
912
  * [ref] 落列收口:`approved` 臂带行上的 `updatedInput`。
913
913
  *
914
- * 行派生 approve 的两道守卫**在这里**施加(五处消费一份判据,见 {@link guardRowDerivedApprove}):
915
- * · 弃单墓碑在场 ⇒ `parked`(park 路由,可逆可再批 —— 绝不把一个证不出载荷的放行交出去);
914
+ * 行派生 approve 的守卫只剩一道**在这里**施加([ref]:弃单墓碑守卫随 [ref] 落列整删,见文件头 🪦 注):
916
915
  * · 编辑在飞 ⇒ `edit-in-flight`(codex R4-[critical] 同族口:载荷还在别人手里的那一拍按行结算 = 用裸
917
916
  * `true` 抢在它前面;持有者随后必定自己结算)。
918
917
  */
@@ -1224,15 +1223,6 @@ export declare class ToolApprovalCoordinator {
1224
1223
  backlog: number;
1225
1224
  truncated: boolean;
1226
1225
  }>;
1227
- /**
1228
- * [ref](案A)+ codex [critical] 的**前置门读口**:本副本对这条持久 askId 还有几条**未结算**的活体登记。
1229
- *
1230
- * 唯一消费者 = durable 回决口(`routes/runs.ts`)在 CAS **之前**判「这次带来的 `updatedInput` 交得出去
1231
- * 吗」—— 交不出去就响亮拒,绝不静默把编辑降级成不带编辑的批准(判据与理由全文在那个调用点)。
1232
- * 返回 0 的两种成因(**刻意不分**):等待的 promise 在**别的副本**,或那条 run 已经死了(重启后本地窗空)。
1233
- * 两者的处置相同(这次编辑在这里都落不了地),而分辨它们需要读 run 面 —— 那不是本口能回答的问题。
1234
- */
1235
- liveConsumerCount(askId: string): number;
1236
1226
  /**
1237
1227
  * 🔴 [ref] + codex R3-[critical](真缺陷,红先复现器在 `test/case-a-reachability-queue.test.ts` 同名格)
1238
1228
  * —— **带编辑的决议在飞期间,观察者不结算**。
@@ -1260,48 +1250,10 @@ export declare class ToolApprovalCoordinator {
1260
1250
  editedDecisionInFlightCount(askId: string): number;
1261
1251
  /** {@link markEditedDecisionInFlight} 的读口:此刻有没有**别的**请求正带着编辑决这只 ask。 */
1262
1252
  editedDecisionInFlight(askId: string): boolean;
1263
- /**
1264
- * 弃单墓碑的读口(**惰性过期**:读到过期即删,不另起定时器 —— park 墓碑表的姿势)。
1265
- * 在场 = 这只 ask 曾有一次带编辑的决议在飞而看门狗放弃了它 ⇒ **行派生**的 approve 一律改走 park 路由。
1266
- *
1267
- * 🔴 **过期/逐出必须 fail-closed**(codex R7-[high],验真后采纳):首版是「到点就删」——而它保护的是
1268
- * 一条**仍然挂着**的本地等待腿。店退化到十分钟以上、或并发弃单超过表帽时,墓碑先消失、腿还在,下一次
1269
- * 行派生 approve 照样把原始命令放出去,正好复活它要防的那件事。判据改成:**丢墓碑之前先把它保护的
1270
- * 消费者收掉**(park 路由,可逆可再批)。收完之后再删就是安全的 —— 没有腿可被误放行了。
1271
- */
1272
- private abandonedEditedDecisionPending;
1273
- /**
1274
- * 🔴 **行派生 approve 的统一守卫**(codex R7-[critical],验真后修)。
1275
- *
1276
- * R6 把弃单墓碑的兑现点收在 {@link notifyExternalDecision} 上,并自称「公共咽喉」—— 那句话**不成立**:
1277
- * 树上还有三处**直接** `settle(true, …)` / 直接把 `true` 交回 core 的行派生回放(`convergeFromDurableState`
1278
- * 的收敛臂、`ensureAsk` 幂等命中终局行的回放、窗到期报错臂的 `readDecidedForRecovery` —— [ref] 起前者与
1279
- * 后者合并成 `readRowForRecovery` 一个读口、三臂共用;[ref] 起读口撤销,同一份判据长在 claim 败方读数的
1280
- * 分类器 `classifyClaimLoss` 里),它们跳过那个口。于是弃单之后从这三条路上仍能放出原始命令。
1281
- *
1282
- * 收口形 = **一个判据函数、五处消费**(上面三处 + 轮询 + HTTP 观察者):把「行说 approve」这件事
1283
- * 在弃单墓碑在场时翻译成 `"unavailable"`(park 路由,可逆可再批),其余取值逐字透传。
1284
- * 判据只认**不带编辑的行派生**放行(裸 `true`)—— 人的当次决议带着 `updatedInput` 走的是另一条路,不经
1285
- * 本函数;[ref] 之后行上带编辑的 approve 回放出来是对象臂 `{allow:true, updatedInput}`,载荷**在行上**,
1286
- * 墓碑要护的「证不出载荷」这件事对它不成立,同样原样透传。
1287
- * ⚠️ 既有边界如实记([ref] 随批复核):列在之后,一只 ask 的决议**要么**带编辑落列(回放恒对象臂,本
1288
- * 守卫永不命中)**要么**从没带过编辑(从没设过在飞标记,永无墓碑)⇒ 本守卫在新行上结构上不再可达,
1289
- * 留着只为老进程/老行的并存窗与既有判据格;拆除是独立件([ref] 登记),不搭本批。
1290
- */
1291
- private guardRowDerivedApprove;
1292
- /** 丢一枚弃单墓碑的**唯一**口(过期 / 容量逐出共用):先把它还在保护的本地消费者按 park 路由收掉,
1293
- * 再删表项 —— 顺序是硬的(先删表再收人 = 中间那一瞬正是可被误放行的窗口)。 */
1294
- private dropAbandonedEditTombstone;
1295
- /** 判据钩子:把「看门狗那一拍」显式地跑一次(留弃单墓碑 + 让在飞标记归零)。
1296
- * 存在理由 = 那条臂的真实触发要等 {@link EDITED_DECISION_MARK_CAP_MS} 的墙钟,而本仓禁固定睡眠钉
1297
- * (fixed-sleep 棘轮);把**语义**开一个口比让判据去睡 10 秒诚实得多。生产路径不调它。 */
1253
+ /** 判据钩子:把「看门狗那一拍」显式地跑一次(= 在飞标记被放弃归零;[ref] 起看门狗不再铸弃单墓碑,
1254
+ * 本钩子随之只剩这一半)。存在理由 = 那条臂的真实触发要等 {@link EDITED_DECISION_MARK_CAP_MS}
1255
+ * 墙钟,而本仓禁固定睡眠钉(fixed-sleep 棘轮)。生产路径不调它。 */
1298
1256
  expireEditedDecisionMarkForTest(askId: string): void;
1299
- /** 判据钩子:把一枚弃单墓碑**拨到已过期**并当场走它的丢弃路(= 真实 TTL 到点那一拍的语义)。
1300
- * 同上理由:真触发要等 {@link ABANDONED_EDIT_TOMBSTONE_TTL_MS} 的墙钟。生产路径不调它。 */
1301
- forceAbandonedEditTombstoneExpiryForTest(askId: string): void;
1302
- /** 判据钩子:把一枚弃单墓碑的到期时刻**改写**成给定值(不触发惰性过期口本身)。用来构造
1303
- * 「墓碑已过期、但还没有任何人来读过它」这一拍 —— 那正是「触发过期的那次调用还护不护」的判据点。 */
1304
- setAbandonedEditTombstoneDeadlineForTest(askId: string, atMs: number): void;
1305
1257
  /** Test/observability hooks. */
1306
1258
  pendingCount(): number;
1307
1259
  /** [ref]:当前挂着的**悬挂**条目数(零投递集合铸的那一类)——运维/判据读它证「悬挂队列有多长」。 */
@@ -1,3 +1,4 @@
1
+ import { contentKindOf } from "./approval-content-kind.js";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
3
4
  import { precheckCardRuleText } from "./rules-consent.js";
@@ -22,8 +23,6 @@ const DEFAULT_UNREACHED_ASK_TTL_MS = 60 * 60_000;
22
23
  const SUSPENDED_POLL_MAX_PER_TICK = 64;
23
24
  const SUSPENDED_POLL_BUDGET_MS = 5_000;
24
25
  const EDITED_DECISION_MARK_CAP_MS = 10_000;
25
- const ABANDONED_EDIT_TOMBSTONE_TTL_MS = 10 * 60_000;
26
- const ABANDONED_EDIT_TOMBSTONE_MAX = 256;
27
26
  const DEFAULT_WINDOW_MARGIN_MS = 10_000;
28
27
  const MAX_ALLOW_SESSIONS = 4096;
29
28
  const DEFAULT_ADMIT_MAX_PER_TASK = 32;
@@ -269,7 +268,6 @@ export class ToolApprovalCoordinator {
269
268
  admitByOwner = new Map();
270
269
  suspendedPollOffset = 0;
271
270
  editedDecisionsInFlight = new Map();
272
- abandonedEditedDecisions = new Map();
273
271
  storeErrorWarned = false;
274
272
  storeErrorTally = 0;
275
273
  governanceAskMarks;
@@ -504,10 +502,10 @@ export class ToolApprovalCoordinator {
504
502
  return { kind: "none" };
505
503
  return { kind: "row", current: projectAskTerminalClaimTruth(row) };
506
504
  }
507
- classifyClaimLoss(askId, current, where) {
505
+ classifyClaimLoss(askId, current, _where) {
508
506
  if (current.state === "absent")
509
507
  return { kind: "absent" };
510
- const replayed = settleArgsOf(this.guardRowDerivedApprove(askId, replayTerminalAskRow(current), where));
508
+ const replayed = settleArgsOf(replayTerminalAskRow(current));
511
509
  if (replayed === "unavailable")
512
510
  return { kind: "parked" };
513
511
  if (!replayed.allowed)
@@ -608,10 +606,6 @@ export class ToolApprovalCoordinator {
608
606
  const entries = this.pendingByAskId.get(askId);
609
607
  if (!entries)
610
608
  return { settled: 0 };
611
- if (allowed && updatedInput === undefined) {
612
- if (this.guardRowDerivedApprove(askId, true, "notify-external-decision") === "unavailable")
613
- return { settled: 0 };
614
- }
615
609
  let settled = 0;
616
610
  for (const entry of [...entries]) {
617
611
  entry.settle(allowed, allowed ? "allowed" : "denied", updatedInput);
@@ -820,7 +814,7 @@ export class ToolApprovalCoordinator {
820
814
  });
821
815
  if (existingOrCreated.row.state !== "STREAM_PENDING") {
822
816
  releaseAdmission();
823
- const replayed = this.guardRowDerivedApprove(askId, replayTerminalAskRow(existingOrCreated.row), "ensure-ask-idempotent-replay");
817
+ const replayed = replayTerminalAskRow(existingOrCreated.row);
824
818
  return replayed === "unavailable" ? this.unattendedOutcome() : replayed;
825
819
  }
826
820
  const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.row.cardJson);
@@ -1859,6 +1853,7 @@ export class ToolApprovalCoordinator {
1859
1853
  for (const [id, e] of this.pending) {
1860
1854
  if (scope !== undefined && e.owner !== scope)
1861
1855
  continue;
1856
+ const contentKind = contentKindOf(e.toolName);
1862
1857
  rows.push({
1863
1858
  approvalId: id,
1864
1859
  toolName: e.toolName,
@@ -1868,6 +1863,7 @@ export class ToolApprovalCoordinator {
1868
1863
  ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
1869
1864
  ...(e.governanceForced ? { governanceForced: true } : {}),
1870
1865
  ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
1866
+ ...(contentKind !== undefined ? { contentKind } : {}),
1871
1867
  });
1872
1868
  }
1873
1869
  return rows;
@@ -1932,13 +1928,6 @@ export class ToolApprovalCoordinator {
1932
1928
  }
1933
1929
  return { polled, settled, backlog, truncated };
1934
1930
  }
1935
- liveConsumerCount(askId) {
1936
- let n = 0;
1937
- for (const e of this.pendingByAskId.get(askId) ?? [])
1938
- if (e.settledOutcome === undefined)
1939
- n++;
1940
- return n;
1941
- }
1942
1931
  markEditedDecisionInFlight(askId) {
1943
1932
  this.editedDecisionsInFlight.set(askId, (this.editedDecisionsInFlight.get(askId) ?? 0) + 1);
1944
1933
  let released = false;
@@ -1957,12 +1946,6 @@ export class ToolApprovalCoordinator {
1957
1946
  if (released)
1958
1947
  return;
1959
1948
  defaultLogger.warn("approval_edited_decision_mark_expired", { askId, capMs: EDITED_DECISION_MARK_CAP_MS });
1960
- this.abandonedEditedDecisions.set(askId, Date.now() + ABANDONED_EDIT_TOMBSTONE_TTL_MS);
1961
- if (this.abandonedEditedDecisions.size > ABANDONED_EDIT_TOMBSTONE_MAX) {
1962
- const oldest = this.abandonedEditedDecisions.keys().next();
1963
- if (!oldest.done)
1964
- this.dropAbandonedEditTombstone(oldest.value, "capacity");
1965
- }
1966
1949
  drop();
1967
1950
  }, EDITED_DECISION_MARK_CAP_MS);
1968
1951
  watchdog.unref?.();
@@ -1974,47 +1957,9 @@ export class ToolApprovalCoordinator {
1974
1957
  editedDecisionInFlight(askId) {
1975
1958
  return (this.editedDecisionsInFlight.get(askId) ?? 0) > 0;
1976
1959
  }
1977
- abandonedEditedDecisionPending(askId) {
1978
- const until = this.abandonedEditedDecisions.get(askId);
1979
- if (until === undefined)
1980
- return false;
1981
- if (Date.now() > until) {
1982
- this.dropAbandonedEditTombstone(askId, "ttl");
1983
- return true;
1984
- }
1985
- return true;
1986
- }
1987
- guardRowDerivedApprove(askId, outcome, where) {
1988
- if (outcome !== true || askId === undefined)
1989
- return outcome;
1990
- if (!this.abandonedEditedDecisionPending(askId))
1991
- return outcome;
1992
- defaultLogger.warn("approval_row_derived_approve_parked_after_abandoned_edit", { askId, where });
1993
- this.settleSameAskIdParkRoute(askId);
1994
- return "unavailable";
1995
- }
1996
- dropAbandonedEditTombstone(askId, why) {
1997
- const stillWaiting = this.liveConsumerCount(askId);
1998
- if (stillWaiting > 0) {
1999
- defaultLogger.warn("approval_abandoned_edit_tombstone_dropped_parking_consumers", { askId, why, parked: stillWaiting });
2000
- this.settleSameAskIdParkRoute(askId);
2001
- }
2002
- this.abandonedEditedDecisions.delete(askId);
2003
- }
2004
1960
  expireEditedDecisionMarkForTest(askId) {
2005
- this.abandonedEditedDecisions.set(askId, Date.now() + ABANDONED_EDIT_TOMBSTONE_TTL_MS);
2006
1961
  this.editedDecisionsInFlight.delete(askId);
2007
1962
  }
2008
- forceAbandonedEditTombstoneExpiryForTest(askId) {
2009
- if (!this.abandonedEditedDecisions.has(askId))
2010
- return;
2011
- this.abandonedEditedDecisions.set(askId, Date.now() - 1);
2012
- this.abandonedEditedDecisionPending(askId);
2013
- }
2014
- setAbandonedEditTombstoneDeadlineForTest(askId, atMs) {
2015
- if (this.abandonedEditedDecisions.has(askId))
2016
- this.abandonedEditedDecisions.set(askId, atMs);
2017
- }
2018
1963
  pendingCount() {
2019
1964
  return this.pending.size;
2020
1965
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.55.0",
3
+ "version": "7.56.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",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@sema-agent/core": "7.1.0",
58
- "@sema-agent/settings-schema": "1.3.0",
58
+ "@sema-agent/settings-schema": "1.4.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
61
61
  "mysql2": "^3.22.4",
@@ -141,7 +141,7 @@ npx skills init my-xyz-skill
141
141
  ```
142
142
 
143
143
  <!--
144
- TODO (blackboard [832], clay): add our own first-party skill sources here -
144
+ TODO (clay): add our own first-party skill sources here -
145
145
  sema-registry / config-center - as a discovery+install lane alongside the open
146
146
  skills.sh ecosystem. Not every deployment ships a config-center, so the section
147
147
  must probe availability first and fall back to the public ecosystem cleanly.
package/skills/loop.md CHANGED
@@ -35,7 +35,7 @@ Then briefly confirm what's scheduled (id, human cadence, how to cancel with `Cr
35
35
  With no interval, run one iteration of the task now, then call `ScheduleWakeup` to schedule when you resume:
36
36
 
37
37
  - `delaySeconds`: how long to sleep (runtime clamps to [60, 3600]).
38
- - `reason`: one short, specific sentence ("watching CI run #123" beats "waiting") — shown to the user and telemetry.
38
+ - `reason`: one short, specific sentence ("watching CI run #4521" beats "waiting") — shown to the user and telemetry.
39
39
  - `prompt`: the same /loop input verbatim, so the next firing re-enters this skill and continues the loop. For an autonomous loop with no user prompt, pass the literal sentinel `<<autonomous-loop-dynamic>>` (never the CronCreate-mode `<<autonomous-loop>>` sentinel — ScheduleWakeup always uses the `-dynamic` variant).
40
40
  - To END the loop: call `ScheduleWakeup` with `stop: true` and omit every other field — the loop ends immediately and no further wakeups fire.
41
41