@sema-agent/client-core 0.86.0 → 0.88.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 (57) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +18 -12
  3. package/dist/adapt/arms.js +9 -0
  4. package/dist/adapter/downstream/eventToSdkMessage.js +14 -1
  5. package/dist/adapter/downstream/terminalToSdkResult.js +4 -1
  6. package/dist/adapter/downstream/wiringManifestView.d.ts +2 -0
  7. package/dist/adapter/downstream/wiringManifestView.js +1 -0
  8. package/dist/adapter/runStream.js +75 -2
  9. package/dist/adapter/types.d.ts +3 -0
  10. package/dist/decideReceipt.d.ts +1 -1
  11. package/dist/decideReceipt.js +1 -1
  12. package/dist/detachWire.d.ts +6 -1
  13. package/dist/detachWire.js +22 -1
  14. package/dist/displayUntrusted.d.ts +4 -0
  15. package/dist/displayUntrusted.js +1657 -573
  16. package/dist/engineErrorCodes.d.ts +2 -0
  17. package/dist/engineErrorCodes.js +1 -0
  18. package/dist/engineNoticeCodes.d.ts +20 -0
  19. package/dist/engineNoticeCodes.js +71 -0
  20. package/dist/engineWireFor.d.ts +1 -0
  21. package/dist/engineWireFor.js +1 -1
  22. package/dist/gateOutcome.d.ts +4 -0
  23. package/dist/gateOutcome.js +5 -0
  24. package/dist/generated/engineFactTables.d.ts +1 -0
  25. package/dist/generated/engineFactTables.js +3 -0
  26. package/dist/generated/engineNoticeTables.js +2 -0
  27. package/dist/hitl/crashConverged.d.ts +2 -1
  28. package/dist/hitl/crashConverged.js +23 -3
  29. package/dist/hitl/hitlBridge.d.ts +4 -0
  30. package/dist/hitl/hitlBridge.js +6 -0
  31. package/dist/hitl/parkResolver.js +17 -12
  32. package/dist/hitl/planReviewWire.js +30 -10
  33. package/dist/hitl/suspendedReopen.d.ts +7 -0
  34. package/dist/hitl/suspendedReopen.js +38 -7
  35. package/dist/hitl/toolApprovalWire.js +4 -5
  36. package/dist/index.d.ts +7 -0
  37. package/dist/index.js +4 -0
  38. package/dist/promptDelivery.d.ts +20 -0
  39. package/dist/promptDelivery.js +153 -0
  40. package/dist/registryConflict.d.ts +3 -0
  41. package/dist/registryConflict.js +44 -0
  42. package/dist/request/taskRequest.d.ts +1 -0
  43. package/dist/request/taskRequest.js +25 -1
  44. package/dist/resumeRefusalCopy.d.ts +8 -6
  45. package/dist/resumeRefusalCopy.js +41 -19
  46. package/dist/rewindArchiveCapability.js +5 -0
  47. package/dist/seam.d.ts +12 -1
  48. package/dist/seam.js +7 -0
  49. package/dist/sessionMemoryErase.d.ts +76 -0
  50. package/dist/sessionMemoryErase.js +432 -0
  51. package/dist/subagent/engineSubagentResume.js +2 -1
  52. package/dist/toolHistoryMismatch.d.ts +63 -0
  53. package/dist/toolHistoryMismatch.js +425 -0
  54. package/dist/workflowClient.d.ts +3 -2
  55. package/dist/workflowClient.js +3 -3
  56. package/docs/INTEGRATION-CLIENTS.md +598 -13
  57. package/package.json +2 -2
@@ -48,6 +48,8 @@ export declare const REWIND_ERROR_CODE_PREFIXES: readonly ["resume_at.", "rewind
48
48
  export declare function isRewindFamilyCode(code: string | undefined): boolean;
49
49
  export declare const RESUME_USAGE_WINDOW_EXHAUSTED = "resume.usage_window_exhausted";
50
50
  export declare const RESUME_PREFLIGHT_REJECTED = "resume.preflight_rejected";
51
+ export declare const RESUME_SESSION_NOT_FOUND = "resume.session_not_found";
52
+ export type StatusDerivedCoarseCode = (typeof STATUS_DERIVED_COARSE_CODES)[number];
51
53
  export declare const RESUME_PLACEMENT_MISMATCH = "resume.placement_mismatch";
52
54
  export declare const RESUME_CONTEXT_UNAVAILABLE = "conflict.resume_context_unavailable";
53
55
  export declare const RESUME_RETRY_LATER_CODES: readonly string[];
@@ -106,6 +106,7 @@ export function isRewindFamilyCode(code) {
106
106
  }
107
107
  export const RESUME_USAGE_WINDOW_EXHAUSTED = 'resume.usage_window_exhausted';
108
108
  export const RESUME_PREFLIGHT_REJECTED = 'resume.preflight_rejected';
109
+ export const RESUME_SESSION_NOT_FOUND = 'resume.session_not_found';
109
110
  export const RESUME_PLACEMENT_MISMATCH = 'resume.placement_mismatch';
110
111
  export const RESUME_CONTEXT_UNAVAILABLE = 'conflict.resume_context_unavailable';
111
112
  export const RESUME_RETRY_LATER_CODES = Object.freeze([
@@ -137,6 +137,24 @@ export interface LegacyModeSeedMigratedFactsView {
137
137
  runId: string;
138
138
  }
139
139
  export declare function readLegacyModeSeedMigrated(notice: unknown): LegacyModeSeedMigratedFactsView | undefined;
140
+ export interface ToolTurnRepairView {
141
+ form: string;
142
+ toolCallIds: readonly string[];
143
+ }
144
+ export interface ToolTurnRepairedFactsView {
145
+ repairs: readonly ToolTurnRepairView[];
146
+ sessionId: string;
147
+ runId: string;
148
+ }
149
+ export declare function readToolTurnRepaired(notice: unknown): ToolTurnRepairedFactsView | undefined;
150
+ export interface ModelUnreachableFactsView {
151
+ code: string;
152
+ targetHost: string;
153
+ proxyInUse?: string;
154
+ remedy: string;
155
+ sessionId: string;
156
+ }
157
+ export declare function readModelUnreachable(notice: unknown): ModelUnreachableFactsView | undefined;
140
158
  export declare const ENGINE_NOTICE_FACT_READERS: Readonly<{
141
159
  readonly 'mcp.injection_dropped': typeof readMcpInjectionDrop;
142
160
  readonly 'delegation.ask_unresolvable': typeof readAskUnresolvable;
@@ -153,6 +171,8 @@ export declare const ENGINE_NOTICE_FACT_READERS: Readonly<{
153
171
  readonly 'permission.ask_released_by_mode': typeof readAskReleasedByMode;
154
172
  readonly 'permission.read_deny_unapplied': typeof readReadDenyUnapplied;
155
173
  readonly 'resume.legacy_mode_seed_migrated': typeof readLegacyModeSeedMigrated;
174
+ readonly 'context.tool_turn_repaired': typeof readToolTurnRepaired;
175
+ readonly 'server.model_unreachable': typeof readModelUnreachable;
156
176
  }>;
157
177
  type FactReaderTable = typeof ENGINE_NOTICE_FACT_READERS;
158
178
  export type EngineNoticeFactsCode = keyof FactReaderTable;
@@ -7,6 +7,7 @@ export const SERVER_NOTICE_AUDIENCE = Object.freeze({
7
7
  'instructions.source_changed': 'user',
8
8
  'memory.layer_locked_legacy': 'operator',
9
9
  'session.records_skipped': 'user',
10
+ 'server.model_unreachable': 'user',
10
11
  });
11
12
  function audienceRow(table, code) {
12
13
  return Object.hasOwn(table, code) ? table[code] : undefined;
@@ -483,6 +484,74 @@ function readLegacyModeSeedMigratedOf(notice) {
483
484
  const permissionMode = nonEmpty(optionalSeat(mo, 'permissionMode'));
484
485
  return { ...(permissionMode !== undefined ? { permissionMode } : {}), droppedKeys, sessionId, runId };
485
486
  }
487
+ export function readToolTurnRepaired(notice) {
488
+ return guardedRead(notice, readToolTurnRepairedOf);
489
+ }
490
+ function ownSeat(o, key) {
491
+ return Object.hasOwn(o, key) ? o[key] : undefined;
492
+ }
493
+ function optionalOwnSeat(o, key) {
494
+ try {
495
+ return ownSeat(o, key);
496
+ }
497
+ catch {
498
+ return undefined;
499
+ }
500
+ }
501
+ function readToolTurnRepairedOf(notice) {
502
+ if (typeof notice !== 'object' || notice === null || Array.isArray(notice))
503
+ return undefined;
504
+ const n = notice;
505
+ if (n.code !== 'context.tool_turn_repaired')
506
+ return undefined;
507
+ const d = n.detail;
508
+ if (typeof d !== 'object' || d === null || Array.isArray(d))
509
+ return undefined;
510
+ const o = d;
511
+ const rawRepairs = ownSeat(o, 'repairs');
512
+ if (!Array.isArray(rawRepairs) || rawRepairs.length === 0)
513
+ return undefined;
514
+ const count = rawRepairs.length;
515
+ const repairs = [];
516
+ for (let i = 0; i < count; i++) {
517
+ const item = rawRepairs[i];
518
+ if (typeof item !== 'object' || item === null || Array.isArray(item))
519
+ return undefined;
520
+ const it = item;
521
+ const form = nonEmpty(ownSeat(it, 'form'));
522
+ const toolCallIds = nameList(ownSeat(it, 'toolCallIds'));
523
+ if (form === undefined || toolCallIds === undefined || toolCallIds.length === 0)
524
+ return undefined;
525
+ repairs.push(Object.freeze({ form, toolCallIds }));
526
+ }
527
+ const sessionId = nonEmpty(ownSeat(o, 'sessionId'));
528
+ const runId = nonEmpty(ownSeat(o, 'runId'));
529
+ if (sessionId === undefined || runId === undefined)
530
+ return undefined;
531
+ return { repairs: Object.freeze(repairs), sessionId, runId };
532
+ }
533
+ export function readModelUnreachable(notice) {
534
+ return guardedRead(notice, readModelUnreachableOf);
535
+ }
536
+ function readModelUnreachableOf(notice) {
537
+ if (typeof notice !== 'object' || notice === null || Array.isArray(notice))
538
+ return undefined;
539
+ const n = notice;
540
+ if (n.code !== 'server.model_unreachable')
541
+ return undefined;
542
+ const d = n.detail;
543
+ if (typeof d !== 'object' || d === null || Array.isArray(d))
544
+ return undefined;
545
+ const o = d;
546
+ const code = nonEmpty(ownSeat(o, 'code'));
547
+ const targetHost = nonEmpty(ownSeat(o, 'targetHost'));
548
+ const remedy = nonEmpty(ownSeat(o, 'remedy'));
549
+ const sessionId = nonEmpty(ownSeat(o, 'sessionId'));
550
+ if (code === undefined || targetHost === undefined || remedy === undefined || sessionId === undefined)
551
+ return undefined;
552
+ const proxyInUse = nonEmpty(optionalOwnSeat(o, 'proxyInUse'));
553
+ return { code, targetHost, ...(proxyInUse !== undefined ? { proxyInUse } : {}), remedy, sessionId };
554
+ }
486
555
  export const ENGINE_NOTICE_FACT_READERS = Object.freeze({
487
556
  'mcp.injection_dropped': readMcpInjectionDrop,
488
557
  'delegation.ask_unresolvable': readAskUnresolvable,
@@ -499,6 +568,8 @@ export const ENGINE_NOTICE_FACT_READERS = Object.freeze({
499
568
  'permission.ask_released_by_mode': readAskReleasedByMode,
500
569
  'permission.read_deny_unapplied': readReadDenyUnapplied,
501
570
  'resume.legacy_mode_seed_migrated': readLegacyModeSeedMigrated,
571
+ 'context.tool_turn_repaired': readToolTurnRepaired,
572
+ 'server.model_unreachable': readModelUnreachable,
502
573
  });
503
574
  const _factReaderTablePin = true;
504
575
  void _factReaderTablePin;
@@ -40,4 +40,5 @@ export type EngineWireNoAnswerKind = 'deadline' | 'time-limit' | 'connection-los
40
40
  export declare function noAnswerKindOf(e: object | null | undefined): EngineWireNoAnswerKind | null;
41
41
  export declare function noAnswerDetailFrom(e: object | null | undefined): string | null;
42
42
  export declare function sendOnce<T>(signal: AbortSignal | null | undefined, run: () => Promise<T>): Promise<T>;
43
+ export declare function provablyNotSent(e: object | null | undefined): boolean;
43
44
  export declare function settleInjected<T>(ms: number, signal: AbortSignal | null | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
@@ -331,7 +331,7 @@ export async function sendOnce(signal, run) {
331
331
  throw error;
332
332
  }
333
333
  }
334
- function provablyNotSent(e) {
334
+ export function provablyNotSent(e) {
335
335
  try {
336
336
  return e !== null && typeof e === 'object' && (markedNotSent(e) || failedBeforeSend(e));
337
337
  }
@@ -1,5 +1,6 @@
1
1
  import type { DeniedBy, Settlement } from '@sema-agent/sdk';
2
2
  import { type CcToolDenialKind } from './gateVocabulary.js';
3
+ import { CORE_MODE_RELEASE_REASONS } from './generated/engineFactTables.js';
3
4
  export type GateDispositionView = {
4
5
  kind: 'allowed';
5
6
  classifier?: GateClassifierRoundView;
@@ -31,6 +32,9 @@ export interface GateModeReleaseView {
31
32
  mandate?: string;
32
33
  reason?: string;
33
34
  }
35
+ export declare const GATE_MODE_RELEASE_REASON_WORDS: readonly ["rule_unreadable"];
36
+ export type GateModeReleaseReasonWord = (typeof CORE_MODE_RELEASE_REASONS)[number];
37
+ export declare function isGateModeReleaseReasonWord(v: unknown): v is GateModeReleaseReasonWord;
34
38
  export interface SettlementWhoView {
35
39
  party: string;
36
40
  approver?: string;
@@ -1,8 +1,13 @@
1
1
  import { ccToolDenialKindForSettledBy, isCcToolDenialKind, isGateDeniedByWord } from './gateVocabulary.js';
2
2
  import { GATE_PARKED_ERROR_CODE } from './engineErrorCodes.js';
3
+ import { CORE_MODE_RELEASE_REASONS } from './generated/engineFactTables.js';
3
4
  function str(v) {
4
5
  return typeof v === 'string' && v.length > 0 ? v : undefined;
5
6
  }
7
+ export const GATE_MODE_RELEASE_REASON_WORDS = CORE_MODE_RELEASE_REASONS;
8
+ export function isGateModeReleaseReasonWord(v) {
9
+ return typeof v === 'string' && CORE_MODE_RELEASE_REASONS.includes(v);
10
+ }
6
11
  export const SETTLEMENT_KIND_WORDS = Object.freeze([
7
12
  'human_allowed',
8
13
  'human_refused',
@@ -8,3 +8,4 @@ export declare const CORE_DENIED_BY_VALUES: readonly ["policy", "hook", "org", "
8
8
  export declare const CORE_PERMISSION_MODE_FOLD_REASONS: readonly string[];
9
9
  export declare const CORE_ENGINE_CARD_TYPES: readonly string[];
10
10
  export declare const CORE_TOOL_DECLARATION_CODES: readonly string[];
11
+ export declare const CORE_MODE_RELEASE_REASONS: readonly ["rule_unreadable"];
@@ -111,3 +111,6 @@ export const CORE_TOOL_DECLARATION_CODES = Object.freeze([
111
111
  "config.egress_requires_write_effect",
112
112
  "config.reserved_tool_name",
113
113
  ]);
114
+ export const CORE_MODE_RELEASE_REASONS = Object.freeze([
115
+ "rule_unreadable",
116
+ ]);
@@ -65,6 +65,7 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
65
65
  "route.base_url_changed_key_unchanged",
66
66
  "route.response_model_mismatch",
67
67
  "route.tool_call_id_folded",
68
+ "context.tool_turn_repaired",
68
69
  "task.user_steer_undrained",
69
70
  "task.user_followup_undrained",
70
71
  "steering.parked_input_blocked",
@@ -147,6 +148,7 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
147
148
  "route.base_url_changed_key_unchanged": "operator",
148
149
  "route.response_model_mismatch": "operator",
149
150
  "route.tool_call_id_folded": "operator",
151
+ "context.tool_turn_repaired": "operator",
150
152
  "task.user_steer_undrained": "user",
151
153
  "task.user_followup_undrained": "user",
152
154
  "steering.parked_input_blocked": "user",
@@ -8,12 +8,13 @@ export interface CrashConvergedRow {
8
8
  readonly ts: number;
9
9
  readonly expiresAtMs: number;
10
10
  readonly decision: 'denied';
11
- readonly cause: 'crashed_before_park';
11
+ readonly cause: 'crashed_before_park' | 'shutdown_before_park';
12
12
  readonly orphanState: CrashConvergedOrphanState;
13
13
  readonly originalDecision?: 'approve';
14
14
  readonly decidedAtMs?: number;
15
15
  readonly convergedAtMs: number;
16
16
  readonly resumeSafe: boolean;
17
+ readonly needsHumanReason?: 'approved_then_interrupted' | 'pending_unsafe' | 'unstable_row' | 'contradictory';
17
18
  }
18
19
  export interface ApprovalsListEnvelope {
19
20
  pending: PendingCheckpoint[];
@@ -1,3 +1,8 @@
1
+ const CRASH_CONVERGED_CAUSE_WORDS = Object.freeze({
2
+ crashed_before_park: true,
3
+ shutdown_before_park: true,
4
+ });
5
+ const isCrashConvergedCause = (v) => typeof v === 'string' && Object.hasOwn(CRASH_CONVERGED_CAUSE_WORDS, v);
1
6
  const isString = (v) => typeof v === 'string';
2
7
  const isNumber = (v) => typeof v === 'number';
3
8
  const nonEmptyString = (v) => typeof v === 'string' && v.length > 0;
@@ -45,7 +50,7 @@ function readCrashConvergedRow(v) {
45
50
  return null;
46
51
  if (o.decision !== 'denied')
47
52
  return null;
48
- if (o.cause !== 'crashed_before_park')
53
+ if (!isCrashConvergedCause(o.cause))
49
54
  return null;
50
55
  if (o.orphanState !== 'pending' && o.orphanState !== 'decided')
51
56
  return null;
@@ -71,6 +76,15 @@ function readCrashConvergedRow(v) {
71
76
  return null;
72
77
  }
73
78
  }
79
+ function needsHumanReasonOf(reading) {
80
+ if (reading.unstable)
81
+ return 'unstable_row';
82
+ if (reading.orphanState === 'pending' ? reading.hasApprovalEvidence : reading.resumeSafe === true)
83
+ return 'contradictory';
84
+ if (reading.orphanState === 'decided')
85
+ return 'approved_then_interrupted';
86
+ return 'pending_unsafe';
87
+ }
74
88
  export function projectCrashConverged(env) {
75
89
  const resumeSafe = [];
76
90
  const needsHuman = [];
@@ -96,10 +110,16 @@ export function projectCrashConverged(env) {
96
110
  }
97
111
  const row = reading.row;
98
112
  const consistentlyUnexecuted = reading.orphanState === 'pending' && !reading.hasApprovalEvidence;
99
- if (!reading.unstable && consistentlyUnexecuted && reading.resumeSafe === true)
113
+ if (!reading.unstable && consistentlyUnexecuted && reading.resumeSafe === true) {
114
+ Reflect.deleteProperty(row, 'needsHumanReason');
100
115
  resumeSafe.push(row);
101
- else
116
+ }
117
+ else {
118
+ Object.defineProperty(row, 'needsHumanReason', {
119
+ value: needsHumanReasonOf(reading), enumerable: true, writable: true, configurable: true,
120
+ });
102
121
  needsHuman.push(row);
122
+ }
103
123
  }
104
124
  }
105
125
  catch {
@@ -47,6 +47,10 @@ export type PlanReviewModeAfter = (typeof PLAN_REVIEW_MODE_AFTER_WORDS)[number];
47
47
  export declare function isPlanReviewModeAfter(v: string | undefined): v is PlanReviewModeAfter;
48
48
  export type HitlSafetyCode = 'binding_mismatch' | 'no_pending' | 'wrong_gate' | 'bad_plan_edit' | 'bad_plan_mode' | 'empty_answer';
49
49
  export type HitlFailureStage = 'fetch' | 'input' | 'card' | 'decide' | 'orchestration';
50
+ export declare const NO_APPROVAL_CARD_PORT_REASON = "no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)";
51
+ export declare const APPROVAL_CARD_PORT_KEY_MISMATCH_REASON = "no approval card port installed for the default session key, but a non-default sessionKey has one \u2014 likely a keyed-install/default-read mismatch (installApprovalCardPortFor vs the zero-arg default-key read), not a missing install";
52
+ export declare const NO_QUESTION_OVERLAY_REASON = "no question overlay mounted (print/non-REPL mode)";
53
+ export declare function isNoDecisionSurfaceReason(reason: string): boolean;
50
54
  export interface HitlDecideDenyOutcome {
51
55
  decision: 'deny';
52
56
  reason?: string | undefined;
@@ -25,6 +25,12 @@ export const PLAN_REVIEW_MODE_AFTER_WORDS = Object.freeze(['default', 'acceptEdi
25
25
  export function isPlanReviewModeAfter(v) {
26
26
  return v !== undefined && PLAN_REVIEW_MODE_AFTER_WORDS.includes(v);
27
27
  }
28
+ export const NO_APPROVAL_CARD_PORT_REASON = 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)';
29
+ export const APPROVAL_CARD_PORT_KEY_MISMATCH_REASON = 'no approval card port installed for the default session key, but a non-default sessionKey has one — likely a keyed-install/default-read mismatch (installApprovalCardPortFor vs the zero-arg default-key read), not a missing install';
30
+ export const NO_QUESTION_OVERLAY_REASON = 'no question overlay mounted (print/non-REPL mode)';
31
+ export function isNoDecisionSurfaceReason(reason) {
32
+ return reason === NO_APPROVAL_CARD_PORT_REASON || reason === APPROVAL_CARD_PORT_KEY_MISMATCH_REASON || reason === NO_QUESTION_OVERLAY_REASON;
33
+ }
28
34
  export class HitlSafetyError extends Error {
29
35
  code;
30
36
  constructor(message, code) {
@@ -1,5 +1,5 @@
1
1
  import { putOwnKey } from '../ownKey.js';
2
- import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
2
+ import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, NO_QUESTION_OVERLAY_REASON, findPendingForTask, isNoDecisionSurfaceReason, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
3
3
  import { PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode } from '../engineErrorCodes.js';
4
4
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
5
5
  import { hostLog } from '../host.js';
@@ -75,7 +75,7 @@ export function toAnsweredOutput(questions, answer) {
75
75
  }
76
76
  async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGatedCallId, onPresented) {
77
77
  if (!hasQuestionOverlay()) {
78
- return { kind: 'failed', stage: 'card', reason: 'no question overlay mounted (print/non-REPL mode)' };
78
+ return { kind: 'failed', stage: 'card', reason: NO_QUESTION_OVERLAY_REASON };
79
79
  }
80
80
  const found = await findPendingForTask(deps.client, taskId, isAskTool, signal ? { signal } : undefined, parkGatedCallId, (row) => isAskTool(typeof row.toolName === 'string' ? row.toolName : undefined));
81
81
  if (!found.ok)
@@ -301,7 +301,8 @@ export async function resolvePark(park, ctx) {
301
301
  : outcome.kind === 'retracted'
302
302
  ? 'the approval card was retracted by the host without a decision (nothing was sent; the run stays parked and remains decidable elsewhere)'
303
303
  : undefined;
304
- return { kind: 'failsoft', events: failsoftEvents(park, ctx, outcome.kind === 'aborted', failedReason) };
304
+ const noDecisionSurface = outcome.kind === 'failed' && outcome.stage === 'card' && isNoDecisionSurfaceReason(outcome.reason);
305
+ return { kind: 'failsoft', events: failsoftEvents(park, ctx, outcome.kind === 'aborted', failedReason, undefined, noDecisionSurface) };
305
306
  }
306
307
  led.dropHeldForDecidedPark(outcome.gatedCallId, park.gatedCallId);
307
308
  if (outcome.gatedCallId) {
@@ -324,7 +325,15 @@ export async function resolvePark(park, ctx) {
324
325
  ...(handoffTaskId !== undefined ? { handoffTaskId } : {}),
325
326
  };
326
327
  }
327
- function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal) {
328
+ function failsoftReasonSentence(park, failedReason, terminal) {
329
+ if (terminal !== undefined)
330
+ return terminal.errorMessage;
331
+ if (failedReason === undefined)
332
+ return undefined;
333
+ return (`${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${failedReason}` +
334
+ ' — the run is still parked on this approval: decide it on the card when it is shown again, or cancel the run.');
335
+ }
336
+ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, noDecisionSurface = false) {
328
337
  const events = [
329
338
  ...flushHeldWithInterruptRewrite(ctx.led, {
330
339
  terminal: park.pendingDone,
@@ -332,19 +341,15 @@ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal) {
332
341
  gateAbortedByUser,
333
342
  }),
334
343
  ];
344
+ const reason = failsoftReasonSentence(park, failedReason, terminal);
335
345
  if (park.pendingDone) {
336
- events.push(park.pendingDone);
346
+ events.push(reason === undefined || noDecisionSurface ? park.pendingDone : { ...park.pendingDone, _sema_park_failed_reason: reason });
337
347
  }
338
348
  else if (terminal !== undefined) {
339
349
  events.push({ type: 'failed', ...terminal });
340
350
  }
341
- else if (failedReason !== undefined) {
342
- events.push({
343
- type: 'failed',
344
- errorCode: 'hitl_unanswered',
345
- errorMessage: `${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${failedReason}` +
346
- ' — the run is still parked on this approval: decide it on the card when it is shown again, or cancel the run.',
347
- });
351
+ else if (reason !== undefined) {
352
+ events.push({ type: 'failed', errorCode: 'hitl_unanswered', errorMessage: reason });
348
353
  }
349
354
  return events;
350
355
  }
@@ -346,9 +346,11 @@ let decideDispatchSeq = 0;
346
346
  function planReviewOpsOf(taskId) {
347
347
  return planReviewDecisionOps.filter((op) => op.taskId === taskId);
348
348
  }
349
- function settlePlanReviewDecisionOp(op) {
350
- if (op.settledAt === null)
351
- op.settledAt = ++planReviewLedgerClock;
349
+ function settlePlanReviewDecisionOp(op, by) {
350
+ if (op.settledAt !== null)
351
+ return;
352
+ op.settledAt = ++planReviewLedgerClock;
353
+ op.settler = by;
352
354
  }
353
355
  function pushPlanReviewDecisionOp(op) {
354
356
  planReviewDecisionOps.push(op);
@@ -366,18 +368,31 @@ function dropPlanReviewDecisionOp(op) {
366
368
  if (i >= 0)
367
369
  planReviewDecisionOps.splice(i, 1);
368
370
  }
371
+ function claimablePlanReviewDecisionOp(taskId) {
372
+ const open = planReviewDecisionOps.find((o) => o.taskId === taskId && o.phase === 'handed' && o.settledAt === null);
373
+ if (open !== undefined)
374
+ return open;
375
+ let leftBehind;
376
+ for (const o of planReviewDecisionOps) {
377
+ if (o.taskId !== taskId || o.phase !== 'handed' || o.settler !== 'gate_left')
378
+ continue;
379
+ if (leftBehind === undefined || o.handedAt > leftBehind.handedAt)
380
+ leftBehind = o;
381
+ }
382
+ return leftBehind;
383
+ }
369
384
  export function notePlanReviewDecisionHandedOver(taskId) {
370
385
  if (typeof taskId !== 'string' || taskId === '')
371
386
  return null;
372
387
  if (planReviewDecisionOps.some((op) => op.taskId === taskId && op.settledAt === null))
373
388
  return null;
374
- const op = { taskId, handedAt: ++planReviewLedgerClock, settledAt: null, phase: 'handed', notApplied: false, ownCallOpen: false };
389
+ const op = { taskId, handedAt: ++planReviewLedgerClock, settledAt: null, settler: null, phase: 'handed', notApplied: false, ownCallOpen: false };
375
390
  pushPlanReviewDecisionOp(op);
376
391
  return {
377
392
  settle: () => {
378
393
  if (op.phase !== 'handed')
379
394
  return;
380
- settlePlanReviewDecisionOp(op);
395
+ settlePlanReviewDecisionOp(op, 'host');
381
396
  },
382
397
  revoke: () => {
383
398
  if (op.phase !== 'handed' || op.settledAt !== null)
@@ -412,7 +427,7 @@ export function notePlanReviewGateLeft(taskId) {
412
427
  if (typeof taskId !== 'string' || taskId === '')
413
428
  return;
414
429
  for (const op of planReviewOpsOf(taskId))
415
- settlePlanReviewDecisionOp(op);
430
+ settlePlanReviewDecisionOp(op, 'gate_left');
416
431
  }
417
432
  export function latestPlanReviewDecisionMark(taskId) {
418
433
  if (typeof taskId !== 'string' || taskId === '')
@@ -434,12 +449,17 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
434
449
  hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
435
450
  return;
436
451
  }
437
- let op = planReviewDecisionOps.find((o) => o.taskId === taskId && o.phase === 'handed' && o.settledAt === null);
452
+ let op = claimablePlanReviewDecisionOp(taskId);
438
453
  if (op === undefined) {
439
- op = { taskId: typeof taskId === 'string' ? taskId : String(taskId), handedAt: ++planReviewLedgerClock, settledAt: null, phase: 'posting', notApplied: false, ownCallOpen: true };
454
+ op = { taskId: typeof taskId === 'string' ? taskId : String(taskId), handedAt: ++planReviewLedgerClock, settledAt: null, settler: null, phase: 'posting', notApplied: false, ownCallOpen: true };
440
455
  pushPlanReviewDecisionOp(op);
441
456
  }
442
457
  else {
458
+ if (op.settler === 'gate_left') {
459
+ op.settledAt = null;
460
+ op.settler = null;
461
+ op.notApplied = false;
462
+ }
443
463
  op.phase = 'posting';
444
464
  op.ownCallOpen = true;
445
465
  }
@@ -451,7 +471,7 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
451
471
  const settle = (notApplied) => {
452
472
  if (notApplied)
453
473
  mine.notApplied = true;
454
- settlePlanReviewDecisionOp(mine);
474
+ settlePlanReviewDecisionOp(mine, 'package');
455
475
  };
456
476
  const dispatchNo = ++decideDispatchSeq;
457
477
  hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
@@ -460,7 +480,7 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
460
480
  }
461
481
  finally {
462
482
  releaseLatch();
463
- settlePlanReviewDecisionOp(mine);
483
+ settlePlanReviewDecisionOp(mine, 'package');
464
484
  mine.ownCallOpen = false;
465
485
  }
466
486
  }
@@ -7,3 +7,10 @@ export type SuspendedReopenReading = {
7
7
  readonly kind: 'unstated';
8
8
  };
9
9
  export declare function suspendedReopenOf(ev: unknown): SuspendedReopenReading;
10
+ export interface ResumeReopenDetail {
11
+ code: string;
12
+ reason: 'env_failed' | 'tool_unavailable' | 'tool_contract_mismatch';
13
+ retriable: true;
14
+ }
15
+ export declare function resumeReopenFromError(err: unknown): ResumeReopenDetail | null;
16
+ export declare function resumeReopenContent(detail: ResumeReopenDetail): string;
@@ -1,13 +1,44 @@
1
+ import { RESUME_REOPEN_CODES, RESUME_ENV_FAILED, RESUME_TOOL_UNAVAILABLE } from '../engineErrorCodes.js';
1
2
  const UNSTATED = Object.freeze({ kind: 'unstated' });
2
3
  const NOT_REOPENED = Object.freeze({ kind: 'not_reopened' });
3
4
  export function suspendedReopenOf(ev) {
4
- if (ev === null || typeof ev !== 'object' || Array.isArray(ev))
5
+ if (ev === null || typeof ev !== 'object')
5
6
  return UNSTATED;
6
- const o = ev;
7
- if (!Object.hasOwn(o, 'reopened'))
7
+ try {
8
+ if (Array.isArray(ev))
9
+ return UNSTATED;
10
+ const o = ev;
11
+ if (!Object.hasOwn(o, 'reopened'))
12
+ return UNSTATED;
13
+ const v = o.reopened;
14
+ if (v === null)
15
+ return NOT_REOPENED;
16
+ return typeof v === 'string' && v.length > 0 ? { kind: 'reopened', code: v } : UNSTATED;
17
+ }
18
+ catch {
8
19
  return UNSTATED;
9
- const v = o.reopened;
10
- if (v === null)
11
- return NOT_REOPENED;
12
- return typeof v === 'string' && v.length > 0 ? { kind: 'reopened', code: v } : UNSTATED;
20
+ }
21
+ }
22
+ export function resumeReopenFromError(err) {
23
+ if (typeof err !== 'object' || err === null)
24
+ return null;
25
+ const code = err.errorCode;
26
+ if (typeof code !== 'string' || !RESUME_REOPEN_CODES.includes(code))
27
+ return null;
28
+ if (err.retriable !== true)
29
+ return null;
30
+ const reason = code.slice('resume.'.length);
31
+ return { code, reason, retriable: true };
32
+ }
33
+ export function resumeReopenContent(detail) {
34
+ const first = detail.code === RESUME_ENV_FAILED
35
+ ? 'The execution environment for this parked work could not be brought back'
36
+ : detail.code === RESUME_TOOL_UNAVAILABLE
37
+ ? 'The tool this approval was granted for is not available on this engine'
38
+ : 'The tool this approval was granted for has changed since the approval was recorded';
39
+ return [
40
+ first,
41
+ 'The engine re-parked the work before consuming anything — the approval is still pending and your decision was not consumed',
42
+ 'Re-fetch the pending approvals and decide again on an engine where the same tool is available',
43
+ ].join(' · ');
13
44
  }
@@ -1,4 +1,4 @@
1
- import { DEFAULT_DENY_REASON, DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
1
+ import { APPROVAL_CARD_PORT_KEY_MISMATCH_REASON, DEFAULT_DENY_REASON, DecideTransportRetryExhaustedError, HitlBridge, NO_APPROVAL_CARD_PORT_REASON, HitlSafetyError, denyReasonForWire, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
2
2
  import { askParkRowArm } from './askParkRowRouting.js';
3
3
  import { hostLog } from '../host.js';
4
4
  import { readOwnNonEmptyString } from '../ownKey.js';
@@ -120,10 +120,9 @@ function cardPortMissReason() {
120
120
  const otherKeysInstalled = cardPortByKey
121
121
  .keys()
122
122
  .some((k) => k !== DEFAULT_SESSION_KEY && cardPortByKey.get(k) != null);
123
- if (otherKeysInstalled) {
124
- return 'no approval card port installed for the default session key, but a non-default sessionKey has one — likely a keyed-install/default-read mismatch (installApprovalCardPortFor vs the zero-arg default-key read), not a missing install';
125
- }
126
- return 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)';
123
+ if (otherKeysInstalled)
124
+ return APPROVAL_CARD_PORT_KEY_MISMATCH_REASON;
125
+ return NO_APPROVAL_CARD_PORT_REASON;
127
126
  }
128
127
  function rememberRefusedByServer(e) {
129
128
  switch (readWireErrorCode(e)) {
package/dist/index.d.ts CHANGED
@@ -145,6 +145,11 @@ export * from './principalWire.js';
145
145
  export * from './wireErrorTriage.js';
146
146
  export * from './corruptSession.js';
147
147
  export * from './corruptSessionCopy.js';
148
+ export { classifyToolHistoryMismatch, resolveToolHistoryRewindTarget } from './toolHistoryMismatch.js';
149
+ export { classifyRegistryConflict, isScopeGoneError } from './registryConflict.js';
150
+ export type { RegistryConflictClass } from './registryConflict.js';
151
+ export { createPromptDeliveryLedger, promptKnownUndelivered, withoutUndeliveredPrompts } from './promptDelivery.js';
152
+ export type { PromptDeliveryLedger, PromptDeliveryRecord } from './promptDelivery.js';
148
153
  export * from './resumeRefusalCopy.js';
149
154
  export * from './wireRefusalCopy.js';
150
155
  export * from './inheritEnvWire.js';
@@ -197,6 +202,8 @@ export { readEngineAgentRegistry, isEngineAgentRegistryListingSuperseded } from
197
202
  export type { EngineAgentRegistryClient } from './agentSession/engineAgentRegistryRead.js';
198
203
  export { stopEngineSessionBackground, engineSessionBackgroundReceiptReadingOf, ENGINE_SESSION_BACKGROUND_RECEIPT_STATES, } from './agentSession/engineSessionBackgroundStop.js';
199
204
  export type { EngineSessionBackgroundStopClient, EngineSessionBackgroundStopWire, EngineSessionBackgroundStopOptions, EngineSessionBackgroundReceipt, EngineSessionBackgroundStopUnavailableWhy, EngineSessionBackgroundStopRefusedWhy, EngineSessionBackgroundStopResult, EngineSessionBackgroundReceiptState, EngineSessionBackgroundReceiptReading, } from './agentSession/engineSessionBackgroundStop.js';
205
+ export { eraseSessionMemory, sessionMemoryEraseOutcome, SESSION_MEMORY_ERASE_REFUSAL_REASONS, SESSION_MEMORY_ERASE_UNCONFIRMED_WHYS, SESSION_MEMORY_ERASE_RETRIES, } from './sessionMemoryErase.js';
206
+ export type { SessionMemoryEraseWire, SessionMemoryEraseCall, SessionMemoryEraseOptions, SessionMemoryEraseSent, SessionMemoryEraseNotSentWhy, SessionMemoryEraseJson, SessionMemoryEraseAnswer, SessionMemoryEraseRefusalReason, SessionMemoryEraseUnconfirmedWhy, SessionMemoryEraseRetry, SessionMemoryEraseOutcome, } from './sessionMemoryErase.js';
200
207
  export * from './model/catalog.js';
201
208
  export * from './model/catalogLoader.js';
202
209
  export * from './model/providerAuth.js';
package/dist/index.js CHANGED
@@ -133,6 +133,9 @@ export * from './principalWire.js';
133
133
  export * from './wireErrorTriage.js';
134
134
  export * from './corruptSession.js';
135
135
  export * from './corruptSessionCopy.js';
136
+ export { classifyToolHistoryMismatch, resolveToolHistoryRewindTarget } from './toolHistoryMismatch.js';
137
+ export { classifyRegistryConflict, isScopeGoneError } from './registryConflict.js';
138
+ export { createPromptDeliveryLedger, promptKnownUndelivered, withoutUndeliveredPrompts } from './promptDelivery.js';
136
139
  export * from './resumeRefusalCopy.js';
137
140
  export * from './wireRefusalCopy.js';
138
141
  export * from './inheritEnvWire.js';
@@ -181,6 +184,7 @@ export * from './agentSession/backgroundView.js';
181
184
  export { READ_CREDENTIAL_NOT_ACCEPTED_NOTICE } from './readCredentialRefusal.js';
182
185
  export { readEngineAgentRegistry, isEngineAgentRegistryListingSuperseded } from './agentSession/engineAgentRegistryRead.js';
183
186
  export { stopEngineSessionBackground, engineSessionBackgroundReceiptReadingOf, ENGINE_SESSION_BACKGROUND_RECEIPT_STATES, } from './agentSession/engineSessionBackgroundStop.js';
187
+ export { eraseSessionMemory, sessionMemoryEraseOutcome, SESSION_MEMORY_ERASE_REFUSAL_REASONS, SESSION_MEMORY_ERASE_UNCONFIRMED_WHYS, SESSION_MEMORY_ERASE_RETRIES, } from './sessionMemoryErase.js';
184
188
  export * from './model/catalog.js';
185
189
  export * from './model/catalogLoader.js';
186
190
  export * from './model/providerAuth.js';
@@ -0,0 +1,20 @@
1
+ import type { AgentEvent } from '@sema-agent/sdk';
2
+ import { type SelfHealOutcome } from './adapter/activeRunSelfHeal.js';
3
+ export interface PromptDeliveryRecord {
4
+ readonly u: string;
5
+ readonly s: 'a' | 'd';
6
+ }
7
+ export interface PromptDeliveryLedger {
8
+ attempted(batchUuids: readonly string[]): readonly PromptDeliveryRecord[];
9
+ deliveredByFrame(batchUuids: readonly string[], event: AgentEvent): readonly PromptDeliveryRecord[];
10
+ deliveredBySteerReceipt(batchUuids: readonly string[], outcome: SelfHealOutcome): readonly PromptDeliveryRecord[];
11
+ isAttempted(uuid: string): boolean;
12
+ isDelivered(uuid: string): boolean;
13
+ knownUndelivered(uuid: string): boolean;
14
+ batchOf(uuid: string): readonly string[] | undefined;
15
+ toRecords(): readonly PromptDeliveryRecord[];
16
+ fromRecords(records: readonly PromptDeliveryRecord[]): readonly PromptDeliveryRecord[];
17
+ }
18
+ export declare function createPromptDeliveryLedger(): PromptDeliveryLedger;
19
+ export declare function promptKnownUndelivered(ledger: PromptDeliveryLedger, message: object | null | undefined): boolean;
20
+ export declare function withoutUndeliveredPrompts<T>(ledger: PromptDeliveryLedger, messages: readonly T[]): T[];