@sema-agent/client-core 0.83.3 → 0.83.5

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,4 +1,5 @@
1
1
  import type { ActiveRunBusySignal } from './runStream.js';
2
+ import type { PlanReviewReopenTrigger } from '../hitl/planReviewWire.js';
2
3
  export declare const PLAN_REVIEW_GATE_KIND = "plan_review";
3
4
  export declare const PLAN_REVIEW_GATE_KINDS: readonly string[];
4
5
  export declare const ASK_PARK_GATE_KINDS: readonly string[];
@@ -33,6 +34,7 @@ export type ReopenCardVerdict = {
33
34
  decidedWithoutCard?: true;
34
35
  pendingRowGone?: true;
35
36
  _sema_decisionInFlight?: true;
37
+ dismissedByUser?: true;
36
38
  } | {
37
39
  reopened: true;
38
40
  firstSight: boolean;
@@ -54,7 +56,9 @@ export interface ActiveRunSelfHealDeps {
54
56
  listOwnedPendingApprovals?: (opts?: {
55
57
  signal?: AbortSignal;
56
58
  }) => Promise<number>;
57
- reopenPlanReview?: (taskId: string) => ReopenCardVerdict | Promise<ReopenCardVerdict>;
59
+ reopenPlanReview?: (taskId: string, opts?: {
60
+ trigger?: PlanReviewReopenTrigger;
61
+ }) => ReopenCardVerdict | Promise<ReopenCardVerdict>;
58
62
  reopenAskPark?: (taskId: string) => Promise<ReopenCardVerdict>;
59
63
  offerRunningChoice?: (req: RunningChoiceRequest) => Promise<'steer' | 'cancel' | 'wait'>;
60
64
  offerStaleParkChoice?: (req: StaleParkChoiceRequest) => Promise<'cancel' | 'wait'>;
@@ -81,6 +85,7 @@ export type SelfHealOutcome = {
81
85
  decidePath: string | null;
82
86
  decisionInFlight?: true;
83
87
  pendingRowGone?: true;
88
+ dismissedByUser?: true;
84
89
  } | {
85
90
  kind: 'not-parked';
86
91
  taskId: string;
@@ -107,6 +112,7 @@ export type SelfHealOutcome = {
107
112
  kind: 'ask-decided-without-card';
108
113
  taskId: string;
109
114
  released: true;
115
+ releaseConfirmedByEngine?: true;
110
116
  } | {
111
117
  kind: 'ask-decided-release-unknown';
112
118
  taskId: string;
@@ -125,6 +131,7 @@ export type SelfHealOutcome = {
125
131
  } | {
126
132
  kind: 'running-cancelled';
127
133
  taskId: string;
134
+ releaseConfirmedByEngine?: true;
128
135
  } | {
129
136
  kind: 'running-cancel-timeout';
130
137
  taskId: string;
@@ -142,6 +149,7 @@ export type SelfHealOutcome = {
142
149
  } | {
143
150
  kind: 'stale-park-cancelled';
144
151
  taskId: string;
152
+ releaseConfirmedByEngine?: true;
145
153
  } | {
146
154
  kind: 'stale-park-cancel-timeout';
147
155
  taskId: string;
@@ -82,6 +82,16 @@ export function readCancelRequested(body) {
82
82
  function reopenRefusedForDecisionInFlight(verdict) {
83
83
  return verdict !== null && verdict.reopened === false && verdict._sema_decisionInFlight === true;
84
84
  }
85
+ function reopenRefusedForUserDismissal(verdict) {
86
+ return verdict !== null && verdict.reopened === false && verdict.dismissedByUser === true;
87
+ }
88
+ function planReviewReopenTriggerFor(origin) {
89
+ if (origin === 'user')
90
+ return 'user';
91
+ if (origin === 'injected')
92
+ return 'automatic';
93
+ return undefined;
94
+ }
85
95
  function reopenDelivered(verdict) {
86
96
  return verdict.reopened === true && verdict.presented !== false;
87
97
  }
@@ -287,7 +297,9 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
287
297
  }
288
298
  async function planVerdict(taskId, deps) {
289
299
  try {
290
- return (await deps?.reopenPlanReview?.(taskId)) ?? { reopened: false };
300
+ const trigger = planReviewReopenTriggerFor(deps?.submissionOrigin);
301
+ const verdict = trigger === undefined ? deps?.reopenPlanReview?.(taskId) : deps?.reopenPlanReview?.(taskId, { trigger });
302
+ return (await verdict) ?? { reopened: false };
291
303
  }
292
304
  catch {
293
305
  return { reopened: false };
@@ -311,6 +323,7 @@ async function planReviewArm(taskId, signal, deps) {
311
323
  decidePath: signal.pendingGate?.decidePath ?? null,
312
324
  ...(reopenRefusedForDecisionInFlight(verdict) ? { decisionInFlight: true } : {}),
313
325
  ...(verdict.pendingRowGone === true ? { pendingRowGone: true } : {}),
326
+ ...(reopenRefusedForUserDismissal(verdict) ? { dismissedByUser: true } : {}),
314
327
  };
315
328
  }
316
329
  async function askParkArm(taskId, signal, runs, deps) {
@@ -328,8 +341,14 @@ async function askParkArm(taskId, signal, runs, deps) {
328
341
  ...(deps?.signal !== undefined ? { signal: deps.signal } : {}),
329
342
  ...sessionOpts(deps),
330
343
  });
331
- if (release.released)
332
- return { kind: 'ask-decided-without-card', taskId, released: true };
344
+ if (release.released) {
345
+ return {
346
+ kind: 'ask-decided-without-card',
347
+ taskId,
348
+ released: true,
349
+ ...(release.lastHolder === 'released' ? { releaseConfirmedByEngine: true } : {}),
350
+ };
351
+ }
333
352
  if (release.lastStatus !== null && RUNNING_STATES.includes(release.lastStatus)) {
334
353
  return runningChoiceArm(taskId, release.lastStatus, signal, false, runs, deps);
335
354
  }
@@ -434,8 +453,9 @@ async function staleParkArm(taskId, busy, runs, deps) {
434
453
  if (stillGone !== 0)
435
454
  return reopenFailedAfterCard;
436
455
  const verdict = await cancelAndConfirmRelease(taskId, runs, durableGet, deps);
437
- if (verdict.outcome === 'released')
438
- return { kind: 'stale-park-cancelled', taskId };
456
+ if (verdict.outcome === 'released') {
457
+ return { kind: 'stale-park-cancelled', taskId, ...(verdict.engineStatedRelease ? { releaseConfirmedByEngine: true } : {}) };
458
+ }
439
459
  if (verdict.outcome === 'failed') {
440
460
  return { kind: 'stale-park-cancel-failed', taskId, detail: verdict.detail, delivery: verdict.delivery };
441
461
  }
@@ -475,7 +495,7 @@ async function cancelAndConfirmRelease(taskId, durable, durableGet, deps) {
475
495
  ...sessionOpts(deps),
476
496
  });
477
497
  if (verdict.released)
478
- return { outcome: 'released' };
498
+ return { outcome: 'released', engineStatedRelease: verdict.lastHolder === 'released' };
479
499
  if (cancelFailure !== null)
480
500
  return { outcome: 'failed', ...cancelFailure };
481
501
  return {
@@ -567,8 +587,9 @@ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps
567
587
  }
568
588
  if (choice === 'cancel' && canCancel && runs?.cancel !== undefined && runs.get !== undefined) {
569
589
  const verdict = await cancelAndConfirmRelease(taskId, runs, runs.get, deps);
570
- if (verdict.outcome === 'released')
571
- return { kind: 'running-cancelled', taskId };
590
+ if (verdict.outcome === 'released') {
591
+ return { kind: 'running-cancelled', taskId, ...(verdict.engineStatedRelease ? { releaseConfirmedByEngine: true } : {}) };
592
+ }
572
593
  if (verdict.outcome === 'failed') {
573
594
  return { kind: 'running-cancel-failed', taskId, detail: verdict.detail, delivery: verdict.delivery };
574
595
  }
@@ -617,7 +638,8 @@ export function activeRunSelfHealRow(outcome, signal, copy, origin, follow) {
617
638
  const engineDecide = copy?.engineDecidePath !== false;
618
639
  const keep = (outcome.kind === 'plan-review-reopen-failed' || outcome.kind === 'ask-reopen-failed') &&
619
640
  outcome.decisionInFlight !== true &&
620
- outcome.pendingRowGone !== true
641
+ outcome.pendingRowGone !== true &&
642
+ (outcome.kind !== 'plan-review-reopen-failed' || outcome.dismissedByUser !== true)
621
643
  ? keepSessionWayOutOf(copy)
622
644
  : null;
623
645
  const base = activeRunSelfHealBaseRow(outcome, signal, copy?.wayOut ?? DEFAULT_WAY_OUT, following, keep, engineDecide);
@@ -632,6 +654,7 @@ export function activeRunSelfHealRow(outcome, signal, copy, origin, follow) {
632
654
  }
633
655
  }
634
656
  const INJECTED_LEAD = 'A follow-up message sema sent on its own (not one you typed)';
657
+ const USER_CLOSED_CARD_NOT_PUT_BACK = 'sema does not put a card you closed back on screen by itself';
635
658
  function injectedSubmissionRow(outcome, following = false) {
636
659
  const lead = INJECTED_LEAD;
637
660
  const id = 'taskId' in outcome && outcome.taskId ? ` (id ${outcome.taskId})` : '';
@@ -647,6 +670,11 @@ function injectedSubmissionRow(outcome, following = false) {
647
670
  return (`${lead} was not delivered: this session is still busy with an earlier reply${id}, and your answer to the decision ` +
648
671
  `it is waiting on is still on its way. sema did not retry, so the model has not seen it.`);
649
672
  }
673
+ if (outcome.kind === 'plan-review-reopen-failed' && outcome.dismissedByUser === true) {
674
+ return (`${lead} was not delivered: this session is held by an earlier reply${id} whose plan is waiting for your review, and ` +
675
+ `you closed that approval card — ${USER_CLOSED_CARD_NOT_PUT_BACK}. Send a message when you want the card back; ` +
676
+ `sema did not retry, so the model has not seen it.`);
677
+ }
650
678
  if (outcome.kind === 'running-steered' && outcome.delivery === 'queued') {
651
679
  const queuedOn = `${lead} is queued on an earlier reply${id} that is paused waiting for a decision`;
652
680
  if (outcome.reopened !== null && reopenDelivered(outcome.reopened)) {
@@ -655,6 +683,10 @@ function injectedSubmissionRow(outcome, following = false) {
655
683
  if (reopenRefusedForDecisionInFlight(outcome.reopened)) {
656
684
  return `${queuedOn}; your answer to it is still on its way, and the message is picked up once that answer is applied.`;
657
685
  }
686
+ if (reopenRefusedForUserDismissal(outcome.reopened)) {
687
+ return (`${queuedOn}; you closed that card yourself, so sema did not put it back on screen — send a message to bring it ` +
688
+ `back, and the message is picked up once that decision is made.`);
689
+ }
658
690
  return `${queuedOn} sema could not show here; the message is picked up only once that decision is made.`;
659
691
  }
660
692
  if (outcome.kind === 'running-steered' && outcome.delivery === 'parked_for_wake') {
@@ -666,8 +698,12 @@ function injectedSubmissionRow(outcome, following = false) {
666
698
  return (`${lead} is on hold: this session is busy with an earlier reply${id} that is waiting on a decision. ` +
667
699
  `sema kept the message queued and will send it after you answer the open card.`);
668
700
  case 'resending':
669
- return (`${lead} was held back because this session was busy with an earlier reply${id}; the session is free again, ` +
670
- `so sema is sending the message now — nothing for you to do.`);
701
+ return 'releaseConfirmedByEngine' in outcome && outcome.releaseConfirmedByEngine === true
702
+ ? `${lead} was held back because this session was busy with an earlier reply${id}; the session is free again, ` +
703
+ `so sema is sending the message now — nothing for you to do.`
704
+ : `${lead} was held back because this session was busy with an earlier reply${id}; that reply is now reported ` +
705
+ `as finished, so sema is sending the message now — nothing for you to do (if the session has not quite ` +
706
+ `freed up yet, it may be held back once more).`;
671
707
  case 'handed-off':
672
708
  return following
673
709
  ? `${lead} was passed to the reply already in progress${id} instead of starting a new one. ` +
@@ -725,8 +761,14 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
725
761
  `reopened it. Answer it and send your message again. (sema did NOT cancel the run: cancelling ` +
726
762
  `would have thrown the plan away on your behalf.)`);
727
763
  case 'ask-decided-without-card':
764
+ if (outcome.releaseConfirmedByEngine === true) {
765
+ return (`The previous turn was parked on an approval that your permission rules decided on their own (no card was ` +
766
+ `needed); run ${outcome.taskId} resumed and the engine confirmed it is no longer holding this session — ` +
767
+ `sema is re-sending your message.`);
768
+ }
728
769
  return (`The previous turn was parked on an approval that your permission rules decided on their own (no card was ` +
729
- `needed); run ${outcome.taskId} resumed and has since finished — sema is re-sending your message.`);
770
+ `needed); run ${outcome.taskId} resumed and has since finished — sema is re-sending your message; if that ` +
771
+ `run has not fully let go of the session yet, you may have to send it once more.`);
730
772
  case 'ask-decided-release-unknown':
731
773
  return (`The previous turn was parked on an approval that your permission rules decided on their own (no card was ` +
732
774
  `needed), so run ${outcome.taskId} should be resuming` +
@@ -747,6 +789,11 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
747
789
  case 'plan-review-reopen-failed': {
748
790
  if (outcome.decisionInFlight === true)
749
791
  return decisionInFlightRow(`on a plan review (run ${outcome.taskId})`);
792
+ if (outcome.dismissedByUser === true) {
793
+ return (`The previous turn is parked waiting for a plan review (run ${outcome.taskId}), and you closed its approval card — ` +
794
+ `${USER_CLOSED_CARD_NOT_PUT_BACK}. It did NOT cancel the run — that would have discarded the plan for you. ` +
795
+ `Your message was NOT sent; send it again to bring the card back.`);
796
+ }
750
797
  const viaEngine = engineDecide && outcome.decidePath
751
798
  ? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
752
799
  : '';
@@ -774,8 +821,11 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
774
821
  : reopenRefusedForDecisionInFlight(outcome.reopened)
775
822
  ? `Your answer to that decision is still on its way to the engine, so sema did not show the card again; ` +
776
823
  `that run moves on once the engine applies it.`
777
- : `sema could not surface that decision card here, so nothing will resume that run until that decision is made; ` +
778
- `${wayOut} if you no longer want it.`;
824
+ : reopenRefusedForUserDismissal(outcome.reopened)
825
+ ? `You closed that decision card yourself, so sema did not put it back on screen; nothing will resume that ` +
826
+ `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; ` +
828
+ `${wayOut} if you no longer want it.`;
779
829
  return (`${handle} is not actually working right now: the engine reports it ${parked} on a decision it needs ` +
780
830
  `from you, so it queued your message on that park instead of running it. ${next} ${textOnly} ` +
781
831
  `Nothing was cancelled, and your message did NOT start a new turn.`);
@@ -806,8 +856,12 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
806
856
  `cancel that run. That run may or may not have received your message — watch what it does next ` +
807
857
  `before sending it again.`);
808
858
  case 'running-cancelled':
809
- return (`You chose to cancel run ${outcome.taskId}. The engine confirmed it is no longer holding this session, ` +
810
- `so sema is sending your message now.`);
859
+ return outcome.releaseConfirmedByEngine === true
860
+ ? `You chose to cancel run ${outcome.taskId}. The engine confirmed it is no longer holding this session, ` +
861
+ `so sema is sending your message now.`
862
+ : `You chose to cancel run ${outcome.taskId}. The engine now reports that run as finished, so sema is ` +
863
+ `sending your message now; if that run has not fully let go of the session yet, you may have to send ` +
864
+ `it once more.`;
811
865
  case 'running-cancel-timeout': {
812
866
  const waited = `${String(Math.max(1, Math.round(outcome.waitedMs / 1000)))}s`;
813
867
  if (outcome.aborted) {
@@ -840,6 +894,11 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut, following = false, ke
840
894
  `not allowed to read. Either way sema has nothing it can act on here, so it did NOT cancel anything. ` +
841
895
  `Your message was NOT sent; send it again (if the session really is free it will just run), or ${wayOut}.`);
842
896
  case 'stale-park-cancelled':
897
+ if (outcome.releaseConfirmedByEngine === true) {
898
+ return (`Run ${outcome.taskId} was parked on an approval that no longer exists on the engine, so there was ` +
899
+ `nothing left for you to answer. You chose to cancel that run, and the engine confirmed it is no longer ` +
900
+ `holding this session, so sema is sending your message again now.`);
901
+ }
843
902
  return (`Run ${outcome.taskId} was parked on an approval that no longer exists on the engine, so there was ` +
844
903
  `nothing left for you to answer. You chose to cancel that run: the engine now reports it as finished, ` +
845
904
  `so it will not resume — whatever that turn had left to do is not coming back. sema is sending your ` +
@@ -30,7 +30,7 @@ export const MIGRATED_COMPENSATIONS = [
30
30
  },
31
31
  {
32
32
  id: 'T36',
33
- what: '子代永不发终态 tick ⇒ 四处防御 sweep 兜住面板行,不让它们永远转圈',
33
+ what: '子代终态 tick 缺席时(老服务端 / 丢帧 / 断流)四处防御 sweep 兜住面板行,不让它们永远转圈;终态 tick 在场时按它落终态',
34
34
  disposition: 'in-library',
35
35
  libraryHalf: 'adapt.ts settlePanelTasks 的四个调用点(tool_end 关卡 / result / turn 末 / abort)',
36
36
  chromeArm: 'inline_task_stats',
@@ -6,6 +6,7 @@ export interface DisplayFace {
6
6
  readonly unsafe: RegExp | null;
7
7
  readonly fold: boolean;
8
8
  readonly mark: DisplayMark;
9
+ readonly keepBlanks?: boolean;
9
10
  }
10
11
  export declare const LEGACY_ESCAPE_FACE: DisplayFace;
11
12
  export declare const LEGACY_LABEL_FACE: DisplayFace;
@@ -20,5 +21,17 @@ export interface DisplayUntrustedOptions {
20
21
  readonly keepLayout?: boolean;
21
22
  readonly mark?: DisplayMark;
22
23
  readonly max?: number;
24
+ readonly escapeBackslashes?: boolean;
25
+ readonly blanks?: boolean;
26
+ }
27
+ export interface DisplayMarkSpan {
28
+ readonly start: number;
29
+ readonly end: number;
30
+ readonly codePoint: number;
31
+ }
32
+ export interface DisplayUntrustedMarked {
33
+ readonly text: string;
34
+ readonly marks: readonly DisplayMarkSpan[];
23
35
  }
24
36
  export declare function displayUntrusted(text: string, opts?: DisplayUntrustedOptions): string;
37
+ export declare function displayUntrustedMarks(text: string, opts?: DisplayUntrustedOptions): DisplayUntrustedMarked;