@sema-agent/client-core 0.107.0 → 0.108.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 (98) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +19 -14
  3. package/dist/adapter/activeRunSelfHeal.d.ts +2 -0
  4. package/dist/adapter/activeRunSelfHeal.js +20 -3
  5. package/dist/checkpointRowVerdict.d.ts +5 -0
  6. package/dist/checkpointRowVerdict.js +26 -0
  7. package/dist/cloudConfigWireCaps.d.ts +10 -0
  8. package/dist/cloudConfigWireCaps.js +33 -0
  9. package/dist/coreValuePorts.d.ts +17 -1
  10. package/dist/coreValuePorts.js +5 -0
  11. package/dist/decideReceipt.js +2 -1
  12. package/dist/engineErrorCodes.d.ts +5 -0
  13. package/dist/engineErrorCodes.js +5 -0
  14. package/dist/engineNoticeCodes.d.ts +19 -1
  15. package/dist/engineNoticeCodes.js +65 -3
  16. package/dist/engineTargetSeat.js +1 -0
  17. package/dist/errorSupplementRoles.js +2 -0
  18. package/dist/eventLeg.js +1 -1
  19. package/dist/executionLaneWords.js +2 -1
  20. package/dist/gateOutcome.d.ts +10 -1
  21. package/dist/gateOutcome.js +48 -1
  22. package/dist/generated/engineFactTables.d.ts +1 -0
  23. package/dist/generated/engineFactTables.js +5 -0
  24. package/dist/generated/engineNoticeTables.js +3 -0
  25. package/dist/generated/settingsSchemaFns.d.ts +4 -2
  26. package/dist/generated/settingsSchemaFns.js +9 -3
  27. package/dist/generated/settingsSchemaMcpRequestEntry.d.ts +1 -1
  28. package/dist/generated/settingsSchemaMcpRequestEntry.js +9 -18
  29. package/dist/generated/settingsSchemaReservedMemberName.d.ts +1 -0
  30. package/dist/generated/settingsSchemaReservedMemberName.js +7 -0
  31. package/dist/generated/settingsSchemaTables.d.ts +2 -0
  32. package/dist/generated/settingsSchemaTables.js +10 -0
  33. package/dist/generated/settingsSchemaWireName.d.ts +1 -0
  34. package/dist/generated/settingsSchemaWireName.js +8 -0
  35. package/dist/headlessReconnectWire.js +12 -7
  36. package/dist/hitl/approvalAskers.d.ts +13 -0
  37. package/dist/hitl/approvalAskers.js +79 -0
  38. package/dist/hitl/approvalResolution.d.ts +3 -0
  39. package/dist/hitl/approvalResolution.js +22 -4
  40. package/dist/hitl/approvalsFeed.js +3 -12
  41. package/dist/hitl/askParkRowRouting.js +5 -1
  42. package/dist/hitl/decisionRefusal.d.ts +1 -0
  43. package/dist/hitl/decisionRefusal.js +5 -2
  44. package/dist/hitl/decisionSend.js +2 -6
  45. package/dist/hitl/frameRouter.d.ts +3 -2
  46. package/dist/hitl/frameRouter.js +28 -6
  47. package/dist/hitl/gateLedger.d.ts +2 -0
  48. package/dist/hitl/gateLedger.js +3 -1
  49. package/dist/hitl/hitlBridge.js +2 -3
  50. package/dist/hitl/parkResolver.js +32 -23
  51. package/dist/hitl/parkRowBirthWait.d.ts +2 -0
  52. package/dist/hitl/parkRowBirthWait.js +8 -1
  53. package/dist/hitl/persistedRulesWire.d.ts +5 -1
  54. package/dist/hitl/persistedRulesWire.js +20 -4
  55. package/dist/hitl/planReviewWire.js +12 -16
  56. package/dist/hitl/policyDenyCause.d.ts +3 -0
  57. package/dist/hitl/policyDenyCause.js +16 -0
  58. package/dist/hitl/sessionPolicyDeliverable.js +1 -1
  59. package/dist/hitl/toolApprovalWire.d.ts +8 -2
  60. package/dist/hitl/toolApprovalWire.js +22 -3
  61. package/dist/hooksReaderGenerations.d.ts +1 -0
  62. package/dist/hooksReaderGenerations.js +12 -0
  63. package/dist/hooksWireCaps.js +8 -4
  64. package/dist/i18n/messages.en.d.ts +38 -1
  65. package/dist/i18n/messages.en.js +40 -3
  66. package/dist/i18n/messages.zh.js +45 -3
  67. package/dist/index.d.ts +6 -1
  68. package/dist/index.js +4 -1
  69. package/dist/liveModelCatalog.d.ts +4 -2
  70. package/dist/liveModelCatalog.js +164 -26
  71. package/dist/model/catalogDefault.d.ts +25 -0
  72. package/dist/model/catalogDefault.js +44 -0
  73. package/dist/permissionWireCaps.d.ts +1 -0
  74. package/dist/permissionWireCaps.js +11 -0
  75. package/dist/pluginHooksWire.d.ts +5 -3
  76. package/dist/pluginHooksWire.js +116 -7
  77. package/dist/queuedDelivery.d.ts +1 -0
  78. package/dist/queuedDelivery.js +12 -0
  79. package/dist/resultFrameKeys.js +4 -2
  80. package/dist/resumeRefusalCopy.js +15 -8
  81. package/dist/runPermissionModeSwitch.d.ts +2 -1
  82. package/dist/runPermissionModeSwitch.js +16 -6
  83. package/dist/seatContract.d.ts +63 -10
  84. package/dist/seatContract.js +164 -11
  85. package/dist/sessionEngine.d.ts +2 -1
  86. package/dist/sessionEngine.js +5 -0
  87. package/dist/settingsHooksSalvage.d.ts +5 -0
  88. package/dist/settingsHooksSalvage.js +54 -0
  89. package/dist/subagentContentStore.js +1 -1
  90. package/dist/supportFloor.js +2 -0
  91. package/dist/taskNotificationAdmission.d.ts +19 -0
  92. package/dist/taskNotificationAdmission.js +84 -0
  93. package/dist/toolResult.js +2 -1
  94. package/dist/webSearchWireCaps.js +9 -4
  95. package/dist/wireRefusalCopy.d.ts +2 -1
  96. package/dist/wireRefusalCopy.js +2 -2
  97. package/docs/INTEGRATION-CLIENTS.md +816 -14
  98. package/package.json +4 -4
@@ -11,8 +11,7 @@ import { suspendedReopenOf } from './suspendedReopen.js';
11
11
  import { closeDecisionOp, decisionLedgerKeysOf, openDecisionOp } from './decisionLedger.js';
12
12
  import { askParkBlockGeneration, noteAskParkDecideSettled } from './askParkAutoReopenBlock.js';
13
13
  import { checkpointQueueRowSortOf, QUEUE_ROW_FAMILY_UNREADABLE_GATE } from './approvalQueueRow.js';
14
- import { checkpointRowBarOf, checkpointRowBarSentence } from '../checkpointRowVerdict.js';
15
- import { msg, renderMessage } from '../i18n/translate.js';
14
+ import { checkpointDeadlineStateOf, checkpointRowBarOf, checkpointRowBarSentence, unboundWayOutContent } from '../checkpointRowVerdict.js';
16
15
  export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
17
16
  export const MAX_DENY_REASON_CHARS = 4096;
18
17
  export function denyReasonForWire(reason, tag) {
@@ -365,7 +364,7 @@ export class HitlBridge {
365
364
  if (typeof boundCallId !== 'string' || boundCallId.length === 0 || typeof boundInputHash !== 'string' || boundInputHash.length === 0) {
366
365
  throw new HitlSafetyError(`pending checkpoint ${p.id ?? '<no-id>'} is missing D-1 binding fields ` +
367
366
  `(boundCallId=${String(boundCallId)}, boundInputHash=${String(boundInputHash)}) — ` +
368
- `the row itself carries no binding to echo, so ${renderMessage(msg('approval.decide.unboundWayOut'))}`, 'binding_missing');
367
+ `the row itself carries no binding to echo, so ${unboundWayOutContent(checkpointDeadlineStateOf(p))}`, 'binding_missing');
369
368
  }
370
369
  return { boundCallId, boundInputHash };
371
370
  }
@@ -1,18 +1,20 @@
1
1
  import { putOwnKey, readOwnDataValue } from '../ownKey.js';
2
2
  import { HitlBridge, NO_QUESTION_OVERLAY_REASON, findPendingForTask, isNoDecisionSurfaceReason, } from './hitlBridge.js';
3
- import { approvalResolutionDetail, directSafetyStopOf, resolutionOfDecisionFailure } from './approvalResolution.js';
3
+ import { approvalResolutionDetail, approvalResolutionStatusTag, directSafetyStopOf, resolutionOfDecisionFailure } from './approvalResolution.js';
4
4
  import { readDecisionRefusalAsk } from './decisionRefusal.js';
5
5
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
6
6
  import { hostLog } from '../host.js';
7
7
  import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
8
8
  import { INTERRUPT_DENY_REASON, observeCancelByDeny } from './hitlHostSurface.js';
9
- import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
9
+ import { denyAttributionOf, flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
10
+ import { invokeHostCallbackIsolated } from '../hostCallbackGuard.js';
10
11
  import { inheritFrameBinding } from '../frameBinding.js';
11
12
  import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
12
13
  import { readDecideReceipt } from '../decideReceipt.js';
13
14
  import { decideFailureNote } from '../decideFailureNote.js';
14
15
  import { wireFailureTextsOf } from '../wireFailureShape.js';
15
16
  import { msg, renderMessage } from '../i18n/translate.js';
17
+ import { refusedParkFromError } from '../resumeRefusalCopy.js';
16
18
  export const MAX_GATE_HOPS = 2;
17
19
  export function nextHopBudget(prev, round) {
18
20
  const total = (prev?.total ?? 0) + 1;
@@ -160,6 +162,10 @@ function failureReasonOf(r) {
160
162
  return '';
161
163
  return r.detail ?? approvalResolutionDetail(r) ?? r.cause;
162
164
  }
165
+ function failsoftReasonOf(r) {
166
+ const tag = approvalResolutionStatusTag(r);
167
+ return tag === '' ? failureReasonOf(r) : `${failureReasonOf(r)} ${tag}`;
168
+ }
163
169
  function orchestrationEnd(park, gatedCallId, detail) {
164
170
  return { kind: 'unsettled', lane: park.gate === 'fs' ? 'decide' : 'ask', cause: 'orchestration', ...(gatedCallId !== undefined ? { gatedCallId } : {}), detail };
165
171
  }
@@ -180,10 +186,7 @@ async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
180
186
  ...(decidedByReading !== undefined ? { approvalDecidedBy: decidedByReading } : {}),
181
187
  }, taskId, argsByCallOf(led), ctx.signal, gatedCallId, park.toolName);
182
188
  if (outcome.kind === 'decided' && outcome.decision === 'deny' && outcome.gatedCallId) {
183
- led.markDenied(outcome.gatedCallId, {
184
- ...(outcome.deny?.settledBy !== undefined ? { settledBy: outcome.deny.settledBy } : {}),
185
- ...(outcome.deny?.reason !== undefined ? { reason: outcome.deny.reason } : {}),
186
- });
189
+ led.markDenied(outcome.gatedCallId, denyAttributionOf(outcome.deny));
187
190
  }
188
191
  return outcome;
189
192
  }
@@ -283,10 +286,9 @@ export async function resolvePark(park, ctx) {
283
286
  ? undefined
284
287
  : outcome.kind === 'not_sent' && outcome.cause === 'retracted'
285
288
  ? 'the approval card was retracted by the host without a decision (nothing was sent; the run stays parked and remains decidable elsewhere)'
286
- : failureReasonOf(outcome);
289
+ : failsoftReasonOf(outcome);
287
290
  const noDecisionSurface = outcome.kind === 'not_sent' && outcome.cause === 'card_unavailable' && outcome.detail !== undefined && isNoDecisionSurfaceReason(outcome.detail);
288
- const undecidableHere = outcome.kind === 'not_sent' && (outcome.cause === 'row_unresumable' || (outcome.cause === 'safety_stop' && outcome.safetyCode === 'binding_missing'));
289
- return { kind: 'failsoft', events: failsoftEvents(park, ctx, interrupted, failedReason, undefined, noDecisionSurface, undecidableHere) };
291
+ return { kind: 'failsoft', events: failsoftEvents(park, ctx, interrupted, failedReason, undefined, noDecisionSurface, failsoftWayOutOf(outcome)) };
290
292
  }
291
293
  led.dropHeldForDecidedPark(outcome.gatedCallId, park.gatedCallId);
292
294
  if (outcome.gatedCallId) {
@@ -309,17 +311,27 @@ export async function resolvePark(park, ctx) {
309
311
  ...(handoffTaskId !== undefined ? { handoffTaskId } : {}),
310
312
  };
311
313
  }
312
- function failsoftReasonSentence(park, failedReason, terminal, reasonCarriesWayOut = false) {
314
+ function failsoftWayOutOf(outcome) {
315
+ if (outcome.kind === 'not_sent' && (outcome.cause === 'row_unresumable' || (outcome.cause === 'safety_stop' && outcome.safetyCode === 'binding_missing')))
316
+ return 'reason';
317
+ if (outcome.kind === 'unsettled' && outcome.cause === 'engine_refused' && refusedParkFromError(outcome.refusal) !== null)
318
+ return 'cancel';
319
+ return 'card';
320
+ }
321
+ function failsoftReasonSentence(park, failedReason, terminal, wayOut = 'card') {
313
322
  if (terminal !== undefined)
314
323
  return terminal.errorMessage;
315
324
  if (failedReason === undefined)
316
325
  return undefined;
317
- if (reasonCarriesWayOut)
318
- return renderMessage(msg('approval.park.stillParked', { gate: park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion', reason: failedReason }));
326
+ const gate = park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion';
327
+ if (wayOut === 'reason')
328
+ return renderMessage(msg('approval.park.stillParked', { gate, reason: failedReason }));
329
+ if (wayOut === 'cancel')
330
+ return renderMessage(msg('approval.park.refusedNotResumable', { gate, reason: failedReason }));
319
331
  return (`${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${failedReason}` +
320
332
  ' — the run is still parked on this approval: decide it on the card when it is shown again, or cancel the run.');
321
333
  }
322
- function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, noDecisionSurface = false, reasonCarriesWayOut = false) {
334
+ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, noDecisionSurface = false, wayOut = 'card') {
323
335
  const events = [
324
336
  ...flushHeldWithInterruptRewrite(ctx.led, {
325
337
  terminal: park.pendingDone,
@@ -327,7 +339,7 @@ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, no
327
339
  gateAbortedByUser,
328
340
  }),
329
341
  ];
330
- const reason = failsoftReasonSentence(park, failedReason, terminal, reasonCarriesWayOut);
342
+ const reason = failsoftReasonSentence(park, failedReason, terminal, wayOut);
331
343
  if (park.pendingDone) {
332
344
  events.push(reason === undefined || noDecisionSurface ? park.pendingDone : inheritFrameBinding(park.pendingDone, { ...park.pendingDone, _sema_park_failed_reason: reason }));
333
345
  }
@@ -356,15 +368,12 @@ export function stalledTerminal(park, ctx, stalledRounds, lastStallReason, decid
356
368
  return { kind: 'failsoft', events: failsoftEvents(park, ctx, false, reason) };
357
369
  }
358
370
  function notifyParkReattach(ctx, attempt, max, reason) {
359
- const cb = ctx.deps.onParkReattach;
360
- if (typeof cb !== 'function')
361
- return;
362
- try {
363
- cb({ attempt, max, reason });
364
- }
365
- catch (e) {
366
- hostLog('debug', `liveHitlAskWire: onParkReattach threw (${String(e)}) — ignored`);
367
- }
371
+ invokeHostCallbackIsolated('liveHitlAskWire: onParkReattach', () => {
372
+ const cb = ctx.deps.onParkReattach;
373
+ if (typeof cb !== 'function' || ctx.signal?.aborted === true)
374
+ return;
375
+ return cb.call(ctx.deps, { attempt, max, reason });
376
+ });
368
377
  }
369
378
  function transportReasonToken(reason) {
370
379
  const m = /(ECONNREFUSED|ECONNRESET|ETIMEDOUT|EPIPE|EAI_AGAIN|TimeoutError|AbortError|fetch failed|network)/i.exec(reason);
@@ -11,6 +11,7 @@ export type ParkRowProbe<Row> = {
11
11
  rowVerdict?: Exclude<CheckpointRowVerdictReading, {
12
12
  readonly kind: 'ok';
13
13
  }>;
14
+ rowTaskId?: string;
14
15
  };
15
16
  export type ParkRowWaitVerdict<Row> = {
16
17
  kind: 'row';
@@ -30,6 +31,7 @@ export type ParkRowWaitVerdict<Row> = {
30
31
  rowVerdict?: Exclude<CheckpointRowVerdictReading, {
31
32
  readonly kind: 'ok';
32
33
  }>;
34
+ rowTaskId?: string;
33
35
  } | {
34
36
  kind: 'aborted';
35
37
  waitedMs: number;
@@ -103,7 +103,14 @@ export async function waitForParkRowBirth(deps) {
103
103
  if (probed.kind === 'row')
104
104
  return { kind: 'row', row: probed.row, waitedMs: now() - startedAt, probes };
105
105
  if (probed.kind === 'settled') {
106
- return { kind: 'settled', reason: probed.reason, waitedMs: now() - startedAt, probes, ...(probed.rowVerdict !== undefined ? { rowVerdict: probed.rowVerdict } : {}) };
106
+ return {
107
+ kind: 'settled',
108
+ reason: probed.reason,
109
+ waitedMs: now() - startedAt,
110
+ probes,
111
+ ...(probed.rowVerdict !== undefined ? { rowVerdict: probed.rowVerdict } : {}),
112
+ ...(probed.rowTaskId !== undefined ? { rowTaskId: probed.rowTaskId } : {}),
113
+ };
107
114
  }
108
115
  lastReason = probed.reason;
109
116
  const remainingMs = budgetMs - (now() - startedAt);
@@ -31,12 +31,16 @@ export interface CcImportRedeemRuleKey {
31
31
  readonly rule: string;
32
32
  readonly scope: string;
33
33
  }
34
+ export interface CcImportRedeemRefused {
35
+ readonly key?: CcImportRedeemRuleKey;
36
+ }
34
37
  export interface CcImportRedeemCounts {
35
38
  persisted: number;
36
39
  deduped: number;
37
40
  indeterminate: readonly CcImportRedeemIndeterminate[];
41
+ refused?: readonly CcImportRedeemRefused[];
38
42
  }
39
- export type CcImportReceiptOutcome = 'persisted' | 'no-op' | 'indeterminate';
43
+ export type CcImportReceiptOutcome = 'persisted' | 'no-op' | 'refused' | 'indeterminate';
40
44
  export declare function readCcImportRedeemCounts(body: unknown): CcImportRedeemCounts | null;
41
45
  export type RulesFailure = {
42
46
  kind: 'lane-unavailable';
@@ -43,21 +43,29 @@ function ownNonEmpty(o, key) {
43
43
  const r = slotReadOf(o, key);
44
44
  return r.presence === 'present' && typeof r.value === 'string' && r.value !== '' ? r.value : undefined;
45
45
  }
46
- function indeterminateRowOf(row) {
46
+ function ruleKeyOfRow(row) {
47
47
  const k = slotReadOf(row, 'key');
48
48
  const keyObj = k.presence === 'present' && typeof k.value === 'object' && k.value !== null ? k.value : undefined;
49
49
  const behavior = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'behavior');
50
50
  const rule = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'rule');
51
51
  const scope = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'scope');
52
+ return behavior !== undefined && rule !== undefined && scope !== undefined ? Object.freeze({ behavior, rule, scope }) : undefined;
53
+ }
54
+ function indeterminateRowOf(row) {
55
+ const key = ruleKeyOfRow(row);
52
56
  const reason = ownNonEmpty(row, 'reason');
53
57
  const d = slotReadOf(row, 'detail');
54
58
  const shown = d.presence === 'present' && typeof d.value === 'string' ? displayUntrusted(d.value, { max: INDETERMINATE_DETAIL_SHOWN_MAX }) : undefined;
55
59
  return Object.freeze({
56
- ...(behavior !== undefined && rule !== undefined && scope !== undefined ? { key: Object.freeze({ behavior, rule, scope }) } : {}),
60
+ ...(key !== undefined ? { key } : {}),
57
61
  ...(reason !== undefined ? { reason } : {}),
58
62
  ...(shown !== undefined && shown.trim() !== '' ? { detail: shown } : {}),
59
63
  });
60
64
  }
65
+ function refusedRowOf(row) {
66
+ const key = ruleKeyOfRow(row);
67
+ return Object.freeze(key !== undefined ? { key } : {});
68
+ }
61
69
  const RECEIPT_ROW_ARMS = {
62
70
  persisted: (acc) => {
63
71
  acc.persisted += 1;
@@ -65,12 +73,15 @@ const RECEIPT_ROW_ARMS = {
65
73
  'no-op': (acc) => {
66
74
  acc.deduped += 1;
67
75
  },
76
+ refused: (acc, row) => {
77
+ appendOwnElement(acc.refused, refusedRowOf(row));
78
+ },
68
79
  indeterminate: (acc, row) => {
69
80
  appendOwnElement(acc.indeterminate, indeterminateRowOf(row));
70
81
  },
71
82
  };
72
83
  function receiptCountsOf(rows) {
73
- const acc = { persisted: 0, deduped: 0, indeterminate: [] };
84
+ const acc = { persisted: 0, deduped: 0, indeterminate: [], refused: [] };
74
85
  for (const row of rows) {
75
86
  if (typeof row !== 'object' || row === null)
76
87
  return null;
@@ -80,7 +91,12 @@ function receiptCountsOf(rows) {
80
91
  return null;
81
92
  RECEIPT_ROW_ARMS[word](acc, row);
82
93
  }
83
- return { persisted: acc.persisted, deduped: acc.deduped, indeterminate: acc.indeterminate.length === 0 ? NO_INDETERMINATE : Object.freeze(acc.indeterminate) };
94
+ return {
95
+ persisted: acc.persisted,
96
+ deduped: acc.deduped,
97
+ indeterminate: acc.indeterminate.length === 0 ? NO_INDETERMINATE : Object.freeze(acc.indeterminate),
98
+ ...(acc.refused.length === 0 ? {} : { refused: Object.freeze(acc.refused) }),
99
+ };
84
100
  }
85
101
  function legacyEnvelopeCountsOf(result) {
86
102
  if (typeof result !== 'object' || result === null)
@@ -1,5 +1,6 @@
1
1
  import { publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionResponder, unregisterLocalQuestionResponder, hasLocalQuestionResponder, hasQuestionOverlayFor, } from '../liveQuestionStore.js';
2
2
  import { hostLog } from '../host.js';
3
+ import { invokeHostCallbackIsolated } from '../hostCallbackGuard.js';
3
4
  import { abortableSleep, settleWithinDeadline } from '../abortableSleep.js';
4
5
  import { clientHasVerbs, engineWireFor, noAnswerKindOf, sendOnce } from '../engineWireFor.js';
5
6
  import { engineWireTarget } from '../engineWireTarget.js';
@@ -1028,22 +1029,17 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
1028
1029
  hostLog('debug', `planReviewWire: outcome enqueue threw: ${String(e)}`);
1029
1030
  }
1030
1031
  if (onOutcome !== undefined) {
1031
- try {
1032
- onOutcome({
1033
- taskId,
1034
- dispatchNo,
1035
- decision: semanticDecision,
1036
- effect,
1037
- ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}),
1038
- detail: outcome,
1039
- tail,
1040
- prompt,
1041
- ...(statusSeen !== undefined ? { status: statusSeen } : {}),
1042
- });
1043
- }
1044
- catch (e) {
1045
- hostLog('debug', `planReviewWire: onOutcome callback threw for task ${taskId} (ignored): ${String(e)}`);
1046
- }
1032
+ invokeHostCallbackIsolated(`planReviewWire: onOutcome callback (task ${taskId})`, () => onOutcome({
1033
+ taskId,
1034
+ dispatchNo,
1035
+ decision: semanticDecision,
1036
+ effect,
1037
+ ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}),
1038
+ detail: outcome,
1039
+ tail,
1040
+ prompt,
1041
+ ...(statusSeen !== undefined ? { status: statusSeen } : {}),
1042
+ }));
1047
1043
  }
1048
1044
  }
1049
1045
  function reopenIdSuffix() {
@@ -0,0 +1,3 @@
1
+ export declare const POLICY_DENY_CAUSES: readonly ["no_approval_ui", "dont_ask", "deny_rule", "engine_floor"];
2
+ export type PolicyDenyCause = (typeof POLICY_DENY_CAUSES)[number];
3
+ export declare function readPolicyDenyCause(decision: object | null | undefined): PolicyDenyCause | undefined;
@@ -0,0 +1,16 @@
1
+ export const POLICY_DENY_CAUSES = Object.freeze(['no_approval_ui', 'dont_ask', 'deny_rule', 'engine_floor']);
2
+ export function readPolicyDenyCause(decision) {
3
+ if (decision === null || typeof decision !== 'object')
4
+ return undefined;
5
+ let d;
6
+ try {
7
+ d = Object.getOwnPropertyDescriptor(decision, 'policyCause');
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ if (d === undefined || !Object.hasOwn(d, 'value'))
13
+ return undefined;
14
+ const v = d.value;
15
+ return typeof v === 'string' && POLICY_DENY_CAUSES.includes(v) ? v : undefined;
16
+ }
@@ -12,7 +12,7 @@ const WITHHELD_NOTICE = {
12
12
  qualified_deny: 'Not written to this session’s rule record: that record holds whole tool names and command names only, and this rule has a part in parentheses it cannot carry — recording just the name could block more than this rule does.',
13
13
  peer_wide: 'Not written to this session’s rule record: this rule names a server or agent peer without naming one of its tools, and an engine that matches that record by exact tool name would block none of that peer’s tools.',
14
14
  wildcard: 'Not written to this session’s rule record: this rule uses a wildcard (*), and an engine that matches that record by exact tool name would block none of the tools it covers.',
15
- not_a_tool_name: 'Not written to this session’s rule record: this entry is not exactly one tool name — it is empty, not text, has spaces around the name, is not a single tool name in rule syntax (for example it contains a backslash or a parenthesis, or is longer than a rule can be), or spells its server or agent peer part in a way the engine never uses when naming tools (for example with a dot or a space), so no tool would ever match it.',
15
+ not_a_tool_name: 'Not written to this session’s rule record: this entry is not exactly one tool name — it is empty, not text, has spaces around the name, is not a single tool name in rule syntax (for example it contains a backslash or a parenthesis, or is longer than a rule can be), or spells its server or agent peer part in a form this record does not accept (for example with a dot or a space) · This record does not take entries of that form; record the exact name of a single tool instead.',
16
16
  legacy_tool_name: `Not written to this session’s rule record: unless a tool by exactly this name is available, the engine refuses it — it is a retired tool name, or it contains "__" but does not start with ${PREFIXES_IN_PROSE} — and with it in that record every later run of this session would fail at startup until that entry is removed from the record. ${REFUSED_NAME_FIX}`,
17
17
  not_in_roster: 'Not written to this session’s rule record: the tool roster supplied for this check has no tool this rule would block — no tool by exactly this name, as a name or an alias, with letter case counted, and, where the engine also reads the name as naming every tool of a server (a server whose own name contains "__"), no tool of that server either — it may be misspelled, or name a tool this deployment does not provide — so recorded as it is it would block nothing here.',
18
18
  namespace_covering_unconfirmed: msg('session.policy.withheld.namespaceCoveringUnconfirmed'),
@@ -1,4 +1,6 @@
1
1
  import { type HitlClientLike, type DecideOperationObserver } from './hitlBridge.js';
2
+ import { type ApprovalAskReason } from './approvalAskers.js';
3
+ export type { ApprovalAskReason, ApprovalAskReasonByHook, ApprovalAskReasonByPolicy } from './approvalAskers.js';
2
4
  import { readApprovalCardWord, type ApprovalCardWord } from './approvalCardReading.js';
3
5
  import { type ApprovalAllowRuleShadow } from './allowRuleShadow.js';
4
6
  import { CORE_PERSISTED_RULE_MATCHES } from '../generated/engineFactTables.js';
@@ -9,6 +11,7 @@ import { type ApprovalResolution } from './approvalResolution.js';
9
11
  import { type ApprovalMandateWord } from '../gateVocabulary.js';
10
12
  import type { ToolPermissionRequest } from '../seatContract.js';
11
13
  import { type ApprovalDecidedByReading } from '../approvalDecidedByCapability.js';
14
+ import { type PolicyDenyCause } from './policyDenyCause.js';
12
15
  import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS } from '@sema-agent/sdk';
13
16
  import type { ApprovalDecidedBy, ApprovalCardDelegation, ToolApprovalDecision as SdkToolApprovalDecision, ToolApprovalRespondAck, ToolApprovalRespondReplayed, PersistedRuleAnchor, RuleOfferMatch, RuleOfferBatchMember, RuleOfferUncoveredDetail, RuleOfferUncoveredReason, AskOrigin, RuleOffersAbsence, DenialLimitKind, DenialLimitFallback, ToolApprovalFrame, ApprovalRequestFrame, RuleOffer as SdkRuleOffer } from '@sema-agent/sdk';
14
17
  export declare function toolNameIsFsWrite(name: string): boolean;
@@ -71,6 +74,7 @@ export interface ApprovalCardDenyDecision {
71
74
  kind: 'deny';
72
75
  reason?: string;
73
76
  settledBy?: ApprovalDenySettledBy;
77
+ policyCause?: PolicyDenyCause;
74
78
  }
75
79
  export declare const APPROVAL_DENY_SETTLED_BY_WORDS: readonly ["human", "policy"];
76
80
  export type ApprovalDenySettledBy = (typeof APPROVAL_DENY_SETTLED_BY_WORDS)[number];
@@ -125,6 +129,8 @@ export interface ApprovalCardRequest {
125
129
  approvalCard?: ApprovalCardWord;
126
130
  askClass?: string;
127
131
  classes?: readonly string[];
132
+ agentId?: string;
133
+ askReasons?: readonly ApprovalAskReason[];
128
134
  boundGrants?: BoundSkillGrantsWire;
129
135
  grantBinding?: ApprovalGrantBinding;
130
136
  unreadableEvidence?: readonly string[];
@@ -163,7 +169,7 @@ export declare function approvalCardPortFor(sessionKey: string): ApprovalCardPor
163
169
  export declare function __resetSettledApprovalsForTests(): void;
164
170
  export declare function _resetApprovalCardPortForTest(): void;
165
171
  export declare function surfaceApprovalCard(req: ApprovalCardRequest): Promise<ApprovalCardDecision>;
166
- declare const REMEMBER_REFUSAL_CODES: readonly ["remember_not_in_proof", "capability.store_required", "decide.workflow_remember_unsupported", "decide.parked_remember_unsupported"];
172
+ declare const REMEMBER_REFUSAL_CODES: readonly ["remember_not_in_proof", "capability.store_required", "decide.workflow_remember_unsupported", "decide.parked_remember_unsupported", "remember_requires_binding"];
167
173
  export type RememberRefusalCode = (typeof REMEMBER_REFUSAL_CODES)[number];
168
174
  export declare function rememberRefusalOf(error: unknown): RememberRefusalCode | undefined;
169
175
  export declare function surfaceFsApprovalAndDecide(deps: FsApprovalWireDeps, taskId: string, argsByCall: Map<string, unknown>, signal?: AbortSignal, parkGatedCallId?: string, parkToolName?: string): Promise<ApprovalResolution>;
@@ -276,7 +282,7 @@ export declare function approvalSupersedesDetail(card: Pick<ApprovalCardRequest,
276
282
  export declare function readDenialLimitFallback(v: unknown): DenialLimitFallback | undefined;
277
283
  export declare function readRuleOffers(v: unknown): RuleOfferReading[] | undefined;
278
284
  export declare function readRuleOfferSupply(offers: unknown): RuleOfferReading[] | undefined;
279
- export type ApprovalCardV1Facts = Pick<ApprovalCardRequest, 'toolName' | 'args'> & Pick<ApprovalCardRequest, 'toolCallId' | 'workerBadge' | 'wireNote' | 'argsUnavailable' | 'governanceForced' | 'delegation' | 'ruleOffersReadOnly' | 'inputHasBidi' | 'allowRuleShadow' | 'probeCause' | 'rememberRefused' | 'ruleEvidence' | 'requiresRealApproval' | 'ruleOffersAbsence' | 'denialLimitFallback' | 'askClass' | 'classes' | 'grantBinding' | 'unreadableEvidence' | 'message' | 'approvalCard'>;
285
+ export type ApprovalCardV1Facts = Pick<ApprovalCardRequest, 'toolName' | 'args'> & Pick<ApprovalCardRequest, 'toolCallId' | 'workerBadge' | 'wireNote' | 'argsUnavailable' | 'governanceForced' | 'delegation' | 'ruleOffersReadOnly' | 'inputHasBidi' | 'allowRuleShadow' | 'probeCause' | 'rememberRefused' | 'ruleEvidence' | 'requiresRealApproval' | 'ruleOffersAbsence' | 'denialLimitFallback' | 'askClass' | 'classes' | 'agentId' | 'askReasons' | 'grantBinding' | 'unreadableEvidence' | 'message' | 'approvalCard'>;
280
286
  export type ApprovalCardFacts = ApprovalCardV1Facts & Pick<ApprovalCardRequest, 'argsRewritten' | 'ruleOffers' | 'probeReason' | 'mandated' | 'mandate' | 'readRootCandidate' | 'origin' | 'boundGrants' | 'classifierNoRuling' | 'allowSessionCarried' | 'cardPayloadUnreadable'>;
281
287
  export interface ApprovalCardFrameFactsOptions {
282
288
  readonly streamArgs?: object;
@@ -7,6 +7,7 @@ import { trimEditedRuleText } from './editedRuleTextPrecheck.js';
7
7
  import { appendOwnElement, guardedRead, readAskOrigin, readOwnDataNonEmptyString, readOwnDataValue, readOwnNonEmptyString } from '../ownKey.js';
8
8
  import { ownReadingSlot } from '../engineWireFor.js';
9
9
  import { DEPLOYMENT_LIST_ASK_CLASSES, readAskClassPair } from './askClassPair.js';
10
+ import { approvalAgentIdOf, approvalAskReasonsOf } from './approvalAskers.js';
10
11
  import { invokeHostCallbackIsolated } from '../hostCallbackGuard.js';
11
12
  import { approvalWindowKeysOf, readApprovalCardWord, readRequiresRealApproval } from './approvalCardReading.js';
12
13
  import { readAllowRuleShadowFromWire, retiredAllowRuleShadowKeyPresent, RETIRED_SHADOW_EVIDENCE_LABEL } from './allowRuleShadow.js';
@@ -28,6 +29,7 @@ import { parkRowDeadlineState } from './parkExpiryJudgements.js';
28
29
  import { isApprovalMandateWord } from '../gateVocabulary.js';
29
30
  import { approvalDecidedByAdmitted } from '../approvalDecidedByCapability.js';
30
31
  import { decideFailureNote } from '../decideFailureNote.js';
32
+ import { readPolicyDenyCause } from './policyDenyCause.js';
31
33
  import { slotPresenceOf, wireFailureTextsOf } from '../wireFailureShape.js';
32
34
  import { APPROVAL_LIVE_FACE_KEYS, approvalLiveFaceOf, hasOwnStrictTrue } from './approvalLiveFace.js';
33
35
  import { subagentSourceSessionOf } from './subagentSource.js';
@@ -157,11 +159,13 @@ function cardPortMissReason() {
157
159
  return APPROVAL_CARD_PORT_KEY_MISMATCH_REASON;
158
160
  return NO_APPROVAL_CARD_PORT_REASON;
159
161
  }
162
+ const LEGACY_REMEMBER_REFUSAL_CODE = 'remember_requires_binding';
160
163
  const REMEMBER_REFUSAL_CODES = Object.freeze([
161
164
  'remember_not_in_proof',
162
165
  'capability.store_required',
163
166
  DECIDE_WORKFLOW_REMEMBER_UNSUPPORTED,
164
167
  'decide.parked_remember_unsupported',
168
+ LEGACY_REMEMBER_REFUSAL_CODE,
165
169
  ]);
166
170
  export function rememberRefusalOf(error) {
167
171
  return rememberRefusalIn(readWireErrorCodeReading(error));
@@ -517,6 +521,7 @@ async function presentFsApprovalAndDecide(deps, taskId, argsByCall, signal, foun
517
521
  ...approvalLiveFaceOf(pending).facts,
518
522
  ...((w) => (w !== undefined ? { approvalCard: w } : {}))(readApprovalCardWord(pending)),
519
523
  ...(readAskClassPair(pending) ?? {}),
524
+ ...((r) => (r !== undefined ? { askReasons: r } : {}))(approvalAskReasonsOf(pending)),
520
525
  ...(pending.gateKind === 'irreversible_ask' ? { irreversibleParkGate: true } : {}),
521
526
  ...(pending.hasBidiControls === true ? { hasBidiControls: true } : {}),
522
527
  ...(ruleOffersReadOnly !== undefined ? { ruleOffersReadOnly } : {}),
@@ -609,6 +614,7 @@ async function presentFsApprovalAndDecide(deps, taskId, argsByCall, signal, foun
609
614
  }
610
615
  case 'deny': {
611
616
  const denySettledByForWire = readApprovalDenySettledBy(card);
617
+ const denyPolicyCauseForWire = readPolicyDenyCause(card);
612
618
  const attributionKeys = decisionAttributionKeyOf(decidedByReading, denySettledByForWire);
613
619
  const denyReasonSnapshot = ((r) => (typeof r === 'string' ? r : undefined))(card.reason);
614
620
  let lastSendCarriedAttribution = false;
@@ -642,7 +648,7 @@ async function presentFsApprovalAndDecide(deps, taskId, argsByCall, signal, foun
642
648
  const attributionDropped = ladder.dropped;
643
649
  const attributionRenamed = ladder.renamed;
644
650
  const receipt = readDecideReceipt(raw);
645
- const deny = denyFactsOf(denySettledByForWire, denyReasonSnapshot, attributionDropped, attributionRenamed);
651
+ const deny = denyFactsOf(denySettledByForWire, denyReasonSnapshot, attributionDropped, attributionRenamed, denyPolicyCauseForWire);
646
652
  return {
647
653
  kind: 'decided',
648
654
  ...at,
@@ -1014,14 +1020,16 @@ function readDecidedByReadingOf(holder) {
1014
1020
  function decidedByReadingSnapshotOf(holder) {
1015
1021
  return approvalDecidedByAdmitted(readDecidedByReadingOf(holder)) ? Object.freeze({ kind: 'present', value: true }) : undefined;
1016
1022
  }
1017
- function denyFactsOf(settledBy, reasonSnapshot, dropped, renamed) {
1023
+ function denyFactsOf(settledBy, reasonSnapshot, dropped, renamed, policyCauseSnapshot) {
1018
1024
  const reason = reasonSnapshot !== undefined && reasonSnapshot.trim() !== '' ? reasonSnapshot : undefined;
1019
1025
  const attributionKey = dropped ? 'dropped' : renamed ? 'renamed' : undefined;
1026
+ const policyCause = settledBy === 'policy' ? policyCauseSnapshot : undefined;
1020
1027
  if (settledBy === undefined && reason === undefined && attributionKey === undefined)
1021
1028
  return undefined;
1022
1029
  return {
1023
1030
  ...(settledBy !== undefined ? { settledBy } : {}),
1024
1031
  ...(reason !== undefined ? { reason } : {}),
1032
+ ...(policyCause !== undefined ? { policyCause } : {}),
1025
1033
  ...(attributionKey !== undefined ? { attributionKey } : {}),
1026
1034
  };
1027
1035
  }
@@ -1822,6 +1830,16 @@ function cardFactsOf(carrier, leg, opts, liveFacts, approvalId) {
1822
1830
  if (w !== undefined)
1823
1831
  out.approvalCard = w;
1824
1832
  });
1833
+ v1Bit(() => {
1834
+ const id = approvalAgentIdOf(carrier);
1835
+ if (id !== undefined)
1836
+ out.agentId = id;
1837
+ });
1838
+ v1Bit(() => {
1839
+ const reasons = approvalAskReasonsOf(carrier);
1840
+ if (reasons !== undefined)
1841
+ out.askReasons = reasons;
1842
+ });
1825
1843
  }
1826
1844
  v1Bit(() => {
1827
1845
  const pair = readAskClassPair(carrier);
@@ -2059,6 +2077,7 @@ async function presentToolApprovalFrameAndRespond(frame, carriers, approvalId, r
2059
2077
  hostLog('debug', `liveToolApprovalWire: ${approvalId} card carried a note on Yes (len=${card.note.length}) — this engine has no field that brings it to the model, so it was not sent`);
2060
2078
  }
2061
2079
  const denySettledByForWire = card.kind === 'deny' ? readApprovalDenySettledBy(card) : undefined;
2080
+ const denyPolicyCauseForWire = card.kind === 'deny' ? readPolicyDenyCause(card) : undefined;
2062
2081
  const attributionKeys = decision === 'deny' ? decisionAttributionKeyOf(decidedByReading, denySettledByForWire) : {};
2063
2082
  const sentAttributionKey = carriesAttributionKey(attributionKeys);
2064
2083
  let lastSendCarriedAttribution = sentAttributionKey;
@@ -2300,7 +2319,7 @@ async function presentToolApprovalFrameAndRespond(frame, carriers, approvalId, r
2300
2319
  'this answer was not delivered, reporting the earlier decision');
2301
2320
  return { kind: 'decided', lane: 'respond', decision: replayed.decision, ack: replayed };
2302
2321
  }
2303
- const deny = decision === 'deny' ? denyFactsOf(denySettledByForWire, denyReasonSnapshot, attributionDropped, attributionRenamed) : undefined;
2322
+ const deny = decision === 'deny' ? denyFactsOf(denySettledByForWire, denyReasonSnapshot, attributionDropped, attributionRenamed, denyPolicyCauseForWire) : undefined;
2304
2323
  return { kind: 'decided', lane: 'respond', decision, ...(ack !== undefined ? { ack } : {}), ...(deny !== undefined ? { deny } : {}) };
2305
2324
  }
2306
2325
  catch (e) {
@@ -0,0 +1 @@
1
+ export declare function hooksReaderNotReadKeys(serverVersion: string | undefined): readonly string[];
@@ -0,0 +1,12 @@
1
+ import { engineVersionTier } from './supportFloor.js';
2
+ const HOOKS_READER_GENERATIONS = Object.freeze([
3
+ Object.freeze({ from: '7.139.0', below: '7.147.0', notRead: Object.freeze(['onFailure']) }),
4
+ ]);
5
+ const HOOKS_READER_FAIL_CLOSED = Object.freeze([...new Set(HOOKS_READER_GENERATIONS.flatMap((g) => g.notRead))]);
6
+ export function hooksReaderNotReadKeys(serverVersion) {
7
+ for (const g of HOOKS_READER_GENERATIONS) {
8
+ if (engineVersionTier(serverVersion, g.from) === 'engine' && engineVersionTier(serverVersion, g.below) === 'legacy')
9
+ return g.notRead;
10
+ }
11
+ return HOOKS_READER_FAIL_CLOSED;
12
+ }
@@ -1,4 +1,5 @@
1
- import { engineCapNestedTrue, engineCapState, engineCapValue } from './engineCapsCache.js';
1
+ import { engineCapNestedTrue, engineCapState, engineCapString, engineCapValue } from './engineCapsCache.js';
2
+ import { hooksReaderNotReadKeys } from './hooksReaderGenerations.js';
2
3
  import { msg, renderMessage } from './i18n/translate.js';
3
4
  import { capForDisplay, collapseLabel } from './fleetTaskDesc.js';
4
5
  import { hostLog } from './host.js';
@@ -185,7 +186,8 @@ export function hooksWirePlan(opts) {
185
186
  const types = typesByEvent.get(event);
186
187
  return types === undefined || (typeof type === 'string' && types.has(type));
187
188
  };
188
- return buildHooksWirePlan(read, engine, shellRuns, promptAuthoredByUserOf(o), suppressedLocallyOf(o), compactionOnShellOf(o));
189
+ const readerNotRead = hooksReaderNotReadKeys(engineCapString(o.baseUrl, 'version'));
190
+ return buildHooksWirePlan(read, engine, shellRuns, promptAuthoredByUserOf(o), suppressedLocallyOf(o), readerNotRead, compactionOnShellOf(o));
189
191
  }
190
192
  function taskHooksCapability(baseUrl) {
191
193
  const v = engineCapValue(baseUrl, 'taskSettings');
@@ -298,7 +300,7 @@ function withheldRowsOf(candidates, trust, layers) {
298
300
  }
299
301
  return rows;
300
302
  }
301
- function buildHooksWirePlan(read, engine, shellRuns, promptAuthoredByUser, suppressedLocally, compactionOnShell = false) {
303
+ function buildHooksWirePlan(read, engine, shellRuns, promptAuthoredByUser, suppressedLocally, readerNotRead, compactionOnShell = false) {
302
304
  const settingsDropped = [];
303
305
  const settingsVerdicts = [];
304
306
  const settingsExcluded = [];
@@ -344,6 +346,7 @@ function buildHooksWirePlan(read, engine, shellRuns, promptAuthoredByUser, suppr
344
346
  shellRuns,
345
347
  promptAuthoredByUser,
346
348
  occupied: new Map(),
349
+ readerNotRead,
347
350
  }));
348
351
  if (d === undefined)
349
352
  return planOf('failed', undefined);
@@ -465,7 +468,7 @@ function buildHooksWirePlan(read, engine, shellRuns, promptAuthoredByUser, suppr
465
468
  continue;
466
469
  if (!Array.isArray(groups) || groups.length === 0)
467
470
  continue;
468
- const kept = filterSettingsHookGroups(source, event, groups, engine.pluginHooksCapable === true, (type) => shellRuns(event, type), judge, layer.mask);
471
+ const kept = filterSettingsHookGroups(source, event, groups, engine.pluginHooksCapable === true, (type) => shellRuns(event, type), judge, readerNotRead, layer.mask);
469
472
  settingsDropped.push(...kept.drops);
470
473
  settingsVerdicts.push(...kept.verdicts);
471
474
  settingsExcluded.push(...kept.excluded);
@@ -531,6 +534,7 @@ function buildHooksWirePlan(read, engine, shellRuns, promptAuthoredByUser, suppr
531
534
  promptAuthoredByUser,
532
535
  occupied,
533
536
  ...(suppressedLocally !== undefined ? { suppressedLocally } : {}),
537
+ readerNotRead,
534
538
  });
535
539
  });
536
540
  if (d === undefined)