@sema-agent/client-core 0.93.0 → 0.94.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 (156) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/README.md +31 -18
  3. package/dist/adapt/arms.js +8 -5
  4. package/dist/adapter/downstream/terminalToSdkResult.js +2 -3
  5. package/dist/agentSession/engineSessionBackgroundStop.js +2 -1
  6. package/dist/agentsWireCaps.d.ts +15 -0
  7. package/dist/agentsWireCaps.js +111 -1
  8. package/dist/approvalReachWire.d.ts +18 -0
  9. package/dist/approvalReachWire.js +58 -0
  10. package/dist/approvalsStreamLiveCapability.js +16 -11
  11. package/dist/callerCwdCapability.d.ts +16 -0
  12. package/dist/callerCwdCapability.js +47 -0
  13. package/dist/controlRouter.js +3 -10
  14. package/dist/coreValuePorts.d.ts +64 -1
  15. package/dist/coreValuePorts.js +30 -0
  16. package/dist/detachWire.d.ts +1 -4
  17. package/dist/detachWire.js +1 -1
  18. package/dist/deviceExecutorManagementCapability.js +39 -34
  19. package/dist/deviceFlowPolling.d.ts +5 -0
  20. package/dist/deviceFlowPolling.js +28 -0
  21. package/dist/displayUntrusted.js +652 -21
  22. package/dist/engineErrorCodes.d.ts +5 -1
  23. package/dist/engineErrorCodes.js +9 -12
  24. package/dist/engineNoticeCodes.d.ts +22 -2
  25. package/dist/engineNoticeCodes.js +73 -6
  26. package/dist/engineRefusedToolName.d.ts +1 -0
  27. package/dist/engineRefusedToolName.js +8 -0
  28. package/dist/executionLaneCapability.js +14 -9
  29. package/dist/fileHistoryCaptureCapability.js +14 -9
  30. package/dist/fileHistoryLegNotice.d.ts +11 -0
  31. package/dist/fileHistoryLegNotice.js +50 -13
  32. package/dist/fleet/fleetLedger.js +5 -0
  33. package/dist/gateOutcome.d.ts +2 -1
  34. package/dist/gateOutcome.js +3 -0
  35. package/dist/gateVocabulary.js +1 -1
  36. package/dist/generated/engineAskFaceTables.d.ts +3 -0
  37. package/dist/generated/engineAskFaceTables.js +12 -0
  38. package/dist/generated/engineFactTables.d.ts +4 -2
  39. package/dist/generated/engineFactTables.js +13 -0
  40. package/dist/generated/engineNoticeTables.d.ts +5 -0
  41. package/dist/generated/engineNoticeTables.js +7 -0
  42. package/dist/generated/sdkFactTables.d.ts +3 -0
  43. package/dist/generated/sdkFactTables.js +17 -0
  44. package/dist/generated/settingsSchemaTables.d.ts +3 -0
  45. package/dist/generated/settingsSchemaTables.js +3 -0
  46. package/dist/headlessPermissionModeWire.d.ts +2 -2
  47. package/dist/headlessPermissionModeWire.js +11 -21
  48. package/dist/hitl/approvalHolds.d.ts +36 -0
  49. package/dist/hitl/approvalHolds.js +89 -0
  50. package/dist/hitl/approvalOutcomeNote.js +2 -1
  51. package/dist/hitl/approvalResolution.d.ts +3 -2
  52. package/dist/hitl/approvalResolution.js +21 -0
  53. package/dist/hitl/approvalsFeed.js +5 -0
  54. package/dist/hitl/askGateWire.js +3 -0
  55. package/dist/hitl/automaticAnswerVerdict.d.ts +8 -2
  56. package/dist/hitl/automaticAnswerVerdict.js +69 -21
  57. package/dist/hitl/decisionRefusal.d.ts +19 -0
  58. package/dist/hitl/decisionRefusal.js +63 -0
  59. package/dist/hitl/frameRouter.d.ts +10 -0
  60. package/dist/hitl/frameRouter.js +59 -3
  61. package/dist/hitl/gateLedger.d.ts +3 -0
  62. package/dist/hitl/gateLedger.js +17 -0
  63. package/dist/hitl/hitlBridge.d.ts +2 -2
  64. package/dist/hitl/hitlBridge.js +8 -10
  65. package/dist/hitl/hitlHostSurface.d.ts +6 -0
  66. package/dist/hitl/hitlHostSurface.js +25 -4
  67. package/dist/hitl/localAllowRule.js +2 -1
  68. package/dist/hitl/noAllowCause.d.ts +9 -0
  69. package/dist/hitl/noAllowCause.js +26 -0
  70. package/dist/hitl/parkResolver.js +6 -3
  71. package/dist/hitl/parkRowBirthWait.js +2 -1
  72. package/dist/hitl/persistedRulesWire.d.ts +6 -1
  73. package/dist/hitl/persistedRulesWire.js +12 -4
  74. package/dist/hitl/planReviewWire.d.ts +0 -1
  75. package/dist/hitl/planReviewWire.js +30 -9
  76. package/dist/hitl/sessionPolicyDeliverable.d.ts +1 -1
  77. package/dist/hitl/sessionPolicyDeliverable.js +3 -8
  78. package/dist/hitl/sessionPolicyWire.js +1 -1
  79. package/dist/hitl/toolApprovalWire.d.ts +37 -2
  80. package/dist/hitl/toolApprovalWire.js +392 -13
  81. package/dist/humanTurnRead.d.ts +15 -1
  82. package/dist/humanTurnRead.js +28 -2
  83. package/dist/index.d.ts +18 -6
  84. package/dist/index.js +14 -5
  85. package/dist/limitsWire.d.ts +0 -1
  86. package/dist/limitsWire.js +2 -10
  87. package/dist/liveModelCatalog.d.ts +14 -0
  88. package/dist/liveModelCatalog.js +61 -0
  89. package/dist/mcpProbeCapability.js +14 -9
  90. package/dist/mcpReconnect.js +13 -8
  91. package/dist/memoryComplianceCapability.js +15 -10
  92. package/dist/memoryOriginCapability.js +15 -10
  93. package/dist/modelIdentityResolvability.d.ts +4 -2
  94. package/dist/modelIdentityResolvability.js +31 -4
  95. package/dist/outputStyleSeatCapability.d.ts +16 -0
  96. package/dist/outputStyleSeatCapability.js +47 -0
  97. package/dist/ownKey.d.ts +1 -0
  98. package/dist/ownKey.js +10 -0
  99. package/dist/peerLaneCapability.js +16 -11
  100. package/dist/permissionRuleIssue.js +1 -1
  101. package/dist/permissionRulesWriteCapability.js +16 -11
  102. package/dist/permissionsAllowGrantCapability.d.ts +2 -0
  103. package/dist/permissionsAllowGrantCapability.js +38 -16
  104. package/dist/permissionsDeclaration.d.ts +10 -1
  105. package/dist/permissionsDeclaration.js +97 -15
  106. package/dist/protocolSegment.d.ts +1 -0
  107. package/dist/protocolSegment.js +13 -1
  108. package/dist/request/taskRequest.d.ts +11 -3
  109. package/dist/request/taskRequest.js +13 -1
  110. package/dist/resultFrameKeys.d.ts +4 -0
  111. package/dist/resultFrameKeys.js +13 -0
  112. package/dist/resumeRefusalCopy.js +6 -16
  113. package/dist/retryStatus.js +6 -4
  114. package/dist/rewindArchiveCapability.js +13 -8
  115. package/dist/{hitl/rosterMountedNames.d.ts → rosterMountedNames.d.ts} +1 -1
  116. package/dist/runCancelContext.js +2 -1
  117. package/dist/sealedKeyCapability.js +19 -14
  118. package/dist/seatContract.d.ts +33 -5
  119. package/dist/seatContract.js +143 -2
  120. package/dist/sessionPolicyCapability.js +15 -10
  121. package/dist/sessionReadRedactionCapability.d.ts +16 -0
  122. package/dist/sessionReadRedactionCapability.js +47 -0
  123. package/dist/sessionSyncFileHistoryCapability.js +14 -9
  124. package/dist/skillToolListsCapability.js +14 -9
  125. package/dist/sqlEngineCapability.js +18 -13
  126. package/dist/steerPriorityCapability.js +21 -16
  127. package/dist/subagent/agentStateDetail.d.ts +1 -0
  128. package/dist/subagent/agentStateDetail.js +41 -0
  129. package/dist/subagent/engineCompactWire.d.ts +15 -0
  130. package/dist/subagent/engineCompactWire.js +75 -13
  131. package/dist/supportFloor.js +1 -1
  132. package/dist/taskRequestWords.d.ts +2 -4
  133. package/dist/taskRequestWords.js +3 -2
  134. package/dist/types/engineState.d.ts +1 -1
  135. package/dist/userAttachmentEnvelope.d.ts +22 -1
  136. package/dist/userAttachmentEnvelope.js +90 -2
  137. package/dist/userInvokedSkillsCapability.js +14 -9
  138. package/dist/webSearchBackendCapability.js +21 -16
  139. package/dist/wireErrorTriage.d.ts +11 -0
  140. package/dist/wireErrorTriage.js +35 -0
  141. package/dist/wireFailureShape.d.ts +6 -0
  142. package/dist/wireFailureShape.js +30 -3
  143. package/dist/wireRefusalCopy.d.ts +6 -1
  144. package/dist/wireRefusalCopy.js +22 -8
  145. package/dist/wiringInstructionsAttribution.d.ts +10 -0
  146. package/dist/wiringInstructionsAttribution.js +67 -0
  147. package/dist/workflowClient.js +22 -1
  148. package/dist/workflowsJournalCapability.js +14 -9
  149. package/dist/writeProtectionCapability.js +28 -23
  150. package/docs/INTEGRATION-CLIENTS.md +817 -45
  151. package/package.json +5 -5
  152. package/dist/askSurvivesStandingAnswer.d.ts +0 -13
  153. package/dist/askSurvivesStandingAnswer.js +0 -53
  154. package/dist/hitl/askSurvivesPosture.d.ts +0 -20
  155. package/dist/hitl/askSurvivesPosture.js +0 -48
  156. /package/dist/{hitl/rosterMountedNames.js → rosterMountedNames.js} +0 -0
@@ -0,0 +1,63 @@
1
+ import { refusalSaysStillPending } from '../engineErrorCodes.js';
2
+ import { readLivePendingRows } from './livePendingAsk.js';
3
+ import { readToolApprovalRespondRefusal } from './toolApprovalWire.js';
4
+ const ASK_DECIDED_CODE = 'conflict.ask_decided';
5
+ const TRANSIENT_STATUSES = new Set([408, 425, 429, 502, 503, 504]);
6
+ const DISPLAYABLE_CODE = /^[A-Za-z0-9._:-]{1,64}$/;
7
+ export function classifyDecisionRefusal(err) {
8
+ const r = readToolApprovalRespondRefusal(err);
9
+ const status = r?.status;
10
+ if (status === undefined || !Number.isInteger(status) || status < 400 || status > 599)
11
+ return undefined;
12
+ const code = r?.errorCode;
13
+ const errorCode = code !== undefined && DISPLAYABLE_CODE.test(code) ? code : undefined;
14
+ const standing = status === 404 || status === 410 || (status === 409 && code === ASK_DECIDED_CODE)
15
+ ? 'settled-elsewhere'
16
+ : status >= 500 && status !== 501
17
+ ? 'ambiguous'
18
+ : 'refused-pending';
19
+ const transient = TRANSIENT_STATUSES.has(status) || (typeof err === 'object' && err !== null && refusalSaysStillPending(err));
20
+ return Object.freeze({ standing, status, ...(errorCode !== undefined ? { errorCode } : {}), ...(transient ? { transient: true } : {}) });
21
+ }
22
+ export function liveAskPendingReading(envelope, approvalId) {
23
+ if (typeof approvalId !== 'string' || approvalId === '')
24
+ return 'unknown';
25
+ try {
26
+ const reading = readLivePendingRows(envelope);
27
+ if (reading.kind !== 'present')
28
+ return 'unknown';
29
+ if (reading.rows.some((row) => row.approvalId === approvalId))
30
+ return 'pending';
31
+ return reading.dropped === 0 ? 'not_pending' : 'unknown';
32
+ }
33
+ catch {
34
+ return 'unknown';
35
+ }
36
+ }
37
+ export function decisionRefusalClosesOut(refusal, pending) {
38
+ if (pending !== 'pending' || typeof refusal !== 'object' || refusal === null)
39
+ return false;
40
+ try {
41
+ const standing = refusal.standing;
42
+ if (refusal.transient === true)
43
+ return false;
44
+ return standing === 'ambiguous' || standing === 'refused-pending';
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ export function undeliverableDecisionContent(facts) {
51
+ const code = typeof facts.errorCode === 'string' && DISPLAYABLE_CODE.test(facts.errorCode) ? ` ${facts.errorCode}` : '';
52
+ const head = `The engine refused the approval decision this client sent (HTTP ${String(facts.status)}${code}), so the call it asked about could not be answered from here and did not run`;
53
+ const runId = typeof facts.runId === 'string' && DISPLAYABLE_CODE.test(facts.runId) ? ` ${facts.runId}` : '';
54
+ switch (facts.end) {
55
+ case 'run-stopped':
56
+ return `${head}; the run was stopped instead of being left waiting for the approval window to close. Update the client, or run the task in an interactive session and answer the approval there.`;
57
+ case 'run-unconfirmed':
58
+ return `${head}; this client stopped waiting, but the engine did not confirm that it stopped the run${runId}, so that approval may stay open until its window closes. Update the client, or run the task in an interactive session and answer the approval there.`;
59
+ case 'turn-stopped':
60
+ default:
61
+ return `${head}; this turn was stopped instead of being left waiting. Update the client, or switch to a permission mode that shows you the approval and try again.`;
62
+ }
63
+ }
@@ -2,9 +2,19 @@ import type { AgentEvent } from '@sema-agent/sdk';
2
2
  import type { DecideOperationObserver, HitlClientLike } from './hitlBridge.js';
3
3
  import { type RespondToolApprovalFn, type ToolApprovalFrame, type ToolApprovalFrameLaneOpts, type ToolApprovalFrameOutcome } from './toolApprovalWire.js';
4
4
  import type { GateLedger } from './gateLedger.js';
5
+ import type { NoAllowCause } from './noAllowCause.js';
5
6
  import { type ReplayedApprovalDisposition } from './replayedApproval.js';
6
7
  export declare const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
7
8
  export declare const HITL_POLICY_DENY_MESSAGE: string;
9
+ export type PolicyDenyLane = 'no_approval_ui' | 'dont_ask';
10
+ export interface PolicyDenyReasonFacts {
11
+ readonly lane: PolicyDenyLane;
12
+ readonly toolName: string;
13
+ readonly cause?: NoAllowCause;
14
+ readonly allowRuleFlag?: string;
15
+ readonly budgetSetting?: string;
16
+ }
17
+ export declare function policyDenyReason(facts: PolicyDenyReasonFacts): string;
8
18
  export declare const HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool use]";
9
19
  export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
10
20
  export declare function toolEndOutputText(output: unknown): string | undefined;
@@ -4,6 +4,8 @@ import { classifierDenyFromToolEnd } from '../classifierVerdictWire.js';
4
4
  import { GATE_PARKED_ERROR_CODE } from '../engineErrorCodes.js';
5
5
  import { readRunTerminal, runTerminalCode, runTerminalGateKind, runTerminalGateToolName } from '../runTerminal.js';
6
6
  import { surfaceForCurrentSession, surfaceReplayedApprovalListUnreadable } from './hitlHostSurface.js';
7
+ import { liveCardHoldOf, supersedeLiveCard } from './approvalHolds.js';
8
+ import { liveFrameCallKey } from './gateIdentity.js';
7
9
  import { SEMA_DENIAL_KIND_KEY, hasLedgerPositionKey } from './gateLedger.js';
8
10
  import { replayedApprovalDisposition } from './replayedApproval.js';
9
11
  import { settleWithinDeadline } from '../abortableSleep.js';
@@ -14,6 +16,38 @@ export const HITL_REJECT_MESSAGE = REJECT_MESSAGE;
14
16
  export const HITL_POLICY_DENY_MESSAGE = 'Permission for this tool use was denied: it requires interactive approval, and permission prompts are ' +
15
17
  'not available in this session. The action was NOT performed. Do not claim it succeeded, and do not retry ' +
16
18
  'it in this session \u2014 report the limitation to the user, or suggest an alternative.';
19
+ const SENTENCE_TOOL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.:@-]{0,199}$/;
20
+ export function policyDenyReason(facts) {
21
+ const one = typeof facts.toolName === 'string' && SENTENCE_TOOL_NAME.test(facts.toolName) ? `this one "${facts.toolName}" call` : 'this one call';
22
+ const flag = typeof facts.allowRuleFlag === 'string' && SENTENCE_TOOL_NAME.test(facts.allowRuleFlag.replace(/^--?/, '')) ? ` (or ${facts.allowRuleFlag})` : '';
23
+ const budgetKnob = typeof facts.budgetSetting === 'string' && SENTENCE_TOOL_NAME.test(facts.budgetSetting) ? facts.budgetSetting : undefined;
24
+ const cause = facts.cause === 'no_allow_rule' || facts.cause === 'ask_rule' || facts.cause === 'budget' ? facts.cause : 'other';
25
+ if (facts.lane === 'dont_ask') {
26
+ const mode = 'denied by the dontAsk permission mode';
27
+ switch (cause) {
28
+ case 'no_allow_rule':
29
+ return `${mode}: ${one} needed approval, no permission rule allowed it, and this mode refuses anything that would ask — calls that need no approval still run; an allow rule in settings${flag} can approve calls like this ahead of time`;
30
+ case 'ask_rule':
31
+ return `${mode}: an ask rule matched ${one}, and this mode refuses anything that would ask (an ask rule outranks allow rules) — narrow or remove that ask rule, or use another permission mode to answer it`;
32
+ case 'budget':
33
+ return `${mode}: ${one} needed approval, and this session's automatic-approval budget is used up, so saved rules no longer approve calls${budgetKnob !== undefined ? ` — raise ${budgetKnob} before launch to change the cap` : ''}`;
34
+ default:
35
+ return `${mode}: ${one} needed a person's approval that saved permission rules cannot approve here (the engine or your deployment requires a person for it, or it could not be checked against your rules), and this mode refuses anything that would ask — use another permission mode to answer it`;
36
+ }
37
+ }
38
+ const lane = 'no approval UI on this lane';
39
+ const tail = 'nobody can be asked here, so only this call was refused — calls that need no approval still run';
40
+ switch (cause) {
41
+ case 'no_allow_rule':
42
+ return `${lane}: ${one} needed a person's approval, no permission rule answered it, and ${tail}; a matching allow rule in settings${flag} can approve calls like this ahead of time`;
43
+ case 'ask_rule':
44
+ return `${lane}: an ask rule matched ${one}, so it needed a person's answer, and ${tail}; an ask rule outranks allow rules, so narrow or remove that ask rule, or run the task interactively to answer it`;
45
+ case 'budget':
46
+ return `${lane}: ${one} needed a person's approval, and this session's automatic-approval budget is used up, so saved rules no longer approve calls; ${tail}${budgetKnob !== undefined ? `; raise ${budgetKnob} before launch to change the cap` : ''}`;
47
+ default:
48
+ return `${lane}: ${one} needed a person's approval, and saved permission rules cannot approve it here (the engine or your deployment requires a person for it, or it could not be checked against your rules); ${tail}; run the task interactively to answer it`;
49
+ }
50
+ }
17
51
  function denyOutputForRender(attribution) {
18
52
  if (attribution?.settledBy !== 'policy')
19
53
  return HITL_REJECT_MESSAGE;
@@ -232,10 +266,24 @@ const TOOL_END_ARMS = [
232
266
  '改顺序必须同批改 TOOL_END_ARM_ORDER 并说明为什么新顺序仍然对。');
233
267
  }
234
268
  }
269
+ function completeFrameParked(ev) {
270
+ try {
271
+ const d = Object.getOwnPropertyDescriptor(ev, 'parked');
272
+ return d !== undefined && 'value' in d && d.value === true;
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ }
235
278
  async function routeToolApprovalFrame(ev, ctx) {
236
279
  const { led, deps } = ctx;
237
- if (ev.type === 'tool_approval_complete')
280
+ if (ev.type === 'tool_approval_complete') {
281
+ const id = ev.approvalId;
282
+ if (typeof id === 'string' && id !== '' && supersedeLiveCard(liveFrameCallKey(id), completeFrameParked(ev))) {
283
+ hostLog('debug', `liveHitlAskWire: tool_approval_complete for ${id} arrived while its card was waiting — card withdrawn (nothing will be sent from it)`);
284
+ }
238
285
  return { kind: 'skip' };
286
+ }
239
287
  if (!deps.respondToolApproval) {
240
288
  hostLog('debug', `liveHitlAskWire: tool_approval frame ${ev.approvalId} but no respond wire — engine self-settles (TTL deny)`);
241
289
  return { kind: 'skip' };
@@ -269,7 +317,16 @@ async function routeToolApprovalFrame(ev, ctx) {
269
317
  const frameCallId = !fromSubagent && typeof ev.toolCallId === 'string' && ev.toolCallId !== '' ? ev.toolCallId : undefined;
270
318
  const gatedCallId = fromSubagent ? undefined : frameCallId ?? led.lastPendingFsCall();
271
319
  const argsCallId = frameCallId === undefined ? gatedCallId : typeof ev.toolName === 'string' && toolNameIsFsWrite(ev.toolName) ? frameCallId : undefined;
272
- const outcome = await surfaceToolApprovalFrameAndRespond(ev, deps.respondToolApproval, argsOfGatedStart(led, argsCallId), ctx.signal, deps.approvalLane);
320
+ const launched = surfaceToolApprovalFrameAndRespond(ev, deps.respondToolApproval, argsOfGatedStart(led, argsCallId), ctx.signal, deps.approvalLane);
321
+ const hold = liveCardHoldOf(liveFrameCallKey(ev.approvalId));
322
+ const handled = launched.then((outcome) => settleLiveCardOutcome(ev, outcome, ctx, fromSubagent, gatedCallId), (e) => {
323
+ hostLog('debug', `liveHitlAskWire: tool_approval card for ${ev.approvalId} failed: ${String(e).slice(0, 160)}`);
324
+ });
325
+ led.trackLiveCard(() => hold?.phase ?? 'card', handled);
326
+ return { kind: 'skip' };
327
+ }
328
+ function settleLiveCardOutcome(ev, outcome, ctx, fromSubagent, gatedCallId) {
329
+ const { led, deps } = ctx;
273
330
  const { decision } = outcome;
274
331
  if (deps.onToolApprovalOutcome !== undefined) {
275
332
  try {
@@ -294,7 +351,6 @@ async function routeToolApprovalFrame(ev, ctx) {
294
351
  else
295
352
  led.armDenyStamp(attribution);
296
353
  }
297
- return { kind: 'skip' };
298
354
  }
299
355
  const REPLAYED_APPROVAL_LIST_DEADLINE_MS = 3000;
300
356
  async function judgeLedgerApprovalFrame(ev, ctx) {
@@ -23,6 +23,9 @@ export interface GateLedger {
23
23
  notePreambleApproval(approvalId: string): void;
24
24
  preambleApprovals(): ReadonlySet<string>;
25
25
  beginStreamSegment(): void;
26
+ trackLiveCard(phaseOf: () => 'card' | 'respond', handled: Promise<void>): void;
27
+ answeringLiveCards(): Promise<void>[];
28
+ liveCardsSettled(): Promise<void>;
26
29
  markStarted(callId: string): boolean;
27
30
  noteGatedStart(callId: string, ev: AgentEvent): void;
28
31
  gatedStartArgs(): ReadonlyMap<string, unknown>;
@@ -88,6 +88,7 @@ export function createGateLedger() {
88
88
  let denyStampAttribution;
89
89
  let seq;
90
90
  const preambleApprovalIds = new Set();
91
+ const liveCards = new Set();
91
92
  return {
92
93
  noteSeq(ev) {
93
94
  const s = durableSeqOf(ev);
@@ -120,6 +121,22 @@ export function createGateLedger() {
120
121
  beginStreamSegment() {
121
122
  preambleApprovalIds.clear();
122
123
  },
124
+ trackLiveCard(phaseOf, handled) {
125
+ const entry = { phaseOf, handled };
126
+ liveCards.add(entry);
127
+ void handled.then(() => { liveCards.delete(entry); }, () => { liveCards.delete(entry); });
128
+ },
129
+ answeringLiveCards() {
130
+ const out = [];
131
+ for (const e of liveCards)
132
+ if (e.phaseOf() === 'respond')
133
+ out.push(e.handled);
134
+ return out;
135
+ },
136
+ async liveCardsSettled() {
137
+ while (liveCards.size > 0)
138
+ await Promise.allSettled([...liveCards].map((e) => e.handled));
139
+ },
123
140
  markStarted(callId) {
124
141
  if (startedCalls.has(callId))
125
142
  return false;
@@ -1,4 +1,4 @@
1
- import type { AgentEvent, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
1
+ import type { AgentEvent, ApprovalDecidedBy, ApprovalDecision, ApprovalStaleError, PendingCheckpoint, CheckpointGate, PlanReviewRequest, AssistantTaskStatus } from '@sema-agent/sdk';
2
2
  import type { PermissionResult } from '@sema-agent/agent-types';
3
3
  import type { ApprovalsListEnvelope } from './crashConverged.js';
4
4
  import { type SuspendedReopenReading } from './suspendedReopen.js';
@@ -56,7 +56,7 @@ export interface HitlDecideDenyOutcome {
56
56
  decision: 'deny';
57
57
  reason?: string | undefined;
58
58
  settledBy?: 'policy' | undefined;
59
- decidedBy?: 'policy' | undefined;
59
+ decidedBy?: Extract<ApprovalDecidedBy, 'policy'> | undefined;
60
60
  }
61
61
  export declare class HitlSafetyError extends Error {
62
62
  readonly code: HitlSafetyCode;
@@ -3,6 +3,7 @@ import { eventSeq } from '../adapter/types.js';
3
3
  import { hostLog } from '../host.js';
4
4
  import { abortableSleep } from '../abortableSleep.js';
5
5
  import { readOwnDataValue } from '../ownKey.js';
6
+ import { wireFailureTextsOf } from '../wireFailureShape.js';
6
7
  import { suspendedReopenOf } from './suspendedReopen.js';
7
8
  export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
8
9
  const MAX_DENY_REASON_CHARS = 4096;
@@ -208,7 +209,9 @@ export async function findPendingForTask(client, taskId, matches, opts, callId,
208
209
  rows = (await client.approvals.list(opts)).pending;
209
210
  }
210
211
  catch (e) {
211
- return { ok: false, reason: `approvals.list failed: ${String(e)}` };
212
+ const t = wireFailureTextsOf(e);
213
+ hostLog('debug', `hitlBridge: approvals.list failed: ${t.log}`);
214
+ return { ok: false, reason: `approvals.list failed: ${t.reason}` };
212
215
  }
213
216
  const decidable = rows.filter((r) => askParkForeignGateKind(r) === null);
214
217
  const identityRow = callId !== undefined
@@ -327,13 +330,8 @@ export class HitlBridge {
327
330
  throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry editedPlan`, 'bad_plan_edit');
328
331
  }
329
332
  const modeAfter = outcome.permissionModeAfter;
330
- if (modeAfter !== undefined) {
331
- if (outcome.decision !== 'approve') {
332
- throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry permissionModeAfter (it only applies to an approval)`, 'bad_plan_mode');
333
- }
334
- if (!isPlanReviewModeAfter(modeAfter)) {
335
- throw new HitlSafetyError('plan-review permissionModeAfter must be "default" or "acceptEdits"', 'bad_plan_mode');
336
- }
333
+ if (modeAfter !== undefined && !isPlanReviewModeAfter(modeAfter)) {
334
+ throw new HitlSafetyError('plan-review permissionModeAfter must be "default" or "acceptEdits"', 'bad_plan_mode');
337
335
  }
338
336
  const req = { decision: outcome.decision };
339
337
  if (modeAfter !== undefined && isPlanReviewModeAfter(modeAfter))
@@ -416,11 +414,11 @@ export class HitlBridge {
416
414
  throw e;
417
415
  if (isDecideAttemptTimeout(e)) {
418
416
  if (attempts >= DECIDE_TIMEOUT_MIN_ATTEMPTS && Date.now() - startedAt >= decideTimeoutRetryBudgetMs()) {
419
- throw new DecideTransportRetryExhaustedError(attempts, String(e));
417
+ throw new DecideTransportRetryExhaustedError(attempts, wireFailureTextsOf(e).reason);
420
418
  }
421
419
  }
422
420
  else if (netRetriesUsed >= 1) {
423
- throw new DecideTransportRetryExhaustedError(attempts, String(e));
421
+ throw new DecideTransportRetryExhaustedError(attempts, wireFailureTextsOf(e).reason);
424
422
  }
425
423
  else {
426
424
  netRetriesUsed++;
@@ -23,11 +23,17 @@ export declare function hitlHostSurfaceFor(sessionKey: string): HitlHostSurface
23
23
  export declare function _resetHitlHostSurfaceForTest(): void;
24
24
  export declare function surfaceForCurrentSession(): HitlHostSurface | null;
25
25
  export declare const CANCEL_DENY_BUDGET_MS = 2000;
26
+ export declare const INTERRUPT_DENY_REASON = "Interrupted by user";
26
27
  export declare const CANCEL_DENY_WARN_TEXT = "could not cancel the pending question \u2014 the session may stay locked; the run may need engine-side recovery";
27
28
  export declare const REMEMBER_NOT_APPLIED_WARN_TEXT = "this session-wide allow was not remembered by the worker \u2014 you will be asked again for this tool";
28
29
  export declare const EDIT_NOT_FORWARDED_WARN_TEXT = "your edited tool input was NOT forwarded \u2014 the tool is running with the ORIGINAL input you saw before editing";
29
30
  export declare function surfaceRememberNotApplied(): void;
30
31
  export declare function surfaceEditNotForwarded(): void;
32
+ export declare const APPROVAL_PARKING_WARN_TEXT = "This approval is being parked; answer it again on the parked card when it appears.";
33
+ export declare function approvalParkingNoticeText(retryAfterMs?: number): string;
34
+ export declare function surfaceApprovalParking(retryAfterMs?: number): void;
35
+ export declare const COMPACT_NOT_RUNNING_WARN_TEXT = "engine run finished before compaction was applied; nothing was compacted";
36
+ export declare function surfaceCompactNotRunning(sessionKey?: string): void;
31
37
  export declare const REPLAYED_APPROVAL_LIST_UNREADABLE_WARN_TEXT: string;
32
38
  export declare function surfaceReplayedApprovalListUnreadable(): void;
33
39
  export declare const EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT: string;
@@ -29,22 +29,26 @@ export function _resetHitlHostSurfaceForTest() {
29
29
  hostSurfaceMissesByKey.clear();
30
30
  }
31
31
  export function surfaceForCurrentSession() {
32
- const hostSurface = hitlHostSurfaceFor(DEFAULT_SESSION_KEY);
32
+ return surfaceForSession(DEFAULT_SESSION_KEY);
33
+ }
34
+ function surfaceForSession(sessionKey) {
35
+ const hostSurface = hitlHostSurfaceFor(sessionKey);
33
36
  if (hostSurface === null) {
34
- hostSurfaceMissesByKey.set(DEFAULT_SESSION_KEY, (hostSurfaceMissesByKey.get(DEFAULT_SESSION_KEY) ?? 0) + 1);
37
+ hostSurfaceMissesByKey.set(sessionKey, (hostSurfaceMissesByKey.get(sessionKey) ?? 0) + 1);
35
38
  }
36
39
  return hostSurface;
37
40
  }
38
41
  export const CANCEL_DENY_BUDGET_MS = 2000;
42
+ export const INTERRUPT_DENY_REASON = 'Interrupted by user';
39
43
  export const CANCEL_DENY_WARN_TEXT = 'could not cancel the pending question — the session may stay locked; the run may need engine-side recovery';
40
44
  const CANCEL_DENY_WARN_KEY = 'hitl-cancel-deny-warn';
41
45
  const CANCEL_DENY_WARN_TIMEOUT_MS = 10_000;
42
46
  function surfaceCancelDenyWarn(text = CANCEL_DENY_WARN_TEXT) {
43
47
  surfaceSelfClearingWarn(CANCEL_DENY_WARN_KEY, text, CANCEL_DENY_WARN_TIMEOUT_MS);
44
48
  }
45
- function surfaceSelfClearingWarn(key, text, timeoutMs) {
49
+ function surfaceSelfClearingWarn(key, text, timeoutMs, sessionKey = DEFAULT_SESSION_KEY) {
46
50
  try {
47
- const s = surfaceForCurrentSession();
51
+ const s = surfaceForSession(sessionKey);
48
52
  if (!s)
49
53
  return;
50
54
  s.showNotice({ key, text, color: 'warning', priority: 'immediate', timeoutMs });
@@ -72,6 +76,23 @@ export function surfaceRememberNotApplied() {
72
76
  export function surfaceEditNotForwarded() {
73
77
  surfaceSelfClearingWarn(EDIT_NOT_FORWARDED_KEY, EDIT_NOT_FORWARDED_WARN_TEXT, EDIT_NOT_FORWARDED_TIMEOUT_MS);
74
78
  }
79
+ export const APPROVAL_PARKING_WARN_TEXT = 'This approval is being parked; answer it again on the parked card when it appears.';
80
+ const APPROVAL_PARKING_KEY = 'hitl-approval-parking';
81
+ const APPROVAL_PARKING_TIMEOUT_MS = 15_000;
82
+ export function approvalParkingNoticeText(retryAfterMs) {
83
+ if (retryAfterMs === undefined)
84
+ return APPROVAL_PARKING_WARN_TEXT;
85
+ return `${APPROVAL_PARKING_WARN_TEXT} (the engine suggested waiting about ${Math.ceil(retryAfterMs / 100) / 10} s)`;
86
+ }
87
+ export function surfaceApprovalParking(retryAfterMs) {
88
+ surfaceSelfClearingWarn(APPROVAL_PARKING_KEY, approvalParkingNoticeText(retryAfterMs), APPROVAL_PARKING_TIMEOUT_MS);
89
+ }
90
+ export const COMPACT_NOT_RUNNING_WARN_TEXT = 'engine run finished before compaction was applied; nothing was compacted';
91
+ const COMPACT_NOT_RUNNING_KEY = 'engine-compact-not-running';
92
+ const COMPACT_NOT_RUNNING_TIMEOUT_MS = 10_000;
93
+ export function surfaceCompactNotRunning(sessionKey = DEFAULT_SESSION_KEY) {
94
+ surfaceSelfClearingWarn(COMPACT_NOT_RUNNING_KEY, COMPACT_NOT_RUNNING_WARN_TEXT, COMPACT_NOT_RUNNING_TIMEOUT_MS, sessionKey);
95
+ }
75
96
  export const REPLAYED_APPROVAL_LIST_UNREADABLE_WARN_TEXT = "Could not read the pending-approval list, so an approval request read back from this run's event log was not raised as a question, " +
76
97
  'and no decision was sent for it.';
77
98
  const REPLAYED_APPROVAL_LIST_UNREADABLE_KEY = 'hitl-replayed-approval-list-unreadable';
@@ -1,3 +1,4 @@
1
+ import { wireFailureTextsOf } from '../wireFailureShape.js';
1
2
  const BASH_TOOL_NAME_FOR_RULES = 'Bash';
2
3
  export function parseLocalAllowRule(rule, expectedToolName, deps) {
3
4
  if (typeof rule !== 'string' || rule === '') {
@@ -8,7 +9,7 @@ export function parseLocalAllowRule(rule, expectedToolName, deps) {
8
9
  value = deps.parseRule(rule);
9
10
  }
10
11
  catch (e) {
11
- return { ok: false, error: `rule text did not parse (${String(e)})` };
12
+ return { ok: false, error: `rule text did not parse (${wireFailureTextsOf(e).reason})` };
12
13
  }
13
14
  if (typeof value.toolName !== 'string' || value.toolName === '') {
14
15
  return { ok: false, error: 'rule text carries no tool name' };
@@ -0,0 +1,9 @@
1
+ export type NoAllowCause = 'no_allow_rule' | 'ask_rule' | 'budget' | 'other';
2
+ export interface RuleLadderNoAllowFacts {
3
+ readonly allowed?: boolean;
4
+ readonly denied?: boolean;
5
+ readonly askRuleMatched?: boolean;
6
+ readonly budgetExhausted?: boolean;
7
+ readonly noRuleMatched?: boolean;
8
+ }
9
+ export declare function noAllowCauseOf(card: object | null | undefined, ladder: RuleLadderNoAllowFacts | null | undefined): NoAllowCause | undefined;
@@ -0,0 +1,26 @@
1
+ import { automaticAnswerVerdict } from './automaticAnswerVerdict.js';
2
+ function ladderSays(ladder, key) {
3
+ if (typeof ladder !== 'object' || ladder === null)
4
+ return false;
5
+ try {
6
+ const d = Object.getOwnPropertyDescriptor(ladder, key);
7
+ return d !== undefined && 'value' in d && d.value === true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export function noAllowCauseOf(card, ladder) {
14
+ if (ladderSays(ladder, 'allowed') || ladderSays(ladder, 'denied'))
15
+ return undefined;
16
+ if (ladderSays(ladder, 'askRuleMatched'))
17
+ return 'ask_rule';
18
+ if (ladderSays(ladder, 'budgetExhausted'))
19
+ return 'budget';
20
+ const standing = automaticAnswerVerdict(card, 'standing_rule');
21
+ if (standing.kind === 'must_ask' && (standing.askClass === 'ask_rule' || standing.origin === 'ask_rule'))
22
+ return 'ask_rule';
23
+ if (ladderSays(ladder, 'noRuleMatched') && standing.kind === 'may_answer')
24
+ return 'no_allow_rule';
25
+ return 'other';
26
+ }
@@ -4,11 +4,12 @@ import { ASK_GONE, CHECKPOINT_UNSUPPORTED_VERSION, DECIDE_WORKFLOW_PARK_FORKED,
4
4
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
5
5
  import { hostLog } from '../host.js';
6
6
  import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
7
- import { observeCancelByDeny } from './hitlHostSurface.js';
7
+ import { INTERRUPT_DENY_REASON, observeCancelByDeny } from './hitlHostSurface.js';
8
8
  import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
9
9
  import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
10
10
  import { readDecideReceipt } from '../decideReceipt.js';
11
11
  import { decideFailureNote } from '../decideFailureNote.js';
12
+ import { wireFailureTextsOf } from '../wireFailureShape.js';
12
13
  export const MAX_GATE_HOPS = 2;
13
14
  export function nextHopBudget(prev, round) {
14
15
  const total = (prev?.total ?? 0) + 1;
@@ -139,7 +140,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
139
140
  });
140
141
  const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
141
142
  if (answer === null) {
142
- observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
143
+ observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: INTERRUPT_DENY_REASON }, gatedCallId, undefined, pending), taskId);
143
144
  return { kind: 'aborted', gatedCallId };
144
145
  }
145
146
  let declinedReceipt;
@@ -170,11 +171,13 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
170
171
  const currentPending = readDecideCurrentPending(e);
171
172
  const wireCode = readWireErrorCode(e);
172
173
  const wireRetriable = typeof e === 'object' && e !== null && refusalSaysStillPending(e);
174
+ const failureTexts = wireFailureTextsOf(e);
175
+ hostLog('debug', `liveHitlAskWire: decide failed: ${failureTexts.log}`);
173
176
  return {
174
177
  kind: 'failed',
175
178
  stage: 'decide',
176
179
  gatedCallId,
177
- reason: `decide failed: ${String(e)}${decideFailureNote(e, attemptedDecision)}`,
180
+ reason: `decide failed: ${failureTexts.reason}${decideFailureNote(e, attemptedDecision)}`,
178
181
  ...(code !== undefined ? { code } : {}),
179
182
  ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
180
183
  ...(currentPending !== undefined ? { currentPending } : {}),
@@ -1,4 +1,5 @@
1
1
  import { abortableSleep } from '../abortableSleep.js';
2
+ import { wireFailureTextsOf } from '../wireFailureShape.js';
2
3
  function parkRowPollDelay(ms, signal) {
3
4
  return abortableSleep(ms, signal ?? new AbortController().signal);
4
5
  }
@@ -16,7 +17,7 @@ export async function waitForParkRowBirth(deps) {
16
17
  return await deps.probe(attempt, signal);
17
18
  }
18
19
  catch (e) {
19
- return { kind: 'unborn', reason: `probe threw: ${String(e)}` };
20
+ return { kind: 'unborn', reason: `probe threw: ${wireFailureTextsOf(e).reason}` };
20
21
  }
21
22
  };
22
23
  const probeWithinRemaining = async (attempt, remainingMs) => {
@@ -44,6 +44,9 @@ export type RulesFailure = {
44
44
  } | {
45
45
  kind: 'cursor-stale';
46
46
  message: string;
47
+ } | {
48
+ kind: 'query-refused';
49
+ message: string;
47
50
  } | {
48
51
  kind: 'too-many-candidates';
49
52
  message: string;
@@ -78,7 +81,9 @@ export type RulesFailure = {
78
81
  kind: 'error';
79
82
  message: string;
80
83
  };
81
- export declare function classifyRulesFailure(e: unknown): RulesFailure;
84
+ export declare function classifyRulesFailure(e: unknown, context?: {
85
+ readonly cursorSent?: boolean;
86
+ }): RulesFailure;
82
87
  export declare const PERSISTED_RULE_BEHAVIORS: readonly RuleBehavior[];
83
88
  export declare function persistedRuleBehaviorOf(rule: unknown): RuleBehavior | undefined;
84
89
  export declare const PERSISTED_RULE_BEHAVIOR_UNKNOWN = "(unknown)";
@@ -36,7 +36,7 @@ export function readCcImportRedeemCounts(result) {
36
36
  }
37
37
  return null;
38
38
  }
39
- export function classifyRulesFailure(e) {
39
+ export function classifyRulesFailure(e, context) {
40
40
  const { status, errorCode, message, retryAfterSec, committed } = wireFailureShapeOf(e);
41
41
  if (errorCode === 'capability.rule_store_required') {
42
42
  return { kind: 'lane-unavailable', message };
@@ -58,8 +58,16 @@ export function classifyRulesFailure(e) {
58
58
  if (errorCode === 'state.rule_write_failed') {
59
59
  return committed === 'no' ? { kind: 'write-not-committed', message } : { kind: 'write-indeterminate', message };
60
60
  }
61
- if (status === 400 && errorCode === 'request.query_invalid')
62
- return { kind: 'cursor-stale', message };
61
+ if (status === 400 && errorCode === 'request.query_invalid') {
62
+ let cursorSent;
63
+ try {
64
+ cursorSent = context?.cursorSent;
65
+ }
66
+ catch {
67
+ cursorSent = undefined;
68
+ }
69
+ return cursorSent === false ? { kind: 'query-refused', message } : { kind: 'cursor-stale', message };
70
+ }
63
71
  if (status === 400 && errorCode === 'request.field_invalid')
64
72
  return { kind: 'field-refused', message };
65
73
  if (status === 400 && errorCode === 'request.body_shape')
@@ -144,7 +152,7 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
144
152
  res = await facade.list({ limit: PAGE_LIMIT, ...cleanParams, ...(cursor !== undefined ? { cursor } : {}) }, opts);
145
153
  }
146
154
  catch (e) {
147
- const failure = classifyRulesFailure(e);
155
+ const failure = classifyRulesFailure(e, { cursorSent: cursor !== undefined });
148
156
  if (failure.kind === 'cursor-stale' && cursor !== undefined && attempt === 0) {
149
157
  hostLog('debug', 'persistedRulesWire: rule list cursor was minted on another rev/principal/scope — dropping it and re-listing from the top (never resuming a stale keyset)');
150
158
  restart = true;
@@ -48,7 +48,6 @@ export type PlanReviewWireClient = {
48
48
  };
49
49
  export interface PlanReviewWire {
50
50
  client: PlanReviewWireClient;
51
- capsBaseUrl?: string;
52
51
  }
53
52
  export interface PlanReviewDecisionOutcome extends PlanReviewOutcomeMeta {
54
53
  detail: string;
@@ -6,7 +6,8 @@ import { engineWireTarget } from '../engineWireTarget.js';
6
6
  import { enqueuePlanReviewOutcome } from '../notifications.js';
7
7
  import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
8
8
  import { decideFailureNote } from '../decideFailureNote.js';
9
- import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError } from '../resumeRefusalCopy.js';
9
+ import { wireFailureTextsOf } from '../wireFailureShape.js';
10
+ import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError, resumeContextUnavailableContent, resumeContextUnavailableFromError } from '../resumeRefusalCopy.js';
10
11
  import { REQUEST_FIELD_CONFLICT, isUnavailableFaceCode } from '../engineErrorCodes.js';
11
12
  import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
12
13
  import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
@@ -309,6 +310,15 @@ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
309
310
  return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed; TaskOutput("${taskId}") shows where the task stands.`;
310
311
  }
311
312
  }
313
+ function clientOnlyView(wire) {
314
+ if (wire === undefined || wire === null || typeof wire !== 'object')
315
+ return wire;
316
+ return {
317
+ get client() {
318
+ return wire.client;
319
+ },
320
+ };
321
+ }
312
322
  function planReviewDecisionTimedOutOutcome(decision) {
313
323
  return (`The plan_review ${decision} was sent, but no answer came back from the engine within the time limit this host allows for it — ` +
314
324
  `it may have taken effect (${decision === 'approve' ? 'an approval keeps the engine busy until the resumed work finishes' : 'the engine may still be applying it'}).`);
@@ -492,12 +502,12 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
492
502
  let stillParkedProven = false;
493
503
  let statusSeen;
494
504
  const wire = opts?.wire;
495
- const resolved = engineWireFor(wire, DEFAULT_SESSION_KEY, usablePlanReviewWireClient);
496
- const badMode = permissionModeAfter !== undefined && (decision !== 'approve' || !isPlanReviewModeAfter(permissionModeAfter));
505
+ const resolved = engineWireFor(clientOnlyView(wire), DEFAULT_SESSION_KEY, usablePlanReviewWireClient);
506
+ const badMode = permissionModeAfter !== undefined && !isPlanReviewModeAfter(permissionModeAfter);
497
507
  if (badMode) {
498
- hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter is only valid with approve and must be "default" or "acceptEdits"`);
508
+ hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter must be "default" or "acceptEdits"`);
499
509
  effect = 'not_sent';
500
- outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid for this decision (it only applies to an approval, and must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
510
+ outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid (it must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
501
511
  }
502
512
  else if (resolved.kind === 'no-target') {
503
513
  hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
@@ -600,12 +610,15 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
600
610
  const refusedCode = e?.errorCode;
601
611
  const noAnswer = typeof status === 'number' || typeof e !== 'object' ? null : noAnswerKindOf(e);
602
612
  if (status === 400 && refusedCode === REQUEST_FIELD_CONFLICT && permissionModeAfter === 'acceptEdits') {
603
- hostLog('debug', 'planReviewWire: approve + acceptEdits refused (request.field_conflict) — NOT re-sent with a different mode');
613
+ hostLog('debug', `planReviewWire: ${decision} + acceptEdits refused (request.field_conflict) — NOT re-sent with a different mode`);
604
614
  effect = 'still_parked';
605
615
  stillParkedProven = true;
606
616
  outcome =
607
- 'The plan approval was NOT applied: the engine refused "auto-accept edits" for this task (it was not started read-only in plan mode, so that option does not apply). ' +
608
- 'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly, and that approving with "manually approve edits" will go through.';
617
+ decision === 'approve'
618
+ ? 'The plan approval was NOT applied: the engine refused "auto-accept edits" for this task (it was not started read-only in plan mode, so that option does not apply). ' +
619
+ 'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly, and that approving with "manually approve edits" will go through.'
620
+ : `The plan ${decision} was NOT applied: the engine refused the post-approval permission mode that was sent with it (request.field_conflict). ` +
621
+ 'Nothing changed engine-side — the plan is still waiting for review. Tell the user plainly that the decision did not go through.';
609
622
  }
610
623
  else if (status === 404 && refusedCode === 'not_found.plan_review') {
611
624
  hostLog('debug', `planReviewWire: ${decision} → 404 not_found.plan_review (no pending plan review any more — an earlier decision likely settled it)`);
@@ -622,6 +635,12 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
622
635
  stillParkedCause = 'stored_form_unsupported';
623
636
  outcome = `This ${decision} was not applied. ${checkpointUnsupportedVersionContent(cu, { parkKind: 'plan_review', runId: taskId })}`;
624
637
  }
638
+ else if (resumeContextUnavailableFromError(e) !== null) {
639
+ const ru = resumeContextUnavailableFromError(e);
640
+ hostLog('debug', `planReviewWire: ${decision} → 409 conflict.resume_context_unavailable (the session context needed to continue this run is not available)`);
641
+ effect = 'not_applied';
642
+ outcome = `This ${decision} was not applied. ${resumeContextUnavailableContent(ru)}`;
643
+ }
625
644
  else if (typeof status === 'number' && corruptStoredRowWhere(e) !== undefined) {
626
645
  const where = corruptStoredRowWhere(e);
627
646
  hostLog('debug', `planReviewWire: ${decision} → ${String(status)} corrupt (stored resume context unreadable)`);
@@ -643,7 +662,9 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
643
662
  outcome = planReviewDecisionConnectionLostOutcome(decision);
644
663
  }
645
664
  else {
646
- outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
665
+ const failureTexts = wireFailureTextsOf(e);
666
+ hostLog('debug', `planReviewWire: ${decision} could not reach the engine: ${failureTexts.log}`);
667
+ outcome = `The plan_review decision could not reach the engine: ${failureTexts.reason}`;
647
668
  }
648
669
  }
649
670
  }