@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.
- package/CHANGELOG.md +141 -0
- package/README.md +19 -17
- package/dist/adapter/activeRunSelfHeal.d.ts +9 -1
- package/dist/adapter/activeRunSelfHeal.js +75 -16
- package/dist/compensations.js +1 -1
- package/dist/displayUntrusted.d.ts +13 -0
- package/dist/displayUntrusted.js +751 -125
- 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/hooksWireCaps.js +78 -8
- package/dist/host.d.ts +2 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/pluginHooksWire.d.ts +9 -0
- package/dist/pluginHooksWire.js +119 -0
- package/dist/request/taskRequest.js +1 -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 +413 -68
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
2
|
-
import { armedKeyFromQuestionId, PLAN_REVIEW_QUESTION_ID_PREFIX, planReviewQuestionId, REOPEN_ID_TAIL } from './gateIdentity.js';
|
|
2
|
+
import { armedKeyFromQuestionId, PLAN_REVIEW_QUESTION_ID_PREFIX, planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId, } from './gateIdentity.js';
|
|
3
3
|
const armedGatesByKey = createSessionSlot();
|
|
4
4
|
const armedListenersByKey = createSessionSlot();
|
|
5
5
|
const planReviewGenByKey = createSessionSlot();
|
|
@@ -149,10 +149,8 @@ export function notePlanReviewAnsweredFor(sessionKey, questionId) {
|
|
|
149
149
|
if (noted.has(questionId))
|
|
150
150
|
return;
|
|
151
151
|
noted.add(questionId);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const taskId = cut >= 0 ? base.slice(0, cut) : base;
|
|
155
|
-
if (taskId.length === 0)
|
|
152
|
+
const taskId = taskIdFromPlanReviewQuestionId(questionId);
|
|
153
|
+
if (taskId === undefined)
|
|
156
154
|
return;
|
|
157
155
|
clearArmedGateFor(sessionKey, planReviewArmedKeyFor(sessionKey, taskId));
|
|
158
156
|
const gens = genMapFor(sessionKey);
|
|
@@ -4,4 +4,5 @@ export declare function approvalCallKey(gatedCallId: string | undefined, taskId:
|
|
|
4
4
|
export declare function askGateQuestionId(callKey: string): string;
|
|
5
5
|
export declare function planReviewQuestionId(taskId: string): string;
|
|
6
6
|
export declare function liveFrameCallKey(approvalId: string): string;
|
|
7
|
+
export declare function taskIdFromPlanReviewQuestionId(questionId: string): string | undefined;
|
|
7
8
|
export declare function armedKeyFromQuestionId(questionId: string): string;
|
|
@@ -14,6 +14,14 @@ export function planReviewQuestionId(taskId) {
|
|
|
14
14
|
export function liveFrameCallKey(approvalId) {
|
|
15
15
|
return `${HITL_FRAME_CALL_KEY_PREFIX}${approvalId}`;
|
|
16
16
|
}
|
|
17
|
+
export function taskIdFromPlanReviewQuestionId(questionId) {
|
|
18
|
+
if (typeof questionId !== 'string' || !questionId.startsWith(PLAN_REVIEW_QUESTION_ID_PREFIX))
|
|
19
|
+
return undefined;
|
|
20
|
+
const base = questionId.slice(PLAN_REVIEW_QUESTION_ID_PREFIX.length);
|
|
21
|
+
const cut = base.indexOf(REOPEN_ID_TAIL);
|
|
22
|
+
const taskId = cut >= 0 ? base.slice(0, cut) : base;
|
|
23
|
+
return taskId.length > 0 ? taskId : undefined;
|
|
24
|
+
}
|
|
17
25
|
export function armedKeyFromQuestionId(questionId) {
|
|
18
26
|
const base = questionId.startsWith(HITL_ASK_QUESTION_ID_PREFIX)
|
|
19
27
|
? questionId.slice(HITL_ASK_QUESTION_ID_PREFIX.length)
|
|
@@ -25,8 +25,12 @@ export declare function isPlanReviewPark(result: unknown): result is {
|
|
|
25
25
|
sessionId?: string;
|
|
26
26
|
};
|
|
27
27
|
export declare function _resetArmedPlanReviewsForTest(): void;
|
|
28
|
+
export type PlanReviewReopenTrigger = 'user' | 'automatic';
|
|
29
|
+
export type PlanReviewGateInstanceReader = (taskId: string) => Promise<ReadonlySet<string> | null>;
|
|
30
|
+
export declare function clearPlanReviewUserDismissals(sessionKey?: string): void;
|
|
28
31
|
export declare function armPlanReviewApproval(result: unknown, sessionKey?: string, opts?: {
|
|
29
32
|
submittedInPlanMode?: boolean;
|
|
33
|
+
readGateInstanceKeys?: PlanReviewGateInstanceReader;
|
|
30
34
|
}): boolean;
|
|
31
35
|
export type PlanReviewDecisionEffect = 'took_effect' | 'advanced' | 'still_parked' | 'not_sent' | 'not_applied' | 'unconfirmed';
|
|
32
36
|
export interface PlanReviewOutcomeMeta {
|
|
@@ -43,6 +47,9 @@ export interface ReopenPlanReviewOpts {
|
|
|
43
47
|
deliverDecision?: (taskId: string, decision: 'approve' | 'reject') => void | Promise<void>;
|
|
44
48
|
sessionKey?: string;
|
|
45
49
|
presentationReceiptMs?: number;
|
|
50
|
+
trigger?: PlanReviewReopenTrigger;
|
|
51
|
+
readGateInstanceKeys?: PlanReviewGateInstanceReader;
|
|
52
|
+
decisionHandedOver?: (taskId: string) => boolean;
|
|
46
53
|
}
|
|
47
54
|
export declare function _resetActiveReopenRespondersForTest(): void;
|
|
48
55
|
export declare function reopenPlanReviewCard(taskId: string, opts: ReopenPlanReviewOpts & {
|
|
@@ -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 } : {}),
|