@sema-agent/client-core 0.85.0 → 0.85.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.
@@ -1,5 +1,5 @@
1
1
  import { abortableSleep } from '../abortableSleep.js';
2
- import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
2
+ import { corruptStoredRowContent, corruptStoredRowWhere, effectiveWireErrorCode } from '../wireErrorTriage.js';
3
3
  export const PLAN_REVIEW_GATE_KIND = 'plan_review';
4
4
  export const PLAN_REVIEW_GATE_KINDS = Object.freeze([PLAN_REVIEW_GATE_KIND, 'dry_run_review']);
5
5
  export const ASK_PARK_GATE_KINDS = Object.freeze(['human', 'irreversible_ask', 'policy_ask', 'tool_approval']);
@@ -45,20 +45,42 @@ function sessionOpts(deps) {
45
45
  const sessionId = deps?.sessionId;
46
46
  return typeof sessionId === 'string' && sessionId.length > 0 ? { session: sessionId } : {};
47
47
  }
48
+ function errorSlot(e, key) {
49
+ if (e === null || e === undefined)
50
+ return undefined;
51
+ try {
52
+ return e[key];
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
48
58
  function describeFailure(e) {
49
59
  const where = corruptStoredRowWhere(e);
50
60
  const rowNote = where === undefined ? '' : ` — ${corruptStoredRowContent(where)}`;
51
- if (typeof e === 'object' && e !== null && 'message' in e) {
52
- const m = e.message;
53
- if (typeof m === 'string' && m.length > 0)
54
- return `${m.length > 200 ? `${m.slice(0, 200)}…` : m}${rowNote}`;
61
+ let m;
62
+ try {
63
+ m = typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined;
55
64
  }
65
+ catch {
66
+ m = undefined;
67
+ }
68
+ if (typeof m === 'string' && m.length > 0)
69
+ return `${m.length > 200 ? `${m.slice(0, 200)}…` : m}${rowNote}`;
56
70
  return `the engine gave no detail${rowNote}`;
57
71
  }
58
72
  function readStatus(record) {
59
73
  const st = record?.status;
60
74
  return typeof st === 'string' && st.length > 0 ? st : null;
61
75
  }
76
+ function readStatusGuarded(record) {
77
+ try {
78
+ return readStatus(record);
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
62
84
  function readClaimHolder(record) {
63
85
  if (record === null || typeof record !== 'object')
64
86
  return 'unknown';
@@ -79,11 +101,129 @@ export function readCancelRequested(body) {
79
101
  const v = body.cancelRequested;
80
102
  return typeof v === 'boolean' ? v : undefined;
81
103
  }
104
+ const REOPEN_VERDICT_KEYS = [
105
+ 'reopened',
106
+ 'firstSight',
107
+ 'presented',
108
+ 'decidedWithoutCard',
109
+ 'pendingRowGone',
110
+ '_sema_decisionInFlight',
111
+ 'dismissedByUser',
112
+ '_sema_noPresentationSurface',
113
+ '_sema_errorCode',
114
+ ];
115
+ function snapshotReopenVerdict(raw) {
116
+ if (raw === null || raw === undefined)
117
+ return { reopened: false };
118
+ if (typeof raw !== 'object' && typeof raw !== 'function')
119
+ return raw;
120
+ const src = raw;
121
+ let nullProto = false;
122
+ try {
123
+ nullProto = Object.getPrototypeOf(raw) === null;
124
+ }
125
+ catch {
126
+ nullProto = false;
127
+ }
128
+ const snap = nullProto ? Object.create(null) : {};
129
+ const put = (k, v) => {
130
+ Object.defineProperty(snap, k, { value: v, enumerable: true, writable: true, configurable: true });
131
+ };
132
+ let own = [];
133
+ try {
134
+ own = Reflect.ownKeys(raw);
135
+ }
136
+ catch {
137
+ own = [];
138
+ }
139
+ const copied = new Set();
140
+ for (const k of own) {
141
+ let enumerable = false;
142
+ try {
143
+ enumerable = Object.prototype.propertyIsEnumerable.call(raw, k);
144
+ }
145
+ catch {
146
+ enumerable = false;
147
+ }
148
+ if (!enumerable)
149
+ continue;
150
+ let v;
151
+ let readable = true;
152
+ try {
153
+ v = src[k];
154
+ }
155
+ catch {
156
+ readable = false;
157
+ }
158
+ if (!readable)
159
+ continue;
160
+ put(k, v);
161
+ copied.add(k);
162
+ }
163
+ for (const k of REOPEN_VERDICT_KEYS) {
164
+ if (copied.has(k))
165
+ continue;
166
+ let v;
167
+ let readable = true;
168
+ try {
169
+ v = src[k];
170
+ }
171
+ catch {
172
+ readable = false;
173
+ }
174
+ if (readable && v !== undefined)
175
+ put(k, v);
176
+ }
177
+ return snap;
178
+ }
82
179
  function reopenRefusedForDecisionInFlight(verdict) {
83
- return verdict !== null && verdict.reopened === false && verdict._sema_decisionInFlight === true;
180
+ try {
181
+ return verdict !== null && verdict.reopened === false && verdict._sema_decisionInFlight === true;
182
+ }
183
+ catch {
184
+ return false;
185
+ }
84
186
  }
85
187
  function reopenRefusedForUserDismissal(verdict) {
86
- return verdict !== null && verdict.reopened === false && verdict.dismissedByUser === true;
188
+ try {
189
+ return verdict !== null && verdict.reopened === false && verdict.dismissedByUser === true;
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
195
+ function reopenRefusedForNoPresentationSurface(verdict) {
196
+ try {
197
+ return (verdict !== null &&
198
+ verdict.reopened === false &&
199
+ verdict._sema_noPresentationSurface === true);
200
+ }
201
+ catch {
202
+ return false;
203
+ }
204
+ }
205
+ function reopenErrorCodeOf(verdict) {
206
+ try {
207
+ if (verdict.reopened !== false)
208
+ return undefined;
209
+ const v = verdict._sema_errorCode;
210
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
211
+ }
212
+ catch {
213
+ return undefined;
214
+ }
215
+ }
216
+ function failureWireCode(e) {
217
+ try {
218
+ return effectiveWireErrorCode(e);
219
+ }
220
+ catch {
221
+ return undefined;
222
+ }
223
+ }
224
+ function thrownReopenVerdict(e) {
225
+ const code = failureWireCode(e);
226
+ return code !== undefined ? { reopened: false, _sema_errorCode: code } : { reopened: false };
87
227
  }
88
228
  function planReviewReopenTriggerFor(origin) {
89
229
  if (origin === 'user')
@@ -93,23 +233,57 @@ function planReviewReopenTriggerFor(origin) {
93
233
  return undefined;
94
234
  }
95
235
  function reopenDelivered(verdict) {
96
- return verdict.reopened === true && verdict.presented !== false;
236
+ let reopened;
237
+ let presented;
238
+ try {
239
+ reopened = verdict.reopened;
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ if (reopened !== true)
245
+ return false;
246
+ try {
247
+ presented = verdict.presented;
248
+ }
249
+ catch {
250
+ presented = undefined;
251
+ }
252
+ return presented !== false;
253
+ }
254
+ function reopenedOfOutcome(outcome) {
255
+ try {
256
+ return outcome.reopened ?? null;
257
+ }
258
+ catch {
259
+ return null;
260
+ }
97
261
  }
98
262
  export function atMostOnceFailureClass(e) {
99
- const status = e?.status;
263
+ const status = errorSlot(e, 'status');
100
264
  return typeof status === 'number' && status >= 400 && status < 500 ? 'rejected' : 'unknown';
101
265
  }
102
266
  export function readSteerDelivery(receipt) {
103
- if (typeof receipt !== 'object' || receipt === null || !('delivery' in receipt))
267
+ try {
268
+ if (typeof receipt !== 'object' || receipt === null || !('delivery' in receipt))
269
+ return null;
270
+ const d = receipt.delivery;
271
+ return typeof d === 'string' && d.length > 0 ? d : null;
272
+ }
273
+ catch {
104
274
  return null;
105
- const d = receipt.delivery;
106
- return typeof d === 'string' && d.length > 0 ? d : null;
275
+ }
107
276
  }
108
277
  export function readSteerReceiptStatus(receipt) {
109
- if (typeof receipt !== 'object' || receipt === null || !('status' in receipt))
278
+ try {
279
+ if (typeof receipt !== 'object' || receipt === null || !('status' in receipt))
280
+ return null;
281
+ const s = receipt.status;
282
+ return typeof s === 'string' && s.length > 0 ? s : null;
283
+ }
284
+ catch {
110
285
  return null;
111
- const s = receipt.status;
112
- return typeof s === 'string' && s.length > 0 ? s : null;
286
+ }
113
287
  }
114
288
  const CANCEL_RELEASE_WAIT_MS = 10_000;
115
289
  const RUNNING_LIVENESS_RECHECK_TIMEOUT_MS = 4_000;
@@ -227,11 +401,14 @@ async function readOwnedPendingCount(read, deps) {
227
401
  claimPollSleep(OWNED_PENDING_RECHECK_TIMEOUT_MS, recheckCtl.signal).then(() => null),
228
402
  ]);
229
403
  if (deps?.signal?.aborted === true)
230
- return null;
231
- return typeof count === 'number' ? count : null;
404
+ return { count: null };
405
+ return { count: typeof count === 'number' ? count : null };
232
406
  }
233
- catch {
234
- return null;
407
+ catch (e) {
408
+ if (deps?.signal?.aborted === true)
409
+ return { count: null };
410
+ const errorCode = failureWireCode(e);
411
+ return errorCode !== undefined ? { count: null, errorCode } : { count: null };
235
412
  }
236
413
  finally {
237
414
  deps?.signal?.removeEventListener('abort', onCallerAbort);
@@ -248,7 +425,7 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
248
425
  }
249
426
  const callerAborted = () => deps?.signal?.aborted === true;
250
427
  if (pending && typeof deps?.listOwnedPendingApprovals === 'function' && !callerAborted()) {
251
- const count = await readOwnedPendingCount(deps.listOwnedPendingApprovals, deps);
428
+ const { count } = await readOwnedPendingCount(deps.listOwnedPendingApprovals, deps);
252
429
  if (count === 0 && !callerAborted())
253
430
  pending = false;
254
431
  }
@@ -279,7 +456,7 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
279
456
  catch (e) {
280
457
  return { kind: 'state-unknown', taskId, detail: describeFailure(e) };
281
458
  }
282
- status = readStatus(record);
459
+ status = readStatusGuarded(record);
283
460
  }
284
461
  if (status === null) {
285
462
  return { kind: 'state-unknown', taskId, detail: 'the engine reported no status for that run' };
@@ -299,31 +476,41 @@ async function planVerdict(taskId, deps) {
299
476
  try {
300
477
  const trigger = planReviewReopenTriggerFor(deps?.submissionOrigin);
301
478
  const verdict = trigger === undefined ? deps?.reopenPlanReview?.(taskId) : deps?.reopenPlanReview?.(taskId, { trigger });
302
- return (await verdict) ?? { reopened: false };
479
+ return snapshotReopenVerdict(await verdict);
303
480
  }
304
- catch {
305
- return { reopened: false };
481
+ catch (e) {
482
+ if (deps?.signal?.aborted === true)
483
+ return { reopened: false };
484
+ return thrownReopenVerdict(e);
306
485
  }
307
486
  }
308
487
  async function askVerdict(taskId, deps) {
309
488
  try {
310
- return (await deps?.reopenAskPark?.(taskId)) ?? { reopened: false };
489
+ return snapshotReopenVerdict(await deps?.reopenAskPark?.(taskId));
311
490
  }
312
- catch {
313
- return { reopened: false };
491
+ catch (e) {
492
+ if (deps?.signal?.aborted === true)
493
+ return { reopened: false };
494
+ return thrownReopenVerdict(e);
314
495
  }
315
496
  }
316
497
  async function planReviewArm(taskId, signal, deps) {
317
498
  const verdict = await planVerdict(taskId, deps);
318
499
  if (reopenDelivered(verdict))
319
500
  return { kind: 'plan-review-reopened', taskId, firstSight: verdict.firstSight === true };
501
+ const inFlight = reopenRefusedForDecisionInFlight(verdict);
502
+ const dismissed = reopenRefusedForUserDismissal(verdict);
503
+ const noView = reopenRefusedForNoPresentationSurface(verdict);
504
+ const errorCode = inFlight || dismissed || noView ? undefined : reopenErrorCodeOf(verdict);
320
505
  return {
321
506
  kind: 'plan-review-reopen-failed',
322
507
  taskId,
323
508
  decidePath: signal.pendingGate?.decidePath ?? null,
324
- ...(reopenRefusedForDecisionInFlight(verdict) ? { decisionInFlight: true } : {}),
509
+ ...(inFlight ? { decisionInFlight: true } : {}),
325
510
  ...(verdict.pendingRowGone === true ? { pendingRowGone: true } : {}),
326
- ...(reopenRefusedForUserDismissal(verdict) ? { dismissedByUser: true } : {}),
511
+ ...(dismissed ? { dismissedByUser: true } : {}),
512
+ ...(noView ? { noPresentationSurface: true } : {}),
513
+ ...(errorCode !== undefined ? { errorCode } : {}),
327
514
  };
328
515
  }
329
516
  async function askParkArm(taskId, signal, runs, deps) {
@@ -358,29 +545,41 @@ async function askParkArm(taskId, signal, runs, deps) {
358
545
  if (verdict.reopened === false && verdict.decidedWithoutCard !== true && verdict.pendingRowGone === true && !inFlight) {
359
546
  return staleParkArm(taskId, signal, runs, deps);
360
547
  }
548
+ const noView = reopenRefusedForNoPresentationSurface(verdict);
549
+ const errorCode = inFlight || noView || reopenRefusedForUserDismissal(verdict) ? undefined : reopenErrorCodeOf(verdict);
361
550
  return {
362
551
  kind: 'ask-reopen-failed',
363
552
  taskId,
364
553
  decidePath: signal.pendingGate?.decidePath ?? null,
365
554
  ...(inFlight ? { decisionInFlight: true } : {}),
366
555
  ...(verdict.pendingRowGone === true ? { pendingRowGone: true } : {}),
556
+ ...(noView ? { noPresentationSurface: true } : {}),
557
+ ...(errorCode !== undefined ? { errorCode } : {}),
367
558
  };
368
559
  }
369
560
  async function probeRunStatusOnce(taskId, durable, durableGet, deps) {
370
561
  let status = null;
371
562
  let ghost = false;
563
+ let errorCode;
564
+ let record = null;
565
+ let answered = false;
372
566
  const lease = claimProbeLease(deps?.signal, RUNNING_LIVENESS_RECHECK_TIMEOUT_MS);
373
567
  try {
374
- status = readStatus(await durableGet.call(durable, taskId, { signal: lease.signal, ...sessionOpts(deps) }));
568
+ record = await durableGet.call(durable, taskId, { signal: lease.signal, ...sessionOpts(deps) });
569
+ answered = true;
375
570
  }
376
571
  catch (e) {
377
- if (e?.status === 404)
572
+ if (errorSlot(e, 'status') === 404)
378
573
  ghost = true;
574
+ else
575
+ errorCode = failureWireCode(e);
379
576
  }
380
577
  finally {
381
578
  lease.release();
382
579
  }
383
- return { status, ghost };
580
+ if (answered)
581
+ status = readStatusGuarded(record);
582
+ return errorCode !== undefined ? { status, ghost, errorCode } : { status, ghost };
384
583
  }
385
584
  async function staleParkArm(taskId, busy, runs, deps) {
386
585
  const reopenFailed = {
@@ -394,6 +593,7 @@ async function staleParkArm(taskId, busy, runs, deps) {
394
593
  taskId,
395
594
  decidePath: busy.pendingGate?.decidePath ?? null,
396
595
  };
596
+ const withFailureCode = (base, code) => code !== undefined ? { ...base, errorCode: code } : base;
397
597
  if (runs === undefined || typeof runs.get !== 'function')
398
598
  return reopenFailed;
399
599
  const durableGet = runs.get;
@@ -407,7 +607,7 @@ async function staleParkArm(taskId, busy, runs, deps) {
407
607
  return reopenFailed;
408
608
  const fresh = first.status;
409
609
  if (fresh === null)
410
- return reopenFailed;
610
+ return withFailureCode(reopenFailed, first.errorCode);
411
611
  if (RUNNING_STATES.includes(fresh))
412
612
  return runningChoiceArm(taskId, fresh, busy, false, runs, deps);
413
613
  if (PLAN_REVIEW_STATES.includes(fresh))
@@ -446,12 +646,14 @@ async function staleParkArm(taskId, busy, runs, deps) {
446
646
  return { kind: 'ask-run-not-found', taskId };
447
647
  if (callerAborted())
448
648
  return reopenFailedAfterCard;
449
- if (afterCard.status === null || !ASK_PARK_STATES.includes(afterCard.status)) {
450
- return afterCard.status !== null && !CLAIM_HELD_STATES.includes(afterCard.status) ? reopenFailed : reopenFailedAfterCard;
649
+ if (afterCard.status === null)
650
+ return withFailureCode(reopenFailedAfterCard, afterCard.errorCode);
651
+ if (!ASK_PARK_STATES.includes(afterCard.status)) {
652
+ return !CLAIM_HELD_STATES.includes(afterCard.status) ? reopenFailed : reopenFailedAfterCard;
451
653
  }
452
- const stillGone = await readOwnedPendingCount(recheckOwnedPending, deps);
453
- if (stillGone !== 0)
454
- return reopenFailedAfterCard;
654
+ const recheck = await readOwnedPendingCount(recheckOwnedPending, deps);
655
+ if (recheck.count !== 0)
656
+ return withFailureCode(reopenFailedAfterCard, recheck.errorCode);
455
657
  const verdict = await cancelAndConfirmRelease(taskId, runs, durableGet, deps);
456
658
  if (verdict.outcome === 'released') {
457
659
  return { kind: 'stale-park-cancelled', taskId, ...(verdict.engineStatedRelease ? { releaseConfirmedByEngine: true } : {}) };
@@ -655,6 +857,7 @@ export function activeRunSelfHealRow(outcome, signal, copy, origin, follow) {
655
857
  }
656
858
  const INJECTED_LEAD = 'A follow-up message sema sent on its own (not one you typed)';
657
859
  const USER_CLOSED_CARD_NOT_PUT_BACK = 'sema does not put a card you closed back on screen by itself';
860
+ const NO_CARD_VIEW_ATTACHED = 'this session has no view attached that can show decision cards';
658
861
  function injectedSubmissionRow(outcome, following = false) {
659
862
  const lead = INJECTED_LEAD;
660
863
  const id = 'taskId' in outcome && outcome.taskId ? ` (id ${outcome.taskId})` : '';
@@ -677,22 +880,32 @@ function injectedSubmissionRow(outcome, following = false) {
677
880
  }
678
881
  if (outcome.kind === 'running-steered' && outcome.delivery === 'queued') {
679
882
  const queuedOn = `${lead} is queued on an earlier reply${id} that is paused waiting for a decision`;
680
- if (outcome.reopened !== null && reopenDelivered(outcome.reopened)) {
883
+ const reopened = reopenedOfOutcome(outcome);
884
+ if (reopened !== null && reopenDelivered(reopened)) {
681
885
  return `${queuedOn}; sema has shown that decision card, and the message is picked up once you answer it.`;
682
886
  }
683
- if (reopenRefusedForDecisionInFlight(outcome.reopened)) {
887
+ if (reopenRefusedForDecisionInFlight(reopened)) {
684
888
  return `${queuedOn}; your answer to it is still on its way, and the message is picked up once that answer is applied.`;
685
889
  }
686
- if (reopenRefusedForUserDismissal(outcome.reopened)) {
890
+ if (reopenRefusedForUserDismissal(reopened)) {
687
891
  return (`${queuedOn}; you closed that card yourself, so sema did not put it back on screen — send a message to bring it ` +
688
892
  `back, and the message is picked up once that decision is made.`);
689
893
  }
690
- return `${queuedOn} sema could not show here; the message is picked up only once that decision is made.`;
894
+ const noView = reopenRefusedForNoPresentationSurface(reopened) ? ` (${NO_CARD_VIEW_ATTACHED})` : '';
895
+ return `${queuedOn} sema could not show here${noView}; the message is picked up only once that decision is made.`;
691
896
  }
692
897
  if (outcome.kind === 'running-steered' && outcome.delivery === 'parked_for_wake') {
693
898
  return (`${lead} has not reached the model: the earlier reply${id} had already finished, so the message was set aside on it ` +
694
899
  `instead of starting a new reply, and sema did not retry it.`);
695
900
  }
901
+ if (outcome.kind === 'running-steered' && outcome.delivery !== 'applied') {
902
+ const unknown = `${lead} was accepted for the reply already in progress${id}, but how it will be delivered was not reported, ` +
903
+ `so sema cannot tell whether that reply will pick it up`;
904
+ return following
905
+ ? `${unknown}. sema is following that reply and will show whatever it asks for next; check what it does ` +
906
+ `before deciding whether anything needs to be sent again.`
907
+ : `${unknown} — watch that reply before deciding whether anything needs to be sent again.`;
908
+ }
696
909
  switch (selfHealSubmissionDisposition(outcome)) {
697
910
  case 'held-for-decision':
698
911
  return (`${lead} is on hold: this session is busy with an earlier reply${id} that is waiting on a decision. ` +
@@ -710,9 +923,13 @@ function injectedSubmissionRow(outcome, following = false) {
710
923
  `sema is following that reply and will show whatever it asks for next; nothing for you to do now.`
711
924
  : `${lead} was passed to the reply already in progress${id} instead of starting a new one — ` +
712
925
  `watch that reply for what it does with it.`;
713
- case 'not-delivered':
926
+ case 'not-delivered': {
927
+ const noView = (outcome.kind === 'ask-reopen-failed' || outcome.kind === 'plan-review-reopen-failed') && outcome.noPresentationSurface === true
928
+ ? ` (${NO_CARD_VIEW_ATTACHED})`
929
+ : '';
714
930
  return (`${lead} was not delivered: this session was still busy with an earlier reply${id}, and there was no open card ` +
715
- `you could answer to free it. sema did not retry, so the model has not seen it.`);
931
+ `you could answer to free it${noView}. sema did not retry, so the model has not seen it.`);
932
+ }
716
933
  }
717
934
  }
718
935
  function decisionInFlightRow(parkedOn) {
@@ -780,8 +997,9 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
780
997
  const viaEngine = engineDecide && outcome.decidePath
781
998
  ? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
782
999
  : '';
1000
+ const noView = outcome.noPresentationSurface === true ? `: ${NO_CARD_VIEW_ATTACHED}` : '';
783
1001
  return (`The previous turn is parked waiting for your decision (run ${outcome.taskId}) and sema could ` +
784
- `not reopen that card here. It did NOT cancel the run — that would have decided it for you. ` +
1002
+ `not reopen that card here${noView}. It did NOT cancel the run — that would have decided it for you. ` +
785
1003
  (keepSessionWayOut !== null
786
1004
  ? `Your message was NOT sent. To keep this conversation, ${keepSessionWayOut}: the card comes back when the session resumes, if it is still waiting. Or ${wayOut} if you no longer want it.${viaEngine}`
787
1005
  : `Your message was NOT sent; ${wayOut} if you no longer want it.${viaEngine}`));
@@ -797,8 +1015,9 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
797
1015
  const viaEngine = engineDecide && outcome.decidePath
798
1016
  ? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
799
1017
  : '';
1018
+ const noView = outcome.noPresentationSurface === true ? `: ${NO_CARD_VIEW_ATTACHED}` : '';
800
1019
  return (`The previous turn is parked waiting for a plan review (run ${outcome.taskId}) and sema could not ` +
801
- `reopen that approval card. It did NOT cancel the run — that would have discarded the plan for ` +
1020
+ `reopen that approval card${noView}. It did NOT cancel the run — that would have discarded the plan for ` +
802
1021
  (keepSessionWayOut !== null
803
1022
  ? `you. Your message was NOT sent. To keep this conversation, ${keepSessionWayOut}: the card comes back when the session resumes, if it is still waiting. Or ${wayOut} if you no longer want that plan.${viaEngine}`
804
1023
  : `you. Your message was NOT sent; ${wayOut} if you no longer want that plan.${viaEngine}`));
@@ -816,15 +1035,18 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
816
1035
  }
817
1036
  if (outcome.delivery === 'queued') {
818
1037
  const parked = outcome.status ? `parked (status ${outcome.status})` : 'parked';
819
- const next = outcome.reopened !== null && reopenDelivered(outcome.reopened)
1038
+ const reopened = reopenedOfOutcome(outcome);
1039
+ const next = reopened !== null && reopenDelivered(reopened)
820
1040
  ? `sema has surfaced that decision card — answering it is what lets that run resume and pick your message up.`
821
- : reopenRefusedForDecisionInFlight(outcome.reopened)
1041
+ : reopenRefusedForDecisionInFlight(reopened)
822
1042
  ? `Your answer to that decision is still on its way to the engine, so sema did not show the card again; ` +
823
1043
  `that run moves on once the engine applies it.`
824
- : reopenRefusedForUserDismissal(outcome.reopened)
1044
+ : reopenRefusedForUserDismissal(reopened)
825
1045
  ? `You closed that decision card yourself, so sema did not put it back on screen; nothing will resume that ` +
826
1046
  `run until that decision is made — send a message to bring the card back.`
827
- : `sema could not surface that decision card here, so nothing will resume that run until that decision is made; ` +
1047
+ : `sema could not surface that decision card here` +
1048
+ (reopenRefusedForNoPresentationSurface(reopened) ? ` (${NO_CARD_VIEW_ATTACHED})` : '') +
1049
+ `, so nothing will resume that run until that decision is made; ` +
828
1050
  `${wayOut} if you no longer want it.`;
829
1051
  return (`${handle} is not actually working right now: the engine reports it ${parked} on a decision it needs ` +
830
1052
  `from you, so it queued your message on that park instead of running it. ${next} ${textOnly} ` +
@@ -9,6 +9,7 @@ export declare function gateArmedWaitMs(): number;
9
9
  export declare function waitForGateArmed(keys: readonly string[], timeoutMs: number): Promise<boolean>;
10
10
  export declare function waitForGateArmedFor(sessionKey: string, keys: readonly string[], timeoutMs: number): Promise<boolean>;
11
11
  export declare function registerArmedGateFromQuestionId(questionId: unknown): void;
12
+ export declare function registerArmedGateFromQuestionIdFor(sessionKey: string, questionId: unknown): void;
12
13
  export declare function planReviewArmedKey(taskId: string): string;
13
14
  export declare function planReviewArmedKeyFor(sessionKey: string, taskId: string): string;
14
15
  export declare function notePlanReviewAnswered(questionId: unknown): void;
@@ -120,7 +120,7 @@ function armedKeyForQuestionIdFor(sessionKey, questionId) {
120
120
  export function registerArmedGateFromQuestionId(questionId) {
121
121
  registerArmedGateFromQuestionIdFor(DEFAULT_SESSION_KEY, questionId);
122
122
  }
123
- function registerArmedGateFromQuestionIdFor(sessionKey, questionId) {
123
+ export function registerArmedGateFromQuestionIdFor(sessionKey, questionId) {
124
124
  if (typeof questionId !== 'string' || questionId.length === 0)
125
125
  return;
126
126
  if (questionId.startsWith(PLAN_REVIEW_QUESTION_ID_PREFIX) && !questionId.includes(REOPEN_ID_TAIL)) {
@@ -827,6 +827,12 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
827
827
  revokePlanReviewUserDismissal(sessionKey, taskId, markBefore);
828
828
  return { reopened: true, firstSight };
829
829
  }
830
+ function noPresentationSurfaceVerdict(taskId, sessionKey) {
831
+ const verdict = { reopened: false, _sema_noPresentationSurface: true };
832
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — nothing is subscribed to question frames for session ` +
833
+ `${JSON.stringify(sessionKey)} (onQuestionFrameFor), so the card has nowhere to render`);
834
+ return verdict;
835
+ }
830
836
  async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart) {
831
837
  const refusedByUser = { reopened: false, dismissedByUser: true };
832
838
  try {
@@ -863,6 +869,11 @@ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, ma
863
869
  hostLog('debug', `planReviewWire: reopen for task ${taskId} not minted — the dismissal record changed while it was being checked (stale evaluation)`);
864
870
  return await staleDismissalVerdict(current, sessionKey, taskId, receiptMs);
865
871
  }
872
+ if (!hasQuestionOverlayFor(sessionKey)) {
873
+ if (fresh)
874
+ dropDismissalEntry(sessionKey, taskId, mark);
875
+ return noPresentationSurfaceVerdict(taskId, sessionKey);
876
+ }
866
877
  if (!fresh) {
867
878
  hostLog('debug', `planReviewWire: automatic reopen for task ${taskId} refused — the user closed this gate's card (no newer gate instance proven)`);
868
879
  return refusedByUser;
@@ -878,8 +889,7 @@ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, ma
878
889
  putDismissalEntry(sessionKey, taskId, reopening);
879
890
  let verdict = { reopened: false };
880
891
  try {
881
- if (hasQuestionOverlayFor(sessionKey))
882
- verdict = await mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
892
+ verdict = await mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
883
893
  }
884
894
  finally {
885
895
  const presented = verdict.reopened === true;
@@ -937,7 +947,8 @@ export function reopenPlanReviewCard(taskId, opts) {
937
947
  const recheckReadStart = readStart === undefined ? guardStartedAt : Math.min(readStart, guardStartedAt);
938
948
  const sessionKey = opts?.sessionKey ?? DEFAULT_SESSION_KEY;
939
949
  if (!hasQuestionOverlayFor(sessionKey)) {
940
- return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
950
+ const noSurface = noPresentationSurfaceVerdict(taskId, sessionKey);
951
+ return receiptMs === undefined ? noSurface : Promise.resolve(noSurface);
941
952
  }
942
953
  if (sessionKey !== DEFAULT_SESSION_KEY && opts?.deliverDecision === undefined && opts?.wire === undefined) {
943
954
  hostLog('error', `planReviewWire: plan_review 卡未重开 —— sessionKey=${JSON.stringify(sessionKey)} 是非默认槽,` +
package/dist/host.d.ts CHANGED
@@ -56,7 +56,7 @@ export interface HostPorts {
56
56
  }
57
57
  export declare function installHost(ports: HostPorts): () => void;
58
58
  export declare function installHostFor(sessionKey: string, ports: Omit<HostPorts, 'queue'>): () => void;
59
- export declare function hostLog(level: AdapterLogLevel, msg: string, data?: unknown): void;
59
+ export declare function hostLog(level: AdapterLogLevel, msg: string, data?: unknown): boolean;
60
60
  export declare function hostProbe(channel: string, line: string): void;
61
61
  export declare function hostSettings(): SettingsPort | undefined;
62
62
  export declare function hostSettingsFor(sessionKey: string): SettingsPort | undefined;
package/dist/host.js CHANGED
@@ -46,11 +46,20 @@ export function installHostFor(sessionKey, ports) {
46
46
  slot.state = prev;
47
47
  };
48
48
  }
49
+ function callDiagnosticPort(call) {
50
+ try {
51
+ call();
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
49
58
  export function hostLog(level, msg, data) {
50
- hostSlot(DEFAULT_SESSION_KEY).state.log?.(level, msg, data);
59
+ return callDiagnosticPort(() => hostSlot(DEFAULT_SESSION_KEY).state.log?.(level, msg, data));
51
60
  }
52
61
  export function hostProbe(channel, line) {
53
- hostSlot(DEFAULT_SESSION_KEY).state.probe?.(channel, line);
62
+ callDiagnosticPort(() => hostSlot(DEFAULT_SESSION_KEY).state.probe?.(channel, line));
54
63
  }
55
64
  export function hostSettings() {
56
65
  return hostSettingsFor(DEFAULT_SESSION_KEY);
package/dist/index.d.ts CHANGED
@@ -169,7 +169,7 @@ export * from './coreValuePorts.js';
169
169
  export * from './hitl/askGateWire.js';
170
170
  export * from './hitl/planReviewWire.js';
171
171
  export { PLAN_REVIEW_QUESTION_ID_PREFIX, REOPEN_ID_TAIL, approvalCallKey, askGateQuestionId, planReviewQuestionId, liveFrameCallKey, armedKeyFromQuestionId, } from './hitl/gateIdentity.js';
172
- export { _resetArmedGateRegistryForTest, _setGateArmedWaitMsForTest, clearArmedGate, gateArmedWaitMs, notePlanReviewAnswered, onGateArmed, planReviewArmedKey, registerArmedGate, registerArmedGateFor, registerArmedGateFromQuestionId, waitForGateArmed, wasGateArmed, wasGateArmedFor, } from './hitl/armedGateRegistry.js';
172
+ export { _resetArmedGateRegistryForTest, _setGateArmedWaitMsForTest, clearArmedGate, gateArmedWaitMs, notePlanReviewAnswered, onGateArmed, planReviewArmedKey, registerArmedGate, registerArmedGateFor, registerArmedGateFromQuestionId, registerArmedGateFromQuestionIdFor, waitForGateArmed, wasGateArmed, wasGateArmedFor, } from './hitl/armedGateRegistry.js';
173
173
  export * from './hitl/parkOwnership.js';
174
174
  export * from './hitl/parkRowBirthWait.js';
175
175
  export * from './hitl/approvalDecisionNoteAudit.js';
package/dist/index.js CHANGED
@@ -155,7 +155,7 @@ export * from './coreValuePorts.js';
155
155
  export * from './hitl/askGateWire.js';
156
156
  export * from './hitl/planReviewWire.js';
157
157
  export { PLAN_REVIEW_QUESTION_ID_PREFIX, REOPEN_ID_TAIL, approvalCallKey, askGateQuestionId, planReviewQuestionId, liveFrameCallKey, armedKeyFromQuestionId, } from './hitl/gateIdentity.js';
158
- export { _resetArmedGateRegistryForTest, _setGateArmedWaitMsForTest, clearArmedGate, gateArmedWaitMs, notePlanReviewAnswered, onGateArmed, planReviewArmedKey, registerArmedGate, registerArmedGateFor, registerArmedGateFromQuestionId, waitForGateArmed, wasGateArmed, wasGateArmedFor, } from './hitl/armedGateRegistry.js';
158
+ export { _resetArmedGateRegistryForTest, _setGateArmedWaitMsForTest, clearArmedGate, gateArmedWaitMs, notePlanReviewAnswered, onGateArmed, planReviewArmedKey, registerArmedGate, registerArmedGateFor, registerArmedGateFromQuestionId, registerArmedGateFromQuestionIdFor, waitForGateArmed, wasGateArmed, wasGateArmedFor, } from './hitl/armedGateRegistry.js';
159
159
  export * from './hitl/parkOwnership.js';
160
160
  export * from './hitl/parkRowBirthWait.js';
161
161
  export * from './hitl/approvalDecisionNoteAudit.js';
@@ -71,6 +71,8 @@ export interface TaskRequestOmission {
71
71
  readonly cause: TaskRequestOmissionCause;
72
72
  }
73
73
  export declare function taskRequestOmissionCauseDetail(cause: unknown): string;
74
+ export declare const TASK_REQUEST_OMISSION_CAUSE_NOTICES: Readonly<Record<TaskRequestOmissionCause, string>>;
75
+ export declare function taskRequestOmissionCauseNotice(cause: unknown): string;
74
76
  export interface TaskRequestAssembly {
75
77
  readonly request: TaskRequestLike;
76
78
  readonly omitted: readonly TaskRequestOmission[];