@sema-agent/core 5.33.0 → 5.35.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.
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
20
20
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
21
21
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
22
22
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
23
- import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
23
+ import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
24
24
  const PERSISTED_RULE_TOOL = "Bash";
25
25
  import { findAdmittingRule, suggestRulesForCommand } from "../permission-rule-model.js";
26
26
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
@@ -88,6 +88,7 @@ import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
88
88
  import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
89
89
  import { boundInputHashOf } from "../canonical-json.js";
90
90
  import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
91
+ import { durableParkGapFor } from "../park-selfcheck.js";
91
92
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
92
93
  import { deliverEngineNotice } from "../types.js";
93
94
  const announcedMaterializeEnv = new Set();
@@ -192,6 +193,46 @@ async function forgetQuietly(sessions, sessionId) {
192
193
  catch {
193
194
  }
194
195
  }
196
+ function screenGateSettlement(result, settling) {
197
+ const defects = [];
198
+ const reportedBy = result.settledBy;
199
+ let settledBy;
200
+ if (reportedBy !== undefined) {
201
+ if (!isApprovalSettledBy(reportedBy) || (!settling && reportedBy !== "human")) {
202
+ defects.push(`a tool-gate settlement reported settledBy "${String(reportedBy)}" on ${settling ? "a blocked" : "an executing"} call — ` +
203
+ `it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`);
204
+ }
205
+ else
206
+ settledBy = reportedBy;
207
+ }
208
+ const attribution = screenApproverAttribution(result.approver);
209
+ if (attribution.defect !== undefined) {
210
+ defects.push(`a tool-gate settlement reported an attribution this engine refuses: ${attribution.defect}; the frame carries no approver`);
211
+ }
212
+ const record = {
213
+ ...(settledBy !== undefined ? { settledBy } : {}),
214
+ ...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
215
+ };
216
+ return { ...(settledBy !== undefined || attribution.approver !== undefined ? { record } : {}), defects };
217
+ }
218
+ function inheritedAskRuleEvidence(deps) {
219
+ const org = deps.permissionRuleOrg === undefined ? "not_wired" : "not_adjudicated";
220
+ const personal = deps.permissionRuleStore === undefined ? "not_wired" : "not_adjudicated";
221
+ return Object.freeze({ orgRevisionAbsent: org, orgRuleAbsent: org, personalRuleDotsAbsent: personal });
222
+ }
223
+ function orgRevisionEvidenceOf(resolution, onDefect) {
224
+ const reported = resolution.revision;
225
+ if (reported === undefined)
226
+ return {};
227
+ if (typeof reported === "number" && Number.isFinite(reported))
228
+ return { revision: reported };
229
+ onDefect(`the org rule overlay reported revision ${JSON.stringify(reported)} — a snapshot revision is a finite number; ` +
230
+ `the adjudication stands, but the ask carries no revision evidence for this call`);
231
+ return {};
232
+ }
233
+ function persistedRuleHitOf(admitting) {
234
+ return admitting === undefined ? undefined : { rule: admitting.rule, dots: admitting.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) };
235
+ }
195
236
  export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
196
237
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
197
238
  spec = doors.spec;
@@ -204,7 +245,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
204
245
  e.code = "resume.session_not_found";
205
246
  throw e;
206
247
  }
207
- const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects });
248
+ const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
208
249
  const sessionId = acquired.sessionId;
209
250
  const hostTaskId = spec.taskId ?? sessionId;
210
251
  if (compModel !== undefined) {
@@ -2778,6 +2819,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2778
2819
  return {};
2779
2820
  return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
2780
2821
  };
2822
+ const inheritedAskEvidence = inheritedAskRuleEvidence(deps);
2781
2823
  const permissionRuleLane = (() => {
2782
2824
  const provider = deps.permissionRuleStore;
2783
2825
  const localOwnerDeclared = deps.localOwnerRules === true;
@@ -2817,9 +2859,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2817
2859
  message: err instanceof Error ? err.message : String(err),
2818
2860
  ts: Date.now(),
2819
2861
  }));
2820
- return undefined;
2862
+ return { unreadable: true };
2821
2863
  }
2822
- return findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root })?.rule;
2864
+ return persistedRuleHitOf(findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root }));
2823
2865
  },
2824
2866
  };
2825
2867
  })();
@@ -2840,11 +2882,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2840
2882
  }
2841
2883
  if (resolution.status === "unavailable")
2842
2884
  return { status: "unavailable", disclosures: resolution.disclosures };
2885
+ const revisionCell = orgRevisionEvidenceOf(resolution, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
2843
2886
  const command = req.args?.command;
2844
2887
  if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
2845
- return { status: "available" };
2888
+ return { status: "available", ...revisionCell };
2846
2889
  const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
2847
- return verdict === undefined ? { status: "available" } : { status: "available", verdict };
2890
+ return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
2848
2891
  },
2849
2892
  };
2850
2893
  })();
@@ -2926,6 +2969,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2926
2969
  ...riskAxesOf(creq.toolName),
2927
2970
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2928
2971
  ...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
2972
+ ruleEvidence: inheritedAskEvidence,
2929
2973
  }, onAskOf, csignal ?? abortController.signal);
2930
2974
  if (rr.action !== "allow")
2931
2975
  return rr;
@@ -3011,6 +3055,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3011
3055
  ...riskAxesOf(creq.toolName),
3012
3056
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3013
3057
  ...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
3058
+ ruleEvidence: inheritedAskEvidence,
3014
3059
  }, pc.onAsk, csignal ?? abortController.signal);
3015
3060
  const askWaitMs = Math.max(0, now() - askT0);
3016
3061
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3098,6 +3143,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3098
3143
  ...riskAxesOf(creq.toolName),
3099
3144
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3100
3145
  ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3146
+ ruleEvidence: inheritedAskEvidence,
3101
3147
  }, pc.onAsk, csignal ?? abortController.signal);
3102
3148
  const askWaitMs = Math.max(0, now() - askT0);
3103
3149
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3239,7 +3285,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3239
3285
  }
3240
3286
  const preToolContexts = new Map();
3241
3287
  const blockedToolCalls = new Set();
3242
- const approvalSettledBy = new Map();
3288
+ const approvalSettlement = new Map();
3243
3289
  const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
3244
3290
  const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
3245
3291
  const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
@@ -3413,6 +3459,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3413
3459
  ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3414
3460
  ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
3415
3461
  ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
3462
+ ...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
3416
3463
  }, onAsk, abortController.signal);
3417
3464
  const waitMs = Math.max(0, now() - t0);
3418
3465
  if (resolved.approverUnavailable !== true) {
@@ -4230,17 +4277,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4230
4277
  if (blockedTracked && (result.block || result.suspend)) {
4231
4278
  blockedToolCalls.add(e.toolCallId);
4232
4279
  }
4233
- const reported = result.settledBy;
4234
- if (reported !== undefined) {
4235
- const settling = result.block === true;
4236
- if (!isApprovalSettledBy(reported) || (!settling && reported !== "human")) {
4237
- deps.onError?.(new Error(`a tool-gate settlement reported settledBy "${String(reported)}" on ${settling ? "a blocked" : "an executing"} call — ` +
4238
- `it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`), { phase: "config", sessionId });
4239
- }
4240
- else {
4241
- approvalSettledBy.set(e.toolCallId, reported);
4242
- }
4243
- }
4280
+ const settlement = screenGateSettlement(result, result.block === true);
4281
+ for (const defect of settlement.defects)
4282
+ deps.onError?.(new Error(defect), { phase: "config", sessionId });
4283
+ if (settlement.record !== undefined)
4284
+ approvalSettlement.set(e.toolCallId, settlement.record);
4244
4285
  return result.block
4245
4286
  ? { block: true, reason: result.reason }
4246
4287
  : result.updatedInput !== undefined
@@ -4552,7 +4593,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4552
4593
  const effectiveReadFaceObserved = carrierReadFace();
4553
4594
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4554
4595
  const preparedHolder = {};
4555
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4596
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4556
4597
  const prepared = buildPrepared();
4557
4598
  preparedHolder.current = prepared;
4558
4599
  return prepared;
@@ -80,13 +80,18 @@ declare function toolEndBodyFrom(result: unknown, isError: boolean,
80
80
  * a parameter and never derived from `result`: a tool's own `details` (which post-tool hooks may
81
81
  * also replace) is writable by layers that adjudicate nothing, so reading provenance out of it would
82
82
  * let a failing tool claim a person approved it. Omitted ⇒ this call settled no approval. */
83
- settledBy?: ApprovalSettledBy): {
83
+ settledBy?: ApprovalSettledBy,
84
+ /** design/252 G-7 — WHOSE settlement, from the same caller and the same channel as `settledBy`, and
85
+ * for the same reason it is a parameter: an attribution read out of a tool's own result would let a
86
+ * tool name the person who approved it. Omitted ⇒ this call's settlement named nobody. */
87
+ approver?: string): {
84
88
  output?: unknown;
85
89
  truncated?: boolean;
86
90
  totalChars?: number;
87
91
  structured?: unknown;
88
92
  errorCode?: string;
89
93
  settledBy?: ApprovalSettledBy;
94
+ approver?: string;
90
95
  };
91
96
  /**
92
97
  * scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
@@ -1,8 +1,8 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
- import { createSafeNotifier } from "../safe-notify.js";
2
+ import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
3
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
4
4
  import { snapshotActorAssertion } from "../../internal/llm.js";
5
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
5
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
6
6
  import { engineVersion } from "../version.js";
7
7
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
8
8
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
@@ -41,7 +41,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntr
41
41
  import { reconcileInterruptedSession } from "../session-reconcile.js";
42
42
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
43
43
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
44
- import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
44
+ import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
45
45
  import { defaultTaskRegistry } from "../task-registry.js";
46
46
  import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
47
47
  import { ToolDetachHub } from "../tool-detach.js";
@@ -68,6 +68,13 @@ function nextHumanInputSeq(key) {
68
68
  }
69
69
  return ++box.n;
70
70
  }
71
+ function sameAcceptedSteerInput(a, b) {
72
+ return (a.payload === b.payload &&
73
+ a.trusted === b.trusted &&
74
+ a.actor?.id === b.actor?.id &&
75
+ a.actor?.hostAsserted === b.actor?.hostAsserted &&
76
+ a.actor?.issuer === b.actor?.issuer);
77
+ }
71
78
  const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
72
79
  const STOP_HOOK_BLOCK_CAP = 8;
73
80
  const COMPACTION_REGROWTH_FACTOR = 1.5;
@@ -130,7 +137,7 @@ function resumeDecisionWasNegative(resume) {
130
137
  }
131
138
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
132
139
  "NOT executed on resume. If you still need it, issue it again now.";
133
- function toolEndBodyFrom(result, isError, settledBy) {
140
+ function toolEndBodyFrom(result, isError, settledBy, approver) {
134
141
  const o = toolOutputFrom(result);
135
142
  const st = structuredFrom(result);
136
143
  const det = isError ? result?.details : undefined;
@@ -142,6 +149,7 @@ function toolEndBodyFrom(result, isError, settledBy) {
142
149
  ...(st !== undefined ? { structured: st } : {}),
143
150
  ...(typeof code === "string" ? { errorCode: code } : {}),
144
151
  ...(settledBy !== undefined ? { settledBy } : {}),
152
+ ...(approver !== undefined ? { approver } : {}),
145
153
  };
146
154
  }
147
155
  export function reconciledToolEndBody(orphan) {
@@ -1154,7 +1162,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1154
1162
  rs.counters.groundingSignalPostR9 = true;
1155
1163
  }
1156
1164
  const activityArg = primaryActivityArg(event.args);
1157
- internalsNotifier.notify(() => internals?.onActivity?.({ phase: "start", toolCallId: event.toolCallId, toolName: event.toolName, at: startAt, ...(activityArg !== undefined ? { arg: activityArg } : {}) }), "runtask.onActivity.start");
1165
+ internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "start", toolCallId: event.toolCallId, toolName: event.toolName, at: startAt, ...(activityArg !== undefined ? { arg: activityArg } : {}) }), internalsNotifier, "runtask.onActivity.start"), "runtask.onActivity.start");
1158
1166
  pushContent({
1159
1167
  type: "tool_start",
1160
1168
  toolCallId: event.toolCallId,
@@ -1203,7 +1211,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1203
1211
  ok: !event.isError,
1204
1212
  ts: toolNow,
1205
1213
  }));
1206
- internalsNotifier.notify(() => internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow }), "runtask.onActivity.end");
1214
+ internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow }), internalsNotifier, "runtask.onActivity.end"), "runtask.onActivity.end");
1207
1215
  if (prepared.lspDiagnostics && !event.isError) {
1208
1216
  const name = event.toolName;
1209
1217
  if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
@@ -1213,16 +1221,16 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1213
1221
  prepared.lspDiagnostics.nudge(p);
1214
1222
  }
1215
1223
  }
1216
- const settledBy = prepared.approvalSettledBy.get(event.toolCallId);
1217
- if (settledBy !== undefined)
1218
- prepared.approvalSettledBy.delete(event.toolCallId);
1224
+ const settlement = prepared.approvalSettlement.get(event.toolCallId);
1225
+ if (settlement !== undefined)
1226
+ prepared.approvalSettlement.delete(event.toolCallId);
1219
1227
  pushContent({
1220
1228
  type: "tool_end",
1221
1229
  toolCallId: event.toolCallId,
1222
1230
  toolName: event.toolName,
1223
1231
  ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
1224
1232
  isError: event.isError,
1225
- ...toolEndBodyFrom(event.result, event.isError, settledBy),
1233
+ ...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver),
1226
1234
  ...ident(),
1227
1235
  });
1228
1236
  announceWorkspaceMove();
@@ -1444,6 +1452,7 @@ export class Runner {
1444
1452
  };
1445
1453
  let reapHandle;
1446
1454
  let steerChain = Promise.resolve();
1455
+ const acceptedSteerInputs = new Map();
1447
1456
  const notifyRef = {};
1448
1457
  const manualCompactRef = { requested: false, waiters: [] };
1449
1458
  const drainManualCompactWaiters = (outcome) => {
@@ -1565,6 +1574,7 @@ export class Runner {
1565
1574
  queue.push({ type: "done", result: resultValue });
1566
1575
  queue.close();
1567
1576
  });
1577
+ void settled.then(() => acceptedSteerInputs.clear(), () => acceptedSteerInputs.clear());
1568
1578
  const steeringError = (msg, code = "steering.not_running") => {
1569
1579
  const e = new Error(`cannot steer: ${msg}`);
1570
1580
  e.code = code;
@@ -1613,22 +1623,40 @@ export class Runner {
1613
1623
  return suggestionsDone.catch(() => []);
1614
1624
  },
1615
1625
  steer: async (text, options) => {
1616
- if (options?.trusted && sanitizeUntrustedText(text) !== text) {
1626
+ const trusted = options?.trusted ? true : false;
1627
+ if (trusted && sanitizeUntrustedText(text) !== text) {
1617
1628
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
1618
1629
  }
1619
- const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
1630
+ const inputId = options?.inputId;
1631
+ if (inputId !== undefined) {
1632
+ if (typeof inputId !== "string") {
1633
+ throw steeringError("inputId must be a string when supplied", "steering.invalid_content");
1634
+ }
1635
+ if (inputId === "" || inputId.length > MAX_STEER_INPUT_ID_CHARS) {
1636
+ throw steeringError(`inputId must be a non-empty string of at most ${MAX_STEER_INPUT_ID_CHARS} characters`, "steering.invalid_content");
1637
+ }
1638
+ if (inputId === LEGACY_PENDING_STEER_INPUT_ID) {
1639
+ throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
1640
+ }
1641
+ }
1642
+ const actorIn = options?.actor;
1643
+ const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
1620
1644
  const projected = projectHumanInput({ text, actor, source: "steer" });
1621
- const payload = options?.trusted ? formatHookFeedback(projected) : projected;
1645
+ const payload = trusted ? formatHookFeedback(projected) : projected;
1622
1646
  const mintsAFrame = payload.trim().length !== 0;
1623
- const emitAccepted = (h) => {
1647
+ const replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
1648
+ const noteAccepted = (h) => {
1624
1649
  if (!mintsAFrame)
1625
1650
  return;
1651
+ if (typeof inputId === "string")
1652
+ acceptedSteerInputs.set(inputId, replay);
1626
1653
  queue.push({
1627
1654
  ...buildHumanInputEvent({
1628
1655
  carrier: "steer",
1629
1656
  source: "steer",
1630
1657
  delivery: "queued",
1631
1658
  sessionSeq: nextHumanInputSeq(h.harness),
1659
+ ...(typeof inputId === "string" ? { inputId } : {}),
1632
1660
  ...(actor !== undefined ? { actor } : {}),
1633
1661
  ...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
1634
1662
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
@@ -1645,9 +1673,21 @@ export class Runner {
1645
1673
  const h = handle ?? (await orTimeout(ready));
1646
1674
  if (!h)
1647
1675
  throw steeringError("the task is not running");
1676
+ if (typeof inputId === "string") {
1677
+ const prior = acceptedSteerInputs.get(inputId);
1678
+ if (prior !== undefined) {
1679
+ if (resultValue !== undefined || h.loop.ended)
1680
+ throw steeringError("the task is no longer running");
1681
+ if (!sameAcceptedSteerInput(prior, replay)) {
1682
+ throw steeringError("a different steering instruction was already accepted under this inputId — re-issue this one with a fresh inputId " +
1683
+ "(an identical payload would have been an idempotent retry)", "steering.duplicate_input_id");
1684
+ }
1685
+ return;
1686
+ }
1687
+ }
1648
1688
  try {
1649
1689
  await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1650
- emitAccepted(h);
1690
+ noteAccepted(h);
1651
1691
  return;
1652
1692
  }
1653
1693
  catch (e) {
@@ -1658,7 +1698,7 @@ export class Runner {
1658
1698
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1659
1699
  try {
1660
1700
  await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
1661
- emitAccepted(h);
1701
+ noteAccepted(h);
1662
1702
  return;
1663
1703
  }
1664
1704
  catch (e2) {
@@ -2329,8 +2369,11 @@ export class Runner {
2329
2369
  queue.push({ type: "message_committed", entryId, role, ...(toolCallId !== undefined ? { toolCallId } : {}), ...ident() });
2330
2370
  };
2331
2371
  const subagentName = parentToolCallId !== undefined && internals?.agentName !== undefined ? inlineUntrusted(internals.agentName.slice(0, 320), 80) : undefined;
2372
+ const statusSinkNotifier = createSafeNotifier({
2373
+ onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals status sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
2374
+ });
2332
2375
  const statusEmit = (s) => {
2333
- queue.push({
2376
+ const frame = {
2334
2377
  type: "status",
2335
2378
  phase: s.phase,
2336
2379
  ...(s.detail !== undefined ? { detail: s.detail } : {}),
@@ -2340,7 +2383,12 @@ export class Runner {
2340
2383
  ...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
2341
2384
  ...(s.errClass !== undefined ? { errClass: s.errClass } : {}),
2342
2385
  ...ident(),
2343
- });
2386
+ };
2387
+ Object.freeze(frame);
2388
+ const accepted = queue.push(frame);
2389
+ if (accepted && internals?.onStatusEvent !== undefined) {
2390
+ statusSinkNotifier.notify(() => observeThenableRejection(internals.onStatusEvent?.(frame), statusSinkNotifier, "runtask.onStatusEvent"), "runtask.onStatusEvent");
2391
+ }
2344
2392
  };
2345
2393
  const telemetryEmit = (t) => {
2346
2394
  emitTrace(rs.telemetry.tracer, () => t.kind === "failover"
@@ -3656,6 +3704,15 @@ export class Runner {
3656
3704
  throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" with settledBy "${settledBy}" — a non-human settlement is a fail-closed end (nobody answered), so it cannot be the source of an approval that EXECUTES; ` +
3657
3705
  "supply \"human\", or omit the field if the allow came from configuration rather than a person; refusing pre-CAS, the checkpoint stays pending", { field: "settledBy" });
3658
3706
  }
3707
+ const approver = decide.approver;
3708
+ const attribution = screenApproverAttribution(approver);
3709
+ if (attribution.defect !== undefined) {
3710
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
3711
+ }
3712
+ if (attribution.approver !== undefined && settledBy === undefined) {
3713
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume names an approver but no settledBy — an attribution says WHOSE settlement it was and needs the word that says what KIND of end it was beside it; ` +
3714
+ `supply settledBy ("human" / "timeout" / "aborted"), or omit the approver; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
3715
+ }
3659
3716
  const reason = decide.reason;
3660
3717
  assertOutcomeText(reason, "reason");
3661
3718
  plainPolicyOutcome = {
@@ -3667,6 +3724,7 @@ export class Runner {
3667
3724
  ...(reason !== undefined ? { reason } : {}),
3668
3725
  ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
3669
3726
  ...(settledBy !== undefined ? { settledBy } : {}),
3727
+ ...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
3670
3728
  };
3671
3729
  if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
3672
3730
  throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
@@ -4029,7 +4087,7 @@ export class Runner {
4029
4087
  const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
4030
4088
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
4031
4089
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
4032
- const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy) });
4090
+ const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy, outcome.approver) });
4033
4091
  if (outcome.decision === "deny") {
4034
4092
  const defaultDenial = outcome.settledBy === "timeout"
4035
4093
  ? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
@@ -79,3 +79,12 @@ export interface SafeNotifier {
79
79
  * notifier whose callbacks never throw costs one object and nothing per call beyond the `try`.
80
80
  */
81
81
  export declare function createSafeNotifier(opts?: SafeNotifierOptions): SafeNotifier;
82
+ /**
83
+ * Observe a `=> void` host callback's RETURN value for the async dialect: TypeScript accepts an
84
+ * `async` function at a void seat, and a rejected one would escape {@link SafeNotifier.notify}
85
+ * (which deliberately ignores return values — see the scope note above) as an unhandled rejection.
86
+ * Call this with the callback's return value INSIDE the notify thunk: a thenable's rejection is
87
+ * routed back through the same notifier/site, so async and sync failures share one count and one
88
+ * bounded disclosure, and neither can fault the engine's control flow.
89
+ */
90
+ export declare function observeThenableRejection(r: unknown, notifier: SafeNotifier, site: string): void;
@@ -57,3 +57,12 @@ export function createSafeNotifier(opts) {
57
57
  },
58
58
  };
59
59
  }
60
+ export function observeThenableRejection(r, notifier, site) {
61
+ if (typeof r?.then === "function") {
62
+ r.then(undefined, (err) => {
63
+ notifier.notify(() => {
64
+ throw err instanceof Error ? err : new Error(String(err));
65
+ }, site);
66
+ });
67
+ }
68
+ }
@@ -117,8 +117,8 @@ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "revi
117
117
  * are NOT all terminal-errorCode-shaped — see {@link EXACT_CODE_CLASS}'s comment), plus the flat 1.36 codes.
118
118
  * The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see
119
119
  * {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a
120
- * task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`,
121
- * rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
120
+ * task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/
121
+ * `steering.duplicate_input_id`, rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
122
122
  * reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means
123
123
  * "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add").
124
124
  */