@sema-agent/client-core 0.83.3 → 0.83.4

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,6 @@
1
1
  import { publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionResponder, unregisterLocalQuestionResponder, hasLocalQuestionResponder, hasQuestionOverlayFor, } from '../liveQuestionStore.js';
2
2
  import { hostLog } from '../host.js';
3
+ import { abortableSleep } from '../abortableSleep.js';
3
4
  import { makeEngineWireClient } from '../engineWireSdk.js';
4
5
  import { engineWireTarget } from '../engineWireTarget.js';
5
6
  import { enqueuePlanReviewOutcome } from '../notifications.js';
@@ -7,9 +8,9 @@ import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTria
7
8
  import { decideFailureNote } from '../decideFailureNote.js';
8
9
  import { isPlanReviewModeAfter } from './hitlBridge.js';
9
10
  import { engineCapString } from '../engineCapsCache.js';
10
- import { planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
11
+ import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
11
12
  import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, registerArmedGateFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
12
- import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
13
+ import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
13
14
  import { PLAN_REVIEW_GATE_KIND } from '../adapter/activeRunSelfHeal.js';
14
15
  import { readRunTerminal, runTerminalGateKind } from '../runTerminal.js';
15
16
  const PLAN_REVIEW_RESUME_TIMEOUT_MS = 6 * 60 * 60_000;
@@ -84,6 +85,13 @@ function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
84
85
  if (planReviewDecisionFromAnswer(answer ?? { answers: [] }, ALL_KNOWN_LABELS) === 'dismissed')
85
86
  return;
86
87
  notePlanReviewAnsweredFor(sessionKey, questionId);
88
+ const taskId = typeof questionId === 'string' ? taskIdFromPlanReviewQuestionId(questionId) : undefined;
89
+ if (taskId === undefined)
90
+ return;
91
+ const current = dismissalEntryFor(sessionKey, taskId);
92
+ if (current?.kind === 'dismissed' && current.questionId === questionId)
93
+ return;
94
+ notePlanReviewDecisiveAnswerFor(sessionKey, taskId);
87
95
  }
88
96
  export function isPlanReviewPark(result) {
89
97
  const read = readRunTerminal(result);
@@ -99,6 +107,93 @@ const offerModeAfterByTask = new Map();
99
107
  export function _resetArmedPlanReviewsForTest() {
100
108
  armedPlanReviewTaskIds.clear();
101
109
  offerModeAfterByTask.clear();
110
+ planReviewDismissalsByKey.clear();
111
+ }
112
+ const ON_SCREEN = Promise.resolve(true);
113
+ const planReviewDismissalsByKey = createSessionSlot();
114
+ const MAX_DISMISSAL_ENTRIES_PER_SESSION = 32;
115
+ const GATE_INSTANCE_READ_WAIT_MS = 1_500;
116
+ function dismissalEntryFor(sessionKey, taskId) {
117
+ return planReviewDismissalsByKey.get(sessionKey)?.get(taskId);
118
+ }
119
+ function putDismissalEntry(sessionKey, taskId, entry) {
120
+ let ledger = planReviewDismissalsByKey.get(sessionKey);
121
+ if (ledger === undefined) {
122
+ ledger = new Map();
123
+ planReviewDismissalsByKey.set(sessionKey, ledger);
124
+ }
125
+ ledger.delete(taskId);
126
+ ledger.set(taskId, entry);
127
+ while (ledger.size > MAX_DISMISSAL_ENTRIES_PER_SESSION) {
128
+ const oldest = ledger.keys().next();
129
+ if (oldest.done === true)
130
+ break;
131
+ ledger.delete(oldest.value);
132
+ }
133
+ }
134
+ function planReviewUserDismissalMarkFor(sessionKey, taskId) {
135
+ const entry = dismissalEntryFor(sessionKey, taskId);
136
+ return entry?.kind === 'dismissed' ? entry : undefined;
137
+ }
138
+ function startGateInstanceRead(reader, taskId) {
139
+ if (typeof reader !== 'function')
140
+ return null;
141
+ let raw;
142
+ try {
143
+ raw = reader(taskId);
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ return Promise.resolve(raw).then((v) => {
149
+ try {
150
+ return v instanceof Set ? new Set(v) : null;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }, () => null);
156
+ }
157
+ async function boundedGateInstanceRead(p) {
158
+ if (p === null)
159
+ return null;
160
+ const ac = new AbortController();
161
+ try {
162
+ return await Promise.race([
163
+ p.then((v) => v, () => null),
164
+ abortableSleep(GATE_INSTANCE_READ_WAIT_MS, ac.signal).then(() => null),
165
+ ]);
166
+ }
167
+ finally {
168
+ ac.abort();
169
+ }
170
+ }
171
+ function notePlanReviewUserDismissalFor(sessionKey, taskId, reader, questionId) {
172
+ try {
173
+ putDismissalEntry(sessionKey, taskId, { kind: 'dismissed', closedGates: startGateInstanceRead(reader, taskId), questionId });
174
+ }
175
+ catch (err) {
176
+ hostLog('debug', `planReviewWire: could not record that the user closed the plan card for task ${taskId}: ${String(err)}`);
177
+ }
178
+ }
179
+ function notePlanReviewDecisiveAnswerFor(sessionKey, taskId) {
180
+ if (dismissalEntryFor(sessionKey, taskId) === undefined)
181
+ return;
182
+ putDismissalEntry(sessionKey, taskId, { kind: 'decided', closedGates: null });
183
+ }
184
+ function revokePlanReviewUserDismissal(sessionKey, taskId, mark) {
185
+ if (mark === undefined || mark.kind !== 'dismissed')
186
+ return;
187
+ if (dismissalEntryFor(sessionKey, taskId) === mark)
188
+ putDismissalEntry(sessionKey, taskId, { kind: 'reopened', closedGates: null, onScreen: ON_SCREEN });
189
+ }
190
+ function dropDismissalEntry(sessionKey, taskId, entry) {
191
+ const ledger = planReviewDismissalsByKey.get(sessionKey);
192
+ if (ledger?.get(taskId) === entry)
193
+ ledger.delete(taskId);
194
+ }
195
+ export function clearPlanReviewUserDismissals(sessionKey = DEFAULT_SESSION_KEY) {
196
+ planReviewDismissalsByKey.set(sessionKey, undefined);
102
197
  }
103
198
  const ARM_QUESTION = 'The plan is ready for review. Approve it and start the implementation?';
104
199
  const REOPEN_QUESTION = 'This plan is still waiting for your review — it is what is holding this session. Approve it and start the implementation?';
@@ -158,9 +253,11 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
158
253
  const shown = planReviewCardOptions(offerModeAfterByTask.get(taskId) === true).map(o => o.label);
159
254
  const choice = planReviewChoiceFromAnswer(answer, shown);
160
255
  if (choice === 'dismissed') {
256
+ notePlanReviewUserDismissalFor(DEFAULT_SESSION_KEY, taskId, opts?.readGateInstanceKeys, questionId);
161
257
  return { ok: true };
162
258
  }
163
259
  const decided = choice.decision;
260
+ notePlanReviewDecisiveAnswerFor(DEFAULT_SESSION_KEY, taskId);
164
261
  const modeAfter = choice.decision === 'approve' ? choice.permissionModeAfter : undefined;
165
262
  offerModeAfterByTask.delete(taskId);
166
263
  notePlanReviewAnswered(questionId);
@@ -375,10 +472,245 @@ function retireActiveReopen(sessionKey, canonicalId) {
375
472
  }
376
473
  export function _resetActiveReopenRespondersForTest() {
377
474
  activeReopenResponders.clear();
475
+ reopenFlights.clear();
476
+ }
477
+ const reopenFlights = new Map();
478
+ function joinReopenFlight(flight, taskId, receiptMs) {
479
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} joined the card already on its way (no second card, its answer path kept)`);
480
+ if (receiptMs === undefined)
481
+ return { reopened: true, firstSight: false };
482
+ const ac = new AbortController();
483
+ return Promise.race([
484
+ flight.presented.then((v) => v === true, () => false),
485
+ abortableSleep(Math.max(0, receiptMs), ac.signal).then(() => false),
486
+ ]).then((presented) => {
487
+ ac.abort();
488
+ return presented ? { reopened: true, firstSight: false } : { reopened: false };
489
+ }, () => {
490
+ ac.abort();
491
+ return { reopened: false };
492
+ });
493
+ }
494
+ function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
495
+ const flightKey = `${sessionKey}\u0000${taskId}`;
496
+ const inFlight = reopenFlights.get(flightKey);
497
+ if (inFlight !== undefined)
498
+ return joinReopenFlight(inFlight, taskId, receiptMs);
499
+ let land = () => { };
500
+ const flight = {
501
+ presented: new Promise((resolve) => {
502
+ land = resolve;
503
+ }),
504
+ };
505
+ reopenFlights.set(flightKey, flight);
506
+ const settle = (presented) => {
507
+ land(presented);
508
+ if (reopenFlights.get(flightKey) === flight)
509
+ reopenFlights.delete(flightKey);
510
+ };
511
+ let verdict;
512
+ try {
513
+ verdict = mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs);
514
+ }
515
+ catch (e) {
516
+ settle(false);
517
+ throw e;
518
+ }
519
+ if (verdict instanceof Promise) {
520
+ return verdict.then((v) => {
521
+ settle(v.reopened === true);
522
+ return v;
523
+ }, (e) => {
524
+ settle(false);
525
+ throw e;
526
+ });
527
+ }
528
+ settle(verdict.reopened === true);
529
+ return verdict;
530
+ }
531
+ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
532
+ const markBefore = dismissalEntryFor(sessionKey, taskId);
533
+ const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => {
534
+ if (!presented)
535
+ return { reopened: false };
536
+ revokePlanReviewUserDismissal(sessionKey, taskId, markBefore);
537
+ return { reopened: true, firstSight };
538
+ }, () => ({ reopened: false }));
539
+ const canonicalId = planReviewQuestionId(taskId);
540
+ const armedKey = planReviewArmedKeyFor(sessionKey, taskId);
541
+ const firstSight = !wasGateArmedFor(sessionKey, armedKey);
542
+ const mintFresh = opts?.mintFreshQuestionId !== false;
543
+ if (!mintFresh && hasLocalQuestionResponder(canonicalId)) {
544
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} reusing the canonical responder (path=reopen-canonical)`);
545
+ if (opts?.deliverDecision !== undefined) {
546
+ hostLog('debug', `planReviewWire: reopen canonical-reuse branch for task ${taskId} ignores opts.deliverDecision (answers route through the existing arm responder — documented exception)`);
547
+ }
548
+ retireActiveReopen(sessionKey, canonicalId);
549
+ if (receiptMs !== undefined) {
550
+ const receipt = waitForGateArmedFor(sessionKey, [canonicalId], receiptMs);
551
+ publishQuestionFrameFor(sessionKey, {
552
+ type: 'question',
553
+ questionId: canonicalId,
554
+ questions: planReviewQuestions(ARM_QUESTION, offerModeAfterByTask.get(taskId) === true),
555
+ });
556
+ return settleOnReceipt(receipt, firstSight);
557
+ }
558
+ publishQuestionFrameFor(sessionKey, {
559
+ type: 'question',
560
+ questionId: canonicalId,
561
+ questions: planReviewQuestions(ARM_QUESTION, offerModeAfterByTask.get(taskId) === true),
562
+ });
563
+ registerArmedGateFor(sessionKey, armedKey);
564
+ revokePlanReviewUserDismissal(sessionKey, taskId, markBefore);
565
+ return { reopened: true, firstSight };
566
+ }
567
+ retireActiveReopen(sessionKey, canonicalId);
568
+ if (unregisterLocalQuestionResponder(canonicalId)) {
569
+ hostLog('debug', `planReviewWire: reopen retired the canonical responder for task ${taskId} (single-active discipline)`);
570
+ }
571
+ publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: canonicalId });
572
+ const questionId = `${canonicalId}${REOPEN_ID_TAIL}${reopenIdSuffix()}`;
573
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} minting a fresh card ${questionId} (path=reopen-fresh)`);
574
+ const deliver = opts?.deliverDecision ??
575
+ ((tid, decision) => {
576
+ hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
577
+ void decidePlanReview(tid, decision);
578
+ });
579
+ const activeKey = `${sessionKey}\u0000${canonicalId}`;
580
+ const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
581
+ unregister();
582
+ if (activeReopenResponders.get(activeKey)?.questionId === questionId) {
583
+ activeReopenResponders.delete(activeKey);
584
+ }
585
+ const decided = planReviewDecisionFromAnswer(answer, LEGACY_TWO_CHOICE_LABELS);
586
+ if (decided === 'dismissed') {
587
+ notePlanReviewUserDismissalFor(sessionKey, taskId, opts?.readGateInstanceKeys, questionId);
588
+ return { ok: true };
589
+ }
590
+ notePlanReviewDecisiveAnswerFor(sessionKey, taskId);
591
+ notePlanReviewAnsweredFor(sessionKey, questionId);
592
+ try {
593
+ await deliver(taskId, decided);
594
+ }
595
+ catch (e) {
596
+ hostLog('error', `planReviewWire: reopen decision delivery threw for task ${taskId}: ${String(e)}`);
597
+ }
598
+ return { ok: true };
599
+ });
600
+ activeReopenResponders.set(activeKey, { questionId, unregister });
601
+ if (receiptMs !== undefined) {
602
+ const receipt = waitForGateArmedFor(sessionKey, [questionId], receiptMs);
603
+ publishQuestionFrameFor(sessionKey, {
604
+ type: 'question',
605
+ questionId,
606
+ questions: planReviewQuestions(REOPEN_QUESTION),
607
+ });
608
+ return settleOnReceipt(receipt, firstSight);
609
+ }
610
+ registerArmedGateFor(sessionKey, armedKey);
611
+ publishQuestionFrameFor(sessionKey, {
612
+ type: 'question',
613
+ questionId,
614
+ questions: planReviewQuestions(REOPEN_QUESTION),
615
+ });
616
+ revokePlanReviewUserDismissal(sessionKey, taskId, markBefore);
617
+ return { reopened: true, firstSight };
618
+ }
619
+ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark) {
620
+ const refusedByUser = { reopened: false, dismissedByUser: true };
621
+ try {
622
+ let fresh = false;
623
+ const closed = await boundedGateInstanceRead(mark.closedGates);
624
+ if (closed !== null && closed.size > 0) {
625
+ const now = await boundedGateInstanceRead(startGateInstanceRead(opts.readGateInstanceKeys, taskId));
626
+ if (now !== null) {
627
+ for (const k of now) {
628
+ if (!closed.has(k)) {
629
+ fresh = true;
630
+ break;
631
+ }
632
+ }
633
+ }
634
+ }
635
+ let handedOver = false;
636
+ if (!decideInFlight.has(taskId)) {
637
+ try {
638
+ handedOver = opts.decisionHandedOver?.(taskId) === true;
639
+ }
640
+ catch (err) {
641
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — the host's decision hand-over check threw: ${String(err)}`);
642
+ return { reopened: false };
643
+ }
644
+ }
645
+ if (decideInFlight.has(taskId) || handedOver) {
646
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} refused after the dismissal check — a decision for this task went out while the check was reading`);
647
+ return { reopened: false, _sema_decisionInFlight: true };
648
+ }
649
+ const current = dismissalEntryFor(sessionKey, taskId);
650
+ if (current !== mark) {
651
+ hostLog('debug', `planReviewWire: reopen for task ${taskId} not minted — the dismissal record changed while it was being checked (stale evaluation)`);
652
+ return await staleDismissalVerdict(current, sessionKey, taskId, receiptMs);
653
+ }
654
+ if (!fresh) {
655
+ hostLog('debug', `planReviewWire: automatic reopen for task ${taskId} refused — the user closed this gate's card (no newer gate instance proven)`);
656
+ return refusedByUser;
657
+ }
658
+ let settle = () => { };
659
+ const reopening = {
660
+ kind: 'reopened',
661
+ closedGates: null,
662
+ onScreen: new Promise((resolve) => {
663
+ settle = resolve;
664
+ }),
665
+ };
666
+ putDismissalEntry(sessionKey, taskId, reopening);
667
+ let verdict = { reopened: false };
668
+ try {
669
+ if (hasQuestionOverlayFor(sessionKey))
670
+ verdict = await mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
671
+ }
672
+ finally {
673
+ const presented = verdict.reopened === true;
674
+ settle(presented);
675
+ if (!presented)
676
+ dropDismissalEntry(sessionKey, taskId, reopening);
677
+ }
678
+ return verdict;
679
+ }
680
+ catch {
681
+ return refusedByUser;
682
+ }
683
+ }
684
+ async function staleDismissalVerdict(current, sessionKey, taskId, receiptMs) {
685
+ const ac = new AbortController();
686
+ const windowEnd = abortableSleep(Math.max(0, receiptMs), ac.signal).then(() => false);
687
+ try {
688
+ let entry = current;
689
+ for (let round = 0; round < 8; round++) {
690
+ if (entry?.kind === 'decided')
691
+ return { reopened: false, _sema_decisionInFlight: true };
692
+ if (entry?.kind === 'dismissed')
693
+ return { reopened: false, dismissedByUser: true };
694
+ if (entry?.kind !== 'reopened' || entry.onScreen === undefined)
695
+ return { reopened: false };
696
+ const onScreen = await Promise.race([
697
+ entry.onScreen.then((v) => v === true, () => false),
698
+ windowEnd,
699
+ ]);
700
+ const now = dismissalEntryFor(sessionKey, taskId);
701
+ if (now === entry) {
702
+ return onScreen ? { reopened: true, firstSight: !wasGateArmedFor(sessionKey, planReviewArmedKeyFor(sessionKey, taskId)) } : { reopened: false };
703
+ }
704
+ entry = now;
705
+ }
706
+ return { reopened: false };
707
+ }
708
+ finally {
709
+ ac.abort();
710
+ }
378
711
  }
379
712
  export function reopenPlanReviewCard(taskId, opts) {
380
713
  const receiptMs = opts?.presentationReceiptMs;
381
- const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => (presented ? { reopened: true, firstSight } : { reopened: false }), () => ({ reopened: false }));
382
714
  try {
383
715
  if (typeof taskId !== 'string' || taskId.length === 0) {
384
716
  return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
@@ -399,81 +731,18 @@ export function reopenPlanReviewCard(taskId, opts) {
399
731
  '此处拒绝而不是照发 —— 照发会把这条会话的决断发到另一个槽上。');
400
732
  return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
401
733
  }
402
- const canonicalId = planReviewQuestionId(taskId);
403
- const armedKey = planReviewArmedKeyFor(sessionKey, taskId);
404
- const firstSight = !wasGateArmedFor(sessionKey, armedKey);
405
- const mintFresh = opts?.mintFreshQuestionId !== false;
406
- if (!mintFresh && hasLocalQuestionResponder(canonicalId)) {
407
- hostLog('debug', `planReviewWire: reopen for task ${taskId} reusing the canonical responder (path=reopen-canonical)`);
408
- if (opts?.deliverDecision !== undefined) {
409
- hostLog('debug', `planReviewWire: reopen canonical-reuse branch for task ${taskId} ignores opts.deliverDecision (answers route through the existing arm responder — documented exception)`);
410
- }
411
- retireActiveReopen(sessionKey, canonicalId);
412
- if (receiptMs !== undefined) {
413
- const receipt = waitForGateArmedFor(sessionKey, [canonicalId], receiptMs);
414
- publishQuestionFrameFor(sessionKey, {
415
- type: 'question',
416
- questionId: canonicalId,
417
- questions: planReviewQuestions(ARM_QUESTION, offerModeAfterByTask.get(taskId) === true),
418
- });
419
- return settleOnReceipt(receipt, firstSight);
420
- }
421
- publishQuestionFrameFor(sessionKey, {
422
- type: 'question',
423
- questionId: canonicalId,
424
- questions: planReviewQuestions(ARM_QUESTION, offerModeAfterByTask.get(taskId) === true),
425
- });
426
- registerArmedGateFor(sessionKey, armedKey);
427
- return { reopened: true, firstSight };
428
- }
429
- retireActiveReopen(sessionKey, canonicalId);
430
- if (unregisterLocalQuestionResponder(canonicalId)) {
431
- hostLog('debug', `planReviewWire: reopen retired the canonical responder for task ${taskId} (single-active discipline)`);
432
- }
433
- publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: canonicalId });
434
- const questionId = `${canonicalId}${REOPEN_ID_TAIL}${reopenIdSuffix()}`;
435
- hostLog('debug', `planReviewWire: reopen for task ${taskId} minting a fresh card ${questionId} (path=reopen-fresh)`);
436
- const deliver = opts?.deliverDecision ??
437
- ((tid, decision) => {
438
- hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
439
- void decidePlanReview(tid, decision);
440
- });
441
- const activeKey = `${sessionKey}\u0000${canonicalId}`;
442
- const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
443
- unregister();
444
- if (activeReopenResponders.get(activeKey)?.questionId === questionId) {
445
- activeReopenResponders.delete(activeKey);
446
- }
447
- const decided = planReviewDecisionFromAnswer(answer, LEGACY_TWO_CHOICE_LABELS);
448
- if (decided === 'dismissed') {
449
- return { ok: true };
450
- }
451
- notePlanReviewAnsweredFor(sessionKey, questionId);
452
- try {
453
- await deliver(taskId, decided);
454
- }
455
- catch (e) {
456
- hostLog('error', `planReviewWire: reopen decision delivery threw for task ${taskId}: ${String(e)}`);
734
+ const trigger = opts?.trigger === undefined ? undefined : opts.trigger === 'user' ? 'user' : 'automatic';
735
+ if (trigger === 'automatic') {
736
+ const mark = planReviewUserDismissalMarkFor(sessionKey, taskId);
737
+ if (mark !== undefined) {
738
+ if (receiptMs === undefined || opts === undefined) {
739
+ hostLog('debug', `planReviewWire: automatic reopen for task ${taskId} refused — the user closed this gate's card`);
740
+ return { reopened: false, dismissedByUser: true };
741
+ }
742
+ return reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark);
457
743
  }
458
- return { ok: true };
459
- });
460
- activeReopenResponders.set(activeKey, { questionId, unregister });
461
- if (receiptMs !== undefined) {
462
- const receipt = waitForGateArmedFor(sessionKey, [questionId], receiptMs);
463
- publishQuestionFrameFor(sessionKey, {
464
- type: 'question',
465
- questionId,
466
- questions: planReviewQuestions(REOPEN_QUESTION),
467
- });
468
- return settleOnReceipt(receipt, firstSight);
469
744
  }
470
- registerArmedGateFor(sessionKey, armedKey);
471
- publishQuestionFrameFor(sessionKey, {
472
- type: 'question',
473
- questionId,
474
- questions: planReviewQuestions(REOPEN_QUESTION),
475
- });
476
- return { reopened: true, firstSight };
745
+ return mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
477
746
  }
478
747
  catch {
479
748
  return receiptMs === undefined ? { reopened: false } : Promise.resolve({ reopened: false });
@@ -1,6 +1,7 @@
1
1
  import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike, type DecideOperationObserver } from './hitlBridge.js';
2
2
  import { type RuleStoreUnreadableKind } from '../gateVocabulary.js';
3
3
  import { type ApprovalMandateWord } from '../gateVocabulary.js';
4
+ import type { ToolPermissionRequest } from '../seatContract.js';
4
5
  import { type DenyAttributionRefusalDetail } from '../wireRefusalCopy.js';
5
6
  import { type DecideReceiptView } from '../decideReceipt.js';
6
7
  import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS } from '@sema-agent/sdk';
@@ -207,6 +208,7 @@ export declare function approvalIsMandated(v: ApprovalMandatedFacts | undefined
207
208
  export declare function readApprovalMandate(v: {
208
209
  readonly mandate?: string;
209
210
  } | undefined | null): ApprovalMandateWord | undefined;
211
+ export declare function toolPermissionRequestAskBits(card: object | null | undefined): Pick<ToolPermissionRequest, 'mandated' | 'mandate' | 'origin' | 'ruleOffersAbsence'>;
210
212
  export declare function isFromSubagent(frame: ToolApprovalFrame): boolean;
211
213
  export type ToolApprovalRespondDecision = 'allow' | 'allow_session' | 'deny';
212
214
  export interface RespondToolApprovalOpts {
@@ -206,7 +206,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
206
206
  : {}),
207
207
  ...(pending.governanceForced === true ? { governanceForced: true } : {}),
208
208
  ...(pending.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
209
- ...(pending.mandated === true ? { mandated: true } : {}),
209
+ ...(hasOwnStrictTrue(pending, 'mandated') ? { mandated: true } : {}),
210
210
  ...((w) => (w !== undefined ? { mandate: w } : {}))(readApprovalMandate(pending)),
211
211
  ...((o) => (o !== undefined ? { origin: o } : {}))(readOwnNonEmptyString(pending, 'origin')),
212
212
  ...(pending.gateKind === 'irreversible_ask' ? { irreversibleParkGate: true } : {}),
@@ -403,9 +403,11 @@ const MANDATED_ABSENCE_WORD = 'mandated';
403
403
  export function approvalIsMandated(v) {
404
404
  if (v === null || v === undefined)
405
405
  return false;
406
- if (v.mandated === true)
406
+ if (typeof v !== 'object')
407
+ return false;
408
+ if (hasOwnStrictTrue(v, 'mandated'))
407
409
  return true;
408
- if (v.ruleOffersAbsence === MANDATED_ABSENCE_WORD)
410
+ if (readOwnNonEmptyString(v, 'ruleOffersAbsence') === MANDATED_ABSENCE_WORD)
409
411
  return true;
410
412
  return readApprovalMandate(v) !== undefined;
411
413
  }
@@ -415,6 +417,23 @@ export function readApprovalMandate(v) {
415
417
  const raw = v.mandate;
416
418
  return isApprovalMandateWord(raw) ? raw : undefined;
417
419
  }
420
+ export function toolPermissionRequestAskBits(card) {
421
+ if (typeof card !== 'object' || card === null)
422
+ return {};
423
+ const mandated = hasOwnStrictTrue(card, 'mandated');
424
+ const mandate = readApprovalMandate(card);
425
+ const origin = readOwnNonEmptyString(card, 'origin');
426
+ const ruleOffersAbsence = readOwnNonEmptyString(card, 'ruleOffersAbsence');
427
+ return {
428
+ ...(mandated ? { mandated: true } : {}),
429
+ ...(mandate !== undefined ? { mandate } : {}),
430
+ ...(origin !== undefined ? { origin } : {}),
431
+ ...(ruleOffersAbsence !== undefined ? { ruleOffersAbsence } : {}),
432
+ };
433
+ }
434
+ function hasOwnStrictTrue(v, key) {
435
+ return Object.hasOwn(v, key) && v[key] === true;
436
+ }
418
437
  function isToolApprovalDelegation(v) {
419
438
  if (v === null || typeof v !== 'object')
420
439
  return false;
@@ -840,7 +859,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
840
859
  ...(isWireRecordCarrier(frame.probeCause) ? { probeCause: frame.probeCause } : {}),
841
860
  ...(isWireRecordCarrier(frame.ruleEvidence) ? { ruleEvidence: frame.ruleEvidence } : {}),
842
861
  ...(frame.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
843
- ...(frame.mandated === true ? { mandated: true } : {}),
862
+ ...(hasOwnStrictTrue(frame, 'mandated') ? { mandated: true } : {}),
844
863
  ...((w) => (w !== undefined ? { mandate: w } : {}))(readApprovalMandate(frame)),
845
864
  ...(() => { const rc = readReadRootCandidate(frame.readRootCandidate); return rc !== undefined ? { readRootCandidate: rc } : {}; })(),
846
865
  ...(frame.inputHasBidi === true ? { inputHasBidi: true } : {}),
package/dist/index.d.ts CHANGED
@@ -66,6 +66,7 @@ export * from './controlRouter.js';
66
66
  export * from './sseIdleTriage.js';
67
67
  export * from './engineWireSdk.js';
68
68
  export * from './classifierVerdictWire.js';
69
+ export { stripSystemReminderBlocks, unwrapSystemReminder } from './systemReminderTag.js';
69
70
  export * from './engineErrorCodes.js';
70
71
  export * from './agentsWireCaps.js';
71
72
  export * from './attachmentsWireCaps.js';
@@ -154,7 +155,7 @@ export * from './hitl/editedRuleTextPrecheck.js';
154
155
  export * from './coreValuePorts.js';
155
156
  export * from './hitl/askGateWire.js';
156
157
  export * from './hitl/planReviewWire.js';
157
- export * from './hitl/gateIdentity.js';
158
+ export { PLAN_REVIEW_QUESTION_ID_PREFIX, REOPEN_ID_TAIL, approvalCallKey, askGateQuestionId, planReviewQuestionId, liveFrameCallKey, armedKeyFromQuestionId, } from './hitl/gateIdentity.js';
158
159
  export * from './hitl/armedGateRegistry.js';
159
160
  export * from './hitl/parkOwnership.js';
160
161
  export * from './hitl/parkRowBirthWait.js';
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ export * from './controlRouter.js';
64
64
  export * from './sseIdleTriage.js';
65
65
  export * from './engineWireSdk.js';
66
66
  export * from './classifierVerdictWire.js';
67
+ export { stripSystemReminderBlocks, unwrapSystemReminder } from './systemReminderTag.js';
67
68
  export * from './engineErrorCodes.js';
68
69
  export * from './agentsWireCaps.js';
69
70
  export * from './attachmentsWireCaps.js';
@@ -151,7 +152,7 @@ export * from './hitl/editedRuleTextPrecheck.js';
151
152
  export * from './coreValuePorts.js';
152
153
  export * from './hitl/askGateWire.js';
153
154
  export * from './hitl/planReviewWire.js';
154
- export * from './hitl/gateIdentity.js';
155
+ export { PLAN_REVIEW_QUESTION_ID_PREFIX, REOPEN_ID_TAIL, approvalCallKey, askGateQuestionId, planReviewQuestionId, liveFrameCallKey, armedKeyFromQuestionId, } from './hitl/gateIdentity.js';
155
156
  export * from './hitl/armedGateRegistry.js';
156
157
  export * from './hitl/parkOwnership.js';
157
158
  export * from './hitl/parkRowBirthWait.js';
@@ -2,6 +2,7 @@ import type { ReasoningEffort } from "./effortWire.js";
2
2
  import type { LaneProof } from "./seam.js";
3
3
  import type { QuestionAnswer } from "./liveQuestionStore.js";
4
4
  import type { ToolApprovalDelegation } from "./hitl/toolApprovalWire.js";
5
+ import type { ApprovalMandateWord } from "./gateVocabulary.js";
5
6
  export interface SeatValidationFailure {
6
7
  reason: string;
7
8
  field?: string;
@@ -55,6 +56,10 @@ export interface ToolPermissionRequest {
55
56
  createdAtMs?: number;
56
57
  governanceForced?: true;
57
58
  delegation?: ToolApprovalDelegation;
59
+ mandated?: true;
60
+ mandate?: ApprovalMandateWord;
61
+ origin?: string;
62
+ ruleOffersAbsence?: string;
58
63
  }
59
64
  declare const SEAT_EVENT_TYPE_TUPLE: readonly ["start", "close", "session_updated", "message", "stream_event", "chrome_event", "error", "tool_permission_resolved"];
60
65
  export type SeatEventType = (typeof SEAT_EVENT_TYPE_TUPLE)[number];
@@ -255,7 +260,7 @@ export declare const SEAT_METHOD_NAMES: readonly ["start", "sendMessage", "inter
255
260
  export declare const SEAT_STORE_PROPS: readonly ["sessionsStateStore", "focusedSessionStateStore", "settingsStateStore"];
256
261
  export declare const SEAT_EVENT_METHOD_NAMES: readonly ["onEvent", "onToolPermissionRequest"];
257
262
  export declare const LOCAL_SESSION_EVENT_KEYS: readonly ["type", "sessionId", "message", "messages", "error", "errorCategory", "code", "request", "data", "sources", "permissionMode", "session", "userMessageUuid", "hasPreClearSession", "requestId", "outcome", "chrome"];
258
- export declare const TOOL_PERMISSION_REQUEST_KEYS: readonly ["requestId", "sessionId", "toolName", "toolCallId", "input", "message", "toctouBinding", "createdAtMs", "governanceForced", "delegation"];
263
+ export declare const TOOL_PERMISSION_REQUEST_KEYS: readonly ["requestId", "sessionId", "toolName", "toolCallId", "input", "message", "toctouBinding", "createdAtMs", "governanceForced", "delegation", "mandated", "mandate", "origin", "ruleOffersAbsence"];
259
264
  export declare const LOCAL_SESSION_RECORD_KEYS: readonly ["sessionId", "engineSessionId", "model", "title", "isRunning", "createdAt", "lastActivityAt", "queuedMessages", "parked"];
260
265
  export declare const SEND_MESSAGE_REQUEST_KEYS: readonly ["sessionId", "text", "model", "userMessageUuid", "permissionMode", "attachmentIds", "reasoningEffort", "retainBackgroundProcesses", "forwardSubagentEvents", "retainSubagentSessions", "settings", "scenario"];
261
266
  export declare const SEAT_SEND_MESSAGE_KEY_ORIGINS: {
@@ -318,6 +323,18 @@ export declare const SEAT_VALIDATOR_KEY_COVERAGE: {
318
323
  }, {
319
324
  readonly key: "delegation";
320
325
  readonly why: "展示增强位,权威结构门在 hitl wire 卡口(结构读+仅子代 ask 在场,坏形降缺席);座位门同款理由不设第二判官。消费端坏形降缺席,绝不拒行。";
326
+ }, {
327
+ readonly key: "mandated";
328
+ readonly why: "展示增强位(CC-203),权威窄读在卡口与过境口 toolPermissionRequestAskBits(严格 true 才上座);座位门再裁一次会为一个徽标键拒掉整条 request、把安全 ask 丢给引擎 TTL deny。消费端经 approvalIsMandated 读,其余一律当缺席。";
329
+ }, {
330
+ readonly key: "mandate";
331
+ readonly why: "展示增强位(CC-203),权威窄读在卡口与过境口(闭集六词精确字节,表外不上座);座位门不设第二判官,理由同 mandated。消费端经 readApprovalMandate 读,表外值读成缺席。";
332
+ }, {
333
+ readonly key: "origin";
334
+ readonly why: "喂姿态谓词 askSurvivesPosture 的安全位(CC-203),按开集读(非空串原文字节、词表不校);权威窄读在卡口与过境口 toolPermissionRequestAskBits,座位门不设第二判官(为一个位拒掉整条 request = 把安全 ask 丢给引擎 TTL deny)。坏形的两种失效方向:非串 / 空串读成缺席 ⇒ 宿主报了没命中时姿态谓词可答可以放(放宽方向:真出身若是必问词就丢了);表外串读成不认识的词 ⇒ 答 unknown,宿主退回自己的规则(不放宽)。不经过境口自铸座位请求的宿主要自己按同一把窄读。";
335
+ }, {
336
+ readonly key: "ruleOffersAbsence";
337
+ readonly why: "喂强制判据口 approvalIsMandated 的位(CC-203;寄存行那条腿的强制只以缺席词 mandated 表达),按开集读(非空串原文字节、词表不校);权威窄读在卡口与过境口,座位门不设第二判官,理由同 mandated。坏形读成缺席 ⇒ 这一路不判强制(放宽方向:寄存行上的强制就丢了;另两路 mandated / mandate 照判);表外串只是不等于 mandated。不经过境口自铸座位请求的宿主要自己按同一把窄读。";
321
338
  }];
322
339
  };
323
340
  readonly LocalSessionEvent: {