@sema-agent/client-core 0.104.1 → 0.105.0
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 +90 -0
- package/README.md +11 -4
- package/dist/adapt/arms.js +14 -6
- package/dist/adapter/activeRunSelfHeal.d.ts +1 -0
- package/dist/adapter/activeRunSelfHeal.js +10 -5
- package/dist/adapter/downstream/eventToSdkMessage.js +4 -4
- package/dist/adapter/downstream/wiringManifestView.d.ts +6 -0
- package/dist/adapter/downstream/wiringManifestView.js +25 -0
- package/dist/adapter/runStream.js +5 -1
- package/dist/cloudConfigWireCaps.js +3 -0
- package/dist/compensations.js +6 -6
- package/dist/coreValuePorts.d.ts +3 -1
- package/dist/coreValuePorts.js +4 -0
- package/dist/displayUntrusted.d.ts +4 -2
- package/dist/displayUntrusted.js +45 -22
- package/dist/engineGenerationPort.d.ts +11 -2
- package/dist/engineGenerationPort.js +63 -9
- package/dist/engineHealthFacts.d.ts +5 -0
- package/dist/engineHealthFacts.js +11 -0
- package/dist/eventLeg.d.ts +1 -1
- package/dist/eventLeg.js +23 -3
- package/dist/generated/engineFactTables.d.ts +1 -0
- package/dist/generated/engineFactTables.js +1 -0
- package/dist/generated/sdkFactTables.d.ts +2 -0
- package/dist/generated/sdkFactTables.js +9 -0
- package/dist/headlessReconnectWire.js +31 -4
- package/dist/hitl/askGateWire.js +26 -2
- package/dist/hitl/frameRouter.d.ts +4 -0
- package/dist/hitl/frameRouter.js +14 -0
- package/dist/hitl/gateIdentity.d.ts +1 -0
- package/dist/hitl/gateIdentity.js +3 -0
- package/dist/hitl/planReviewCard.d.ts +21 -0
- package/dist/hitl/planReviewCard.js +229 -0
- package/dist/hitl/planReviewWire.d.ts +38 -13
- package/dist/hitl/planReviewWire.js +355 -133
- package/dist/hostSettingsSource.d.ts +1 -1
- package/dist/hostSettingsSource.js +8 -4
- package/dist/hostTurnSettings.d.ts +1 -1
- package/dist/hostTurnSettings.js +29 -10
- package/dist/i18n/messages.en.d.ts +50 -0
- package/dist/i18n/messages.en.js +50 -0
- package/dist/i18n/messages.zh.js +51 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +9 -1
- package/dist/mcpTransportKindsCapability.d.ts +18 -0
- package/dist/mcpTransportKindsCapability.js +75 -0
- package/dist/mcpWireCaps.d.ts +4 -1
- package/dist/mcpWireCaps.js +24 -13
- package/dist/notifications.d.ts +9 -0
- package/dist/notifications.js +11 -2
- package/dist/ownKey.d.ts +4 -0
- package/dist/ownKey.js +8 -0
- package/dist/permissionModeWriteCapability.d.ts +13 -0
- package/dist/permissionModeWriteCapability.js +35 -0
- package/dist/permissionsAllowGrantCapability.d.ts +3 -0
- package/dist/permissionsAllowGrantCapability.js +26 -3
- package/dist/permissionsDeclaration.d.ts +23 -1
- package/dist/permissionsDeclaration.js +60 -30
- package/dist/planReviewCardFacts.d.ts +116 -0
- package/dist/planReviewCardFacts.js +469 -0
- package/dist/reattachWindow.d.ts +8 -1
- package/dist/reattachWindow.js +44 -12
- package/dist/runFollow.d.ts +30 -0
- package/dist/runFollow.js +117 -0
- package/dist/runLedgerSnapshot.d.ts +20 -0
- package/dist/runLedgerSnapshot.js +272 -0
- package/dist/runPermissionModeSwitch.d.ts +57 -0
- package/dist/runPermissionModeSwitch.js +135 -0
- package/dist/runReattachPolicy.d.ts +13 -5
- package/dist/runReattachPolicy.js +58 -23
- package/dist/seam.d.ts +3 -0
- package/dist/seam.js +1 -1
- package/dist/seatContract.js +1 -0
- package/dist/sessionMemoryErase.d.ts +1 -1
- package/dist/sessionMemoryErase.js +5 -0
- package/dist/sessionMemoryStatus.d.ts +27 -1
- package/dist/sessionMemoryStatus.js +117 -0
- package/dist/sseIdleTriage.d.ts +3 -1
- package/dist/sseIdleTriage.js +19 -4
- package/dist/subagent/engineSubagentTail.js +2 -0
- package/dist/subagentContentStore.d.ts +3 -0
- package/dist/subagentContentStore.js +10 -1
- package/dist/turnFailurePresentation.d.ts +5 -2
- package/dist/turnFailurePresentation.js +12 -4
- package/docs/INTEGRATION-CLIENTS.md +493 -10
- package/package.json +2 -2
|
@@ -15,10 +15,14 @@ import { refusalDeliveryOf } from './decisionRefusal.js';
|
|
|
15
15
|
import { directFailureVerdictOf, directSafetyStopOf } from './approvalResolution.js';
|
|
16
16
|
import { __resetDecisionLedgerForTests, decisionInFlightOf, decisionKeyInFlight, decisionLedgerKeysOf, decisionOpsWithKey, decisionReadMark, dropDecisionOp, latestDecisionHandedMark, markLatestDecisionNotApplied, openDecisionOp, planDecisionKey, readMarkOf, settleDecisionOp, settleDecisionOpsOnGateLeft, } from './decisionLedger.js';
|
|
17
17
|
import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
|
|
18
|
-
import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
|
|
18
|
+
import { freshPlanReviewQuestionId, planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
|
|
19
|
+
import { isPlanReviewPark, mintPlanReviewCardFacts, notePlanReviewCardIssued, pausedCheckpointIdOf, planReviewCardFactsFrom, planReviewFactsCurrent, registeredPlanReviewCardFacts, samePlanReviewCardFacts, unknownBody, _resetPlanReviewRunsForTest, } from '../planReviewCardFacts.js';
|
|
20
|
+
import { planReviewAskAgainKey, planReviewCardNoteRef, planReviewUnreadableNoteRef, takePlanReviewFacts, _resetPlanReviewFactsFlightsForTest, } from './planReviewCard.js';
|
|
21
|
+
import { EN_MESSAGES } from '../i18n/messages.en.js';
|
|
22
|
+
import { msg, renderMessage, DATA_FACE_LANGUAGE } from '../i18n/translate.js';
|
|
19
23
|
import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
|
|
20
24
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
21
|
-
import {
|
|
25
|
+
import { durableDecideLegOf } from '../adapter/activeRunSelfHeal.js';
|
|
22
26
|
import { readRunTerminal, runTerminalGateKind } from '../runTerminal.js';
|
|
23
27
|
const PLAN_REVIEW_RESUME_TIMEOUT_MS = 6 * 60 * 60_000;
|
|
24
28
|
export const PLAN_REVIEW_APPROVE_LABEL = 'Yes, approve and run the plan';
|
|
@@ -27,17 +31,33 @@ export const PLAN_REVIEW_APPROVE_AUTO_LABEL = 'Yes, and auto-accept edits';
|
|
|
27
31
|
export const PLAN_REVIEW_APPROVE_MANUAL_LABEL = 'Yes, and manually approve edits';
|
|
28
32
|
const APPROVE_LABEL = PLAN_REVIEW_APPROVE_LABEL;
|
|
29
33
|
const REJECT_LABEL = PLAN_REVIEW_REJECT_LABEL;
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
34
|
+
const ASK_AGAIN_LABEL = EN_MESSAGES['approval.planReview.option.askAgain.label'];
|
|
35
|
+
const REJECT_OPTION = Object.freeze({ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it', choice: 'reject' });
|
|
36
|
+
const OPTIONS_BY_SHAPE = Object.freeze({
|
|
37
|
+
two_choice: Object.freeze([
|
|
38
|
+
Object.freeze({ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)', choice: 'approve' }),
|
|
39
|
+
REJECT_OPTION,
|
|
40
|
+
]),
|
|
41
|
+
three_choice: Object.freeze([
|
|
42
|
+
Object.freeze({ label: PLAN_REVIEW_APPROVE_AUTO_LABEL, description: 'Resume and execute the plan; edits inside the working directory are not asked about one by one', choice: 'approve_accept_edits' }),
|
|
43
|
+
Object.freeze({ label: PLAN_REVIEW_APPROVE_MANUAL_LABEL, description: 'Resume and execute the plan; every edit still asks for approval', choice: 'approve_manual' }),
|
|
44
|
+
REJECT_OPTION,
|
|
45
|
+
]),
|
|
46
|
+
withheld: Object.freeze([REJECT_OPTION, Object.freeze({ label: ASK_AGAIN_LABEL, description: EN_MESSAGES['approval.planReview.option.askAgain.description'], choice: 'ask_again' })]),
|
|
47
|
+
});
|
|
48
|
+
const LEGACY_TWO_CHOICE_LABELS = Object.freeze(OPTIONS_BY_SHAPE.two_choice.map((o) => o.label));
|
|
49
|
+
const THREE_CHOICE_LABELS = Object.freeze(OPTIONS_BY_SHAPE.three_choice.map((o) => o.label));
|
|
50
|
+
const WITHHELD_LABELS = Object.freeze(OPTIONS_BY_SHAPE.withheld.map((o) => o.label));
|
|
51
|
+
const LABELS_BY_SHAPE = Object.freeze({ three_choice: THREE_CHOICE_LABELS, two_choice: LEGACY_TWO_CHOICE_LABELS, withheld: WITHHELD_LABELS });
|
|
52
|
+
const ANY_CARD_SHAPE_LABELS = Object.freeze([...new Set([...THREE_CHOICE_LABELS, ...LEGACY_TWO_CHOICE_LABELS, ...WITHHELD_LABELS])]);
|
|
33
53
|
export function planReviewDecisionFromAnswer(answer, shownLabels = LEGACY_TWO_CHOICE_LABELS) {
|
|
34
54
|
const { choice } = readPlanReviewAnswer(answer, shownLabels);
|
|
35
|
-
if (choice === '
|
|
36
|
-
return
|
|
55
|
+
if (typeof choice === 'string')
|
|
56
|
+
return choice;
|
|
37
57
|
return choice.decision;
|
|
38
58
|
}
|
|
39
|
-
function
|
|
40
|
-
return { choice
|
|
59
|
+
function readNonDecision(choice) {
|
|
60
|
+
return { choice, noteIgnoredOnApprove: false };
|
|
41
61
|
}
|
|
42
62
|
function freeTextNoteOf(entry) {
|
|
43
63
|
try {
|
|
@@ -50,23 +70,25 @@ function freeTextNoteOf(entry) {
|
|
|
50
70
|
}
|
|
51
71
|
function readPlanReviewAnswer(answer, shownLabels) {
|
|
52
72
|
const answers = answer?.answers;
|
|
53
|
-
if (!Array.isArray(answers) || answers.length
|
|
54
|
-
return
|
|
73
|
+
if (!Array.isArray(answers) || answers.length === 0)
|
|
74
|
+
return readNonDecision('dismissed');
|
|
75
|
+
if (answers.length !== 1)
|
|
76
|
+
return readNonDecision('inadmissible');
|
|
55
77
|
const entry = answers[0];
|
|
56
78
|
const picked = entry?.selected;
|
|
57
79
|
if (!Array.isArray(picked))
|
|
58
|
-
return
|
|
80
|
+
return readNonDecision('dismissed');
|
|
59
81
|
const note = freeTextNoteOf(entry);
|
|
60
82
|
if (picked.length === 0) {
|
|
61
83
|
if (note === undefined || !shownLabels.includes(REJECT_LABEL))
|
|
62
|
-
return
|
|
84
|
+
return readNonDecision('dismissed');
|
|
63
85
|
return { choice: { decision: 'reject', reason: note }, noteIgnoredOnApprove: false };
|
|
64
86
|
}
|
|
65
87
|
if (picked.length !== 1)
|
|
66
|
-
return
|
|
88
|
+
return readNonDecision('inadmissible');
|
|
67
89
|
const selected = picked[0];
|
|
68
90
|
if (typeof selected !== 'string' || !shownLabels.includes(selected))
|
|
69
|
-
return
|
|
91
|
+
return readNonDecision('inadmissible');
|
|
70
92
|
const ignored = note !== undefined;
|
|
71
93
|
if (selected === PLAN_REVIEW_APPROVE_AUTO_LABEL)
|
|
72
94
|
return { choice: { decision: 'approve', permissionModeAfter: 'acceptEdits' }, noteIgnoredOnApprove: ignored };
|
|
@@ -76,7 +98,9 @@ function readPlanReviewAnswer(answer, shownLabels) {
|
|
|
76
98
|
return { choice: { decision: 'approve' }, noteIgnoredOnApprove: ignored };
|
|
77
99
|
if (selected === REJECT_LABEL)
|
|
78
100
|
return { choice: note === undefined ? { decision: 'reject' } : { decision: 'reject', reason: note }, noteIgnoredOnApprove: false };
|
|
79
|
-
|
|
101
|
+
if (selected === ASK_AGAIN_LABEL)
|
|
102
|
+
return { choice: note === undefined ? { decision: 'ask_again' } : { decision: 'ask_again', reason: note }, noteIgnoredOnApprove: false };
|
|
103
|
+
return readNonDecision('inadmissible');
|
|
80
104
|
}
|
|
81
105
|
export function planReviewChoiceFromAnswer(answer, shownLabels = LEGACY_TWO_CHOICE_LABELS) {
|
|
82
106
|
const { choice, noteIgnoredOnApprove } = readPlanReviewAnswer(answer, shownLabels);
|
|
@@ -85,21 +109,53 @@ export function planReviewChoiceFromAnswer(answer, shownLabels = LEGACY_TWO_CHOI
|
|
|
85
109
|
return choice;
|
|
86
110
|
}
|
|
87
111
|
export function planReviewCardOptions(offerModeAfter) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
return OPTIONS_BY_SHAPE[offerModeAfter ? 'three_choice' : 'two_choice'].map(({ label, description }) => ({ label, description }));
|
|
113
|
+
}
|
|
114
|
+
export function planReviewOptionsOf(facts, opts) {
|
|
115
|
+
return OPTIONS_BY_SHAPE[cardShapeOf(facts, submittedInPlanModeOf(opts))];
|
|
116
|
+
}
|
|
117
|
+
function submittedInPlanModeOf(opts) {
|
|
118
|
+
try {
|
|
119
|
+
return readOwnDataValue(opts, 'submittedInPlanMode') === true;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function cardShapeOf(facts, submittedInPlanMode) {
|
|
126
|
+
try {
|
|
127
|
+
if (planReviewCardFactsFrom(facts)?.body.kind !== 'shown')
|
|
128
|
+
return 'withheld';
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return 'withheld';
|
|
132
|
+
}
|
|
133
|
+
return submittedInPlanMode ? 'three_choice' : 'two_choice';
|
|
134
|
+
}
|
|
135
|
+
export function planReviewCardViewOf(frame, lang) {
|
|
136
|
+
try {
|
|
137
|
+
const questionId = readOwnDataNonEmptyString(frame ?? undefined, 'questionId');
|
|
138
|
+
const taskId = questionId === undefined ? undefined : taskIdFromPlanReviewQuestionId(questionId);
|
|
139
|
+
if (taskId === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
const facts = planReviewCardFactsFrom(readOwnDataValue(frame ?? undefined, '_sema_planReview'));
|
|
142
|
+
if (facts === undefined || facts.taskId !== taskId) {
|
|
143
|
+
return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang) });
|
|
144
|
+
}
|
|
145
|
+
const shape = cardShapeOf(facts, frame !== null && frame !== undefined && offeredModeFromLabels(frame) === 'three_choice');
|
|
146
|
+
const noteRef = planReviewCardNoteRef(facts);
|
|
147
|
+
return Object.freeze({ facts, options: OPTIONS_BY_SHAPE[shape], ...(noteRef !== undefined ? { note: renderMessage(noteRef, lang) } : {}) });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang) });
|
|
91
151
|
}
|
|
92
|
-
return [
|
|
93
|
-
{ label: PLAN_REVIEW_APPROVE_AUTO_LABEL, description: 'Resume and execute the plan; edits inside the working directory are not asked about one by one' },
|
|
94
|
-
{ label: PLAN_REVIEW_APPROVE_MANUAL_LABEL, description: 'Resume and execute the plan; every edit still asks for approval' },
|
|
95
|
-
reject,
|
|
96
|
-
];
|
|
97
152
|
}
|
|
98
153
|
export function notePlanReviewAnsweredIfDecisive(questionId, answer) {
|
|
99
154
|
notePlanReviewAnsweredIfDecisiveFor(DEFAULT_SESSION_KEY, questionId, answer);
|
|
100
155
|
}
|
|
101
156
|
function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
|
|
102
|
-
|
|
157
|
+
const decided = planReviewDecisionFromAnswer(answer ?? { answers: [] }, planReviewShownLabelsOf(questionId) ?? ANY_CARD_SHAPE_LABELS);
|
|
158
|
+
if (decided === 'dismissed' || decided === 'inadmissible')
|
|
103
159
|
return;
|
|
104
160
|
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
105
161
|
const taskId = typeof questionId === 'string' ? taskIdFromPlanReviewQuestionId(questionId) : undefined;
|
|
@@ -110,27 +166,7 @@ function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
|
|
|
110
166
|
return;
|
|
111
167
|
notePlanReviewDecisiveAnswerFor(sessionKey, taskId);
|
|
112
168
|
}
|
|
113
|
-
export
|
|
114
|
-
const read = readRunTerminal(result);
|
|
115
|
-
if (read === null || read.kind !== 'paused')
|
|
116
|
-
return false;
|
|
117
|
-
if (runTerminalGateKind(read) !== PLAN_REVIEW_GATE_KIND)
|
|
118
|
-
return false;
|
|
119
|
-
const taskId = result?.taskId;
|
|
120
|
-
return typeof taskId === 'string' && taskId.length > 0;
|
|
121
|
-
}
|
|
122
|
-
function pausedCheckpointIdOf(carrier) {
|
|
123
|
-
try {
|
|
124
|
-
const read = readRunTerminal(carrier);
|
|
125
|
-
if (read === null || read.kind !== 'paused')
|
|
126
|
-
return undefined;
|
|
127
|
-
const id = read.checkpointId;
|
|
128
|
-
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
169
|
+
export { isPlanReviewPark };
|
|
134
170
|
function pausedGateKindOf(carrier) {
|
|
135
171
|
try {
|
|
136
172
|
const read = readRunTerminal(carrier);
|
|
@@ -143,15 +179,15 @@ function pausedGateKindOf(carrier) {
|
|
|
143
179
|
return undefined;
|
|
144
180
|
}
|
|
145
181
|
}
|
|
146
|
-
const
|
|
147
|
-
const offerModeAfterByTask = new Map();
|
|
182
|
+
const armedPlanReviews = new Map();
|
|
148
183
|
const offeredModeByQuestionId = new Map();
|
|
149
184
|
const OFFERED_MODE_LEDGER_MAX = 64;
|
|
150
185
|
export function _resetArmedPlanReviewsForTest() {
|
|
151
|
-
|
|
152
|
-
offerModeAfterByTask.clear();
|
|
186
|
+
armedPlanReviews.clear();
|
|
153
187
|
offeredModeByQuestionId.clear();
|
|
154
188
|
planReviewDismissalsByKey.clear();
|
|
189
|
+
_resetPlanReviewRunsForTest();
|
|
190
|
+
_resetPlanReviewFactsFlightsForTest();
|
|
155
191
|
}
|
|
156
192
|
const ON_SCREEN = Promise.resolve(true);
|
|
157
193
|
const planReviewDismissalsByKey = createSessionSlot();
|
|
@@ -241,31 +277,40 @@ export function clearPlanReviewUserDismissals(sessionKey = DEFAULT_SESSION_KEY)
|
|
|
241
277
|
}
|
|
242
278
|
const ARM_QUESTION = 'The plan is ready for review. Approve it and start the implementation?';
|
|
243
279
|
const REOPEN_QUESTION = 'This plan is still waiting for your review — it is what is holding this session. Approve it and start the implementation?';
|
|
244
|
-
function planReviewQuestions(question,
|
|
280
|
+
function planReviewQuestions(question, shape) {
|
|
245
281
|
return [
|
|
246
282
|
{
|
|
247
283
|
header: 'Plan review',
|
|
248
284
|
question,
|
|
249
|
-
options:
|
|
285
|
+
options: OPTIONS_BY_SHAPE[shape].map(({ label, description }) => ({ label, description })),
|
|
250
286
|
multiSelect: false,
|
|
251
287
|
},
|
|
252
288
|
];
|
|
253
289
|
}
|
|
254
|
-
function planReviewQuestionFrame(questionId, question,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
offeredModeByQuestionId.delete(oldest.value);
|
|
263
|
-
}
|
|
290
|
+
function planReviewQuestionFrame(questionId, question, shape, facts) {
|
|
291
|
+
offeredModeByQuestionId.delete(questionId);
|
|
292
|
+
offeredModeByQuestionId.set(questionId, shape);
|
|
293
|
+
while (offeredModeByQuestionId.size > OFFERED_MODE_LEDGER_MAX) {
|
|
294
|
+
const oldest = offeredModeByQuestionId.keys().next();
|
|
295
|
+
if (oldest.done === true)
|
|
296
|
+
break;
|
|
297
|
+
offeredModeByQuestionId.delete(oldest.value);
|
|
264
298
|
}
|
|
265
|
-
return {
|
|
299
|
+
return {
|
|
300
|
+
type: 'question',
|
|
301
|
+
questionId,
|
|
302
|
+
questions: planReviewQuestions(question, shape),
|
|
303
|
+
...(facts !== undefined ? { _sema_planReview: facts } : {}),
|
|
304
|
+
};
|
|
266
305
|
}
|
|
267
|
-
function
|
|
268
|
-
publishQuestionFrame(planReviewQuestionFrame(
|
|
306
|
+
function publishArmedPlanReviewCard(inst) {
|
|
307
|
+
publishQuestionFrame(planReviewQuestionFrame(inst.questionId, ARM_QUESTION, inst.shape, inst.facts));
|
|
308
|
+
}
|
|
309
|
+
function retireArmedPlanReview(taskId, inst) {
|
|
310
|
+
if (armedPlanReviews.get(taskId) === inst)
|
|
311
|
+
armedPlanReviews.delete(taskId);
|
|
312
|
+
unregisterLocalQuestionResponder(inst.questionId);
|
|
313
|
+
publishQuestionFrame({ type: 'question_complete', questionId: inst.questionId });
|
|
269
314
|
}
|
|
270
315
|
const OFFERED_MODE_SCAN_MAX = 32;
|
|
271
316
|
function ownElementsOf(v, max) {
|
|
@@ -280,6 +325,7 @@ function ownElementsOf(v, max) {
|
|
|
280
325
|
function offeredModeFromLabels(card) {
|
|
281
326
|
let modeAfter = false;
|
|
282
327
|
let plain = false;
|
|
328
|
+
let askAgain = false;
|
|
283
329
|
for (const q of ownElementsOf(readOwnDataValue(card, 'questions'), OFFERED_MODE_SCAN_MAX)) {
|
|
284
330
|
if (typeof q !== 'object' || q === null)
|
|
285
331
|
continue;
|
|
@@ -291,8 +337,12 @@ function offeredModeFromLabels(card) {
|
|
|
291
337
|
modeAfter = true;
|
|
292
338
|
else if (label === APPROVE_LABEL)
|
|
293
339
|
plain = true;
|
|
340
|
+
else if (label === ASK_AGAIN_LABEL)
|
|
341
|
+
askAgain = true;
|
|
294
342
|
}
|
|
295
343
|
}
|
|
344
|
+
if (askAgain)
|
|
345
|
+
return modeAfter || plain ? undefined : 'withheld';
|
|
296
346
|
if (modeAfter && !plain)
|
|
297
347
|
return 'three_choice';
|
|
298
348
|
if (plain && !modeAfter)
|
|
@@ -310,10 +360,7 @@ export function planReviewTaskIdFromQuestionId(questionId) {
|
|
|
310
360
|
function offeredModeFromQuestionId(questionId) {
|
|
311
361
|
if (taskIdFromPlanReviewQuestionId(questionId) === undefined)
|
|
312
362
|
return 'unknown';
|
|
313
|
-
|
|
314
|
-
if (recorded !== undefined)
|
|
315
|
-
return recorded ? 'three_choice' : 'two_choice';
|
|
316
|
-
return questionId.includes(REOPEN_ID_TAIL) ? 'two_choice' : 'unknown';
|
|
363
|
+
return offeredModeByQuestionId.get(questionId) ?? 'unknown';
|
|
317
364
|
}
|
|
318
365
|
export function planReviewOfferedModeOf(card) {
|
|
319
366
|
try {
|
|
@@ -337,14 +384,16 @@ export function planReviewShownLabelsOf(questionId) {
|
|
|
337
384
|
return undefined;
|
|
338
385
|
const recorded = offeredModeByQuestionId.get(questionId);
|
|
339
386
|
if (recorded !== undefined)
|
|
340
|
-
return recorded
|
|
341
|
-
for (const
|
|
342
|
-
if (
|
|
343
|
-
return
|
|
387
|
+
return LABELS_BY_SHAPE[recorded];
|
|
388
|
+
for (const inst of armedPlanReviews.values()) {
|
|
389
|
+
if (inst.questionId === questionId)
|
|
390
|
+
return LABELS_BY_SHAPE[inst.shape];
|
|
344
391
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
392
|
+
for (const r of activeReopenResponders.values()) {
|
|
393
|
+
if (r.questionId === questionId)
|
|
394
|
+
return LABELS_BY_SHAPE[r.shape];
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
348
397
|
}
|
|
349
398
|
catch {
|
|
350
399
|
return undefined;
|
|
@@ -384,28 +433,81 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
384
433
|
: undefined;
|
|
385
434
|
const { taskId } = result;
|
|
386
435
|
const gateBefore = pausedCheckpointIdOf(result);
|
|
436
|
+
if (gateBefore === undefined) {
|
|
437
|
+
presentArmedPlanReview(mintPlanReviewCardFacts(taskId, undefined, unknownBody('gate_unidentified')), delivery);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
const taking = takePlanReviewFacts(DEFAULT_SESSION_KEY, taskId, gateBefore, () => planReviewLedgerSourceFor(wire, DEFAULT_SESSION_KEY));
|
|
441
|
+
if (taking.kind === 'now') {
|
|
442
|
+
presentArmedPlanReview(taking.taken.facts, delivery);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
hostLog('debug', `planReviewWire: arm-await-ledger task=${taskId}`);
|
|
446
|
+
void taking.pending.then((taken) => {
|
|
447
|
+
try {
|
|
448
|
+
if (taken !== undefined && planReviewFactsCurrent(taken)) {
|
|
449
|
+
presentArmedPlanReview(taken.facts, delivery);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const current = registeredPlanReviewCardFacts(DEFAULT_SESSION_KEY, taskId, gateBefore);
|
|
453
|
+
if (current !== undefined) {
|
|
454
|
+
presentArmedPlanReview(current, delivery);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
hostLog('debug', `planReviewWire: arm-superseded task=${taskId}`);
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
if (armedTaskId !== undefined)
|
|
461
|
+
armedPlanReviews.delete(armedTaskId);
|
|
462
|
+
hostLog('debug', `planReviewWire: arm-publish-failed task=${taskId} err=${String(e)}`);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
catch (e) {
|
|
468
|
+
if (armedTaskId !== undefined)
|
|
469
|
+
armedPlanReviews.delete(armedTaskId);
|
|
470
|
+
hostLog('debug', `planReviewWire: arm failed (fail-soft): ${String(e)}`);
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
function presentArmedPlanReview(facts, delivery) {
|
|
474
|
+
if (!isPlanReviewPark(result))
|
|
475
|
+
return;
|
|
476
|
+
const { taskId } = result;
|
|
477
|
+
const gateBefore = pausedCheckpointIdOf(result);
|
|
478
|
+
const shape = cardShapeOf(facts, submittedInPlanModeOf(opts));
|
|
387
479
|
retireActiveReopen(DEFAULT_SESSION_KEY, planReviewQuestionId(taskId));
|
|
388
|
-
|
|
389
|
-
|
|
480
|
+
const prev = armedPlanReviews.get(taskId);
|
|
481
|
+
const hadArmedInstance = prev !== undefined;
|
|
482
|
+
if (prev !== undefined) {
|
|
483
|
+
if (hasLocalQuestionResponder(prev.questionId) &&
|
|
484
|
+
prev.checkpointId !== undefined &&
|
|
485
|
+
prev.checkpointId === gateBefore &&
|
|
486
|
+
samePlanReviewCardFacts(prev.facts, facts)) {
|
|
390
487
|
hostLog('debug', `planReviewWire: arm replayed — re-presenting the still-armed card for task ${taskId}`);
|
|
391
|
-
|
|
392
|
-
return
|
|
488
|
+
publishArmedPlanReviewCard(prev);
|
|
489
|
+
return;
|
|
393
490
|
}
|
|
394
|
-
hostLog('debug', `planReviewWire:
|
|
395
|
-
|
|
491
|
+
hostLog('debug', `planReviewWire: arm-retire task=${taskId} card=${prev.questionId}`);
|
|
492
|
+
retireArmedPlanReview(taskId, prev);
|
|
396
493
|
}
|
|
397
|
-
|
|
494
|
+
const issuedBefore = notePlanReviewCardIssued(DEFAULT_SESSION_KEY, taskId);
|
|
495
|
+
const questionId = issuedBefore || hadArmedInstance ? freshPlanReviewQuestionId(taskId, reopenIdSuffix()) : planReviewQuestionId(taskId);
|
|
496
|
+
const instance = Object.freeze({ questionId, ...(gateBefore !== undefined ? { checkpointId: gateBefore } : {}), facts, shape });
|
|
497
|
+
armedPlanReviews.set(taskId, instance);
|
|
398
498
|
armedTaskId = taskId;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
offerModeAfterByTask.set(taskId, true);
|
|
402
|
-
else
|
|
403
|
-
offerModeAfterByTask.delete(taskId);
|
|
404
|
-
const questionId = planReviewQuestionId(taskId);
|
|
405
|
-
const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
|
|
499
|
+
let unregister = () => { };
|
|
500
|
+
const responder = async (_id, answer) => {
|
|
406
501
|
const choice = planReviewAnswerChoiceOf(questionId, answer);
|
|
502
|
+
if (choice === 'inadmissible') {
|
|
503
|
+
if (armedPlanReviews.get(taskId) === instance && !hasLocalQuestionResponder(questionId))
|
|
504
|
+
unregister = registerLocalQuestionResponder(questionId, responder);
|
|
505
|
+
hostLog('debug', `planReviewWire: answer-inadmissible card=${questionId}`);
|
|
506
|
+
return { ok: false, inadmissible: true };
|
|
507
|
+
}
|
|
407
508
|
unregister();
|
|
408
|
-
|
|
509
|
+
if (armedPlanReviews.get(taskId) === instance)
|
|
510
|
+
armedPlanReviews.delete(taskId);
|
|
409
511
|
if (choice === 'dismissed') {
|
|
410
512
|
notePlanReviewUserDismissalFor(DEFAULT_SESSION_KEY, taskId, opts?.readGateInstanceKeys, questionId);
|
|
411
513
|
return { ok: true };
|
|
@@ -413,25 +515,16 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
|
|
|
413
515
|
const decided = choice.decision;
|
|
414
516
|
notePlanReviewDecisiveAnswerFor(DEFAULT_SESSION_KEY, taskId);
|
|
415
517
|
const modeAfter = choice.decision === 'approve' ? choice.permissionModeAfter : undefined;
|
|
416
|
-
const cardReason = choice.decision === '
|
|
518
|
+
const cardReason = choice.decision === 'approve' ? undefined : choice.reason;
|
|
417
519
|
const cardDelivery = cardReason !== undefined ? { ...delivery, reason: cardReason } : delivery;
|
|
418
|
-
offerModeAfterByTask.delete(taskId);
|
|
419
520
|
notePlanReviewAnswered(questionId);
|
|
420
521
|
hostLog('debug', `planReviewWire: arm responder delivering ${decided} for task ${taskId} (path=arm)`);
|
|
421
|
-
void decidePlanReviewFor(taskId, decided, modeAfter, cardDelivery, gateBefore);
|
|
522
|
+
void decidePlanReviewFor(taskId, decided, modeAfter, cardDelivery, gateBefore, facts);
|
|
422
523
|
return { ok: true };
|
|
423
|
-
}
|
|
424
|
-
|
|
524
|
+
};
|
|
525
|
+
unregister = registerLocalQuestionResponder(questionId, responder);
|
|
526
|
+
publishArmedPlanReviewCard(instance);
|
|
425
527
|
hostLog('debug', `planReviewWire: approval card armed for parked plan (task ${taskId})`);
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
catch (e) {
|
|
429
|
-
if (armedTaskId !== undefined) {
|
|
430
|
-
armedPlanReviewTaskIds.delete(armedTaskId);
|
|
431
|
-
offerModeAfterByTask.delete(armedTaskId);
|
|
432
|
-
}
|
|
433
|
-
hostLog('debug', `planReviewWire: arm failed (fail-soft): ${String(e)}`);
|
|
434
|
-
return false;
|
|
435
528
|
}
|
|
436
529
|
}
|
|
437
530
|
export function planReviewOutcomeTail(taskId, decision, effect, cause) {
|
|
@@ -588,7 +681,17 @@ function bodySaysReparked(body) {
|
|
|
588
681
|
return false;
|
|
589
682
|
}
|
|
590
683
|
}
|
|
591
|
-
|
|
684
|
+
function askAgainReasonOf(card, opts) {
|
|
685
|
+
try {
|
|
686
|
+
const sentence = renderMessage(msg(planReviewAskAgainKey(card === undefined ? undefined : planReviewCardFactsFrom(card))), DATA_FACE_LANGUAGE);
|
|
687
|
+
const note = opts !== null && typeof opts === 'object' ? opts.reason : undefined;
|
|
688
|
+
return typeof note === 'string' && note.trim() !== '' ? renderMessage(msg('approval.planReview.askAgain.withNote', { sentence, note }), DATA_FACE_LANGUAGE) : sentence;
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return renderMessage(msg('approval.planReview.askAgain.unknown'), DATA_FACE_LANGUAGE);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, gateBefore, card) {
|
|
592
695
|
if (typeof taskId === 'string' && planReviewOpsOf(taskId).some((op) => op.phase === 'posting')) {
|
|
593
696
|
hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
|
|
594
697
|
return;
|
|
@@ -619,7 +722,9 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
|
|
|
619
722
|
const dispatchNo = ++decideDispatchSeq;
|
|
620
723
|
hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
|
|
621
724
|
try {
|
|
622
|
-
|
|
725
|
+
const wireDecision = decision === 'ask_again' ? 'reject' : decision;
|
|
726
|
+
const wireOpts = decision === 'ask_again' ? { ...opts, reason: askAgainReasonOf(card, opts) } : opts;
|
|
727
|
+
await decidePlanReviewInner(taskId, wireDecision, permissionModeAfter, dispatchNo, releaseLatch, settle, wireOpts, gateBefore, decision);
|
|
623
728
|
}
|
|
624
729
|
finally {
|
|
625
730
|
releaseLatch();
|
|
@@ -640,7 +745,7 @@ function planReviewClientsFor(resolved) {
|
|
|
640
745
|
repull: () => resolved.make({ timeoutMs: 15_000 }),
|
|
641
746
|
};
|
|
642
747
|
}
|
|
643
|
-
async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, gateBefore) {
|
|
748
|
+
async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, gateBefore, semanticDecision) {
|
|
644
749
|
let outcome;
|
|
645
750
|
let effect = 'unconfirmed';
|
|
646
751
|
let stillParkedCause;
|
|
@@ -839,7 +944,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
839
944
|
const onOutcome = opts?.onOutcome;
|
|
840
945
|
settle(effect === 'not_sent' || effect === 'still_parked');
|
|
841
946
|
try {
|
|
842
|
-
const delivered = enqueuePlanReviewOutcome(prompt, { taskId, dispatchNo, decision, effect, ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}) });
|
|
947
|
+
const delivered = enqueuePlanReviewOutcome(prompt, { taskId, dispatchNo, decision: semanticDecision, effect, ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}) });
|
|
843
948
|
if (!delivered) {
|
|
844
949
|
if (onOutcome !== undefined) {
|
|
845
950
|
hostLog('debug', `planReviewWire: outcome queue port not installed for task ${taskId} — outcome handed to the host's onOutcome callback only`);
|
|
@@ -857,7 +962,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
857
962
|
onOutcome({
|
|
858
963
|
taskId,
|
|
859
964
|
dispatchNo,
|
|
860
|
-
decision,
|
|
965
|
+
decision: semanticDecision,
|
|
861
966
|
effect,
|
|
862
967
|
...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}),
|
|
863
968
|
detail: outcome,
|
|
@@ -907,8 +1012,8 @@ function joinReopenFlight(flight, taskId, receiptMs) {
|
|
|
907
1012
|
return refusedReopen('not_presented');
|
|
908
1013
|
});
|
|
909
1014
|
}
|
|
910
|
-
function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
|
|
911
|
-
const flightKey = `${sessionKey}\u0000${taskId}`;
|
|
1015
|
+
function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs, taken) {
|
|
1016
|
+
const flightKey = `${sessionKey}\u0000${taskId}\u0000${taken.facts.checkpointId ?? ''}`;
|
|
912
1017
|
const inFlight = reopenFlights.get(flightKey);
|
|
913
1018
|
if (inFlight !== undefined)
|
|
914
1019
|
return joinReopenFlight(inFlight, taskId, receiptMs);
|
|
@@ -926,7 +1031,7 @@ function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
|
|
|
926
1031
|
};
|
|
927
1032
|
let verdict;
|
|
928
1033
|
try {
|
|
929
|
-
verdict = mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs);
|
|
1034
|
+
verdict = mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken);
|
|
930
1035
|
}
|
|
931
1036
|
catch (e) {
|
|
932
1037
|
settle(false);
|
|
@@ -940,7 +1045,12 @@ function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
|
|
|
940
1045
|
throw e;
|
|
941
1046
|
});
|
|
942
1047
|
}
|
|
943
|
-
function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
1048
|
+
function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
|
|
1049
|
+
if (!planReviewFactsCurrent(taken)) {
|
|
1050
|
+
hostLog('debug', `planReviewWire: reopen-superseded task=${taskId}`);
|
|
1051
|
+
return Promise.resolve(refusedReopen('not_presented'));
|
|
1052
|
+
}
|
|
1053
|
+
const facts = taken.facts;
|
|
944
1054
|
const markBefore = dismissalEntryFor(sessionKey, taskId);
|
|
945
1055
|
const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => {
|
|
946
1056
|
if (!presented)
|
|
@@ -952,20 +1062,27 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
952
1062
|
const armedKey = planReviewArmedKeyFor(sessionKey, taskId);
|
|
953
1063
|
const firstSight = !wasGateArmedFor(sessionKey, armedKey);
|
|
954
1064
|
const mintFresh = opts?.mintFreshQuestionId !== false;
|
|
955
|
-
|
|
1065
|
+
const armed = armedPlanReviews.get(taskId);
|
|
1066
|
+
if (!mintFresh &&
|
|
1067
|
+
armed !== undefined &&
|
|
1068
|
+
hasLocalQuestionResponder(armed.questionId) &&
|
|
1069
|
+
armed.checkpointId !== undefined &&
|
|
1070
|
+
armed.checkpointId === facts.checkpointId &&
|
|
1071
|
+
samePlanReviewCardFacts(armed.facts, facts)) {
|
|
956
1072
|
hostLog('debug', `planReviewWire: reopen for task ${taskId} reusing the canonical responder (path=reopen-canonical)`);
|
|
957
1073
|
retireActiveReopen(sessionKey, canonicalId);
|
|
958
|
-
const receipt = waitForGateArmedFor(sessionKey, [
|
|
959
|
-
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(
|
|
1074
|
+
const receipt = waitForGateArmedFor(sessionKey, [armed.questionId], receiptMs);
|
|
1075
|
+
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(armed.questionId, ARM_QUESTION, armed.shape, armed.facts));
|
|
960
1076
|
return settleOnReceipt(receipt, firstSight);
|
|
961
1077
|
}
|
|
962
1078
|
retireActiveReopen(sessionKey, canonicalId);
|
|
963
|
-
|
|
964
|
-
|
|
1079
|
+
const armedId = armed?.questionId ?? canonicalId;
|
|
1080
|
+
if (unregisterLocalQuestionResponder(armedId)) {
|
|
1081
|
+
hostLog('debug', `planReviewWire: reopen-retire-armed task=${taskId} card=${armedId}`);
|
|
965
1082
|
}
|
|
966
|
-
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId:
|
|
967
|
-
const questionId =
|
|
968
|
-
hostLog('debug', `planReviewWire: reopen
|
|
1083
|
+
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: armedId });
|
|
1084
|
+
const questionId = freshPlanReviewQuestionId(taskId, reopenIdSuffix());
|
|
1085
|
+
hostLog('debug', `planReviewWire: path=reopen-fresh task=${taskId} card=${questionId}`);
|
|
969
1086
|
const delivery = opts?.wire !== undefined || opts?.onOutcome !== undefined
|
|
970
1087
|
? { ...(opts.wire !== undefined ? { wire: opts.wire } : {}), ...(opts.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
|
|
971
1088
|
: undefined;
|
|
@@ -973,21 +1090,29 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
973
1090
|
((tid, decision, reason) => {
|
|
974
1091
|
hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
|
|
975
1092
|
const withReason = reason === undefined ? delivery : { ...delivery, reason };
|
|
976
|
-
void (
|
|
1093
|
+
void decidePlanReviewFor(tid, decision, undefined, withReason, parkedGateIdentityOf(withReason), facts);
|
|
977
1094
|
});
|
|
978
1095
|
const activeKey = `${sessionKey}\u0000${canonicalId}`;
|
|
979
|
-
|
|
1096
|
+
let unregister = () => { };
|
|
1097
|
+
const responder = async (_id, answer) => {
|
|
1098
|
+
const choice = planReviewAnswerChoiceOf(questionId, answer);
|
|
1099
|
+
if (choice === 'inadmissible') {
|
|
1100
|
+
if (activeReopenResponders.get(activeKey)?.questionId === questionId && !hasLocalQuestionResponder(questionId)) {
|
|
1101
|
+
unregister = registerLocalQuestionResponder(questionId, responder);
|
|
1102
|
+
}
|
|
1103
|
+
hostLog('debug', `planReviewWire: answer-inadmissible card=${questionId}`);
|
|
1104
|
+
return { ok: false, inadmissible: true };
|
|
1105
|
+
}
|
|
980
1106
|
unregister();
|
|
981
1107
|
if (activeReopenResponders.get(activeKey)?.questionId === questionId) {
|
|
982
1108
|
activeReopenResponders.delete(activeKey);
|
|
983
1109
|
}
|
|
984
|
-
const choice = planReviewAnswerChoiceOf(questionId, answer);
|
|
985
1110
|
if (choice === 'dismissed') {
|
|
986
1111
|
notePlanReviewUserDismissalFor(sessionKey, taskId, opts?.readGateInstanceKeys, questionId);
|
|
987
1112
|
return { ok: true };
|
|
988
1113
|
}
|
|
989
1114
|
const decided = choice.decision;
|
|
990
|
-
const cardReason = choice.decision === '
|
|
1115
|
+
const cardReason = choice.decision === 'approve' ? undefined : choice.reason;
|
|
991
1116
|
notePlanReviewDecisiveAnswerFor(sessionKey, taskId);
|
|
992
1117
|
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
993
1118
|
try {
|
|
@@ -1000,10 +1125,12 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
1000
1125
|
hostLog('error', `planReviewWire: reopen decision delivery threw for task ${taskId}: ${String(e)}`);
|
|
1001
1126
|
}
|
|
1002
1127
|
return { ok: true };
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1128
|
+
};
|
|
1129
|
+
const shape = cardShapeOf(facts, false);
|
|
1130
|
+
unregister = registerLocalQuestionResponder(questionId, responder);
|
|
1131
|
+
activeReopenResponders.set(activeKey, { questionId, unregister: () => unregister(), shape });
|
|
1005
1132
|
const receipt = waitForGateArmedFor(sessionKey, [questionId], receiptMs);
|
|
1006
|
-
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(questionId, REOPEN_QUESTION,
|
|
1133
|
+
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(questionId, REOPEN_QUESTION, shape, facts));
|
|
1007
1134
|
return settleOnReceipt(receipt, firstSight);
|
|
1008
1135
|
}
|
|
1009
1136
|
function refusedReopen(cause) {
|
|
@@ -1015,7 +1142,7 @@ function noPresentationSurfaceVerdict(taskId, sessionKey) {
|
|
|
1015
1142
|
`${JSON.stringify(sessionKey)} (onQuestionFrameFor), so the card has nowhere to render`);
|
|
1016
1143
|
return verdict;
|
|
1017
1144
|
}
|
|
1018
|
-
async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart) {
|
|
1145
|
+
async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart, taken) {
|
|
1019
1146
|
const refusedByUser = { reopened: false, dismissedByUser: true };
|
|
1020
1147
|
try {
|
|
1021
1148
|
let fresh = false;
|
|
@@ -1061,7 +1188,7 @@ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, ma
|
|
|
1061
1188
|
putDismissalEntry(sessionKey, taskId, reopening);
|
|
1062
1189
|
let verdict = { reopened: false };
|
|
1063
1190
|
try {
|
|
1064
|
-
verdict = await mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
|
|
1191
|
+
verdict = await mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs, taken);
|
|
1065
1192
|
}
|
|
1066
1193
|
finally {
|
|
1067
1194
|
const presented = verdict.reopened === true;
|
|
@@ -1140,15 +1267,110 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
1140
1267
|
'此处拒绝而不是照发 —— 照发会把这条会话的决断发到另一个槽上。');
|
|
1141
1268
|
return Promise.resolve(refusedReopen('no_delivery_path'));
|
|
1142
1269
|
}
|
|
1270
|
+
const taking = takeReopenPlanReviewFacts(taskId, opts, sessionKey);
|
|
1271
|
+
if (taking.kind === 'now')
|
|
1272
|
+
return reopenWithPlanReviewFacts(taskId, opts, sessionKey, receiptMs, recheckReadStart, taking.taken);
|
|
1273
|
+
return taking.pending.then((taken) => {
|
|
1274
|
+
if (taken === undefined) {
|
|
1275
|
+
hostLog('debug', `planReviewWire: reopen-superseded task=${taskId}`);
|
|
1276
|
+
return refusedReopen('not_presented');
|
|
1277
|
+
}
|
|
1278
|
+
if (decisionInFlightOf({ gate: 'plan_review', taskId }, recheckReadStart)) {
|
|
1279
|
+
hostLog('debug', `planReviewWire: reopen-refused-in-flight-after-read task=${taskId}`);
|
|
1280
|
+
return { reopened: false, _sema_decisionInFlight: true };
|
|
1281
|
+
}
|
|
1282
|
+
return reopenWithPlanReviewFacts(taskId, opts, sessionKey, receiptMs, recheckReadStart, taken);
|
|
1283
|
+
}, () => refusedReopen('internal_error'));
|
|
1284
|
+
}
|
|
1285
|
+
catch {
|
|
1286
|
+
return Promise.resolve(refusedReopen('internal_error'));
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function takeReopenPlanReviewFacts(taskId, opts, sessionKey) {
|
|
1290
|
+
const raw = opts.checkpointId;
|
|
1291
|
+
const checkpointId = typeof raw === 'string' && raw.length > 0 ? raw : undefined;
|
|
1292
|
+
const wire = opts.mintFreshQuestionId === false ? undefined : opts.wire;
|
|
1293
|
+
return takePlanReviewFacts(sessionKey, taskId, checkpointId, () => planReviewLedgerSourceFor(wire, sessionKey));
|
|
1294
|
+
}
|
|
1295
|
+
function reopenWithPlanReviewFacts(taskId, opts, sessionKey, receiptMs, recheckReadStart, taken) {
|
|
1296
|
+
try {
|
|
1143
1297
|
const trigger = opts.trigger === undefined ? undefined : opts.trigger === 'user' ? 'user' : 'automatic';
|
|
1144
1298
|
if (trigger === 'automatic') {
|
|
1145
1299
|
const mark = planReviewUserDismissalMarkFor(sessionKey, taskId);
|
|
1146
1300
|
if (mark !== undefined)
|
|
1147
|
-
return reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart);
|
|
1301
|
+
return reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart, taken);
|
|
1148
1302
|
}
|
|
1149
|
-
return mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
|
|
1303
|
+
return mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs, taken);
|
|
1150
1304
|
}
|
|
1151
1305
|
catch {
|
|
1152
1306
|
return Promise.resolve(refusedReopen('internal_error'));
|
|
1153
1307
|
}
|
|
1154
1308
|
}
|
|
1309
|
+
const PLAN_REVIEW_LEDGER_CLIENT_TIMEOUT_MS = 15_000;
|
|
1310
|
+
function usableLedgerClient(client) {
|
|
1311
|
+
return clientHasVerbs(client, [['runs', 'get'], ['runs', 'events']]);
|
|
1312
|
+
}
|
|
1313
|
+
function planReviewLedgerSourceFor(wire, sessionKey) {
|
|
1314
|
+
try {
|
|
1315
|
+
const resolved = engineWireFor(wire, sessionKey, usableLedgerClient);
|
|
1316
|
+
let client;
|
|
1317
|
+
if (resolved.kind === 'injected')
|
|
1318
|
+
client = resolved.client;
|
|
1319
|
+
else if (resolved.kind === 'target')
|
|
1320
|
+
client = resolved.make({ timeoutMs: PLAN_REVIEW_LEDGER_CLIENT_TIMEOUT_MS });
|
|
1321
|
+
else
|
|
1322
|
+
client = null;
|
|
1323
|
+
if (client === null)
|
|
1324
|
+
return undefined;
|
|
1325
|
+
const c = client;
|
|
1326
|
+
return {
|
|
1327
|
+
runRow: (taskId, signal) => c.runs.get(taskId, { signal }),
|
|
1328
|
+
frames: (taskId, signal) => c.runs.events(taskId, { signal }),
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
return undefined;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function abortSignalOf(v) {
|
|
1336
|
+
if (typeof v !== 'object' || v === null)
|
|
1337
|
+
return undefined;
|
|
1338
|
+
const s = v;
|
|
1339
|
+
return typeof s.aborted === 'boolean' && typeof s.addEventListener === 'function' && typeof s.removeEventListener === 'function' ? v : undefined;
|
|
1340
|
+
}
|
|
1341
|
+
export async function readPlanReviewCardFacts(taskId, opts) {
|
|
1342
|
+
if (typeof taskId !== 'string' || taskId.length === 0)
|
|
1343
|
+
return mintPlanReviewCardFacts('', undefined, unknownBody('gate_unidentified'));
|
|
1344
|
+
let checkpointId;
|
|
1345
|
+
try {
|
|
1346
|
+
checkpointId = readOwnDataNonEmptyString(opts, 'checkpointId');
|
|
1347
|
+
const wire = readOwnDataValue(opts, 'wire');
|
|
1348
|
+
const signal = abortSignalOf(readOwnDataValue(opts, 'signal'));
|
|
1349
|
+
if (signal?.aborted === true)
|
|
1350
|
+
return mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable'));
|
|
1351
|
+
const taking = takePlanReviewFacts(DEFAULT_SESSION_KEY, taskId, checkpointId, () => planReviewLedgerSourceFor(wire, DEFAULT_SESSION_KEY), signal);
|
|
1352
|
+
if (taking.kind === 'now')
|
|
1353
|
+
return taking.taken.facts;
|
|
1354
|
+
let taken;
|
|
1355
|
+
if (signal === undefined)
|
|
1356
|
+
taken = await taking.pending;
|
|
1357
|
+
else {
|
|
1358
|
+
let onAbort;
|
|
1359
|
+
const aborted = new Promise((resolve) => {
|
|
1360
|
+
onAbort = () => resolve(undefined);
|
|
1361
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1362
|
+
});
|
|
1363
|
+
try {
|
|
1364
|
+
taken = await Promise.race([taking.pending, aborted]);
|
|
1365
|
+
}
|
|
1366
|
+
finally {
|
|
1367
|
+
if (onAbort !== undefined)
|
|
1368
|
+
signal.removeEventListener('abort', onAbort);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return taken?.facts ?? mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable'));
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
return mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable'));
|
|
1375
|
+
}
|
|
1376
|
+
}
|