@sema-agent/client-core 0.87.0 → 0.89.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 (56) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +20 -15
  3. package/dist/adapter/activeRunSelfHeal.js +4 -2
  4. package/dist/adapter/downstream/eventToSdkMessage.js +17 -2
  5. package/dist/adapter/downstream/wiringManifestView.d.ts +11 -0
  6. package/dist/adapter/downstream/wiringManifestView.js +28 -0
  7. package/dist/controlRouter.d.ts +7 -1
  8. package/dist/controlRouter.js +43 -1
  9. package/dist/disableSkillsWire.d.ts +17 -0
  10. package/dist/disableSkillsWire.js +52 -0
  11. package/dist/displayUntrusted.d.ts +4 -0
  12. package/dist/displayUntrusted.js +1742 -591
  13. package/dist/engineErrorCodes.d.ts +2 -0
  14. package/dist/engineErrorCodes.js +1 -0
  15. package/dist/engineNoticeCodes.d.ts +44 -1
  16. package/dist/engineNoticeCodes.js +211 -39
  17. package/dist/engineWireFor.d.ts +1 -0
  18. package/dist/engineWireFor.js +1 -1
  19. package/dist/gateOutcome.d.ts +4 -0
  20. package/dist/gateOutcome.js +5 -0
  21. package/dist/generated/engineFactTables.d.ts +1 -0
  22. package/dist/generated/engineFactTables.js +3 -0
  23. package/dist/generated/engineNoticeTables.d.ts +3 -0
  24. package/dist/generated/engineNoticeTables.js +110 -0
  25. package/dist/hitl/crashConverged.d.ts +1 -1
  26. package/dist/hitl/crashConverged.js +6 -1
  27. package/dist/hitl/planReviewWire.js +30 -10
  28. package/dist/hitl/toolApprovalWire.d.ts +6 -2
  29. package/dist/hitl/toolApprovalWire.js +30 -0
  30. package/dist/hooksWireCaps.js +80 -12
  31. package/dist/index.d.ts +12 -3
  32. package/dist/index.js +8 -3
  33. package/dist/pluginHooksWire.d.ts +46 -4
  34. package/dist/pluginHooksWire.js +277 -24
  35. package/dist/promptDelivery.d.ts +20 -0
  36. package/dist/promptDelivery.js +153 -0
  37. package/dist/providerRejection.d.ts +6 -0
  38. package/dist/providerRejection.js +52 -0
  39. package/dist/registryConflict.d.ts +3 -0
  40. package/dist/registryConflict.js +44 -0
  41. package/dist/request/taskRequest.d.ts +3 -0
  42. package/dist/request/taskRequest.js +64 -8
  43. package/dist/resumeRefusalCopy.d.ts +7 -0
  44. package/dist/resumeRefusalCopy.js +45 -1
  45. package/dist/runTerminal.d.ts +6 -0
  46. package/dist/runTerminal.js +23 -0
  47. package/dist/seam.d.ts +4 -0
  48. package/dist/seatContract.d.ts +9 -1
  49. package/dist/seatContract.js +10 -0
  50. package/dist/sessionMemoryErase.d.ts +76 -0
  51. package/dist/sessionMemoryErase.js +432 -0
  52. package/dist/subagent/engineSubagentResume.js +2 -1
  53. package/dist/subagent/engineSubagentSteer.d.ts +8 -1
  54. package/dist/subagent/engineSubagentSteer.js +26 -3
  55. package/docs/INTEGRATION-CLIENTS.md +767 -14
  56. package/package.json +3 -3
@@ -478,6 +478,8 @@ export const TOOL_PERMISSION_REQUEST_KEYS = [
478
478
  "mandate",
479
479
  "origin",
480
480
  "ruleOffersAbsence",
481
+ "askClass",
482
+ "classes",
481
483
  ];
482
484
  export const LOCAL_SESSION_RECORD_KEYS = [
483
485
  "sessionId",
@@ -563,6 +565,14 @@ export const SEAT_VALIDATOR_KEY_COVERAGE = {
563
565
  key: "origin",
564
566
  why: "喂姿态谓词 askSurvivesPosture 的安全位(CC-203),按开集读(非空串原文字节、词表不校);权威窄读在卡口与过境口 toolPermissionRequestAskBits,座位门不设第二判官(为一个位拒掉整条 request = 把安全 ask 丢给引擎 TTL deny)。坏形的两种失效方向:非串 / 空串读成缺席 ⇒ 宿主报了没命中时姿态谓词可答可以放(放宽方向:真出身若是必问词就丢了);表外串读成不认识的词 ⇒ 答 unknown,宿主退回自己的规则(不放宽)。不经过境口自铸座位请求的宿主要自己按同一把窄读。",
565
567
  },
568
+ {
569
+ key: "askClass",
570
+ why: "展示位(CC-284),权威成对窄读在卡口与过境口 toolPermissionRequestAskBits(与 classes 同在同缺、首员等于 askClass、不判成员、不重排);座位门不设第二判官,理由同 mandated(为一个展示位拒掉整条 request = 把安全 ask 丢给引擎超时拒)。坏形读成缺席 ⇒ 卡上不显示问类,不改决断;缺席不是「普通问」。不经过境口自铸座位请求的宿主要自己按同一把成对窄读。",
571
+ },
572
+ {
573
+ key: "classes",
574
+ why: "与 askClass 同一对(CC-284),同在同缺、同一只成对窄读(非空、每员非空串、无重复、首员等于 askClass;开集、原样顺序);座位门不设第二判官,理由同 askClass。",
575
+ },
566
576
  {
567
577
  key: "ruleOffersAbsence",
568
578
  why: "喂强制判据口 approvalIsMandated 的位(CC-203;寄存行那条腿的强制只以缺席词 mandated 表达),按开集读(非空串原文字节、词表不校);权威窄读在卡口与过境口,座位门不设第二判官,理由同 mandated。坏形读成缺席 ⇒ 这一路不判强制(放宽方向:寄存行上的强制就丢了;另两路 mandated / mandate 照判);表外串只是不等于 mandated。不经过境口自铸座位请求的宿主要自己按同一把窄读。",
@@ -0,0 +1,76 @@
1
+ import type { EngineWireClientConfig } from './engineWireSdk.js';
2
+ import type { MemoryEraseReceiptReading, MemoryEraseUnreadableCause } from './memoryEntriesWire.js';
3
+ export type SessionMemoryEraseWire = Pick<EngineWireClientConfig, 'baseUrl' | 'token' | 'principal' | 'timeoutMs' | 'fetchImpl'>;
4
+ export interface SessionMemoryEraseCall {
5
+ sessionId: string;
6
+ requestId: string;
7
+ allowUnevidenced?: boolean;
8
+ }
9
+ export interface SessionMemoryEraseOptions {
10
+ signal?: AbortSignal | null;
11
+ }
12
+ export interface SessionMemoryEraseSent {
13
+ sessionId: string;
14
+ requestId: string;
15
+ allowUnevidenced?: boolean;
16
+ }
17
+ export type SessionMemoryEraseNotSentWhy = 'bad_input' | 'no_connection' | 'cancelled';
18
+ export type SessionMemoryEraseJson = null | boolean | number | string | readonly SessionMemoryEraseJson[] | {
19
+ readonly [key: string]: SessionMemoryEraseJson;
20
+ };
21
+ export type SessionMemoryEraseAnswer = {
22
+ kind: 'not_sent';
23
+ why: SessionMemoryEraseNotSentWhy;
24
+ detail: string;
25
+ } | {
26
+ kind: 'answered';
27
+ sent: SessionMemoryEraseSent;
28
+ status: number;
29
+ body: SessionMemoryEraseJson | undefined;
30
+ } | {
31
+ kind: 'no_answer';
32
+ sent: SessionMemoryEraseSent;
33
+ detail: string;
34
+ };
35
+ export declare function eraseSessionMemory(call: SessionMemoryEraseCall, wire: SessionMemoryEraseWire, opts?: SessionMemoryEraseOptions): Promise<SessionMemoryEraseAnswer>;
36
+ export type SessionMemoryEraseRefusalReason = 'bad_input' | 'no_connection' | 'cancelled_before_send' | 'credentials_required' | 'not_permitted' | 'memory_engine_absent' | 'session_not_found' | 'route_absent' | 'request_rejected' | 'evidence_unavailable' | 'request_id_reused' | 'store_unreadable';
37
+ export type SessionMemoryEraseUnconfirmedWhy = 'no_answer' | 'receipt_unreadable' | 'receipt_mismatch' | 'store_link_outside' | 'control_plane_corrupt' | 'engine_fault' | 'unclassified' | 'no_code' | 'unreadable';
38
+ export type SessionMemoryEraseRetry = 'not_needed' | 'same_request_id' | 'new_request_id' | 'needs_operator' | 'needs_consent' | 'human_decision' | 'pointless';
39
+ type SessionMemoryEraseReceipt = Extract<MemoryEraseReceiptReading, {
40
+ kind: 'present';
41
+ }>;
42
+ export type SessionMemoryEraseOutcome = {
43
+ kind: 'erased';
44
+ receipt: SessionMemoryEraseReceipt;
45
+ text: string;
46
+ retry: 'not_needed';
47
+ retryText: string;
48
+ } | {
49
+ kind: 'partial';
50
+ receipt: SessionMemoryEraseReceipt;
51
+ text: string;
52
+ retry: 'same_request_id' | 'human_decision';
53
+ retryText: string;
54
+ } | {
55
+ kind: 'refused';
56
+ reason: SessionMemoryEraseRefusalReason;
57
+ text: string;
58
+ retry: SessionMemoryEraseRetry;
59
+ retryText: string;
60
+ status?: number;
61
+ errorCode?: string;
62
+ } | {
63
+ kind: 'unconfirmed';
64
+ why: SessionMemoryEraseUnconfirmedWhy;
65
+ text: string;
66
+ retry: SessionMemoryEraseRetry;
67
+ retryText: string;
68
+ status?: number;
69
+ errorCode?: string;
70
+ receiptWhy?: MemoryEraseUnreadableCause;
71
+ };
72
+ export declare const SESSION_MEMORY_ERASE_REFUSAL_REASONS: readonly SessionMemoryEraseRefusalReason[];
73
+ export declare const SESSION_MEMORY_ERASE_UNCONFIRMED_WHYS: readonly SessionMemoryEraseUnconfirmedWhy[];
74
+ export declare const SESSION_MEMORY_ERASE_RETRIES: readonly SessionMemoryEraseRetry[];
75
+ export declare function sessionMemoryEraseOutcome(answer: SessionMemoryEraseAnswer): SessionMemoryEraseOutcome;
76
+ export {};
@@ -0,0 +1,432 @@
1
+ import { isLoopbackWireUrl, resolveWireAuth } from './engineWireSdk.js';
2
+ import { INJECTED_WIRE_SETTLE_MS, connectionLostDetail, noAnswerDetailFrom, noAnswerKindOf, provablyNotSent, settleInjected } from './engineWireFor.js';
3
+ import { memoryEraseReplayPosture, readMemoryEraseReceipt } from './memoryEntriesWire.js';
4
+ import { normalizeWirePrincipal } from './principalWire.js';
5
+ const PRINCIPAL_HEADER = 'x-agent-principal';
6
+ const MAX_TIMER_MS = 2_147_483_647;
7
+ const NOT_SENT = ' — nothing was sent';
8
+ const DETAIL_BAD_CALL = `the erase call needs a session id (a non-empty string that is not a dot segment) and a request id (a non-empty string), and allowUnevidenced must be true or false when given${NOT_SENT}`;
9
+ const DETAIL_BAD_OPTIONS = `the options could not be read, or the signal is not an AbortSignal${NOT_SENT}`;
10
+ const DETAIL_BAD_WIRE = `the connection could not be read (it needs a baseUrl string, a known token form, a positive time limit and a fetch function)${NOT_SENT}`;
11
+ const DETAIL_BROWSER = `a connection that carries a token is refused in a browser; use the same-origin relay form${NOT_SENT}`;
12
+ const DETAIL_NO_FETCH = `no fetch function is available${NOT_SENT}`;
13
+ const DETAIL_CANCELLED = `the call was cancelled before the request was sent${NOT_SENT}`;
14
+ const DETAIL_CONNECT_FAILED = `the connection to the engine could not be opened${NOT_SENT}`;
15
+ const DETAIL_ANSWER_UNREADABLE = 'an answer came back but could not be read (the request may still have been received)';
16
+ class AnswerUnreadable extends Error {
17
+ }
18
+ function plainObject(v) {
19
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
20
+ }
21
+ function nonEmptyString(v) {
22
+ return typeof v === 'string' && v !== '';
23
+ }
24
+ function notSent(why, detail) {
25
+ return { kind: 'not_sent', why, detail };
26
+ }
27
+ function tokenFormOf(token, baseUrl) {
28
+ if (token === undefined)
29
+ return { kind: 'value', read: () => undefined };
30
+ if (typeof token === 'string')
31
+ return { kind: 'value', read: () => token };
32
+ if (typeof token === 'function') {
33
+ const fn = token;
34
+ return {
35
+ kind: 'value',
36
+ read: () => {
37
+ try {
38
+ return fn();
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ },
44
+ };
45
+ }
46
+ if (plainObject(token)) {
47
+ let mode;
48
+ try {
49
+ mode = token.mode;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ if (mode === 'same-origin-relay')
55
+ return { kind: 'relay' };
56
+ if (mode === 'loopback-unauthed' && isLoopbackWireUrl(baseUrl))
57
+ return { kind: 'unauthed' };
58
+ return null;
59
+ }
60
+ return null;
61
+ }
62
+ function inBrowser() {
63
+ try {
64
+ return typeof globalThis.window !== 'undefined';
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ function outboundHeaders(baseUrl, form, principal) {
71
+ const h = {};
72
+ if (form.kind === 'value') {
73
+ const resolved = resolveWireAuth(baseUrl, form.read());
74
+ if (typeof resolved === 'string')
75
+ h.authorization = `Bearer ${resolved}`;
76
+ }
77
+ if (principal !== undefined)
78
+ h[PRINCIPAL_HEADER] = principal;
79
+ h['content-type'] = 'application/json';
80
+ return h;
81
+ }
82
+ async function readAnswer(res) {
83
+ let status;
84
+ let textOf;
85
+ try {
86
+ if (res === null || typeof res !== 'object')
87
+ throw new AnswerUnreadable();
88
+ status = res.status;
89
+ textOf = res.text;
90
+ }
91
+ catch {
92
+ throw new AnswerUnreadable();
93
+ }
94
+ if (typeof status !== 'number' || !Number.isInteger(status) || status < 100 || status > 599 || typeof textOf !== 'function')
95
+ throw new AnswerUnreadable();
96
+ let text;
97
+ try {
98
+ text = await textOf.call(res);
99
+ }
100
+ catch (e) {
101
+ if (e !== null && typeof e === 'object' && noAnswerKindOf(e) !== null)
102
+ throw e;
103
+ throw new AnswerUnreadable();
104
+ }
105
+ if (typeof text !== 'string')
106
+ throw new AnswerUnreadable();
107
+ return { status, text };
108
+ }
109
+ export async function eraseSessionMemory(call, wire, opts) {
110
+ let sent;
111
+ let sessionSegment;
112
+ try {
113
+ if (!plainObject(call))
114
+ return notSent('bad_input', DETAIL_BAD_CALL);
115
+ const sessionId = call.sessionId;
116
+ const requestId = call.requestId;
117
+ const allowUnevidenced = Object.hasOwn(call, 'allowUnevidenced') ? call.allowUnevidenced : undefined;
118
+ if (!nonEmptyString(sessionId) || !nonEmptyString(requestId))
119
+ return notSent('bad_input', DETAIL_BAD_CALL);
120
+ if (sessionId === '.' || sessionId === '..')
121
+ return notSent('bad_input', DETAIL_BAD_CALL);
122
+ if (allowUnevidenced !== undefined && typeof allowUnevidenced !== 'boolean')
123
+ return notSent('bad_input', DETAIL_BAD_CALL);
124
+ sessionSegment = encodeURIComponent(sessionId);
125
+ sent = { sessionId, requestId, ...(allowUnevidenced !== undefined ? { allowUnevidenced } : {}) };
126
+ }
127
+ catch {
128
+ return notSent('bad_input', DETAIL_BAD_CALL);
129
+ }
130
+ let signal;
131
+ try {
132
+ if (opts !== undefined && opts !== null && typeof opts !== 'object')
133
+ return notSent('bad_input', DETAIL_BAD_OPTIONS);
134
+ const sig = opts?.signal;
135
+ if (sig !== undefined && sig !== null) {
136
+ if (typeof sig !== 'object' || typeof sig.addEventListener !== 'function' || typeof sig.aborted !== 'boolean') {
137
+ return notSent('bad_input', DETAIL_BAD_OPTIONS);
138
+ }
139
+ if (typeof AbortSignal.any === 'function' && !(sig instanceof AbortSignal))
140
+ return notSent('bad_input', DETAIL_BAD_OPTIONS);
141
+ signal = sig;
142
+ }
143
+ }
144
+ catch {
145
+ return notSent('bad_input', DETAIL_BAD_OPTIONS);
146
+ }
147
+ let baseUrl;
148
+ let form;
149
+ let principal;
150
+ let timeoutMs;
151
+ let fetchImpl;
152
+ try {
153
+ if (!plainObject(wire))
154
+ return notSent('no_connection', DETAIL_BAD_WIRE);
155
+ const rawBase = wire.baseUrl;
156
+ const rawToken = wire.token;
157
+ const rawPrincipal = wire.principal;
158
+ const rawTimeout = wire.timeoutMs;
159
+ const rawFetch = wire.fetchImpl;
160
+ if (typeof rawBase !== 'string')
161
+ return notSent('no_connection', DETAIL_BAD_WIRE);
162
+ const f = tokenFormOf(rawToken, rawBase);
163
+ if (f === null)
164
+ return notSent('no_connection', DETAIL_BAD_WIRE);
165
+ if (rawPrincipal !== undefined && typeof rawPrincipal !== 'string')
166
+ return notSent('no_connection', DETAIL_BAD_WIRE);
167
+ if (rawTimeout !== undefined && (typeof rawTimeout !== 'number' || !Number.isFinite(rawTimeout) || rawTimeout <= 0 || rawTimeout > MAX_TIMER_MS)) {
168
+ return notSent('no_connection', DETAIL_BAD_WIRE);
169
+ }
170
+ if (rawFetch !== undefined && typeof rawFetch !== 'function')
171
+ return notSent('no_connection', DETAIL_BAD_WIRE);
172
+ if (f.kind !== 'relay' && inBrowser())
173
+ return notSent('no_connection', DETAIL_BROWSER);
174
+ const globalFetch = globalThis.fetch;
175
+ const chosen = rawFetch ?? (typeof globalFetch === 'function' ? globalFetch.bind(globalThis) : undefined);
176
+ if (typeof chosen !== 'function')
177
+ return notSent('no_connection', DETAIL_NO_FETCH);
178
+ baseUrl = rawBase;
179
+ form = f;
180
+ principal = normalizeWirePrincipal(rawPrincipal);
181
+ timeoutMs = rawTimeout ?? INJECTED_WIRE_SETTLE_MS;
182
+ fetchImpl = chosen;
183
+ }
184
+ catch {
185
+ return notSent('no_connection', DETAIL_BAD_WIRE);
186
+ }
187
+ try {
188
+ if (signal !== undefined && signal.aborted === true)
189
+ return notSent('cancelled', DETAIL_CANCELLED);
190
+ }
191
+ catch {
192
+ return notSent('bad_input', DETAIL_BAD_OPTIONS);
193
+ }
194
+ const url = `${baseUrl.replace(/\/+$/, '')}/v1/sessions/${sessionSegment}/memory/erase`;
195
+ const payload = JSON.stringify(sent.allowUnevidenced !== undefined ? { requestId: sent.requestId, allowUnevidenced: sent.allowUnevidenced } : { requestId: sent.requestId });
196
+ const once = (s) => fetchImpl(url, { method: 'POST', headers: outboundHeaders(baseUrl, form, principal), body: payload, signal: s }).then(readAnswer);
197
+ let got;
198
+ try {
199
+ got = await settleInjected(timeoutMs, signal, once);
200
+ }
201
+ catch (e) {
202
+ if (e instanceof AnswerUnreadable)
203
+ return { kind: 'no_answer', sent, detail: DETAIL_ANSWER_UNREADABLE };
204
+ const obj = e !== null && typeof e === 'object' ? e : null;
205
+ if (obj !== null && noAnswerKindOf(obj) !== null)
206
+ return { kind: 'no_answer', sent, detail: noAnswerDetailFrom(obj) ?? connectionLostDetail() };
207
+ if (obj !== null && provablyNotSent(obj))
208
+ return notSent('no_connection', DETAIL_CONNECT_FAILED);
209
+ return { kind: 'no_answer', sent, detail: connectionLostDetail() };
210
+ }
211
+ let body;
212
+ try {
213
+ body = got.text === '' ? undefined : JSON.parse(got.text);
214
+ }
215
+ catch {
216
+ body = undefined;
217
+ }
218
+ return { kind: 'answered', sent, status: got.status, body };
219
+ }
220
+ const REFUSAL_TEXT = {
221
+ bad_input: 'Nothing was erased: the erase request could not be built from what was given, so it was not sent.',
222
+ no_connection: 'Nothing was erased: there is no usable connection to the engine, so the request was not sent.',
223
+ cancelled_before_send: 'Nothing was erased: the erase was cancelled before the request was sent.',
224
+ credentials_required: 'Nothing was erased: this deployment does not accept memory erase requests until its operator configures a service credential.',
225
+ not_permitted: "Nothing was erased: this caller is not permitted to erase this session's memory.",
226
+ memory_engine_absent: "Nothing was erased: this deployment has no memory store that supports erasing a session's memory.",
227
+ session_not_found: 'Nothing was erased: the engine does not recognise this session for this caller.',
228
+ route_absent: "Nothing was erased: this engine version cannot erase a session's memory.",
229
+ request_rejected: 'Nothing was erased: the engine rejected the erase request as malformed.',
230
+ evidence_unavailable: "Nothing was erased: this deployment's memory store cannot keep proof of an erase, so the erase was declined.",
231
+ request_id_reused: 'Nothing was erased: this request id was already used for a different erase.',
232
+ store_unreadable: 'Nothing was erased: part of the memory store could not be read, so the erase was declined before anything changed; the deployment operator needs to repair the store.',
233
+ };
234
+ const UNCONFIRMED_TEXT = {
235
+ no_answer: 'Not confirmed: no answer came back from the engine, so the erase may or may not have happened.',
236
+ receipt_unreadable: 'Not confirmed: the engine answered, but its erase receipt could not be read.',
237
+ receipt_mismatch: 'Not confirmed: the engine answered with a receipt for a different erase request.',
238
+ store_link_outside: 'Not confirmed: the memory store contains a link that leads outside it, so the erase stopped before finishing and may have erased part of this memory or none of it; the deployment operator needs to remove the link.',
239
+ control_plane_corrupt: "Not confirmed: the memory store's own records could not be read cleanly, so the erase may have changed nothing or may already be committed; the deployment operator needs to repair them.",
240
+ engine_fault: 'Not confirmed: the engine reported an internal failure during the erase, so it may or may not have happened.',
241
+ unclassified: 'Not confirmed: the engine refused in a way this client does not recognise.',
242
+ no_code: 'Not confirmed: an answer came back without the engine error code, so it may not have come from the engine at all.',
243
+ unreadable: 'Not confirmed: the result of this erase could not be read.',
244
+ };
245
+ const ERASED_TEXT = 'Erased: every memory entry recorded as coming from this session was erased or was already gone.';
246
+ const PARTIAL_TEXT = 'Partly erased: some memory entries from this session were not erased yet.';
247
+ const RETRY_TEXT = {
248
+ not_needed: 'Nothing more to do.',
249
+ same_request_id: 'It is safe to try again with the same request id.',
250
+ new_request_id: "To erase this session's memory, try again with a new request id.",
251
+ needs_operator: 'Ask the deployment operator to fix this first, then try again with the same request id.',
252
+ needs_consent: 'It can only go ahead if someone explicitly agrees to erase without keeping proof.',
253
+ human_decision: 'Trying again may erase a second time, so a person should decide whether to retry.',
254
+ pointless: 'Trying again as it is will not help.',
255
+ };
256
+ const REFUSAL_RETRY = {
257
+ bad_input: 'pointless',
258
+ no_connection: 'same_request_id',
259
+ cancelled_before_send: 'same_request_id',
260
+ credentials_required: 'needs_operator',
261
+ not_permitted: 'pointless',
262
+ memory_engine_absent: 'pointless',
263
+ session_not_found: 'pointless',
264
+ route_absent: 'pointless',
265
+ request_rejected: 'pointless',
266
+ evidence_unavailable: 'needs_consent',
267
+ request_id_reused: 'new_request_id',
268
+ store_unreadable: 'needs_operator',
269
+ };
270
+ export const SESSION_MEMORY_ERASE_REFUSAL_REASONS = Object.freeze(Object.keys(REFUSAL_TEXT));
271
+ export const SESSION_MEMORY_ERASE_UNCONFIRMED_WHYS = Object.freeze(Object.keys(UNCONFIRMED_TEXT));
272
+ export const SESSION_MEMORY_ERASE_RETRIES = Object.freeze(Object.keys(RETRY_TEXT));
273
+ const MAX_DIAG_CODE = 200;
274
+ function refusedBy(reason, diag) {
275
+ const retry = REFUSAL_RETRY[reason];
276
+ return { kind: 'refused', reason, text: REFUSAL_TEXT[reason], retry, retryText: RETRY_TEXT[retry], ...diag };
277
+ }
278
+ function unconfirmedRetry(why, allowUnevidenced) {
279
+ if (allowUnevidenced === true)
280
+ return 'human_decision';
281
+ switch (why) {
282
+ case 'store_link_outside':
283
+ case 'control_plane_corrupt':
284
+ return 'needs_operator';
285
+ case 'receipt_mismatch':
286
+ case 'unreadable':
287
+ return 'human_decision';
288
+ case 'no_answer':
289
+ case 'receipt_unreadable':
290
+ case 'engine_fault':
291
+ case 'unclassified':
292
+ case 'no_code':
293
+ return 'same_request_id';
294
+ default: {
295
+ const unreachable = why;
296
+ return unreachable;
297
+ }
298
+ }
299
+ }
300
+ function unconfirmedBy(why, allowUnevidenced, diag) {
301
+ const retry = unconfirmedRetry(why, allowUnevidenced);
302
+ return { kind: 'unconfirmed', why, text: UNCONFIRMED_TEXT[why], retry, retryText: RETRY_TEXT[retry], ...diag };
303
+ }
304
+ function bandOfCode(code) {
305
+ switch (code) {
306
+ case 'auth.service_token_required':
307
+ return { refused: 'credentials_required' };
308
+ case 'principal_unverified':
309
+ return { refused: 'not_permitted' };
310
+ case 'capability.memory_engine_required':
311
+ return { refused: 'memory_engine_absent' };
312
+ case 'not_found.session':
313
+ return { refused: 'session_not_found' };
314
+ case 'not_found.route':
315
+ return { refused: 'route_absent' };
316
+ case 'request.path_malformed':
317
+ case 'request.body_shape':
318
+ case 'request.request_id_required':
319
+ case 'request.field_invalid':
320
+ case 'config.memory_erasure_request':
321
+ return { refused: 'request_rejected' };
322
+ case 'memory.erasure_evidence_unavailable':
323
+ return { refused: 'evidence_unavailable' };
324
+ case 'memory.erasure_selector_mismatch':
325
+ return { refused: 'request_id_reused' };
326
+ case 'memory.erasure_census_incomplete':
327
+ return { refused: 'store_unreadable' };
328
+ case 'memory.target_outside_store':
329
+ return { unconfirmed: 'store_link_outside' };
330
+ case 'internal.memory_control_plane_corrupt':
331
+ return { unconfirmed: 'control_plane_corrupt' };
332
+ case 'internal.error':
333
+ return { unconfirmed: 'engine_fault' };
334
+ default:
335
+ return code.startsWith('auth.') ? { refused: 'not_permitted' } : undefined;
336
+ }
337
+ }
338
+ function selectEchoMatches(body, sessionId) {
339
+ try {
340
+ if (!plainObject(body) || !Object.hasOwn(body, 'select'))
341
+ return true;
342
+ const sel = body.select;
343
+ if (!plainObject(sel) || !Object.hasOwn(sel, 'sessionId'))
344
+ return false;
345
+ return sel.sessionId === sessionId;
346
+ }
347
+ catch {
348
+ return false;
349
+ }
350
+ }
351
+ export function sessionMemoryEraseOutcome(answer) {
352
+ let kind;
353
+ let why;
354
+ let sessionId;
355
+ let requestId;
356
+ let allowUnevidenced;
357
+ let status;
358
+ let body;
359
+ const a = answer;
360
+ try {
361
+ if (!plainObject(a))
362
+ return unconfirmedBy('unreadable', undefined, {});
363
+ kind = a.kind;
364
+ if (kind === 'not_sent') {
365
+ why = a.why;
366
+ }
367
+ else if (kind === 'answered' || kind === 'no_answer') {
368
+ const sent = a.sent;
369
+ if (!plainObject(sent))
370
+ return unconfirmedBy('unreadable', undefined, {});
371
+ sessionId = sent.sessionId;
372
+ requestId = sent.requestId;
373
+ allowUnevidenced = Object.hasOwn(sent, 'allowUnevidenced') ? sent.allowUnevidenced : undefined;
374
+ if (kind === 'answered') {
375
+ status = a.status;
376
+ body = a.body;
377
+ }
378
+ }
379
+ }
380
+ catch {
381
+ return unconfirmedBy('unreadable', undefined, {});
382
+ }
383
+ if (kind === 'not_sent') {
384
+ if (why === 'bad_input')
385
+ return refusedBy('bad_input', {});
386
+ if (why === 'no_connection')
387
+ return refusedBy('no_connection', {});
388
+ if (why === 'cancelled')
389
+ return refusedBy('cancelled_before_send', {});
390
+ return unconfirmedBy('unreadable', undefined, {});
391
+ }
392
+ if (kind !== 'answered' && kind !== 'no_answer')
393
+ return unconfirmedBy('unreadable', undefined, {});
394
+ if (!nonEmptyString(sessionId) || !nonEmptyString(requestId) || (allowUnevidenced !== undefined && typeof allowUnevidenced !== 'boolean')) {
395
+ return unconfirmedBy('unreadable', undefined, {});
396
+ }
397
+ const signed = allowUnevidenced;
398
+ if (kind === 'no_answer')
399
+ return unconfirmedBy('no_answer', signed, {});
400
+ if (typeof status !== 'number' || !Number.isInteger(status) || status < 100 || status > 599)
401
+ return unconfirmedBy('unreadable', undefined, {});
402
+ if (status >= 200 && status < 300) {
403
+ const reading = readMemoryEraseReceipt(body);
404
+ if (reading.kind !== 'present')
405
+ return unconfirmedBy('receipt_unreadable', signed, { status, receiptWhy: reading.why });
406
+ if (reading.requestId !== requestId || !selectEchoMatches(body, sessionId))
407
+ return unconfirmedBy('receipt_mismatch', signed, { status });
408
+ if (reading.status === 'complete') {
409
+ return { kind: 'erased', receipt: reading, text: ERASED_TEXT, retry: 'not_needed', retryText: RETRY_TEXT.not_needed };
410
+ }
411
+ const retry = memoryEraseReplayPosture(reading) === 'idempotent' ? 'same_request_id' : 'human_decision';
412
+ return { kind: 'partial', receipt: reading, text: PARTIAL_TEXT, retry, retryText: RETRY_TEXT[retry] };
413
+ }
414
+ if (status < 400)
415
+ return unconfirmedBy('unclassified', signed, { status });
416
+ let rawCode;
417
+ try {
418
+ rawCode = plainObject(body) && Object.hasOwn(body, 'errorCode') ? body.errorCode : undefined;
419
+ }
420
+ catch {
421
+ return unconfirmedBy('unreadable', undefined, {});
422
+ }
423
+ if (!nonEmptyString(rawCode))
424
+ return unconfirmedBy('no_code', signed, { status });
425
+ const diag = { status, ...(rawCode.length <= MAX_DIAG_CODE ? { errorCode: rawCode } : {}) };
426
+ const band = bandOfCode(rawCode);
427
+ if (band !== undefined && 'refused' in band)
428
+ return refusedBy(band.refused, diag);
429
+ if (band !== undefined)
430
+ return unconfirmedBy(band.unconfirmed, signed, diag);
431
+ return unconfirmedBy(status >= 500 ? 'engine_fault' : 'unclassified', signed, diag);
432
+ }
@@ -1,5 +1,6 @@
1
1
  import { hostLog } from '../host.js';
2
2
  import { resumeRetryLaterFromError } from '../wireErrorTriage.js';
3
+ import { RESUME_SESSION_NOT_FOUND } from '../engineErrorCodes.js';
3
4
  import { displayUntrusted } from '../displayUntrusted.js';
4
5
  import { engineCapTrue } from '../engineCapsCache.js';
5
6
  import { clientHasVerbs, engineWireFor, INJECTED_WIRE_SETTLE_MS, noAnswerDetailFrom, sendOnce, settleInjected } from '../engineWireFor.js';
@@ -21,7 +22,7 @@ export function resolveSubagentContinueTarget(rowId, displayName) {
21
22
  const RETENTION_LAPSED_CODES = new Set([
22
23
  'resume.evicted',
23
24
  'resume.cap',
24
- 'resume.session_not_found',
25
+ RESUME_SESSION_NOT_FOUND,
25
26
  ]);
26
27
  function shapeOf(e) {
27
28
  const o = (e ?? {});
@@ -4,10 +4,17 @@ export type SubagentSteerWireClient = {
4
4
  runs: Pick<AgentClient['runs'], 'steerSubagent'>;
5
5
  };
6
6
  export type SubagentSteerWire = EngineWireInjection<SubagentSteerWireClient>;
7
+ export interface SubagentSteerNotDelivered {
8
+ ok: false;
9
+ reason: 'not-delivered';
10
+ safety: 'ledger_unavailable';
11
+ detail: string;
12
+ }
7
13
  export type SubagentSteerOutcome = {
8
14
  ok: true;
9
15
  receipt: string;
10
- } | {
16
+ messageId?: string;
17
+ } | SubagentSteerNotDelivered | {
11
18
  ok: false;
12
19
  reason: 'no-wire' | 'no-run' | 'not-running' | 'error';
13
20
  detail?: string;
@@ -3,9 +3,21 @@ import { clientHasVerbs, engineWireFor, INJECTED_WIRE_SETTLE_MS, noAnswerDetailF
3
3
  import { engineSessionParamFor } from '../engineSessionParam.js';
4
4
  import { resolveOwnerContext, resolveOwnerRunId } from './engineSubagentResume.js';
5
5
  import { noteBgOwnerAbsence } from './subagentOwnerAbsence.js';
6
+ import { errCodes, steeringRefusedBeforeEngine } from '../controlRouter.js';
6
7
  function usableSteerClient(client) {
7
8
  return clientHasVerbs(client, [['runs', 'steerSubagent']]);
8
9
  }
10
+ function steerReceiptMessageId(body) {
11
+ try {
12
+ if (typeof body !== 'object' || body === null || !Object.hasOwn(body, 'messageId'))
13
+ return undefined;
14
+ const v = body.messageId;
15
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
9
21
  export async function steerEngineSubagent(target, text, childTaskId, opts) {
10
22
  const named = typeof childTaskId === 'string' && childTaskId !== '' ? childTaskId : undefined;
11
23
  const { record: owner, key } = resolveOwnerContext(named);
@@ -36,13 +48,24 @@ export async function steerEngineSubagent(target, text, childTaskId, opts) {
36
48
  const copy = body?.note ?? body?.message;
37
49
  if (typeof copy === 'string' && copy.length > 0)
38
50
  receipt = copy;
39
- return { ok: true, receipt };
51
+ const messageId = steerReceiptMessageId(body);
52
+ return messageId === undefined ? { ok: true, receipt } : { ok: true, receipt, messageId };
40
53
  }
41
54
  catch (e) {
42
- const status = e?.status;
43
- if (typeof status === 'number') {
55
+ let status;
56
+ try {
57
+ status = errCodes(e).status;
58
+ }
59
+ catch {
60
+ status = undefined;
61
+ }
62
+ if (status !== undefined) {
44
63
  const detail = e instanceof Error ? e.message : String(e);
45
64
  hostLog('debug', `engineSubagentSteer: ${status} ${detail.slice(0, 200)}`);
65
+ const refused = typeof e === 'object' && e !== null ? steeringRefusedBeforeEngine(e) : undefined;
66
+ if (refused !== undefined && refused.safety === 'ledger_unavailable') {
67
+ return { ok: false, reason: 'not-delivered', safety: refused.safety, detail: refused.advice };
68
+ }
46
69
  if (status === 409 || status === 404) {
47
70
  return { ok: false, reason: 'not-running', detail };
48
71
  }