@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.
- package/MIGRATION.md +3 -3
- package/README.md +12 -6
- package/README.zh-CN.md +9 -5
- package/USAGE.md +108 -52
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-ask-audit-store.d.ts +129 -0
- package/dist/approval-ask-audit-store.js +284 -0
- package/dist/approval-card.d.ts +18 -6
- package/dist/approval-card.js +2 -2
- package/dist/approval-content-kind.d.ts +22 -0
- package/dist/approval-content-kind.js +5 -0
- package/dist/approval-reconciler.d.ts +2 -1
- package/dist/approval-reconciler.js +1 -1
- package/dist/boot/coordinators.d.ts +2 -0
- package/dist/boot/coordinators.js +18 -1
- package/dist/boot/leader.d.ts +21 -0
- package/dist/boot/leader.js +6 -0
- package/dist/boot/reapers.d.ts +2 -1
- package/dist/boot/runtime-caps.d.ts +2 -1
- package/dist/boot/runtime-caps.js +3 -1
- package/dist/boot/stores.js +8 -1
- package/dist/config-center/types.d.ts +2 -1
- package/dist/config-provider.js +1 -0
- package/dist/device-store.d.ts +66 -2
- package/dist/device-store.js +35 -0
- package/dist/device-ws-hub.d.ts +8 -0
- package/dist/device-ws-hub.js +6 -0
- package/dist/http/route-ctx.d.ts +15 -3
- package/dist/http/routes/approvals-assistant.js +3 -2
- package/dist/http/routes/devices.d.ts +64 -0
- package/dist/http/routes/devices.js +173 -0
- package/dist/http/routes/leader.js +2 -2
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.d.ts +14 -0
- package/dist/http/server.js +30 -5
- package/dist/leader/endpoint.js +5 -4
- package/dist/leader/wire.d.ts +50 -0
- package/dist/leader/wire.js +19 -7
- package/dist/main.js +4 -2
- package/dist/observability/fail-open.d.ts +7 -3
- package/dist/observability/fail-open.js +7 -3
- package/dist/plugins/approval-ask-store-memory.d.ts +11 -1
- package/dist/plugins/approval-ask-store-memory.js +20 -3
- package/dist/plugins/approval-ask-store-sql.d.ts +82 -0
- package/dist/plugins/approval-ask-store-sql.js +41 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
- package/dist/plugins/checkpoint-store-sql.js +4 -1
- package/dist/plugins/device-store-sql.d.ts +38 -1
- package/dist/plugins/device-store-sql.js +82 -2
- package/dist/plugins/file-run-store.js +2 -0
- package/dist/plugins/local-checkpoint-store.js +2 -0
- package/dist/plugins/local-session-store.d.ts +9 -9
- package/dist/plugins/local-session-store.js +5 -3
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/permission-rule-store-file.d.ts +22 -4
- package/dist/plugins/permission-rule-store-file.js +17 -8
- package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
- package/dist/plugins/permission-rule-store-sql.js +23 -13
- package/dist/plugins/pg-session-storage.js +17 -13
- package/dist/plugins/remote-env-device.js +8 -1
- package/dist/plugins/run-store-sql.js +6 -4
- package/dist/plugins/sql-errors.d.ts +12 -0
- package/dist/plugins/sql-errors.js +10 -0
- package/dist/plugins/store-contracts.d.ts +4 -0
- package/dist/plugins/tidb-session-store.js +18 -14
- package/dist/rules-consent.d.ts +5 -4
- package/dist/rules-consent.js +5 -33
- package/dist/runs.js +1 -0
- package/dist/runtime-caps-resolver.js +3 -1
- package/dist/security.d.ts +7 -0
- package/dist/tool-approval.d.ts +66 -83
- package/dist/tool-approval.js +222 -186
- package/dist/trace/ledger-events.d.ts +9 -0
- package/package.json +3 -3
- package/skills/find-skills.md +1 -1
- package/skills/loop.md +1 -1
package/dist/tool-approval.js
CHANGED
|
@@ -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
|
|
458
|
-
if (!this.askStore)
|
|
459
|
-
return { kind: "
|
|
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(
|
|
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,
|
|
466
|
-
return { kind: "
|
|
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
|
-
|
|
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 =
|
|
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
|
|
929
|
-
if (
|
|
967
|
+
const broadcastClaimTruth = (read) => {
|
|
968
|
+
if (askId === undefined)
|
|
930
969
|
return;
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
970
|
+
switch (read.kind) {
|
|
971
|
+
case "approved":
|
|
972
|
+
this.notifyExternalDecision(askId, true, read.updatedInput);
|
|
934
973
|
return;
|
|
935
|
-
|
|
936
|
-
|
|
974
|
+
case "denied":
|
|
975
|
+
this.notifyExternalDecision(askId, false);
|
|
937
976
|
return;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
965
|
-
|
|
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
|
-
|
|
970
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
984
|
-
if (
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1016
|
-
return;
|
|
1064
|
+
if (read.kind === "edit-in-flight")
|
|
1065
|
+
lastLoss = probed.current;
|
|
1017
1066
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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: "
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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: "
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
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",
|