@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.
- package/CHANGELOG.md +44 -0
- package/README.md +5 -4
- package/dist/adapter/activeRunSelfHeal.d.ts +6 -1
- package/dist/adapter/activeRunSelfHeal.js +36 -4
- package/dist/hitl/armedGateRegistry.js +3 -5
- package/dist/hitl/gateIdentity.d.ts +1 -0
- package/dist/hitl/gateIdentity.js +8 -0
- package/dist/hitl/planReviewWire.d.ts +7 -0
- package/dist/hitl/planReviewWire.js +345 -76
- package/dist/hitl/toolApprovalWire.d.ts +2 -0
- package/dist/hitl/toolApprovalWire.js +23 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/seatContract.d.ts +18 -1
- package/dist/seatContract.js +20 -0
- package/dist/systemReminderTag.js +25 -4
- package/docs/INTEGRATION-CLIENTS.md +139 -8
- package/package.json +2 -2
|
@@ -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
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
406
|
+
if (typeof v !== 'object')
|
|
407
|
+
return false;
|
|
408
|
+
if (hasOwnStrictTrue(v, 'mandated'))
|
|
407
409
|
return true;
|
|
408
|
-
if (v
|
|
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
|
|
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
|
|
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
|
|
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';
|
package/dist/seatContract.d.ts
CHANGED
|
@@ -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: {
|