@sema-agent/server 7.53.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 (72) hide show
  1. package/USAGE.md +5 -0
  2. package/dist/approval-ask-audit-store.d.ts +129 -0
  3. package/dist/approval-ask-audit-store.js +284 -0
  4. package/dist/approval-card.d.ts +18 -6
  5. package/dist/approval-card.js +2 -2
  6. package/dist/approval-reconciler.d.ts +2 -1
  7. package/dist/approval-reconciler.js +1 -1
  8. package/dist/boot/config-center.js +2 -2
  9. package/dist/boot/coordinators.d.ts +2 -0
  10. package/dist/boot/coordinators.js +18 -1
  11. package/dist/boot/leader.js +2 -0
  12. package/dist/boot/reapers.d.ts +2 -1
  13. package/dist/boot/resolve-spec.js +1 -3
  14. package/dist/boot/runner-deps.d.ts +20 -0
  15. package/dist/boot/runner-deps.js +14 -8
  16. package/dist/boot/shutdown.js +1 -1
  17. package/dist/boot/stores.js +8 -1
  18. package/dist/capabilities/team.d.ts +10 -9
  19. package/dist/config-center/apply-effective.d.ts +25 -26
  20. package/dist/config-center/apply-effective.js +28 -66
  21. package/dist/config-center/effective-keys.d.ts +30 -0
  22. package/dist/config-center/effective-keys.js +49 -0
  23. package/dist/config-center/facade.d.ts +3 -3
  24. package/dist/config-center/facade.js +1 -1
  25. package/dist/config-center/restart-signal.js +3 -9
  26. package/dist/config-center/types.d.ts +19 -40
  27. package/dist/config-lkg.js +7 -1
  28. package/dist/config-provider.js +1 -1
  29. package/dist/device-enrollment.d.ts +5 -3
  30. package/dist/device-store.d.ts +81 -3
  31. package/dist/device-store.js +37 -0
  32. package/dist/device-ws-hub.d.ts +15 -2
  33. package/dist/device-ws-hub.js +31 -5
  34. package/dist/http/routes/approvals-assistant.js +1 -0
  35. package/dist/http/routes/devices.d.ts +64 -0
  36. package/dist/http/routes/devices.js +173 -0
  37. package/dist/http/server.d.ts +14 -0
  38. package/dist/http/server.js +32 -2
  39. package/dist/http/wire-types.d.ts +19 -0
  40. package/dist/leader/wire.d.ts +12 -0
  41. package/dist/leader/wire.js +6 -5
  42. package/dist/main.js +3 -1
  43. package/dist/observability/fail-open.d.ts +20 -0
  44. package/dist/observability/fail-open.js +20 -0
  45. package/dist/plugins/approval-ask-store-memory.d.ts +11 -1
  46. package/dist/plugins/approval-ask-store-memory.js +20 -3
  47. package/dist/plugins/approval-ask-store-sql.d.ts +82 -0
  48. package/dist/plugins/approval-ask-store-sql.js +41 -10
  49. package/dist/plugins/device-store-sql.d.ts +38 -1
  50. package/dist/plugins/device-store-sql.js +82 -2
  51. package/dist/plugins/pg-pool.d.ts +29 -2
  52. package/dist/plugins/pg-pool.js +45 -2
  53. package/dist/plugins/remote-env-device.d.ts +4 -2
  54. package/dist/plugins/remote-env-device.js +12 -2
  55. package/dist/plugins/roster-store-sql.d.ts +1 -3
  56. package/dist/plugins/sql-errors.d.ts +12 -0
  57. package/dist/plugins/sql-errors.js +10 -0
  58. package/dist/plugins/store-backend.js +1 -1
  59. package/dist/plugins/tidb-pool.d.ts +13 -0
  60. package/dist/runs.js +1 -0
  61. package/dist/runtime-governance.d.ts +4 -0
  62. package/dist/runtime-governance.js +23 -1
  63. package/dist/tool-approval.d.ts +63 -39
  64. package/dist/tool-approval.js +322 -109
  65. package/dist/trace/engine-notice-wire.d.ts +1 -1
  66. package/dist/trace/engine-notice-wire.js +2 -0
  67. package/dist/trace/injection-tier.d.ts +11 -21
  68. package/dist/trace/injection-tier.js +3 -4
  69. package/dist/trace/ledger-events.d.ts +9 -0
  70. package/dist/trace/project.d.ts +1 -0
  71. package/dist/trace/project.js +1 -0
  72. package/package.json +3 -3
@@ -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,26 @@ 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;
53
+ function ambiguousCasFailOpenTag(intent) {
54
+ switch (intent) {
55
+ case "expire":
56
+ return "server.approval.ambiguous-cas.expire";
57
+ case "cancel":
58
+ return "server.approval.ambiguous-cas.cancel";
59
+ case "unreachable":
60
+ return "server.approval.ambiguous-cas.unreachable";
61
+ default: {
62
+ const unreachable = intent;
63
+ throw new Error(`ambiguousCasFailOpenTag: unknown intent ${String(unreachable)}`);
64
+ }
65
+ }
66
+ }
49
67
  class ApprovalStoreDeadlineError extends Error {
50
68
  constructor(label, timeoutMs) {
51
69
  super(`approval store call ${label} timed out after ${timeoutMs}ms`);
@@ -214,6 +232,30 @@ function settleArgsOf(outcome) {
214
232
  export class ToolApprovalCoordinator {
215
233
  als = new AsyncLocalStorage();
216
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
+ }
217
259
  pendingByAskId = new Map();
218
260
  allowAllSessions = new Set();
219
261
  streams = new Map();
@@ -239,6 +281,7 @@ export class ToolApprovalCoordinator {
239
281
  parkTombstoneTtlMs;
240
282
  parkTombstonePerPrincipalMax;
241
283
  parkedRedeem;
284
+ askAudit;
242
285
  constructor(opts) {
243
286
  this.ruleConsent = opts?.ruleConsent;
244
287
  this.ruleScopeRootFor = opts?.ruleScopeRootFor;
@@ -254,6 +297,7 @@ export class ToolApprovalCoordinator {
254
297
  this.parkTombstoneMax = opts?.parkTombstoneMax ?? DEFAULT_PARK_TOMBSTONE_MAX;
255
298
  this.parkTombstoneTtlMs = opts?.parkTombstoneTtlMs ?? DEFAULT_PARK_TOMBSTONE_TTL_MS;
256
299
  this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
300
+ this.askAudit = opts?.askAudit;
257
301
  }
258
302
  unattendedOutcome() {
259
303
  return this.unattendedPolicy === "deny" ? false : "unavailable";
@@ -440,26 +484,39 @@ export class ToolApprovalCoordinator {
440
484
  return false;
441
485
  }
442
486
  }
443
- async readDecidedForRecovery(askId) {
444
- if (!this.askStore)
445
- return undefined;
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);
495
+ let row;
446
496
  try {
447
- const row = await this.withStoreDeadline(this.askStore.getAsk(askId), "getAsk(expire-error-recovery)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
448
- if (row === null || row.state !== "DECIDED")
449
- return undefined;
450
- if (replayTerminalAskRow(row) === true && this.abandonedEditedDecisionPending(askId)) {
451
- this.guardRowDerivedApprove(askId, true, "expire-error-recovery");
452
- return undefined;
453
- }
454
- if (this.editedDecisionInFlight(askId))
455
- return undefined;
456
- const replayed = settleArgsOf(replayTerminalAskRow(row));
457
- return replayed === "unavailable" ? undefined : replayed;
497
+ row = await this.withStoreDeadline(raw, "getAsk(yield-probe)", Math.min(DURABLE_CONVERGE_READ_TIMEOUT_MS, budgetMs));
458
498
  }
459
499
  catch (err) {
460
- this.noteStoreError(err, "getAsk(expire-error-recovery)");
461
- return undefined;
500
+ this.noteStoreError(err, "getAsk(yield-probe)");
501
+ return { kind: "none" };
462
502
  }
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")
509
+ return { kind: "absent" };
510
+ const replayed = settleArgsOf(this.guardRowDerivedApprove(askId, replayTerminalAskRow(current), where));
511
+ if (replayed === "unavailable")
512
+ return { kind: "parked" };
513
+ if (!replayed.allowed)
514
+ return { kind: "denied" };
515
+ if (replayed.updatedInput !== undefined)
516
+ return { kind: "approved", updatedInput: replayed.updatedInput };
517
+ if (this.editedDecisionInFlight(askId))
518
+ return { kind: "edit-in-flight" };
519
+ return { kind: "approved" };
463
520
  }
464
521
  hasOtherSuspendedOwner(askId, self) {
465
522
  if (askId === undefined)
@@ -490,13 +547,17 @@ export class ToolApprovalCoordinator {
490
547
  const now = Date.now();
491
548
  let ownCount = 0;
492
549
  let ownExpiredVictim;
550
+ let ownBridgeVictim;
493
551
  let ownPlainVictim;
494
552
  let ownEvictable;
495
553
  let globalExpiredVictim;
554
+ let globalBridgeVictim;
496
555
  for (const [k, t] of this.parkTombstones) {
497
556
  const expired = now - t.parkedAtMs > this.parkTombstoneTtlMs;
498
557
  if (globalExpiredVictim === undefined && expired && t.inflight === undefined)
499
558
  globalExpiredVictim = k;
559
+ if (globalBridgeVictim === undefined && t.kind === "voidBridge" && t.inflight === undefined)
560
+ globalBridgeVictim = k;
500
561
  if (t.owner !== tomb.owner)
501
562
  continue;
502
563
  ownCount++;
@@ -506,13 +567,16 @@ export class ToolApprovalCoordinator {
506
567
  ownEvictable = k;
507
568
  if (ownExpiredVictim === undefined && expired)
508
569
  ownExpiredVictim = k;
509
- if (ownPlainVictim === undefined && t.redeemed === undefined)
570
+ if (ownBridgeVictim === undefined && t.kind === "voidBridge")
571
+ ownBridgeVictim = k;
572
+ if (ownPlainVictim === undefined && t.kind === "park" && t.redeemed === undefined)
510
573
  ownPlainVictim = k;
511
574
  }
575
+ const insertingBridge = tomb.kind === "voidBridge";
512
576
  if (ownCount >= effectivePerPrincipalMax) {
513
- const victim = ownExpiredVictim ?? ownPlainVictim ?? ownEvictable;
577
+ const victim = insertingBridge ? (ownExpiredVictim ?? ownBridgeVictim) : (ownExpiredVictim ?? ownBridgeVictim ?? ownPlainVictim ?? ownEvictable);
514
578
  if (victim === undefined) {
515
- recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"} reason=all-inflight`);
579
+ recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"} reason=${insertingBridge ? "bridge-would-evict-redeemable" : "all-inflight"}`);
516
580
  return;
517
581
  }
518
582
  this.parkTombstones.delete(victim);
@@ -520,8 +584,11 @@ export class ToolApprovalCoordinator {
520
584
  else if (this.parkTombstones.size >= this.parkTombstoneMax && globalExpiredVictim !== undefined) {
521
585
  this.parkTombstones.delete(globalExpiredVictim);
522
586
  }
587
+ else if (this.parkTombstones.size >= this.parkTombstoneMax && !insertingBridge && globalBridgeVictim !== undefined) {
588
+ this.parkTombstones.delete(globalBridgeVictim);
589
+ }
523
590
  else if (this.parkTombstones.size >= this.parkTombstoneMax) {
524
- recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"}`);
591
+ recordFailOpen("server.approval.park-tombstone-global-cap-refused", `owner=${tomb.owner ?? "<none>"}${insertingBridge ? " reason=bridge-refused-at-global-cap" : ""}`);
525
592
  return;
526
593
  }
527
594
  this.parkTombstones.set(approvalId, tomb);
@@ -601,6 +668,7 @@ export class ToolApprovalCoordinator {
601
668
  if (p.targetCtxs?.has(ctx))
602
669
  p.onTargetGone?.(ctx);
603
670
  }
671
+ this.askAudit?.noteLegClosed(ctx.taskId);
604
672
  }
605
673
  }));
606
674
  }
@@ -878,7 +946,7 @@ export class ToolApprovalCoordinator {
878
946
  sig.removeEventListener("abort", fn);
879
947
  this.pending.delete(id);
880
948
  if (windowRouteUnavailable && askId !== undefined && batchId !== undefined && this.unattendedOutcome() !== false) {
881
- this.recordParkTombstone(wireApprovalId, { askId, batchId, owner: origin.owner, parkedAtMs: Date.now() });
949
+ this.recordParkTombstone(wireApprovalId, { kind: "park", askId, batchId, owner: origin.owner, parkedAtMs: Date.now() });
882
950
  parkRouted = true;
883
951
  }
884
952
  if (askId !== undefined) {
@@ -893,55 +961,171 @@ export class ToolApprovalCoordinator {
893
961
  releaseAdmission();
894
962
  if (selfEntry)
895
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
+ });
896
971
  resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}) });
897
972
  };
898
- const convergeFromDurableState = async (where) => {
899
- if (!this.askStore || askId === undefined)
973
+ const broadcastClaimTruth = (read) => {
974
+ if (askId === undefined)
900
975
  return;
901
- const store = this.askStore;
976
+ switch (read.kind) {
977
+ case "approved":
978
+ this.notifyExternalDecision(askId, true, read.updatedInput);
979
+ return;
980
+ case "denied":
981
+ this.notifyExternalDecision(askId, false);
982
+ return;
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)}`);
992
+ }
993
+ }
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":
1006
+ windowRouteUnavailable = true;
1007
+ 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)}`);
1016
+ }
1017
+ }
1018
+ };
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) => {
1038
+ if (askId === undefined) {
1039
+ arm.settleByIntent();
1040
+ return;
1041
+ }
902
1042
  const pinnedAskId = askId;
903
- for (let attempt = 0; attempt < DURABLE_CONVERGE_BACKOFF_MS.length; attempt++) {
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) {
904
1055
  if (done)
905
1056
  return;
906
- try {
907
- const row = await this.withStoreDeadline(store.getAsk(pinnedAskId), `getAsk(${where})`, DURABLE_CONVERGE_READ_TIMEOUT_MS);
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());
908
1060
  if (done)
909
1061
  return;
910
- if (row === null) {
911
- settle(false, "expired");
912
- return;
913
- }
914
- if (row.state === "STREAM_PENDING" || this.editedDecisionInFlight(pinnedAskId)) {
915
- await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
1062
+ if (probed.kind === "denied") {
1063
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
916
1064
  continue;
917
1065
  }
918
- const outcome = settleArgsOf(this.guardRowDerivedApprove(pinnedAskId, replayTerminalAskRow(row), `converge:${where}`));
919
- if (outcome === "unavailable") {
920
- windowRouteUnavailable = true;
921
- settle(false, "expired");
922
- }
923
- else if (outcome.allowed) {
924
- settle(true, "allowed", outcome.updatedInput);
1066
+ if (probed.kind === "row") {
1067
+ const read = this.classifyClaimLoss(pinnedAskId, probed.current, `yield-probe:${arm.intent}`);
1068
+ if (settleFromClaimLoss(read))
1069
+ return;
1070
+ if (read.kind === "edit-in-flight")
1071
+ lastLoss = probed.current;
925
1072
  }
926
- else {
927
- settle(false, "denied");
1073
+ else if (this.terminalClaimsInFlightCount(pinnedAskId) > 0) {
1074
+ await nap(TERMINAL_CLAIM_CAP_POLL_MS);
1075
+ continue;
928
1076
  }
929
- return;
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;
930
1090
  }
931
1091
  catch (err) {
932
- this.noteStoreError(err, `getAsk(${where})`);
933
- await new Promise((r) => setTimeout(r, DURABLE_CONVERGE_BACKOFF_MS[attempt]).unref?.());
1092
+ this.noteStoreError(err, arm.storeLabel);
1093
+ rejects++;
1094
+ await nap(backoffAfter);
1095
+ attempt++;
1096
+ continue;
934
1097
  }
935
- }
936
- if (!done) {
937
- if (this.editedDecisionInFlight(pinnedAskId)) {
938
- windowRouteUnavailable = true;
939
- settle(false, "expired");
1098
+ if (outcome.claimed) {
1099
+ arm.onWon(outcome);
1100
+ return;
940
1101
  }
941
- else {
942
- settle(false, "expired");
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;
943
1113
  }
1114
+ lastLoss = outcome.current;
1115
+ await nap(backoffAfter);
1116
+ attempt++;
944
1117
  }
1118
+ if (done)
1119
+ return;
1120
+ if (this.editedDecisionInFlight(pinnedAskId)) {
1121
+ windowRouteUnavailable = true;
1122
+ settle(false, "expired");
1123
+ return;
1124
+ }
1125
+ if (lastLoss !== undefined && settleFromClaimLoss(this.classifyClaimLoss(pinnedAskId, lastLoss, `claim-tail:${arm.intent}`)))
1126
+ return;
1127
+ recordFailOpen(ambiguousCasFailOpenTag(arm.intent), `askId=${pinnedAskId} attempts=${attempt} claims=${claimsIssued} rejects=${rejects} elapsedMs=${Date.now() - ladderStartedAt}`);
1128
+ arm.settleByIntent();
945
1129
  };
946
1130
  const settleParkRouteIfUnsettled = (hostSettledBy) => {
947
1131
  if (done)
@@ -955,34 +1139,30 @@ export class ToolApprovalCoordinator {
955
1139
  settleParkRouteIfUnsettled(hostSelfReport);
956
1140
  return;
957
1141
  }
958
- try {
959
- const res = await this.withStoreDeadline(this.askStore.expireAsk(askId, batchId), "expireAsk(window)", DURABLE_CALL_TIMEOUT_MS, (late) => {
960
- if (!late.won)
1142
+ const store = this.askStore;
1143
+ const pinnedAskId = askId;
1144
+ const pinnedBatchId = batchId;
1145
+ await resolveLocalOutcome({
1146
+ intent: "expire",
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");
961
1151
  return;
962
- this.settleSameAskIdParkRoute(askId);
1152
+ }
1153
+ this.settleSameAskIdParkRoute(pinnedAskId);
963
1154
  this.settleVoidedSiblings(late.voidedSiblings);
964
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(batchId, late.voidedSiblings, "superseded_by_park", Date.now()));
965
- });
966
- if (res.won) {
1155
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, late.voidedSiblings, "superseded_by_park", Date.now(), origin.taskId));
1156
+ }),
1157
+ onWon: (res) => {
967
1158
  windowRouteUnavailable = true;
968
1159
  settle(false, "expired");
969
- this.settleSameAskIdParkRoute(askId);
1160
+ this.settleSameAskIdParkRoute(pinnedAskId);
970
1161
  this.settleVoidedSiblings(res.voidedSiblings);
971
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(batchId, res.voidedSiblings, "superseded_by_park", Date.now()));
972
- }
973
- else {
974
- await convergeFromDurableState("windowExpired");
975
- }
976
- }
977
- catch (err) {
978
- this.noteStoreError(err, "expireAsk");
979
- const decided = await this.readDecidedForRecovery(askId);
980
- if (decided !== undefined && !done) {
981
- settle(decided.allowed, decided.allowed ? "allowed" : "denied", decided.updatedInput);
982
- return;
983
- }
984
- settleParkRouteIfUnsettled(hostSelfReport);
985
- }
1162
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, res.voidedSiblings, "superseded_by_park", Date.now(), origin.taskId));
1163
+ },
1164
+ settleByIntent: () => settleParkRouteIfUnsettled(hostSelfReport),
1165
+ });
986
1166
  })();
987
1167
  };
988
1168
  const forceParkNow = () => {
@@ -1005,26 +1185,36 @@ export class ToolApprovalCoordinator {
1005
1185
  settle(false, "expired");
1006
1186
  return;
1007
1187
  }
1008
- try {
1009
- const won = await this.withStoreDeadline(this.askStore.transitionAsk(askId, "STREAM_PENDING", "VOID", { updatedAtMs: Date.now() }), "transitionAsk(cancel)", DURABLE_CALL_TIMEOUT_MS, (lateWon) => {
1010
- if (!lateWon)
1188
+ const store = this.askStore;
1189
+ const pinnedAskId = askId;
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
+ };
1196
+ await resolveLocalOutcome({
1197
+ intent: "cancel",
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");
1011
1203
  return;
1012
- this.settleVoidedSiblings([askId]);
1013
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(batchId, [askId], "aborted", Date.now()));
1014
- });
1015
- if (won) {
1204
+ }
1205
+ this.settleVoidedSiblings([pinnedAskId]);
1206
+ this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1207
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1208
+ }),
1209
+ onWon: () => {
1016
1210
  settle(false, "expired");
1017
- this.settleVoidedSiblings([askId]);
1018
- this.emitRevokeTo(aliveCtxs, buildRevokeFrame(batchId, [askId], "aborted", Date.now()));
1019
- }
1020
- else {
1021
- await convergeFromDurableState("runCancel");
1022
- }
1023
- }
1024
- catch (err) {
1025
- this.noteStoreError(err, "transitionAsk(cancel)");
1026
- settle(false, "expired");
1027
- }
1211
+ this.settleVoidedSiblings([pinnedAskId]);
1212
+ this.recordParkTombstone(wireApprovalId, { kind: "voidBridge", askId: pinnedAskId, batchId: pinnedBatchId, owner: origin.owner, parkedAtMs: Date.now() });
1213
+ this.emitRevokeTo(aliveCtxs, buildRevokeFrame(pinnedBatchId, [pinnedAskId], "aborted", Date.now(), origin.taskId));
1214
+ },
1215
+ onLostTruth: cancelLostTruth,
1216
+ settleByIntent: () => settle(false, "expired"),
1217
+ });
1028
1218
  };
1029
1219
  if (signal) {
1030
1220
  const onTaskAbort = () => void runCancel();
@@ -1082,6 +1272,18 @@ export class ToolApprovalCoordinator {
1082
1272
  }
1083
1273
  siblings.add(pendingEntry);
1084
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
+ });
1085
1287
  const cardFrame = askId !== undefined && cardForFrame !== undefined
1086
1288
  ? buildApprovalRequestFrame({ askId, taskId: origin.taskId, approvalId: wireApprovalId, card: cardForFrame, expiresAtMs: persistedExpiresAtMs, nowMs: Date.now() })
1087
1289
  : undefined;
@@ -1114,31 +1316,42 @@ export class ToolApprovalCoordinator {
1114
1316
  const emitAllP = (async () => {
1115
1317
  const results = await Promise.all(openByCtx.values());
1116
1318
  const anySucceeded = reachability === "unreached" || this.hasOtherSuspendedOwner(askId, selfEntry) || results.some((r) => r.legacy || r.card);
1319
+ emitSettled = true;
1117
1320
  if (!anySucceeded && !done) {
1118
1321
  if (this.askStore && askId !== undefined && batchId !== undefined) {
1119
- try {
1120
- const res = await this.withStoreDeadline(this.askStore.expireAsk(askId, batchId), "expireAsk(emit-failed)");
1121
- if (res.won && !done) {
1122
- emitFailed = true;
1123
- settle(false, "expired");
1124
- this.settleSameAskIdParkRoute(askId);
1322
+ const store = this.askStore;
1323
+ const pinnedAskId = askId;
1324
+ const pinnedBatchId = batchId;
1325
+ const settleUnreachable = () => {
1326
+ if (done)
1327
+ return;
1328
+ emitFailed = true;
1329
+ settle(false, "expired");
1330
+ };
1331
+ await resolveLocalOutcome({
1332
+ intent: "unreachable",
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");
1337
+ return;
1338
+ }
1339
+ this.settleSameAskIdParkRoute(pinnedAskId);
1340
+ this.settleVoidedSiblings(late.voidedSiblings);
1341
+ }),
1342
+ onWon: (res) => {
1343
+ settleUnreachable();
1344
+ this.settleSameAskIdParkRoute(pinnedAskId);
1125
1345
  this.settleVoidedSiblings(res.voidedSiblings);
1126
- }
1127
- }
1128
- catch (err) {
1129
- this.noteStoreError(err, "expireAsk(emit-failed)");
1130
- if (!done) {
1131
- emitFailed = true;
1132
- settle(false, "expired");
1133
- }
1134
- }
1346
+ },
1347
+ settleByIntent: settleUnreachable,
1348
+ });
1135
1349
  }
1136
1350
  else if (!done) {
1137
1351
  emitFailed = true;
1138
1352
  settle(false, "expired");
1139
1353
  }
1140
1354
  }
1141
- emitSettled = true;
1142
1355
  })();
1143
1356
  await Promise.race([emitAllP, allowedP]);
1144
1357
  const settled = await allowedP;
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
22
22
  export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
@@ -13,6 +13,8 @@ export const ENGINE_NOTICE_WIRE_CODES = [
13
13
  "task.user_followup_undrained",
14
14
  "memory.capture_opted_out",
15
15
  "memory.capture_optout_unpersisted",
16
+ "task.halt_unconsumed",
17
+ "task.late_approval",
16
18
  ];
17
19
  const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
18
20
  export function isEngineNoticeWireCode(code) {
@@ -1,27 +1,17 @@
1
1
  /**
2
- * [ref] 注入档位(core 5.61.0)的**闭集单点**([ref])
2
+ * [ref] 注入档位(core 5.61.0)的**闭集单点**([ref])——2026-08-31 起改为 **core 根导出直调**。
3
3
  *
4
- * ## 为什么本仓要自己留一份词表
5
- * 理想写法是复用 core 的谓词 `isSystemInjectionPriority` / 数据表 `SYSTEM_INJECTION_PRIORITIES`。
6
- * core 5.61.0 **没有把它们放进根 barrel**(`dist/index.d.ts` 只导出了 `SystemInjectionPriority`
7
- * 这个**类型**),而 `@sema-agent/core` 的 `package.json#exports` 只开了 `.` / `./bench` / `./fixtures`
8
- * 三口 深导入被 exports map 关死,结构上够不着运行期的那份。
9
- *
10
- * 于是用本仓既有的**双向锚**姿势(`http/routes/notify-wake.ts` 的 `NOTIFY_STATUSES` 先例):
11
- * · `satisfies` 管「表内不许有词表外的词」—— core 删/改档位 ⇒ 编译红;
12
- * · 穷举断言管「词表不许有表外的词」—— core 加档位 ⇒ 编译红。
13
- * 两条合起来 = 上游任何一侧的增删都在编译期被逼着处置,而不是运行时静默漂。
14
- *
15
- * ## 为什么单独一个叶子模块
16
- * 消费点有两个且分属两条依赖链:`trace/project.ts`(wire 投影)与
17
- * `orchestration/workflow-completion-inbox.ts`(durable park 载荷)。手抄两份就是值拷贝漂移源
18
- * (本仓 [ref] 起的老病族);把它挂在任一方又会在两条链之间拉一条没必要的边。零依赖叶子最干净。
19
- *
20
- * 🔴 上游哪天补了根导出:删掉本文件,两处改成一行谓词调用。
4
+ * ## 收口条款兑现([ref] PM 审计④「core 前提句过期族」)
5
+ * 本文件 5.61 时代的顶注写着「core 没有把 `SYSTEM_INJECTION_PRIORITIES` / `isSystemInjectionPriority`
6
+ * 放进根 barrel 本仓自留双向锚词表;上游哪天补了根导出:删掉本文件,两处改成一行谓词调用」。
7
+ * core 7.0.2 已把**表和谓词都放进根导出**(运行期亲证:`SYSTEM_INJECTION_PRIORITIES` 数组 +
8
+ * `isSystemInjectionPriority` 函数均可 require 到)——按条款执行:自留词表退役,改直调;本文件保留为
9
+ * 两个消费点(`trace/project.ts` / `orchestration/workflow-completion-inbox.ts`)的公共缝(语义注 +
10
+ * `injectionTierOrAbsent` 的丢键方向条款仍是本仓资产,不随词表走)
21
11
  */
22
- import type { SystemInjectionPriority } from "@sema-agent/core";
23
- /** 三档闭集,与 core `SystemInjectionPriority` 双向锚(见顶注)。 */
24
- export declare const INJECTION_TIERS: readonly ["now", "next", "later"];
12
+ import { type SystemInjectionPriority } from "@sema-agent/core";
13
+ /** 三档闭集 —— 现在就是 core 的那份表(单源;保留导出名,两个消费点与既有测试不动)。 */
14
+ export declare const INJECTION_TIERS: readonly SystemInjectionPriority[];
25
15
  /**
26
16
  * 边界守卫 —— 闭集内的值原样返回,**其余一律 `undefined`**(= 缺席)。
27
17
  *