@sema-agent/client-core 0.82.0 → 0.82.2

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 (48) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -7
  3. package/dist/adapt/toolCards.d.ts +15 -0
  4. package/dist/adapt/toolCards.js +34 -19
  5. package/dist/adapter/activeRunSelfHeal.d.ts +3 -0
  6. package/dist/adapter/activeRunSelfHeal.js +37 -7
  7. package/dist/adapter/downstream/eventToSdkMessage.js +7 -4
  8. package/dist/adapter/downstream/terminalToSdkResult.d.ts +2 -0
  9. package/dist/adapter/downstream/terminalToSdkResult.js +44 -5
  10. package/dist/adapter/runStream.js +33 -4
  11. package/dist/adapter/types.d.ts +2 -0
  12. package/dist/agentsWireCaps.d.ts +1 -0
  13. package/dist/agentsWireCaps.js +110 -29
  14. package/dist/cloudConfigWireCaps.d.ts +77 -2
  15. package/dist/cloudConfigWireCaps.js +405 -8
  16. package/dist/decideFailureNote.d.ts +1 -0
  17. package/dist/decideFailureNote.js +8 -0
  18. package/dist/engineNoticeCodes.js +2 -0
  19. package/dist/executionLaneCapability.js +3 -0
  20. package/dist/fileHistoryCaptureCapability.d.ts +18 -0
  21. package/dist/fileHistoryCaptureCapability.js +58 -0
  22. package/dist/gateOutcome.js +2 -0
  23. package/dist/hitl/askGateWire.d.ts +1 -1
  24. package/dist/hitl/askGateWire.js +1 -1
  25. package/dist/hitl/frameRouter.d.ts +2 -1
  26. package/dist/hitl/hitlBridge.d.ts +29 -1
  27. package/dist/hitl/hitlBridge.js +135 -3
  28. package/dist/hitl/hitlHostSurface.js +2 -3
  29. package/dist/hitl/parkResolver.d.ts +1 -0
  30. package/dist/hitl/parkResolver.js +17 -9
  31. package/dist/hitl/planReviewWire.d.ts +6 -0
  32. package/dist/hitl/planReviewWire.js +8 -2
  33. package/dist/hitl/toolApprovalWire.d.ts +2 -1
  34. package/dist/hitl/toolApprovalWire.js +105 -102
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +2 -0
  37. package/dist/modelIdentityResolvability.d.ts +31 -0
  38. package/dist/modelIdentityResolvability.js +145 -0
  39. package/dist/notifications.d.ts +3 -1
  40. package/dist/notifications.js +18 -2
  41. package/dist/printToolResultFrame.js +15 -1
  42. package/dist/resumeRefusalCopy.d.ts +12 -0
  43. package/dist/resumeRefusalCopy.js +45 -1
  44. package/dist/seam.d.ts +2 -0
  45. package/dist/toolResult.d.ts +2 -2
  46. package/dist/toolResult.js +7 -6
  47. package/docs/INTEGRATION-CLIENTS.md +168 -11
  48. package/package.json +3 -3
@@ -64,6 +64,29 @@ export declare class DecideTransportRetryExhaustedError extends Error {
64
64
  readonly attempts: number;
65
65
  constructor(attempts: number, lastFailureText: string);
66
66
  }
67
+ export interface DecideOperationIdentity {
68
+ readonly operation: object;
69
+ readonly taskId: string;
70
+ readonly sessionId: string;
71
+ readonly boundCallId?: string;
72
+ }
73
+ export interface DecideOperationStart extends DecideOperationIdentity {
74
+ readonly phase: 'start';
75
+ }
76
+ export interface DecideOperationEnd extends DecideOperationIdentity {
77
+ readonly phase: 'end';
78
+ readonly ok: boolean;
79
+ readonly attempts: number;
80
+ readonly retryExhausted: boolean;
81
+ }
82
+ export type DecideOperationEvent = DecideOperationStart | DecideOperationEnd;
83
+ export type DecideOperationObserver = (ev: DecideOperationEvent) => void | Promise<void>;
84
+ export interface HitlBridgeOptions {
85
+ onDecideOperation?: DecideOperationObserver;
86
+ }
87
+ export interface DecideOperationSpan {
88
+ close(): void;
89
+ }
67
90
  export interface ActiveGate {
68
91
  gate: CheckpointGate;
69
92
  seq?: string | undefined;
@@ -85,7 +108,8 @@ export declare class HitlBridge {
85
108
  private readonly client;
86
109
  private readonly taskId;
87
110
  private active;
88
- constructor(client: HitlClientLike, taskId: string);
111
+ private readonly decideObserver;
112
+ constructor(client: HitlClientLike, taskId: string, opts?: HitlBridgeOptions);
89
113
  observe(ev: AgentEvent): void;
90
114
  currentGate(): CheckpointGate | null;
91
115
  currentGateReopen(): SuspendedReopenReading | null;
@@ -97,6 +121,7 @@ export declare class HitlBridge {
97
121
  remember?: 'session' | undefined;
98
122
  } | HitlDecideDenyOutcome, toolUseID?: string, opts?: {
99
123
  signal?: AbortSignal;
124
+ operation?: DecideOperationSpan;
100
125
  }, preResolvedPending?: PendingCheckpoint): Promise<unknown>;
101
126
  answerQuestion(answers: AskAnswer[], toolUseID?: string, opts?: {
102
127
  signal?: AbortSignal;
@@ -108,6 +133,9 @@ export declare class HitlBridge {
108
133
  signal?: AbortSignal;
109
134
  }): Promise<AssistantTaskStatus>;
110
135
  private decideRaw;
136
+ private decideIdentityOf;
137
+ openDecideOperation(): DecideOperationSpan;
138
+ private decideRawAttempts;
111
139
  }
112
140
  export type GatePrompt<D extends HitlPermissionDecisionLike = HitlPermissionDecisionLike> = (ctx: {
113
141
  toolName: string;
@@ -106,6 +106,94 @@ export class DecideTransportRetryExhaustedError extends Error {
106
106
  this.name = 'DecideTransportRetryExhaustedError';
107
107
  }
108
108
  }
109
+ class DecideSpan {
110
+ owner;
111
+ observer;
112
+ identity;
113
+ attempts = 0;
114
+ ok = false;
115
+ retryExhausted = false;
116
+ closedFlag = false;
117
+ inFlight = 0;
118
+ ended = false;
119
+ constructor(owner, observer) {
120
+ this.owner = owner;
121
+ this.observer = observer;
122
+ }
123
+ admits(bridge, identity) {
124
+ if (bridge !== this.owner || this.closedFlag)
125
+ return false;
126
+ const mine = this.identity;
127
+ return mine === undefined || (mine.sessionId === identity.sessionId && mine.boundCallId === identity.boundCallId);
128
+ }
129
+ async run(identity, send) {
130
+ if (this.observer !== undefined && this.identity === undefined) {
131
+ this.identity = identity;
132
+ notifyDecideObserver(this.observer, Object.freeze({ ...this.identity, phase: 'start' }));
133
+ }
134
+ this.inFlight++;
135
+ const tally = { attempts: 0 };
136
+ try {
137
+ const r = await send(tally);
138
+ this.ok = true;
139
+ this.retryExhausted = false;
140
+ return r;
141
+ }
142
+ catch (e) {
143
+ this.ok = false;
144
+ this.retryExhausted = e instanceof DecideTransportRetryExhaustedError;
145
+ throw e;
146
+ }
147
+ finally {
148
+ this.attempts += tally.attempts;
149
+ this.inFlight--;
150
+ if (this.closedFlag && this.inFlight === 0)
151
+ this.emitEnd();
152
+ }
153
+ }
154
+ close() {
155
+ if (this.closedFlag)
156
+ return;
157
+ this.closedFlag = true;
158
+ if (this.inFlight === 0)
159
+ this.emitEnd();
160
+ }
161
+ emitEnd() {
162
+ if (this.ended)
163
+ return;
164
+ this.ended = true;
165
+ if (this.observer === undefined || this.identity === undefined)
166
+ return;
167
+ notifyDecideObserver(this.observer, Object.freeze({ ...this.identity, phase: 'end', ok: this.ok, attempts: this.attempts, retryExhausted: this.retryExhausted }));
168
+ }
169
+ }
170
+ function reportObserverFault(phase, what, e) {
171
+ try {
172
+ hostLog('debug', `hitlBridge: onDecideOperation(${phase}) ${what}: ${String(e).slice(0, 160)}`);
173
+ return true;
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
179
+ function wireOptsOf(opts) {
180
+ if (opts === undefined || opts.operation === undefined)
181
+ return opts;
182
+ return opts.signal !== undefined ? { signal: opts.signal } : undefined;
183
+ }
184
+ function notifyDecideObserver(observer, ev) {
185
+ try {
186
+ const out = observer(ev);
187
+ if (out && typeof out.then === 'function') {
188
+ void Promise.resolve(out).then(undefined, (e) => {
189
+ reportObserverFault(ev.phase, 'rejected', e);
190
+ });
191
+ }
192
+ }
193
+ catch (e) {
194
+ reportObserverFault(ev.phase, 'threw', e);
195
+ }
196
+ }
109
197
  const ANY_TOOL_FAMILY = () => true;
110
198
  export async function findPendingForTask(client, taskId, matches, opts, callId, identityAccepts = (r) => matches(typeof r.toolName === 'string' ? r.toolName : undefined)) {
111
199
  let rows;
@@ -132,9 +220,11 @@ export class HitlBridge {
132
220
  client;
133
221
  taskId;
134
222
  active = null;
135
- constructor(client, taskId) {
223
+ decideObserver;
224
+ constructor(client, taskId, opts) {
136
225
  this.client = client;
137
226
  this.taskId = taskId;
227
+ this.decideObserver = typeof opts?.onDecideOperation === 'function' ? opts.onDecideOperation : undefined;
138
228
  }
139
229
  observe(ev) {
140
230
  switch (ev.type) {
@@ -180,7 +270,7 @@ export class HitlBridge {
180
270
  return { boundCallId: p.boundCallId, boundInputHash: p.boundInputHash };
181
271
  }
182
272
  async decideTool(outcome, toolUseID, opts, preResolvedPending) {
183
- const pending = preResolvedPending ?? (await this.pendingForDecide(toolUseID, opts));
273
+ const pending = preResolvedPending ?? (await this.pendingForDecide(toolUseID, wireOptsOf(opts)));
184
274
  if (!pending) {
185
275
  throw new HitlSafetyError('no pending checkpoint to decide (resolved/expired under the human) — refetch + re-present', 'no_pending');
186
276
  }
@@ -253,7 +343,48 @@ export class HitlBridge {
253
343
  }
254
344
  return this.client.assistant.resume(this.taskId, opts);
255
345
  }
256
- async decideRaw(sessionId, decision, opts) {
346
+ async decideRaw(sessionId, decision, optsIn) {
347
+ const opts = wireOptsOf(optsIn);
348
+ const op = optsIn?.operation;
349
+ if (op instanceof DecideSpan) {
350
+ const identity = this.decideIdentityOf(sessionId, decision);
351
+ if (op.admits(this, identity))
352
+ return op.run(identity, (tally) => this.decideRawAttempts(sessionId, decision, opts, tally));
353
+ }
354
+ const observer = this.decideObserver;
355
+ const tally = { attempts: 0 };
356
+ if (observer === undefined)
357
+ return this.decideRawAttempts(sessionId, decision, opts, tally);
358
+ const identity = this.decideIdentityOf(sessionId, decision);
359
+ notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'start' }));
360
+ let ok = false;
361
+ let retryExhausted = false;
362
+ try {
363
+ const r = await this.decideRawAttempts(sessionId, decision, opts, tally);
364
+ ok = true;
365
+ return r;
366
+ }
367
+ catch (e) {
368
+ retryExhausted = e instanceof DecideTransportRetryExhaustedError;
369
+ throw e;
370
+ }
371
+ finally {
372
+ notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'end', ok, attempts: tally.attempts, retryExhausted }));
373
+ }
374
+ }
375
+ decideIdentityOf(sessionId, decision) {
376
+ const bound = decision.boundCallId;
377
+ return {
378
+ operation: Object.freeze({}),
379
+ taskId: this.taskId,
380
+ sessionId,
381
+ ...(typeof bound === 'string' && bound.length > 0 ? { boundCallId: bound } : {}),
382
+ };
383
+ }
384
+ openDecideOperation() {
385
+ return new DecideSpan(this, this.decideObserver);
386
+ }
387
+ async decideRawAttempts(sessionId, decision, opts, tally) {
257
388
  const callerAborted = () => opts?.signal?.aborted === true;
258
389
  const startedAt = Date.now();
259
390
  let attempts = 0;
@@ -261,6 +392,7 @@ export class HitlBridge {
261
392
  let backoff = DECIDE_TRANSPORT_RETRY_BACKOFF_MS;
262
393
  for (;;) {
263
394
  attempts++;
395
+ tally.attempts = attempts;
264
396
  try {
265
397
  const r = await this.client.approvals.decide(sessionId, decision, opts);
266
398
  return r;
@@ -1,7 +1,7 @@
1
1
  import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
2
2
  import { hostLog } from '../host.js';
3
3
  import { unrefTimer } from '../unrefTimer.js';
4
- import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
4
+ import { decideFailureNote } from '../decideFailureNote.js';
5
5
  import { HitlSafetyError } from './hitlBridge.js';
6
6
  const hostSurfaceByKey = createSessionSlot();
7
7
  const hostSurfaceMissesByKey = new Map();
@@ -113,8 +113,7 @@ export function observeCancelByDeny(settle, taskId) {
113
113
  hostLog('debug', `[hitl-cancel-deny] cancel-by-deny no_pending for run ${taskId} (benign — already resolved)`);
114
114
  return;
115
115
  }
116
- const where = corruptStoredRowWhere(e);
117
- const rowNote = where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`;
116
+ const rowNote = decideFailureNote(e);
118
117
  hostLog('error', `[hitl-cancel-deny] cancel-by-deny FAILED for run ${taskId}: ${String(e)}${rowNote}`);
119
118
  surfaceCancelDenyWarn(`${CANCEL_DENY_WARN_TEXT}${rowNote}`);
120
119
  })
@@ -33,6 +33,7 @@ export type GateOutcome = {
33
33
  currentPending?: GateCurrentPending | undefined;
34
34
  };
35
35
  export declare function isAlreadyResolvedGateReason(reason: unknown): boolean;
36
+ export declare function isGateStandingErrorCode(code: string | null | undefined): boolean;
36
37
  export declare function toAnsweredOutput(questions: unknown[], answer: QuestionAnswer): AskAnsweredOutput;
37
38
  export type ParkResolution = {
38
39
  kind: 'reattach';
@@ -1,5 +1,6 @@
1
1
  import { putOwnKey } from '../ownKey.js';
2
2
  import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
3
+ import { RESUME_POLICY_FOLD_CODE } from '../engineErrorCodes.js';
3
4
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
4
5
  import { hostLog } from '../host.js';
5
6
  import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
@@ -7,7 +8,7 @@ import { observeCancelByDeny } from './hitlHostSurface.js';
7
8
  import { flushHeldWithInterruptRewrite, isAskTool } from './frameRouter.js';
8
9
  import { approvalCallKey, askGateQuestionId } from './gateIdentity.js';
9
10
  import { readDecideReceipt } from '../decideReceipt.js';
10
- import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
11
+ import { decideFailureNote } from '../decideFailureNote.js';
11
12
  export const MAX_GATE_HOPS = 2;
12
13
  export function nextHopBudget(prev, round) {
13
14
  const total = (prev?.total ?? 0) + 1;
@@ -41,7 +42,14 @@ function alreadyResolvedReasonToken(reason) {
41
42
  }
42
43
  function isAlreadyResolvedFailure(outcome) {
43
44
  const code = 'code' in outcome && isGateFailureCode(outcome.code) ? outcome.code : undefined;
44
- return code !== undefined ? code === 'no_pending' : isAlreadyResolvedGateReason(outcome.reason);
45
+ if (code !== undefined)
46
+ return code === 'no_pending';
47
+ if ('errorCode' in outcome && isGateStandingErrorCode(outcome.errorCode))
48
+ return false;
49
+ return isAlreadyResolvedGateReason(outcome.reason);
50
+ }
51
+ export function isGateStandingErrorCode(code) {
52
+ return code === RESUME_POLICY_FOLD_CODE || code === 'corrupt';
45
53
  }
46
54
  export function toAnsweredOutput(questions, answer) {
47
55
  const answers = {};
@@ -108,7 +116,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
108
116
  onPresented?.();
109
117
  publishQuestionFrame({ type: 'question', questionId, questions });
110
118
  });
111
- const bridge = new HitlBridge(deps.client, taskId);
119
+ const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
112
120
  if (answer === null) {
113
121
  observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
114
122
  return { kind: 'aborted', gatedCallId };
@@ -141,7 +149,7 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
141
149
  kind: 'failed',
142
150
  stage: 'decide',
143
151
  gatedCallId,
144
- reason: withCorruptStoredRow(`decide failed: ${String(e)}`, e),
152
+ reason: `decide failed: ${String(e)}${decideFailureNote(e)}`,
145
153
  ...(code !== undefined ? { code } : {}),
146
154
  ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
147
155
  ...(currentPending !== undefined ? { currentPending } : {}),
@@ -149,10 +157,6 @@ async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGat
149
157
  };
150
158
  }
151
159
  }
152
- function withCorruptStoredRow(reason, err) {
153
- const where = corruptStoredRowWhere(err);
154
- return where === undefined ? reason : `${reason} — ${corruptStoredRowContent(where)}`;
155
- }
156
160
  function argsByCallOf(led) {
157
161
  return new Map(led.gatedStartArgs());
158
162
  }
@@ -166,7 +170,11 @@ function isFetchStepNoPending(outcome) {
166
170
  async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
167
171
  const { deps, led, taskId } = ctx;
168
172
  if (park.gate === 'fs') {
169
- const outcome = await surfaceFsApprovalAndDecide({ client: deps.client, ...(onPresented ? { onPresented } : {}) }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
173
+ const outcome = await surfaceFsApprovalAndDecide({
174
+ client: deps.client,
175
+ ...(onPresented ? { onPresented } : {}),
176
+ ...(deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : {}),
177
+ }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
170
178
  const outcomeDenied = 'denied' in outcome ? outcome.denied : undefined;
171
179
  if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
172
180
  const denySettledBy = 'denySettledBy' in outcome ? outcome.denySettledBy : undefined;
@@ -29,6 +29,12 @@ export declare function armPlanReviewApproval(result: unknown, sessionKey?: stri
29
29
  submittedInPlanMode?: boolean;
30
30
  }): boolean;
31
31
  export type PlanReviewDecisionEffect = 'took_effect' | 'advanced' | 'still_parked' | 'not_sent' | 'not_applied' | 'unconfirmed';
32
+ export interface PlanReviewOutcomeMeta {
33
+ taskId: string;
34
+ dispatchNo: number;
35
+ decision: 'approve' | 'reject';
36
+ effect: PlanReviewDecisionEffect;
37
+ }
32
38
  export declare function planReviewOutcomeTail(taskId: string, decision: 'approve' | 'reject', effect: PlanReviewDecisionEffect): string;
33
39
  export declare function __resetPlanReviewDecisionLatchForTests(): void;
34
40
  export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject', permissionModeAfter?: string): Promise<void>;
@@ -4,6 +4,7 @@ import { makeEngineWireClient } from '../engineWireSdk.js';
4
4
  import { engineWireTarget } from '../engineWireTarget.js';
5
5
  import { enqueuePlanReviewOutcome } from '../notifications.js';
6
6
  import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
7
+ import { decideFailureNote } from '../decideFailureNote.js';
7
8
  import { isPlanReviewModeAfter } from './hitlBridge.js';
8
9
  import { engineCapString } from '../engineCapsCache.js';
9
10
  import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
@@ -337,7 +338,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
337
338
  const msg = e instanceof Error ? e.message : String(e);
338
339
  hostLog('debug', `planReviewWire: ${decision} → ${status} ${msg.slice(0, 200)}`);
339
340
  effect = status >= 400 && status < 500 ? 'not_applied' : 'unconfirmed';
340
- outcome = `The plan_review decision failed: HTTP ${status} ${msg}`.trim();
341
+ outcome = `${`The plan_review decision failed: HTTP ${status} ${msg}`.trim()}${decideFailureNote(e)}`;
341
342
  }
342
343
  else {
343
344
  outcome = `The plan_review decision could not reach the engine: ${String(e)}`;
@@ -346,7 +347,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
346
347
  }
347
348
  }
348
349
  try {
349
- const delivered = enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}\n${planReviewOutcomeTail(taskId, decision, effect)}\n</plan-review-outcome>`);
350
+ const delivered = enqueuePlanReviewOutcome(`<plan-review-outcome>\n${outcome}\n${planReviewOutcomeTail(taskId, decision, effect)}\n</plan-review-outcome>`, { taskId, dispatchNo, decision, effect });
350
351
  if (!delivered) {
351
352
  hostLog('error', `planReviewWire: outcome enqueue MISSED (queue port not installed or lacks enqueueMetaPrompt) for task ${taskId} — model will not learn the ${decision} result`);
352
353
  }
@@ -382,6 +383,11 @@ export function reopenPlanReviewCard(taskId, opts) {
382
383
  if (typeof taskId !== 'string' || taskId.length === 0) {
383
384
  return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
384
385
  }
386
+ if (decideInFlight.has(taskId)) {
387
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — a decision for this task is in flight (its answer is still being delivered)`);
388
+ const refused = { reopened: false, _sema_decisionInFlight: true };
389
+ return receiptMs === undefined ? refused : Promise.resolve(refused);
390
+ }
385
391
  const sessionKey = opts?.sessionKey ?? DEFAULT_SESSION_KEY;
386
392
  if (!hasQuestionOverlayFor(sessionKey)) {
387
393
  return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
@@ -1,4 +1,4 @@
1
- import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike } from './hitlBridge.js';
1
+ import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike, type DecideOperationObserver } from './hitlBridge.js';
2
2
  import { type RuleStoreUnreadableKind } from '../gateVocabulary.js';
3
3
  import { type DenyAttributionRefusalDetail } from '../wireRefusalCopy.js';
4
4
  import { type DecideReceiptView } from '../decideReceipt.js';
@@ -43,6 +43,7 @@ export type FsApprovalOutcome = {
43
43
  export interface FsApprovalWireDeps {
44
44
  client: HitlClientLike;
45
45
  onPresented?: () => void;
46
+ onDecideOperation?: DecideOperationObserver;
46
47
  }
47
48
  export declare function structurallyEqual(a: unknown, b: unknown): boolean;
48
49
  export type RuleOfferMatch = SdkRuleOfferMatch;
@@ -7,7 +7,7 @@ import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded
7
7
  import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
8
8
  import { isRuleStoreUnreadableKind } from '../gateVocabulary.js';
9
9
  import { denyAttributionRefusalFromError } from '../wireRefusalCopy.js';
10
- import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
10
+ import { decideFailureNote } from '../decideFailureNote.js';
11
11
  import { readDecideReceipt } from '../decideReceipt.js';
12
12
  import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS, } from '@sema-agent/sdk';
13
13
  export function isFsApprovalGate(gate) {
@@ -29,10 +29,6 @@ export function isToolApprovalGate(gate) {
29
29
  return true;
30
30
  return typeof gate?.toolName === 'string' && toolNameIsShellExec(gate.toolName);
31
31
  }
32
- function withCorruptStoredRow(reason, err) {
33
- const where = corruptStoredRowWhere(err);
34
- return where === undefined ? reason : `${reason} — ${corruptStoredRowContent(where)}`;
35
- }
36
32
  export function structurallyEqual(a, b) {
37
33
  if (a === b)
38
34
  return true;
@@ -137,7 +133,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
137
133
  if (typeof args !== 'object' || args === null) {
138
134
  return { kind: 'failed', stage: 'input', gatedCallId, reason: 'gate has no tool input payload' };
139
135
  }
140
- const bridge = new HitlBridge(deps.client, taskId);
136
+ const bridge = new HitlBridge(deps.client, taskId, deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : undefined);
141
137
  const callKey = approvalCallKey(gatedCallId, taskId);
142
138
  const ruleOffersReadOnly = readRuleOfferSupply(pending.ruleOffers, pending.ruleSuggestions);
143
139
  const durableProbeCause = pending.riskDescriptor?.probeCause;
@@ -167,110 +163,117 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
167
163
  ? { toolCallId: pending.toolCallId }
168
164
  : {}),
169
165
  });
170
- switch (card.kind) {
171
- case 'failed':
172
- return { kind: 'failed', stage: 'card', gatedCallId, reason: card.reason };
173
- case 'retracted':
174
- hostLog('debug', `liveHitlAskWire: approval card retracted without a decision for ${gatedCallId ?? '(no call)'} — nothing sent`);
175
- return { kind: 'retracted', gatedCallId };
176
- case 'aborted':
177
- observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
178
- return { kind: 'aborted', gatedCallId };
179
- case 'allow':
180
- try {
181
- if (card.allowSession) {
182
- try {
183
- const raw = await bridge.decideTool({ decision: 'approve', remember: 'session', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, signal ? { signal } : undefined, pending);
184
- const receipt = readDecideReceipt(raw);
185
- return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
186
- }
187
- catch (e) {
188
- if (e instanceof HitlSafetyError || e instanceof DecideTransportRetryExhaustedError)
189
- throw e;
190
- if (readDecideCurrentPending(e) !== undefined)
191
- throw e;
192
- hostLog('debug', `liveToolApprovalWire: decide(approve+remember) failed (${String(e)}) — falling back to plain approve`);
166
+ const decideSpan = bridge.openDecideOperation();
167
+ const decideOpts = { ...(signal ? { signal } : {}), operation: decideSpan };
168
+ try {
169
+ switch (card.kind) {
170
+ case 'failed':
171
+ return { kind: 'failed', stage: 'card', gatedCallId, reason: card.reason };
172
+ case 'retracted':
173
+ hostLog('debug', `liveHitlAskWire: approval card retracted without a decision for ${gatedCallId ?? '(no call)'} — nothing sent`);
174
+ return { kind: 'retracted', gatedCallId };
175
+ case 'aborted':
176
+ observeCancelByDeny(bridge.decideTool({ decision: 'deny', reason: 'Interrupted by user' }, gatedCallId, undefined, pending), taskId);
177
+ return { kind: 'aborted', gatedCallId };
178
+ case 'allow':
179
+ try {
180
+ if (card.allowSession) {
181
+ try {
182
+ const raw = await bridge.decideTool({ decision: 'approve', remember: 'session', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, decideOpts, pending);
183
+ const receipt = readDecideReceipt(raw);
184
+ return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
185
+ }
186
+ catch (e) {
187
+ if (e instanceof HitlSafetyError || e instanceof DecideTransportRetryExhaustedError)
188
+ throw e;
189
+ if (readDecideCurrentPending(e) !== undefined)
190
+ throw e;
191
+ hostLog('debug', `liveToolApprovalWire: decide(approve+remember) failed (${String(e)}) — falling back to plain approve`);
192
+ }
193
193
  }
194
+ const raw = await bridge.decideTool({ decision: 'approve', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, decideOpts, pending);
195
+ const receipt = readDecideReceipt(raw);
196
+ return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
194
197
  }
195
- const raw = await bridge.decideTool({ decision: 'approve', ...(card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}) }, gatedCallId, signal ? { signal } : undefined, pending);
196
- const receipt = readDecideReceipt(raw);
197
- return { kind: 'decided', gatedCallId, ...(receipt !== undefined ? { receipt } : {}) };
198
- }
199
- catch (e) {
200
- const currentPending = readDecideCurrentPending(e);
201
- const wireCode = readWireErrorCode(e);
202
- const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
203
- return {
204
- kind: 'failed',
205
- stage: 'decide',
206
- gatedCallId,
207
- reason: withCorruptStoredRow(`decide(approve) failed: ${String(e)}`, e),
208
- ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
209
- ...(currentPending !== undefined ? { currentPending } : {}),
210
- ...(wireCode !== undefined ? { errorCode: wireCode } : {}),
211
- ...(safetyCode !== undefined ? { safetyCode } : {}),
212
- };
213
- }
214
- case 'deny': {
215
- const denySettledByForWire = readApprovalDenySettledBy(card);
216
- const denyReasonSnapshot = typeof card.reason === 'string' ? card.reason : undefined;
217
- let lastSendCarriedAttribution = false;
218
- try {
219
- const denyBody = {
220
- decision: 'deny',
221
- reason: denyReasonForWire(denyReasonSnapshot, `task ${taskId}`) ?? DEFAULT_DENY_REASON,
222
- ...(denySettledByForWire === 'policy' ? { settledBy: 'policy' } : {}),
223
- };
224
- let attributionDropped = false;
225
- let raw;
226
- lastSendCarriedAttribution = denySettledByForWire === 'policy';
198
+ catch (e) {
199
+ const currentPending = readDecideCurrentPending(e);
200
+ const wireCode = readWireErrorCode(e);
201
+ const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
202
+ return {
203
+ kind: 'failed',
204
+ stage: 'decide',
205
+ gatedCallId,
206
+ reason: `decide(approve) failed: ${String(e)}${decideFailureNote(e)}`,
207
+ ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
208
+ ...(currentPending !== undefined ? { currentPending } : {}),
209
+ ...(wireCode !== undefined ? { errorCode: wireCode } : {}),
210
+ ...(safetyCode !== undefined ? { safetyCode } : {}),
211
+ };
212
+ }
213
+ case 'deny': {
214
+ const denySettledByForWire = readApprovalDenySettledBy(card);
215
+ const denyReasonSnapshot = typeof card.reason === 'string' ? card.reason : undefined;
216
+ let lastSendCarriedAttribution = false;
227
217
  try {
228
- raw = await bridge.decideTool(denyBody, gatedCallId, signal ? { signal } : undefined, pending);
218
+ const denyBody = {
219
+ decision: 'deny',
220
+ reason: denyReasonForWire(denyReasonSnapshot, `task ${taskId}`) ?? DEFAULT_DENY_REASON,
221
+ ...(denySettledByForWire === 'policy' ? { settledBy: 'policy' } : {}),
222
+ };
223
+ let attributionDropped = false;
224
+ let raw;
225
+ lastSendCarriedAttribution = denySettledByForWire === 'policy';
226
+ try {
227
+ raw = await bridge.decideTool(denyBody, gatedCallId, decideOpts, pending);
228
+ }
229
+ catch (first) {
230
+ if (!shouldResendWithoutAttribution(first, lastSendCarriedAttribution, signal))
231
+ throw first;
232
+ const { settledBy: _droppedKey, ...withoutAttribution } = denyBody;
233
+ void _droppedKey;
234
+ lastSendCarriedAttribution = false;
235
+ hostLog('debug', `liveToolApprovalWire: decide(deny) for task ${taskId} was refused because the settlement note is not part of ` +
236
+ 'what this deployment signs — re-sending the SAME decision exactly once with that one key removed ' +
237
+ '(the refusal happened before the approval was judged, so nothing was consumed and this is a re-delivery, not a second act)');
238
+ raw = await bridge.decideTool(withoutAttribution, gatedCallId, decideOpts, pending);
239
+ attributionDropped = true;
240
+ }
241
+ const receipt = readDecideReceipt(raw);
242
+ const denySettledBy = denySettledByForWire;
243
+ const denyReason = denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== '' ? denyReasonSnapshot : undefined;
244
+ return {
245
+ kind: 'decided',
246
+ gatedCallId,
247
+ denied: true,
248
+ ...(denySettledBy !== undefined ? { denySettledBy } : {}),
249
+ ...(denyReason !== undefined ? { denyReason } : {}),
250
+ ...(attributionDropped ? { denyAttributionDropped: true } : {}),
251
+ ...(receipt !== undefined ? { receipt } : {}),
252
+ };
229
253
  }
230
- catch (first) {
231
- if (!shouldResendWithoutAttribution(first, lastSendCarriedAttribution, signal))
232
- throw first;
233
- const { settledBy: _droppedKey, ...withoutAttribution } = denyBody;
234
- void _droppedKey;
235
- lastSendCarriedAttribution = false;
236
- hostLog('debug', `liveToolApprovalWire: decide(deny) for task ${taskId} was refused because the settlement note is not part of ` +
237
- 'what this deployment signs — re-sending the SAME decision exactly once with that one key removed ' +
238
- '(the refusal happened before the approval was judged, so nothing was consumed and this is a re-delivery, not a second act)');
239
- raw = await bridge.decideTool(withoutAttribution, gatedCallId, signal ? { signal } : undefined, pending);
240
- attributionDropped = true;
254
+ catch (e) {
255
+ const currentPending = readDecideCurrentPending(e);
256
+ const wireCode = readWireErrorCode(e);
257
+ const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
258
+ const attributionRefusal = denyAttributionRefusalFromError(e, lastSendCarriedAttribution);
259
+ return {
260
+ kind: 'failed',
261
+ stage: 'decide',
262
+ gatedCallId,
263
+ reason: `decide(deny) failed: ${String(e)}${decideFailureNote(e)}`,
264
+ ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
265
+ ...(currentPending !== undefined ? { currentPending } : {}),
266
+ ...(wireCode !== undefined ? { errorCode: wireCode } : {}),
267
+ ...(safetyCode !== undefined ? { safetyCode } : {}),
268
+ ...(attributionRefusal !== null ? { denyAttributionRefusal: attributionRefusal } : {}),
269
+ };
241
270
  }
242
- const receipt = readDecideReceipt(raw);
243
- const denySettledBy = denySettledByForWire;
244
- const denyReason = denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== '' ? denyReasonSnapshot : undefined;
245
- return {
246
- kind: 'decided',
247
- gatedCallId,
248
- denied: true,
249
- ...(denySettledBy !== undefined ? { denySettledBy } : {}),
250
- ...(denyReason !== undefined ? { denyReason } : {}),
251
- ...(attributionDropped ? { denyAttributionDropped: true } : {}),
252
- ...(receipt !== undefined ? { receipt } : {}),
253
- };
254
- }
255
- catch (e) {
256
- const currentPending = readDecideCurrentPending(e);
257
- const wireCode = readWireErrorCode(e);
258
- const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
259
- const attributionRefusal = denyAttributionRefusalFromError(e, lastSendCarriedAttribution);
260
- return {
261
- kind: 'failed',
262
- stage: 'decide',
263
- gatedCallId,
264
- reason: withCorruptStoredRow(`decide(deny) failed: ${String(e)}`, e),
265
- ...(e instanceof DecideTransportRetryExhaustedError ? { retryExhausted: true } : {}),
266
- ...(currentPending !== undefined ? { currentPending } : {}),
267
- ...(wireCode !== undefined ? { errorCode: wireCode } : {}),
268
- ...(safetyCode !== undefined ? { safetyCode } : {}),
269
- ...(attributionRefusal !== null ? { denyAttributionRefusal: attributionRefusal } : {}),
270
- };
271
271
  }
272
272
  }
273
273
  }
274
+ finally {
275
+ decideSpan.close();
276
+ }
274
277
  }
275
278
  function readReadRootCandidate(v) {
276
279
  if (typeof v !== 'object' || v === null)