@sema-agent/server 7.54.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 (76) 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-ask-audit-store.d.ts +129 -0
  7. package/dist/approval-ask-audit-store.js +284 -0
  8. package/dist/approval-card.d.ts +18 -6
  9. package/dist/approval-card.js +2 -2
  10. package/dist/approval-content-kind.d.ts +22 -0
  11. package/dist/approval-content-kind.js +5 -0
  12. package/dist/approval-reconciler.d.ts +2 -1
  13. package/dist/approval-reconciler.js +1 -1
  14. package/dist/boot/coordinators.d.ts +2 -0
  15. package/dist/boot/coordinators.js +18 -1
  16. package/dist/boot/leader.d.ts +21 -0
  17. package/dist/boot/leader.js +6 -0
  18. package/dist/boot/reapers.d.ts +2 -1
  19. package/dist/boot/runtime-caps.d.ts +2 -1
  20. package/dist/boot/runtime-caps.js +3 -1
  21. package/dist/boot/stores.js +8 -1
  22. package/dist/config-center/types.d.ts +2 -1
  23. package/dist/config-provider.js +1 -0
  24. package/dist/device-store.d.ts +66 -2
  25. package/dist/device-store.js +35 -0
  26. package/dist/device-ws-hub.d.ts +8 -0
  27. package/dist/device-ws-hub.js +6 -0
  28. package/dist/http/route-ctx.d.ts +15 -3
  29. package/dist/http/routes/approvals-assistant.js +3 -2
  30. package/dist/http/routes/devices.d.ts +64 -0
  31. package/dist/http/routes/devices.js +173 -0
  32. package/dist/http/routes/leader.js +2 -2
  33. package/dist/http/routes/workflows.js +1 -1
  34. package/dist/http/server.d.ts +14 -0
  35. package/dist/http/server.js +30 -5
  36. package/dist/leader/endpoint.js +5 -4
  37. package/dist/leader/wire.d.ts +50 -0
  38. package/dist/leader/wire.js +19 -7
  39. package/dist/main.js +4 -2
  40. package/dist/observability/fail-open.d.ts +7 -3
  41. package/dist/observability/fail-open.js +7 -3
  42. package/dist/plugins/approval-ask-store-memory.d.ts +11 -1
  43. package/dist/plugins/approval-ask-store-memory.js +20 -3
  44. package/dist/plugins/approval-ask-store-sql.d.ts +82 -0
  45. package/dist/plugins/approval-ask-store-sql.js +41 -10
  46. package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
  47. package/dist/plugins/checkpoint-store-sql.js +4 -1
  48. package/dist/plugins/device-store-sql.d.ts +38 -1
  49. package/dist/plugins/device-store-sql.js +82 -2
  50. package/dist/plugins/file-run-store.js +2 -0
  51. package/dist/plugins/local-checkpoint-store.js +2 -0
  52. package/dist/plugins/local-session-store.d.ts +9 -9
  53. package/dist/plugins/local-session-store.js +5 -3
  54. package/dist/plugins/memory-run-store.js +2 -0
  55. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  56. package/dist/plugins/permission-rule-store-file.js +17 -8
  57. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  58. package/dist/plugins/permission-rule-store-sql.js +23 -13
  59. package/dist/plugins/pg-session-storage.js +17 -13
  60. package/dist/plugins/remote-env-device.js +8 -1
  61. package/dist/plugins/run-store-sql.js +6 -4
  62. package/dist/plugins/sql-errors.d.ts +12 -0
  63. package/dist/plugins/sql-errors.js +10 -0
  64. package/dist/plugins/store-contracts.d.ts +4 -0
  65. package/dist/plugins/tidb-session-store.js +18 -14
  66. package/dist/rules-consent.d.ts +5 -4
  67. package/dist/rules-consent.js +5 -33
  68. package/dist/runs.js +1 -0
  69. package/dist/runtime-caps-resolver.js +3 -1
  70. package/dist/security.d.ts +7 -0
  71. package/dist/tool-approval.d.ts +66 -83
  72. package/dist/tool-approval.js +222 -186
  73. package/dist/trace/ledger-events.d.ts +9 -0
  74. package/package.json +3 -3
  75. package/skills/find-skills.md +1 -1
  76. package/skills/loop.md +1 -1
@@ -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";
@@ -8,6 +9,7 @@ import { createLogger } from "./observability/logger.js";
8
9
  import { recordFailOpen } from "./observability/fail-open.js";
9
10
  import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
10
11
  import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readProbeCause, readRuleEvidence, readRuleOffersAbsence, } from "./approval-card.js";
12
+ import { projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
11
13
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
12
14
  const defaultLogger = createLogger();
13
15
  export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
@@ -21,8 +23,6 @@ const DEFAULT_UNREACHED_ASK_TTL_MS = 60 * 60_000;
21
23
  const SUSPENDED_POLL_MAX_PER_TICK = 64;
22
24
  const SUSPENDED_POLL_BUDGET_MS = 5_000;
23
25
  const EDITED_DECISION_MARK_CAP_MS = 10_000;
24
- const ABANDONED_EDIT_TOMBSTONE_TTL_MS = 10 * 60_000;
25
- const ABANDONED_EDIT_TOMBSTONE_MAX = 256;
26
26
  const DEFAULT_WINDOW_MARGIN_MS = 10_000;
27
27
  const MAX_ALLOW_SESSIONS = 4096;
28
28
  const DEFAULT_ADMIT_MAX_PER_TASK = 32;
@@ -43,9 +43,12 @@ function durableRuleMaterial(material) {
43
43
  return { ruleCommand: material.command, ruleScopeRoot: material.scopeRoot ?? null };
44
44
  }
45
45
  export const ASK_DECISION_CONSUMER_ABSENT = "absent";
46
+ const MAX_INFLIGHT_TERMINAL_CLAIMS = 2;
46
47
  const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
47
48
  const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
48
49
  const DURABLE_CALL_TIMEOUT_MS = 5_000;
50
+ const TERMINAL_LADDER_WALL_MS = DURABLE_CALL_TIMEOUT_MS + DURABLE_CONVERGE_READ_TIMEOUT_MS + DURABLE_CONVERGE_BACKOFF_MS.reduce((a, b) => a + b, 0);
51
+ const TERMINAL_CLAIM_CAP_POLL_MS = 50;
49
52
  function ambiguousCasFailOpenTag(intent) {
50
53
  switch (intent) {
51
54
  case "expire":
@@ -228,6 +231,30 @@ function settleArgsOf(outcome) {
228
231
  export class ToolApprovalCoordinator {
229
232
  als = new AsyncLocalStorage();
230
233
  pending = new Map();
234
+ terminalClaimsInFlight = new Map();
235
+ admitTerminalClaim(askId) {
236
+ const n = this.terminalClaimsInFlight.get(askId) ?? 0;
237
+ if (n >= MAX_INFLIGHT_TERMINAL_CLAIMS)
238
+ return undefined;
239
+ this.terminalClaimsInFlight.set(askId, n + 1);
240
+ let released = false;
241
+ return () => {
242
+ if (released)
243
+ return;
244
+ released = true;
245
+ const cur = this.terminalClaimsInFlight.get(askId) ?? 0;
246
+ if (cur <= 1)
247
+ this.terminalClaimsInFlight.delete(askId);
248
+ else
249
+ this.terminalClaimsInFlight.set(askId, cur - 1);
250
+ };
251
+ }
252
+ terminalClaimsInFlightCount(askId) {
253
+ return this.terminalClaimsInFlight.get(askId) ?? 0;
254
+ }
255
+ terminalClaimsInFlightForTest(askId) {
256
+ return this.terminalClaimsInFlightCount(askId);
257
+ }
231
258
  pendingByAskId = new Map();
232
259
  allowAllSessions = new Set();
233
260
  streams = new Map();
@@ -241,7 +268,6 @@ export class ToolApprovalCoordinator {
241
268
  admitByOwner = new Map();
242
269
  suspendedPollOffset = 0;
243
270
  editedDecisionsInFlight = new Map();
244
- abandonedEditedDecisions = new Map();
245
271
  storeErrorWarned = false;
246
272
  storeErrorTally = 0;
247
273
  governanceAskMarks;
@@ -253,6 +279,7 @@ export class ToolApprovalCoordinator {
253
279
  parkTombstoneTtlMs;
254
280
  parkTombstonePerPrincipalMax;
255
281
  parkedRedeem;
282
+ askAudit;
256
283
  constructor(opts) {
257
284
  this.ruleConsent = opts?.ruleConsent;
258
285
  this.ruleScopeRootFor = opts?.ruleScopeRootFor;
@@ -268,6 +295,7 @@ export class ToolApprovalCoordinator {
268
295
  this.parkTombstoneMax = opts?.parkTombstoneMax ?? DEFAULT_PARK_TOMBSTONE_MAX;
269
296
  this.parkTombstoneTtlMs = opts?.parkTombstoneTtlMs ?? DEFAULT_PARK_TOMBSTONE_TTL_MS;
270
297
  this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
298
+ this.askAudit = opts?.askAudit;
271
299
  }
272
300
  unattendedOutcome() {
273
301
  return this.unattendedPolicy === "deny" ? false : "unavailable";
@@ -454,23 +482,30 @@ export class ToolApprovalCoordinator {
454
482
  return false;
455
483
  }
456
484
  }
457
- async readRowForRecovery(askId, where) {
458
- if (!this.askStore)
459
- return { kind: "absent" };
485
+ async probeRowWhileYielding(askId, budgetMs) {
486
+ if (!this.askStore || budgetMs <= 0)
487
+ return { kind: "none" };
488
+ const release = this.admitTerminalClaim(askId);
489
+ if (release === undefined)
490
+ return { kind: "denied" };
491
+ const raw = this.askStore.getAsk(askId);
492
+ void raw.then(release, release);
460
493
  let row;
461
494
  try {
462
- row = await this.withStoreDeadline(this.askStore.getAsk(askId), `getAsk(${where})`, DURABLE_CONVERGE_READ_TIMEOUT_MS);
495
+ row = await this.withStoreDeadline(raw, "getAsk(yield-probe)", Math.min(DURABLE_CONVERGE_READ_TIMEOUT_MS, budgetMs));
463
496
  }
464
497
  catch (err) {
465
- this.noteStoreError(err, `getAsk(${where})`);
466
- return { kind: "unreadable" };
498
+ this.noteStoreError(err, "getAsk(yield-probe)");
499
+ return { kind: "none" };
467
500
  }
468
- if (row === null)
501
+ if (row === null || row.state === "STREAM_PENDING")
502
+ return { kind: "none" };
503
+ return { kind: "row", current: projectAskTerminalClaimTruth(row) };
504
+ }
505
+ classifyClaimLoss(askId, current, _where) {
506
+ if (current.state === "absent")
469
507
  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));
508
+ const replayed = settleArgsOf(replayTerminalAskRow(current));
474
509
  if (replayed === "unavailable")
475
510
  return { kind: "parked" };
476
511
  if (!replayed.allowed)
@@ -571,10 +606,6 @@ export class ToolApprovalCoordinator {
571
606
  const entries = this.pendingByAskId.get(askId);
572
607
  if (!entries)
573
608
  return { settled: 0 };
574
- if (allowed && updatedInput === undefined) {
575
- if (this.guardRowDerivedApprove(askId, true, "notify-external-decision") === "unavailable")
576
- return { settled: 0 };
577
- }
578
609
  let settled = 0;
579
610
  for (const entry of [...entries]) {
580
611
  entry.settle(allowed, allowed ? "allowed" : "denied", updatedInput);
@@ -631,6 +662,7 @@ export class ToolApprovalCoordinator {
631
662
  if (p.targetCtxs?.has(ctx))
632
663
  p.onTargetGone?.(ctx);
633
664
  }
665
+ this.askAudit?.noteLegClosed(ctx.taskId);
634
666
  }
635
667
  }));
636
668
  }
@@ -782,7 +814,7 @@ export class ToolApprovalCoordinator {
782
814
  });
783
815
  if (existingOrCreated.row.state !== "STREAM_PENDING") {
784
816
  releaseAdmission();
785
- const replayed = this.guardRowDerivedApprove(askId, replayTerminalAskRow(existingOrCreated.row), "ensure-ask-idempotent-replay");
817
+ const replayed = replayTerminalAskRow(existingOrCreated.row);
786
818
  return replayed === "unavailable" ? this.unattendedOutcome() : replayed;
787
819
  }
788
820
  const persistedEnvelope = ApprovalCardEnvelopeSchema.safeParse(existingOrCreated.row.cardJson);
@@ -923,111 +955,159 @@ export class ToolApprovalCoordinator {
923
955
  releaseAdmission();
924
956
  if (selfEntry)
925
957
  selfEntry.settledOutcome = outcome;
958
+ this.askAudit?.recordSettle({
959
+ approvalId: wireApprovalId,
960
+ outcome,
961
+ ...(windowRouteUnavailable ? { parkRouted: true } : {}),
962
+ ...(hostSettledBy !== undefined ? { settledBy: hostSettledBy } : {}),
963
+ settledAtMs: Date.now(),
964
+ });
926
965
  resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}) });
927
966
  };
928
- const convergeFromDurableState = async (where) => {
929
- if (!this.askStore || askId === undefined)
967
+ const broadcastClaimTruth = (read) => {
968
+ if (askId === undefined)
930
969
  return;
931
- const pinnedAskId = askId;
932
- for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
933
- if (done)
970
+ switch (read.kind) {
971
+ case "approved":
972
+ this.notifyExternalDecision(askId, true, read.updatedInput);
934
973
  return;
935
- const read = await this.readRowForRecovery(pinnedAskId, `converge:${where}`);
936
- if (done)
974
+ case "denied":
975
+ this.notifyExternalDecision(askId, false);
937
976
  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
- }
977
+ case "parked":
978
+ this.settleSameAskIdParkRoute(askId);
979
+ return;
980
+ case "absent":
981
+ case "edit-in-flight":
982
+ return;
983
+ default: {
984
+ const unreachable = read;
985
+ throw new Error(`broadcastClaimTruth: unknown recovery read ${String(unreachable)}`);
962
986
  }
963
987
  }
964
- if (!done) {
965
- if (this.editedDecisionInFlight(pinnedAskId)) {
988
+ };
989
+ const settleFromClaimLoss = (read) => {
990
+ switch (read.kind) {
991
+ case "approved":
992
+ settle(true, "allowed", read.updatedInput);
993
+ broadcastClaimTruth(read);
994
+ return true;
995
+ case "denied":
996
+ settle(false, "denied");
997
+ broadcastClaimTruth(read);
998
+ return true;
999
+ case "parked":
966
1000
  windowRouteUnavailable = true;
967
1001
  settle(false, "expired");
968
- }
969
- else {
970
- settle(false, "expired");
1002
+ broadcastClaimTruth(read);
1003
+ return true;
1004
+ case "absent":
1005
+ case "edit-in-flight":
1006
+ return false;
1007
+ default: {
1008
+ const unreachable = read;
1009
+ throw new Error(`settleFromClaimLoss: unknown recovery read ${String(unreachable)}`);
971
1010
  }
972
1011
  }
973
1012
  };
974
- const recoverFromUnknownStore = async (arm, casStillInFlight) => {
1013
+ const lateClaimLossSettle = (current, intent) => {
1014
+ if (askId === undefined)
1015
+ return;
1016
+ const read = this.classifyClaimLoss(askId, current, `late:${intent}`);
1017
+ if (done) {
1018
+ broadcastClaimTruth(read);
1019
+ return;
1020
+ }
1021
+ settleFromClaimLoss(read);
1022
+ };
1023
+ const boundedClaim = (mk, label, timeoutMs, late) => {
1024
+ const release = this.admitTerminalClaim(askId ?? wireApprovalId);
1025
+ if (release === undefined)
1026
+ return undefined;
1027
+ const raw = mk();
1028
+ void raw.then(release, release);
1029
+ return this.withStoreDeadline(raw, label, timeoutMs, late);
1030
+ };
1031
+ const resolveLocalOutcome = async (arm) => {
975
1032
  if (askId === undefined) {
976
1033
  arm.settleByIntent();
977
1034
  return;
978
1035
  }
979
1036
  const pinnedAskId = askId;
980
- for (let attempt = 0; attempt <= DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
1037
+ let lastLoss;
1038
+ const ladderStartedAt = Date.now();
1039
+ const remainingMs = () => ladderStartedAt + TERMINAL_LADDER_WALL_MS - Date.now();
1040
+ const nap = async (ms) => {
1041
+ const slice = Math.min(ms, remainingMs());
1042
+ if (slice > 0)
1043
+ await new Promise((r) => setTimeout(r, slice).unref?.());
1044
+ };
1045
+ let attempt = 0;
1046
+ let claimsIssued = 0;
1047
+ let rejects = 0;
1048
+ while (attempt <= DURABLE_CONVERGE_BACKOFF_MS.length && remainingMs() > 0) {
981
1049
  if (done)
982
1050
  return;
983
- const read = await this.readRowForRecovery(pinnedAskId, `recovery:${arm.intent}`);
984
- if (done)
985
- return;
986
- switch (read.kind) {
987
- case "approved":
988
- settle(true, "allowed", read.updatedInput);
989
- return;
990
- case "denied":
991
- settle(false, "denied");
1051
+ const backoffAfter = attempt < DURABLE_CONVERGE_BACKOFF_MS.length ? DURABLE_CONVERGE_BACKOFF_MS[attempt] : 0;
1052
+ if (attempt > 0 && this.editedDecisionInFlight(pinnedAskId)) {
1053
+ const probed = await this.probeRowWhileYielding(pinnedAskId, remainingMs());
1054
+ if (done)
992
1055
  return;
993
- case "parked":
994
- windowRouteUnavailable = true;
995
- settle(false, "expired");
996
- 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);
1056
+ if (probed.kind === "denied") {
1057
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
1058
+ continue;
1059
+ }
1060
+ if (probed.kind === "row") {
1061
+ const read = this.classifyClaimLoss(pinnedAskId, probed.current, `yield-probe:${arm.intent}`);
1062
+ if (settleFromClaimLoss(read))
1013
1063
  return;
1014
- }
1015
- await convergeFromDurableState(arm.where);
1016
- return;
1064
+ if (read.kind === "edit-in-flight")
1065
+ lastLoss = probed.current;
1017
1066
  }
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?.());
1067
+ else if (this.terminalClaimsInFlightCount(pinnedAskId) > 0) {
1068
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
1025
1069
  continue;
1026
- default: {
1027
- const unreachable = read;
1028
- throw new Error(`recoverFromUnknownStore: unknown recovery read ${String(unreachable)}`);
1029
1070
  }
1071
+ await nap(backoffAfter);
1072
+ attempt++;
1073
+ continue;
1030
1074
  }
1075
+ const inFlight = arm.claim(Math.min(attempt === 0 ? DURABLE_CALL_TIMEOUT_MS : DURABLE_CONVERGE_READ_TIMEOUT_MS, remainingMs()));
1076
+ if (inFlight === undefined) {
1077
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
1078
+ continue;
1079
+ }
1080
+ claimsIssued++;
1081
+ let outcome;
1082
+ try {
1083
+ outcome = await inFlight;
1084
+ }
1085
+ catch (err) {
1086
+ this.noteStoreError(err, arm.storeLabel);
1087
+ rejects++;
1088
+ await nap(backoffAfter);
1089
+ attempt++;
1090
+ continue;
1091
+ }
1092
+ if (outcome.claimed) {
1093
+ arm.onWon(outcome);
1094
+ return;
1095
+ }
1096
+ arm.onLostTruth?.(outcome.current);
1097
+ const read = this.classifyClaimLoss(pinnedAskId, outcome.current, `claim:${arm.intent}`);
1098
+ if (done) {
1099
+ broadcastClaimTruth(read);
1100
+ return;
1101
+ }
1102
+ if (settleFromClaimLoss(read))
1103
+ return;
1104
+ if (read.kind === "absent") {
1105
+ arm.settleByIntent();
1106
+ return;
1107
+ }
1108
+ lastLoss = outcome.current;
1109
+ await nap(backoffAfter);
1110
+ attempt++;
1031
1111
  }
1032
1112
  if (done)
1033
1113
  return;
@@ -1036,24 +1116,10 @@ export class ToolApprovalCoordinator {
1036
1116
  settle(false, "expired");
1037
1117
  return;
1038
1118
  }
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);
1119
+ if (lastLoss !== undefined && settleFromClaimLoss(this.classifyClaimLoss(pinnedAskId, lastLoss, `claim-tail:${arm.intent}`)))
1050
1120
  return;
1051
- }
1052
- if (arm.won(result)) {
1053
- arm.onWon(result);
1054
- return;
1055
- }
1056
- await convergeFromDurableState(arm.where);
1121
+ recordFailOpen(ambiguousCasFailOpenTag(arm.intent), `askId=${pinnedAskId} attempts=${attempt} claims=${claimsIssued} rejects=${rejects} elapsedMs=${Date.now() - ladderStartedAt}`);
1122
+ arm.settleByIntent();
1057
1123
  };
1058
1124
  const settleParkRouteIfUnsettled = (hostSettledBy) => {
1059
1125
  if (done)
@@ -1072,22 +1138,22 @@ export class ToolApprovalCoordinator {
1072
1138
  const pinnedBatchId = batchId;
1073
1139
  await resolveLocalOutcome({
1074
1140
  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)
1141
+ storeLabel: "claimTerminal(window)",
1142
+ claim: (timeoutMs) => boundedClaim(() => store.claimTerminal(pinnedAskId, pinnedBatchId, "expire"), "claimTerminal(window)", timeoutMs, (late) => {
1143
+ if (!late.claimed) {
1144
+ lateClaimLossSettle(late.current, "expire");
1079
1145
  return;
1146
+ }
1080
1147
  this.settleSameAskIdParkRoute(pinnedAskId);
1081
1148
  this.settleVoidedSiblings(late.voidedSiblings);
1082
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, late.voidedSiblings, "superseded_by_park", Date.now()));
1149
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, late.voidedSiblings, "superseded_by_park", Date.now(), origin.taskId));
1083
1150
  }),
1084
- won: (res) => res.won,
1085
1151
  onWon: (res) => {
1086
1152
  windowRouteUnavailable = true;
1087
1153
  settle(false, "expired");
1088
1154
  this.settleSameAskIdParkRoute(pinnedAskId);
1089
1155
  this.settleVoidedSiblings(res.voidedSiblings);
1090
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, res.voidedSiblings, "superseded_by_park", Date.now()));
1156
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, res.voidedSiblings, "superseded_by_park", Date.now(), origin.taskId));
1091
1157
  },
1092
1158
  settleByIntent: () => settleParkRouteIfUnsettled(hostSelfReport),
1093
1159
  });
@@ -1116,24 +1182,31 @@ export class ToolApprovalCoordinator {
1116
1182
  const store = this.askStore;
1117
1183
  const pinnedAskId = askId;
1118
1184
  const pinnedBatchId = batchId;
1185
+ const cancelLostTruth = (current) => {
1186
+ if (current.state !== "VOID")
1187
+ return;
1188
+ this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1189
+ };
1119
1190
  await resolveLocalOutcome({
1120
1191
  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)
1192
+ storeLabel: "claimTerminal(cancel)",
1193
+ claim: (timeoutMs) => boundedClaim(() => store.claimTerminal(pinnedAskId, pinnedBatchId, "cancel"), "claimTerminal(cancel)", timeoutMs, (late) => {
1194
+ if (!late.claimed) {
1195
+ cancelLostTruth(late.current);
1196
+ lateClaimLossSettle(late.current, "cancel");
1125
1197
  return;
1198
+ }
1126
1199
  this.settleVoidedSiblings([pinnedAskId]);
1127
1200
  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()));
1201
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1129
1202
  }),
1130
- won: (won) => won,
1131
1203
  onWon: () => {
1132
1204
  settle(false, "expired");
1133
1205
  this.settleVoidedSiblings([pinnedAskId]);
1134
1206
  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()));
1207
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1136
1208
  },
1209
+ onLostTruth: cancelLostTruth,
1137
1210
  settleByIntent: () => settle(false, "expired"),
1138
1211
  });
1139
1212
  };
@@ -1193,6 +1266,18 @@ export class ToolApprovalCoordinator {
1193
1266
  }
1194
1267
  siblings.add(pendingEntry);
1195
1268
  }
1269
+ this.askAudit?.recordAsk({
1270
+ approvalId: wireApprovalId,
1271
+ taskId: origin.taskId,
1272
+ ...(req.sourceTaskId !== undefined ? { sourceTaskId: req.sourceTaskId } : {}),
1273
+ ...(origin.sessionId !== undefined ? { sessionId: origin.sessionId } : {}),
1274
+ owner: origin.owner,
1275
+ toolName: req.toolName,
1276
+ toolCallId: req.toolCallId,
1277
+ legTracked: !child && aliveCtxs.some((c) => c.taskId === origin.taskId),
1278
+ expiresAtMs: persistedExpiresAtMs,
1279
+ mintedAtMs: Date.now(),
1280
+ });
1196
1281
  const cardFrame = askId !== undefined && cardForFrame !== undefined
1197
1282
  ? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now() })
1198
1283
  : undefined;
@@ -1239,15 +1324,15 @@ export class ToolApprovalCoordinator {
1239
1324
  };
1240
1325
  await resolveLocalOutcome({
1241
1326
  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)
1327
+ storeLabel: "claimTerminal(emit-failed)",
1328
+ claim: (timeoutMs) => boundedClaim(() => store.claimTerminal(pinnedAskId, pinnedBatchId, "expire"), "claimTerminal(emit-failed)", timeoutMs, (late) => {
1329
+ if (!late.claimed) {
1330
+ lateClaimLossSettle(late.current, "unreachable");
1246
1331
  return;
1332
+ }
1247
1333
  this.settleSameAskIdParkRoute(pinnedAskId);
1248
1334
  this.settleVoidedSiblings(late.voidedSiblings);
1249
1335
  }),
1250
- won: (res) => res.won,
1251
1336
  onWon: (res) => {
1252
1337
  settleUnreachable();
1253
1338
  this.settleSameAskIdParkRoute(pinnedAskId);
@@ -1768,6 +1853,7 @@ export class ToolApprovalCoordinator {
1768
1853
  for (const [id, e] of this.pending) {
1769
1854
  if (scope !== undefined && e.owner !== scope)
1770
1855
  continue;
1856
+ const contentKind = contentKindOf(e.toolName);
1771
1857
  rows.push({
1772
1858
  approvalId: id,
1773
1859
  toolName: e.toolName,
@@ -1777,6 +1863,7 @@ export class ToolApprovalCoordinator {
1777
1863
  ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
1778
1864
  ...(e.governanceForced ? { governanceForced: true } : {}),
1779
1865
  ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
1866
+ ...(contentKind !== undefined ? { contentKind } : {}),
1780
1867
  });
1781
1868
  }
1782
1869
  return rows;
@@ -1841,13 +1928,6 @@ export class ToolApprovalCoordinator {
1841
1928
  }
1842
1929
  return { polled, settled, backlog, truncated };
1843
1930
  }
1844
- liveConsumerCount(askId) {
1845
- let n = 0;
1846
- for (const e of this.pendingByAskId.get(askId) ?? [])
1847
- if (e.settledOutcome === undefined)
1848
- n++;
1849
- return n;
1850
- }
1851
1931
  markEditedDecisionInFlight(askId) {
1852
1932
  this.editedDecisionsInFlight.set(askId, (this.editedDecisionsInFlight.get(askId) ?? 0) + 1);
1853
1933
  let released = false;
@@ -1866,12 +1946,6 @@ export class ToolApprovalCoordinator {
1866
1946
  if (released)
1867
1947
  return;
1868
1948
  defaultLogger.warn("approval_edited_decision_mark_expired", { askId, capMs: EDITED_DECISION_MARK_CAP_MS });
1869
- this.abandonedEditedDecisions.set(askId, Date.now() + ABANDONED_EDIT_TOMBSTONE_TTL_MS);
1870
- if (this.abandonedEditedDecisions.size > ABANDONED_EDIT_TOMBSTONE_MAX) {
1871
- const oldest = this.abandonedEditedDecisions.keys().next();
1872
- if (!oldest.done)
1873
- this.dropAbandonedEditTombstone(oldest.value, "capacity");
1874
- }
1875
1949
  drop();
1876
1950
  }, EDITED_DECISION_MARK_CAP_MS);
1877
1951
  watchdog.unref?.();
@@ -1883,47 +1957,9 @@ export class ToolApprovalCoordinator {
1883
1957
  editedDecisionInFlight(askId) {
1884
1958
  return (this.editedDecisionsInFlight.get(askId) ?? 0) > 0;
1885
1959
  }
1886
- abandonedEditedDecisionPending(askId) {
1887
- const until = this.abandonedEditedDecisions.get(askId);
1888
- if (until === undefined)
1889
- return false;
1890
- if (Date.now() > until) {
1891
- this.dropAbandonedEditTombstone(askId, "ttl");
1892
- return true;
1893
- }
1894
- return true;
1895
- }
1896
- guardRowDerivedApprove(askId, outcome, where) {
1897
- if (outcome !== true || askId === undefined)
1898
- return outcome;
1899
- if (!this.abandonedEditedDecisionPending(askId))
1900
- return outcome;
1901
- defaultLogger.warn("approval_row_derived_approve_parked_after_abandoned_edit", { askId, where });
1902
- this.settleSameAskIdParkRoute(askId);
1903
- return "unavailable";
1904
- }
1905
- dropAbandonedEditTombstone(askId, why) {
1906
- const stillWaiting = this.liveConsumerCount(askId);
1907
- if (stillWaiting > 0) {
1908
- defaultLogger.warn("approval_abandoned_edit_tombstone_dropped_parking_consumers", { askId, why, parked: stillWaiting });
1909
- this.settleSameAskIdParkRoute(askId);
1910
- }
1911
- this.abandonedEditedDecisions.delete(askId);
1912
- }
1913
1960
  expireEditedDecisionMarkForTest(askId) {
1914
- this.abandonedEditedDecisions.set(askId, Date.now() + ABANDONED_EDIT_TOMBSTONE_TTL_MS);
1915
1961
  this.editedDecisionsInFlight.delete(askId);
1916
1962
  }
1917
- forceAbandonedEditTombstoneExpiryForTest(askId) {
1918
- if (!this.abandonedEditedDecisions.has(askId))
1919
- return;
1920
- this.abandonedEditedDecisions.set(askId, Date.now() - 1);
1921
- this.abandonedEditedDecisionPending(askId);
1922
- }
1923
- setAbandonedEditTombstoneDeadlineForTest(askId, atMs) {
1924
- if (this.abandonedEditedDecisions.has(askId))
1925
- this.abandonedEditedDecisions.set(askId, atMs);
1926
- }
1927
1963
  pendingCount() {
1928
1964
  return this.pending.size;
1929
1965
  }
@@ -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.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",
@@ -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",