@sema-agent/server 7.54.0 → 7.55.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 (36) hide show
  1. package/dist/approval-ask-audit-store.d.ts +129 -0
  2. package/dist/approval-ask-audit-store.js +284 -0
  3. package/dist/approval-card.d.ts +18 -6
  4. package/dist/approval-card.js +2 -2
  5. package/dist/approval-reconciler.d.ts +2 -1
  6. package/dist/approval-reconciler.js +1 -1
  7. package/dist/boot/coordinators.d.ts +2 -0
  8. package/dist/boot/coordinators.js +18 -1
  9. package/dist/boot/reapers.d.ts +2 -1
  10. package/dist/boot/stores.js +8 -1
  11. package/dist/device-store.d.ts +66 -2
  12. package/dist/device-store.js +35 -0
  13. package/dist/device-ws-hub.d.ts +8 -0
  14. package/dist/device-ws-hub.js +6 -0
  15. package/dist/http/routes/approvals-assistant.js +1 -0
  16. package/dist/http/routes/devices.d.ts +64 -0
  17. package/dist/http/routes/devices.js +173 -0
  18. package/dist/http/server.d.ts +14 -0
  19. package/dist/http/server.js +13 -2
  20. package/dist/main.js +3 -1
  21. package/dist/observability/fail-open.d.ts +7 -3
  22. package/dist/observability/fail-open.js +7 -3
  23. package/dist/plugins/approval-ask-store-memory.d.ts +11 -1
  24. package/dist/plugins/approval-ask-store-memory.js +20 -3
  25. package/dist/plugins/approval-ask-store-sql.d.ts +82 -0
  26. package/dist/plugins/approval-ask-store-sql.js +41 -10
  27. package/dist/plugins/device-store-sql.d.ts +38 -1
  28. package/dist/plugins/device-store-sql.js +82 -2
  29. package/dist/plugins/remote-env-device.js +8 -1
  30. package/dist/plugins/sql-errors.d.ts +12 -0
  31. package/dist/plugins/sql-errors.js +10 -0
  32. package/dist/runs.js +1 -0
  33. package/dist/tool-approval.d.ts +54 -23
  34. package/dist/tool-approval.js +218 -127
  35. package/dist/trace/ledger-events.d.ts +9 -0
  36. package/package.json +2 -2
@@ -8,6 +8,7 @@ import { createLogger } from "./observability/logger.js";
8
8
  import { recordFailOpen } from "./observability/fail-open.js";
9
9
  import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
10
10
  import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readProbeCause, readRuleEvidence, readRuleOffersAbsence, } from "./approval-card.js";
11
+ import { projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
11
12
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
12
13
  const defaultLogger = createLogger();
13
14
  export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
@@ -43,9 +44,12 @@ function durableRuleMaterial(material) {
43
44
  return { ruleCommand: material.command, ruleScopeRoot: material.scopeRoot ?? null };
44
45
  }
45
46
  export const ASK_DECISION_CONSUMER_ABSENT = "absent";
47
+ const MAX_INFLIGHT_TERMINAL_CLAIMS = 2;
46
48
  const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
47
49
  const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
48
50
  const DURABLE_CALL_TIMEOUT_MS = 5_000;
51
+ const TERMINAL_LADDER_WALL_MS = DURABLE_CALL_TIMEOUT_MS + DURABLE_CONVERGE_READ_TIMEOUT_MS + DURABLE_CONVERGE_BACKOFF_MS.reduce((a, b) => a + b, 0);
52
+ const TERMINAL_CLAIM_CAP_POLL_MS = 50;
49
53
  function ambiguousCasFailOpenTag(intent) {
50
54
  switch (intent) {
51
55
  case "expire":
@@ -228,6 +232,30 @@ function settleArgsOf(outcome) {
228
232
  export class ToolApprovalCoordinator {
229
233
  als = new AsyncLocalStorage();
230
234
  pending = new Map();
235
+ terminalClaimsInFlight = new Map();
236
+ admitTerminalClaim(askId) {
237
+ const n = this.terminalClaimsInFlight.get(askId) ?? 0;
238
+ if (n >= MAX_INFLIGHT_TERMINAL_CLAIMS)
239
+ return undefined;
240
+ this.terminalClaimsInFlight.set(askId, n + 1);
241
+ let released = false;
242
+ return () => {
243
+ if (released)
244
+ return;
245
+ released = true;
246
+ const cur = this.terminalClaimsInFlight.get(askId) ?? 0;
247
+ if (cur <= 1)
248
+ this.terminalClaimsInFlight.delete(askId);
249
+ else
250
+ this.terminalClaimsInFlight.set(askId, cur - 1);
251
+ };
252
+ }
253
+ terminalClaimsInFlightCount(askId) {
254
+ return this.terminalClaimsInFlight.get(askId) ?? 0;
255
+ }
256
+ terminalClaimsInFlightForTest(askId) {
257
+ return this.terminalClaimsInFlightCount(askId);
258
+ }
231
259
  pendingByAskId = new Map();
232
260
  allowAllSessions = new Set();
233
261
  streams = new Map();
@@ -253,6 +281,7 @@ export class ToolApprovalCoordinator {
253
281
  parkTombstoneTtlMs;
254
282
  parkTombstonePerPrincipalMax;
255
283
  parkedRedeem;
284
+ askAudit;
256
285
  constructor(opts) {
257
286
  this.ruleConsent = opts?.ruleConsent;
258
287
  this.ruleScopeRootFor = opts?.ruleScopeRootFor;
@@ -268,6 +297,7 @@ export class ToolApprovalCoordinator {
268
297
  this.parkTombstoneMax = opts?.parkTombstoneMax ?? DEFAULT_PARK_TOMBSTONE_MAX;
269
298
  this.parkTombstoneTtlMs = opts?.parkTombstoneTtlMs ?? DEFAULT_PARK_TOMBSTONE_TTL_MS;
270
299
  this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
300
+ this.askAudit = opts?.askAudit;
271
301
  }
272
302
  unattendedOutcome() {
273
303
  return this.unattendedPolicy === "deny" ? false : "unavailable";
@@ -454,23 +484,30 @@ export class ToolApprovalCoordinator {
454
484
  return false;
455
485
  }
456
486
  }
457
- async readRowForRecovery(askId, where) {
458
- if (!this.askStore)
459
- return { kind: "absent" };
487
+ async probeRowWhileYielding(askId, budgetMs) {
488
+ if (!this.askStore || budgetMs <= 0)
489
+ return { kind: "none" };
490
+ const release = this.admitTerminalClaim(askId);
491
+ if (release === undefined)
492
+ return { kind: "denied" };
493
+ const raw = this.askStore.getAsk(askId);
494
+ void raw.then(release, release);
460
495
  let row;
461
496
  try {
462
- row = await this.withStoreDeadline(this.askStore.getAsk(askId), `getAsk(${where})`, DURABLE_CONVERGE_READ_TIMEOUT_MS);
497
+ row = await this.withStoreDeadline(raw, "getAsk(yield-probe)", Math.min(DURABLE_CONVERGE_READ_TIMEOUT_MS, budgetMs));
463
498
  }
464
499
  catch (err) {
465
- this.noteStoreError(err, `getAsk(${where})`);
466
- return { kind: "unreadable" };
500
+ this.noteStoreError(err, "getAsk(yield-probe)");
501
+ return { kind: "none" };
467
502
  }
468
- if (row === null)
503
+ if (row === null || row.state === "STREAM_PENDING")
504
+ return { kind: "none" };
505
+ return { kind: "row", current: projectAskTerminalClaimTruth(row) };
506
+ }
507
+ classifyClaimLoss(askId, current, where) {
508
+ if (current.state === "absent")
469
509
  return { kind: "absent" };
470
- if (row.state === "STREAM_PENDING") {
471
- return this.editedDecisionInFlight(askId) ? { kind: "edit-in-flight" } : { kind: "pending" };
472
- }
473
- const replayed = settleArgsOf(this.guardRowDerivedApprove(askId, replayTerminalAskRow(row), where));
510
+ const replayed = settleArgsOf(this.guardRowDerivedApprove(askId, replayTerminalAskRow(current), where));
474
511
  if (replayed === "unavailable")
475
512
  return { kind: "parked" };
476
513
  if (!replayed.allowed)
@@ -631,6 +668,7 @@ export class ToolApprovalCoordinator {
631
668
  if (p.targetCtxs?.has(ctx))
632
669
  p.onTargetGone?.(ctx);
633
670
  }
671
+ this.askAudit?.noteLegClosed(ctx.taskId);
634
672
  }
635
673
  }));
636
674
  }
@@ -923,111 +961,159 @@ export class ToolApprovalCoordinator {
923
961
  releaseAdmission();
924
962
  if (selfEntry)
925
963
  selfEntry.settledOutcome = outcome;
964
+ this.askAudit?.recordSettle({
965
+ approvalId: wireApprovalId,
966
+ outcome,
967
+ ...(windowRouteUnavailable ? { parkRouted: true } : {}),
968
+ ...(hostSettledBy !== undefined ? { settledBy: hostSettledBy } : {}),
969
+ settledAtMs: Date.now(),
970
+ });
926
971
  resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}) });
927
972
  };
928
- const convergeFromDurableState = async (where) => {
929
- if (!this.askStore || askId === undefined)
973
+ const broadcastClaimTruth = (read) => {
974
+ if (askId === undefined)
930
975
  return;
931
- const pinnedAskId = askId;
932
- for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
933
- if (done)
976
+ switch (read.kind) {
977
+ case "approved":
978
+ this.notifyExternalDecision(askId, true, read.updatedInput);
934
979
  return;
935
- const read = await this.readRowForRecovery(pinnedAskId, `converge:${where}`);
936
- if (done)
980
+ case "denied":
981
+ this.notifyExternalDecision(askId, false);
937
982
  return;
938
- switch (read.kind) {
939
- case "absent":
940
- settle(false, "expired");
941
- return;
942
- case "pending":
943
- case "edit-in-flight":
944
- case "unreadable":
945
- if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
946
- await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
947
- continue;
948
- case "parked":
949
- windowRouteUnavailable = true;
950
- settle(false, "expired");
951
- return;
952
- case "approved":
953
- settle(true, "allowed", read.updatedInput);
954
- return;
955
- case "denied":
956
- settle(false, "denied");
957
- return;
958
- default: {
959
- const unreachable = read;
960
- throw new Error(`convergeFromDurableState: unknown recovery read ${String(unreachable)}`);
961
- }
983
+ case "parked":
984
+ this.settleSameAskIdParkRoute(askId);
985
+ return;
986
+ case "absent":
987
+ case "edit-in-flight":
988
+ return;
989
+ default: {
990
+ const unreachable = read;
991
+ throw new Error(`broadcastClaimTruth: unknown recovery read ${String(unreachable)}`);
962
992
  }
963
993
  }
964
- if (!done) {
965
- if (this.editedDecisionInFlight(pinnedAskId)) {
994
+ };
995
+ const settleFromClaimLoss = (read) => {
996
+ switch (read.kind) {
997
+ case "approved":
998
+ settle(true, "allowed", read.updatedInput);
999
+ broadcastClaimTruth(read);
1000
+ return true;
1001
+ case "denied":
1002
+ settle(false, "denied");
1003
+ broadcastClaimTruth(read);
1004
+ return true;
1005
+ case "parked":
966
1006
  windowRouteUnavailable = true;
967
1007
  settle(false, "expired");
968
- }
969
- else {
970
- settle(false, "expired");
1008
+ broadcastClaimTruth(read);
1009
+ return true;
1010
+ case "absent":
1011
+ case "edit-in-flight":
1012
+ return false;
1013
+ default: {
1014
+ const unreachable = read;
1015
+ throw new Error(`settleFromClaimLoss: unknown recovery read ${String(unreachable)}`);
971
1016
  }
972
1017
  }
973
1018
  };
974
- const recoverFromUnknownStore = async (arm, casStillInFlight) => {
1019
+ const lateClaimLossSettle = (current, intent) => {
1020
+ if (askId === undefined)
1021
+ return;
1022
+ const read = this.classifyClaimLoss(askId, current, `late:${intent}`);
1023
+ if (done) {
1024
+ broadcastClaimTruth(read);
1025
+ return;
1026
+ }
1027
+ settleFromClaimLoss(read);
1028
+ };
1029
+ const boundedClaim = (mk, label, timeoutMs, late) => {
1030
+ const release = this.admitTerminalClaim(askId ?? wireApprovalId);
1031
+ if (release === undefined)
1032
+ return undefined;
1033
+ const raw = mk();
1034
+ void raw.then(release, release);
1035
+ return this.withStoreDeadline(raw, label, timeoutMs, late);
1036
+ };
1037
+ const resolveLocalOutcome = async (arm) => {
975
1038
  if (askId === undefined) {
976
1039
  arm.settleByIntent();
977
1040
  return;
978
1041
  }
979
1042
  const pinnedAskId = askId;
980
- for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
981
- if (done)
982
- return;
983
- const read = await this.readRowForRecovery(pinnedAskId, `recovery:${arm.intent}`);
1043
+ let lastLoss;
1044
+ const ladderStartedAt = Date.now();
1045
+ const remainingMs = () => ladderStartedAt + TERMINAL_LADDER_WALL_MS - Date.now();
1046
+ const nap = async (ms) => {
1047
+ const slice = Math.min(ms, remainingMs());
1048
+ if (slice > 0)
1049
+ await new Promise((r) => setTimeout(r, slice).unref?.());
1050
+ };
1051
+ let attempt = 0;
1052
+ let claimsIssued = 0;
1053
+ let rejects = 0;
1054
+ while (attempt <= DURABLE_CONVERGE_BACKOFF_MS.length && remainingMs() > 0) {
984
1055
  if (done)
985
1056
  return;
986
- switch (read.kind) {
987
- case "approved":
988
- settle(true, "allowed", read.updatedInput);
989
- return;
990
- case "denied":
991
- settle(false, "denied");
992
- return;
993
- case "parked":
994
- windowRouteUnavailable = true;
995
- settle(false, "expired");
1057
+ const backoffAfter = attempt < DURABLE_CONVERGE_BACKOFF_MS.length ? DURABLE_CONVERGE_BACKOFF_MS[attempt] : 0;
1058
+ if (attempt > 0 && this.editedDecisionInFlight(pinnedAskId)) {
1059
+ const probed = await this.probeRowWhileYielding(pinnedAskId, remainingMs());
1060
+ if (done)
996
1061
  return;
997
- case "pending": {
998
- if (casStillInFlight) {
999
- arm.settleByIntent();
1000
- return;
1001
- }
1002
- let result;
1003
- try {
1004
- result = await arm.cas();
1005
- }
1006
- catch (err) {
1007
- this.noteStoreError(err, arm.storeLabel);
1008
- arm.settleByIntent();
1009
- return;
1010
- }
1011
- if (arm.won(result)) {
1012
- arm.onWon(result);
1062
+ if (probed.kind === "denied") {
1063
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
1064
+ continue;
1065
+ }
1066
+ if (probed.kind === "row") {
1067
+ const read = this.classifyClaimLoss(pinnedAskId, probed.current, `yield-probe:${arm.intent}`);
1068
+ if (settleFromClaimLoss(read))
1013
1069
  return;
1014
- }
1015
- await convergeFromDurableState(arm.where);
1016
- return;
1070
+ if (read.kind === "edit-in-flight")
1071
+ lastLoss = probed.current;
1017
1072
  }
1018
- case "absent":
1019
- arm.settleByIntent();
1020
- return;
1021
- case "edit-in-flight":
1022
- case "unreadable":
1023
- if (attempt < DURABLE_CONVERGE_BACKOFF_MS.length)
1024
- await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
1073
+ else if (this.terminalClaimsInFlightCount(pinnedAskId) > 0) {
1074
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
1025
1075
  continue;
1026
- default: {
1027
- const unreachable = read;
1028
- throw new Error(`recoverFromUnknownStore: unknown recovery read ${String(unreachable)}`);
1029
1076
  }
1077
+ await nap(backoffAfter);
1078
+ attempt++;
1079
+ continue;
1080
+ }
1081
+ const inFlight = arm.claim(Math.min(attempt === 0 ? DURABLE_CALL_TIMEOUT_MS : DURABLE_CONVERGE_READ_TIMEOUT_MS, remainingMs()));
1082
+ if (inFlight === undefined) {
1083
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
1084
+ continue;
1085
+ }
1086
+ claimsIssued++;
1087
+ let outcome;
1088
+ try {
1089
+ outcome = await inFlight;
1090
+ }
1091
+ catch (err) {
1092
+ this.noteStoreError(err, arm.storeLabel);
1093
+ rejects++;
1094
+ await nap(backoffAfter);
1095
+ attempt++;
1096
+ continue;
1097
+ }
1098
+ if (outcome.claimed) {
1099
+ arm.onWon(outcome);
1100
+ return;
1030
1101
  }
1102
+ arm.onLostTruth?.(outcome.current);
1103
+ const read = this.classifyClaimLoss(pinnedAskId, outcome.current, `claim:${arm.intent}`);
1104
+ if (done) {
1105
+ broadcastClaimTruth(read);
1106
+ return;
1107
+ }
1108
+ if (settleFromClaimLoss(read))
1109
+ return;
1110
+ if (read.kind === "absent") {
1111
+ arm.settleByIntent();
1112
+ return;
1113
+ }
1114
+ lastLoss = outcome.current;
1115
+ await nap(backoffAfter);
1116
+ attempt++;
1031
1117
  }
1032
1118
  if (done)
1033
1119
  return;
@@ -1036,24 +1122,10 @@ export class ToolApprovalCoordinator {
1036
1122
  settle(false, "expired");
1037
1123
  return;
1038
1124
  }
1039
- recordFailOpen(ambiguousCasFailOpenTag(arm.intent), `askId=${pinnedAskId} reads=${DURABLE_CONVERGE_BACKOFF_MS.length + 1}`);
1040
- arm.settleByIntent();
1041
- };
1042
- const resolveLocalOutcome = async (arm) => {
1043
- let result;
1044
- try {
1045
- result = await arm.cas();
1046
- }
1047
- catch (err) {
1048
- this.noteStoreError(err, arm.storeLabel);
1049
- await recoverFromUnknownStore(arm, err instanceof ApprovalStoreDeadlineError);
1050
- return;
1051
- }
1052
- if (arm.won(result)) {
1053
- arm.onWon(result);
1125
+ if (lastLoss !== undefined && settleFromClaimLoss(this.classifyClaimLoss(pinnedAskId, lastLoss, `claim-tail:${arm.intent}`)))
1054
1126
  return;
1055
- }
1056
- await convergeFromDurableState(arm.where);
1127
+ recordFailOpen(ambiguousCasFailOpenTag(arm.intent), `askId=${pinnedAskId} attempts=${attempt} claims=${claimsIssued} rejects=${rejects} elapsedMs=${Date.now() - ladderStartedAt}`);
1128
+ arm.settleByIntent();
1057
1129
  };
1058
1130
  const settleParkRouteIfUnsettled = (hostSettledBy) => {
1059
1131
  if (done)
@@ -1072,22 +1144,22 @@ export class ToolApprovalCoordinator {
1072
1144
  const pinnedBatchId = batchId;
1073
1145
  await resolveLocalOutcome({
1074
1146
  intent: "expire",
1075
- storeLabel: "expireAsk",
1076
- where: "windowExpired",
1077
- cas: () => this.withStoreDeadline(store.expireAsk(pinnedAskId, pinnedBatchId), "expireAsk(window)", DURABLE_CALL_TIMEOUT_MS, (late) => {
1078
- if (!late.won)
1147
+ storeLabel: "claimTerminal(window)",
1148
+ claim: (timeoutMs) => boundedClaim(() => store.claimTerminal(pinnedAskId, pinnedBatchId, "expire"), "claimTerminal(window)", timeoutMs, (late) => {
1149
+ if (!late.claimed) {
1150
+ lateClaimLossSettle(late.current, "expire");
1079
1151
  return;
1152
+ }
1080
1153
  this.settleSameAskIdParkRoute(pinnedAskId);
1081
1154
  this.settleVoidedSiblings(late.voidedSiblings);
1082
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, late.voidedSiblings, "superseded_by_park", Date.now()));
1155
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, late.voidedSiblings, "superseded_by_park", Date.now(), origin.taskId));
1083
1156
  }),
1084
- won: (res) => res.won,
1085
1157
  onWon: (res) => {
1086
1158
  windowRouteUnavailable = true;
1087
1159
  settle(false, "expired");
1088
1160
  this.settleSameAskIdParkRoute(pinnedAskId);
1089
1161
  this.settleVoidedSiblings(res.voidedSiblings);
1090
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, res.voidedSiblings, "superseded_by_park", Date.now()));
1162
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, res.voidedSiblings, "superseded_by_park", Date.now(), origin.taskId));
1091
1163
  },
1092
1164
  settleByIntent: () => settleParkRouteIfUnsettled(hostSelfReport),
1093
1165
  });
@@ -1116,24 +1188,31 @@ export class ToolApprovalCoordinator {
1116
1188
  const store = this.askStore;
1117
1189
  const pinnedAskId = askId;
1118
1190
  const pinnedBatchId = batchId;
1191
+ const cancelLostTruth = (current) => {
1192
+ if (current.state !== "VOID")
1193
+ return;
1194
+ this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1195
+ };
1119
1196
  await resolveLocalOutcome({
1120
1197
  intent: "cancel",
1121
- storeLabel: "transitionAsk(cancel)",
1122
- where: "runCancel",
1123
- cas: () => this.withStoreDeadline(store.transitionAsk(pinnedAskId, "STREAM_PENDING", "VOID", { updatedAtMs: Date.now() }), "transitionAsk(cancel)", DURABLE_CALL_TIMEOUT_MS, (lateWon) => {
1124
- if (!lateWon)
1198
+ storeLabel: "claimTerminal(cancel)",
1199
+ claim: (timeoutMs) => boundedClaim(() => store.claimTerminal(pinnedAskId, pinnedBatchId, "cancel"), "claimTerminal(cancel)", timeoutMs, (late) => {
1200
+ if (!late.claimed) {
1201
+ cancelLostTruth(late.current);
1202
+ lateClaimLossSettle(late.current, "cancel");
1125
1203
  return;
1204
+ }
1126
1205
  this.settleVoidedSiblings([pinnedAskId]);
1127
1206
  this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1128
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now()));
1207
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1129
1208
  }),
1130
- won: (won) => won,
1131
1209
  onWon: () => {
1132
1210
  settle(false, "expired");
1133
1211
  this.settleVoidedSiblings([pinnedAskId]);
1134
1212
  this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1135
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now()));
1213
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1136
1214
  },
1215
+ onLostTruth: cancelLostTruth,
1137
1216
  settleByIntent: () => settle(false, "expired"),
1138
1217
  });
1139
1218
  };
@@ -1193,6 +1272,18 @@ export class ToolApprovalCoordinator {
1193
1272
  }
1194
1273
  siblings.add(pendingEntry);
1195
1274
  }
1275
+ this.askAudit?.recordAsk({
1276
+ approvalId: wireApprovalId,
1277
+ taskId: origin.taskId,
1278
+ ...(req.sourceTaskId !== undefined ? { sourceTaskId: req.sourceTaskId } : {}),
1279
+ ...(origin.sessionId !== undefined ? { sessionId: origin.sessionId } : {}),
1280
+ owner: origin.owner,
1281
+ toolName: req.toolName,
1282
+ toolCallId: req.toolCallId,
1283
+ legTracked: !child && aliveCtxs.some((c) => c.taskId === origin.taskId),
1284
+ expiresAtMs: persistedExpiresAtMs,
1285
+ mintedAtMs: Date.now(),
1286
+ });
1196
1287
  const cardFrame = askId !== undefined && cardForFrame !== undefined
1197
1288
  ? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now() })
1198
1289
  : undefined;
@@ -1239,15 +1330,15 @@ export class ToolApprovalCoordinator {
1239
1330
  };
1240
1331
  await resolveLocalOutcome({
1241
1332
  intent: "unreachable",
1242
- storeLabel: "expireAsk(emit-failed)",
1243
- where: "emitFailed",
1244
- cas: () => this.withStoreDeadline(store.expireAsk(pinnedAskId, pinnedBatchId), "expireAsk(emit-failed)", DURABLE_CALL_TIMEOUT_MS, (late) => {
1245
- if (!late.won)
1333
+ storeLabel: "claimTerminal(emit-failed)",
1334
+ claim: (timeoutMs) => boundedClaim(() => store.claimTerminal(pinnedAskId, pinnedBatchId, "expire"), "claimTerminal(emit-failed)", timeoutMs, (late) => {
1335
+ if (!late.claimed) {
1336
+ lateClaimLossSettle(late.current, "unreachable");
1246
1337
  return;
1338
+ }
1247
1339
  this.settleSameAskIdParkRoute(pinnedAskId);
1248
1340
  this.settleVoidedSiblings(late.voidedSiblings);
1249
1341
  }),
1250
- won: (res) => res.won,
1251
1342
  onWon: (res) => {
1252
1343
  settleUnreachable();
1253
1344
  this.settleSameAskIdParkRoute(pinnedAskId);
@@ -38,6 +38,15 @@ export type LedgerEventType = "reasoning" | "text" | "tool_start" | "tool_end" |
38
38
  | "elicitation" | "elicitation_complete" | "question" | "question_complete" | "tool_approval" | "tool_approval_complete"
39
39
  /** [ref] §4.3 新协议的呈卡帧(与上面两只 tool_approval 帧同一条投递面)。 */
40
40
  | "approval_request"
41
+ /**
42
+ * [ref]([ref]):批级撤卡帧在 **bg/resume 两条 durable 腿**的账本落行(与呈卡帧同口
43
+ * `append(type, rest)`,seq 由腿自己的 ledger 链步分配)。此前成文「reaper/收敛器无 seq 可分配故
44
+ * live-only」只对**调用点**成立,不对 ctx 的投递口成立——帧经 `emitRevokeTo` 交给各 ctx 的
45
+ * `emitRevoke`,durable 腿的那只钩子自己就是账本写口。sync 腿维持 live-only(行为零变,
46
+ * `routes/tasks.ts` 装配点注)。重放语义:同一账本里卡帧(`approval_request`)在前、撤帧在后,
47
+ * 全量重读恒同序 ⇒ 幂等诚实;不投影成 trace 2.3 帧(`LEDGER_TYPES_NOT_PROJECTED` 登记)。
48
+ */
49
+ | "approval_revoke"
41
50
  /** 异步 workflow 完成的入箱 drain(`emitPendingWorkflowCompletions` 两个字面型之一)。 */
42
51
  | "workflow_complete"
43
52
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.54.0",
3
+ "version": "7.55.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "7.4.0",
72
+ "@sema-agent/sdk": "8.1.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",