@sema-agent/client-core 0.104.1 → 0.106.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 +310 -0
- package/README.md +36 -14
- package/dist/adapt/arms.js +23 -6
- package/dist/adapter/activeRunSelfHeal.d.ts +14 -2
- package/dist/adapter/activeRunSelfHeal.js +90 -12
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +0 -5
- package/dist/adapter/downstream/eventToSdkMessage.js +24 -23
- package/dist/adapter/downstream/terminalToSdkResult.js +2 -3
- package/dist/adapter/downstream/wiringManifestView.d.ts +7 -3
- package/dist/adapter/downstream/wiringManifestView.js +25 -1
- package/dist/adapter/runStream.js +8 -5
- package/dist/approvalReachWire.d.ts +1 -0
- package/dist/approvalReachWire.js +4 -1
- package/dist/approvalSeat.d.ts +21 -0
- package/dist/approvalSeat.js +73 -0
- package/dist/assistantInbox.d.ts +2 -0
- package/dist/assistantInbox.js +3 -0
- package/dist/checkpointRowVerdict.d.ts +23 -0
- package/dist/checkpointRowVerdict.js +89 -0
- package/dist/cloudConfigWireCaps.js +3 -0
- package/dist/compensations.js +6 -6
- package/dist/coreValuePorts.d.ts +14 -1
- package/dist/coreValuePorts.js +14 -0
- package/dist/decideFailureNote.js +3 -1
- package/dist/decideReceipt.d.ts +9 -0
- package/dist/decideReceipt.js +29 -1
- package/dist/declared/supportFloorDeclared.js +2 -2
- package/dist/deviceRebind.d.ts +7 -0
- package/dist/deviceRebind.js +39 -5
- package/dist/displayUntrusted.d.ts +4 -2
- package/dist/displayUntrusted.js +45 -22
- package/dist/engineAddress.d.ts +3 -0
- package/dist/engineAddress.js +25 -0
- package/dist/engineDirectory.js +9 -17
- package/dist/engineErrorCodes.d.ts +8 -0
- package/dist/engineErrorCodes.js +8 -0
- 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/engineNoticeCodes.d.ts +26 -3
- package/dist/engineNoticeCodes.js +52 -7
- package/dist/engineRowRef.js +1 -1
- package/dist/errorSupplementRoles.js +2 -0
- package/dist/eventLeg.d.ts +3 -3
- package/dist/eventLeg.js +25 -4
- package/dist/fleet/fleetProjection.js +26 -5
- package/dist/generated/engineFactTables.d.ts +3 -1
- package/dist/generated/engineFactTables.js +4 -1
- package/dist/generated/engineNoticeTables.js +19 -4
- package/dist/generated/sdkFactTables.d.ts +5 -0
- package/dist/generated/sdkFactTables.js +24 -0
- package/dist/generated/settingsSchemaFns.d.ts +11 -0
- package/dist/generated/settingsSchemaFns.js +2 -1
- package/dist/generated/settingsSchemaSafetyMergeSpec.js +1 -1
- package/dist/generated/settingsSchemaSettingsSources.d.ts +1 -1
- package/dist/generated/settingsSchemaSettingsSources.js +22 -2
- package/dist/headlessReconnectWire.js +31 -4
- package/dist/hitl/approvalLiveFace.d.ts +17 -0
- package/dist/hitl/approvalLiveFace.js +35 -0
- package/dist/hitl/approvalQueueRow.d.ts +21 -0
- package/dist/hitl/approvalQueueRow.js +69 -0
- package/dist/hitl/approvalsFeed.d.ts +5 -4
- package/dist/hitl/approvalsFeed.js +38 -6
- package/dist/hitl/askGateWire.js +26 -2
- package/dist/hitl/askParkRowRouting.d.ts +4 -3
- package/dist/hitl/askParkRowRouting.js +15 -3
- package/dist/hitl/automaticAnswerVerdict.js +13 -10
- package/dist/hitl/crashConverged.d.ts +5 -1
- package/dist/hitl/decisionConflict.js +2 -1
- package/dist/hitl/decisionRefusal.js +1 -1
- package/dist/hitl/decisionSend.d.ts +1 -0
- package/dist/hitl/decisionSend.js +14 -2
- package/dist/hitl/frameRouter.d.ts +4 -0
- package/dist/hitl/frameRouter.js +18 -5
- package/dist/hitl/gateIdentity.d.ts +1 -0
- package/dist/hitl/gateIdentity.js +3 -0
- package/dist/hitl/hitlBridge.d.ts +1 -1
- package/dist/hitl/hitlBridge.js +26 -5
- package/dist/hitl/livePendingAsk.d.ts +5 -0
- package/dist/hitl/livePendingAsk.js +6 -1
- package/dist/hitl/planReviewCard.d.ts +23 -0
- package/dist/hitl/planReviewCard.js +250 -0
- package/dist/hitl/planReviewWire.d.ts +39 -14
- package/dist/hitl/planReviewWire.js +450 -163
- package/dist/hitl/sessionAllowRoute.d.ts +2 -0
- package/dist/hitl/sessionAllowRoute.js +7 -0
- package/dist/hitl/sessionPolicyWire.d.ts +1 -7
- package/dist/hitl/sessionPolicyWire.js +3 -43
- package/dist/hitl/subagentSource.d.ts +1 -0
- package/dist/hitl/subagentSource.js +13 -0
- package/dist/hitl/toolApprovalWire.d.ts +3 -2
- package/dist/hitl/toolApprovalWire.js +79 -37
- package/dist/hostSettingsSource.d.ts +5 -2
- package/dist/hostSettingsSource.js +19 -10
- package/dist/hostTurnSettings.d.ts +5 -6
- package/dist/hostTurnSettings.js +106 -26
- package/dist/i18n/messages.en.d.ts +85 -12
- package/dist/i18n/messages.en.js +85 -12
- package/dist/i18n/messages.zh.js +88 -12
- package/dist/index.d.ts +35 -4
- package/dist/index.js +19 -2
- package/dist/liveInitToolFace.js +0 -1
- package/dist/liveModelCatalog.d.ts +15 -0
- package/dist/liveModelCatalog.js +40 -0
- package/dist/mcpTransportKindsCapability.d.ts +18 -0
- package/dist/mcpTransportKindsCapability.js +75 -0
- package/dist/mcpWireCaps.d.ts +4 -1
- package/dist/mcpWireCaps.js +25 -14
- package/dist/modelCapabilityProbe.js +66 -33
- package/dist/notifications.d.ts +9 -0
- package/dist/notifications.js +11 -2
- package/dist/ownKey.d.ts +4 -1
- package/dist/ownKey.js +8 -2
- 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 +25 -3
- package/dist/permissionsDeclaration.js +69 -41
- package/dist/planReviewCardFacts.d.ts +116 -0
- package/dist/planReviewCardFacts.js +469 -0
- package/dist/projectSeed.d.ts +39 -0
- package/dist/projectSeed.js +80 -0
- package/dist/promptDelivery.js +1 -1
- package/dist/reattachWindow.d.ts +8 -1
- package/dist/reattachWindow.js +44 -12
- package/dist/replayBody.d.ts +7 -6
- package/dist/replayBody.js +62 -7
- package/dist/request/taskRequest.js +3 -4
- package/dist/ruleCompileVerdict.d.ts +0 -2
- package/dist/ruleCompileVerdict.js +0 -31
- package/dist/ruleRefusalSentence.js +0 -3
- 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 +16 -5
- package/dist/runReattachPolicy.js +72 -24
- package/dist/runTerminal.js +30 -8
- package/dist/sandboxWire.d.ts +0 -3
- package/dist/sandboxWire.js +0 -9
- package/dist/seam.d.ts +17 -7
- package/dist/seam.js +13 -1
- package/dist/seatContract.d.ts +57 -7
- package/dist/seatContract.js +133 -3
- package/dist/serverSupportRange.d.ts +18 -0
- package/dist/serverSupportRange.js +25 -0
- package/dist/sessionEngine.d.ts +156 -0
- package/dist/sessionEngine.js +464 -0
- package/dist/sessionMap.d.ts +11 -1
- package/dist/sessionMap.js +35 -9
- 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/sessionMemorySync.d.ts +31 -0
- package/dist/sessionMemorySync.js +95 -0
- package/dist/sessionRowTag.d.ts +4 -0
- package/dist/sessionRowTag.js +18 -0
- package/dist/settingsLayerAdmission.d.ts +1 -0
- package/dist/settingsLayerAdmission.js +4 -1
- 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/toolResult.js +1 -1
- package/dist/turnFailurePresentation.d.ts +5 -2
- package/dist/turnFailurePresentation.js +12 -4
- package/dist/wireRefusalCopy.js +2 -6
- package/docs/INTEGRATION-CLIENTS.md +1507 -18
- package/package.json +5 -5
|
@@ -2,23 +2,26 @@ import { publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionRes
|
|
|
2
2
|
import { hostLog } from '../host.js';
|
|
3
3
|
import { abortableSleep, settleWithinDeadline } from '../abortableSleep.js';
|
|
4
4
|
import { clientHasVerbs, engineWireFor, noAnswerKindOf, sendOnce } from '../engineWireFor.js';
|
|
5
|
-
import { engineCapTrue } from '../engineCapsCache.js';
|
|
6
5
|
import { engineWireTarget } from '../engineWireTarget.js';
|
|
7
6
|
import { enqueuePlanReviewOutcome } from '../notifications.js';
|
|
8
7
|
import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
|
|
9
8
|
import { decideFailureNote } from '../decideFailureNote.js';
|
|
10
|
-
import { wireFailureTextsOf } from '../wireFailureShape.js';
|
|
9
|
+
import { readThrownExtra, wireFailureTextsOf } from '../wireFailureShape.js';
|
|
11
10
|
import { appendOwnElement, readOwnDataNonEmptyString, readOwnDataValue } from '../ownKey.js';
|
|
12
11
|
import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError, resumeContextUnavailableContent, resumeContextUnavailableFromError } from '../resumeRefusalCopy.js';
|
|
13
|
-
import { REQUEST_FIELD_CONFLICT } from '../engineErrorCodes.js';
|
|
12
|
+
import { APPROVAL_STALE, REQUEST_FIELD_CONFLICT } from '../engineErrorCodes.js';
|
|
14
13
|
import { refusalDeliveryOf } from './decisionRefusal.js';
|
|
15
14
|
import { directFailureVerdictOf, directSafetyStopOf } from './approvalResolution.js';
|
|
16
15
|
import { __resetDecisionLedgerForTests, decisionInFlightOf, decisionKeyInFlight, decisionLedgerKeysOf, decisionOpsWithKey, decisionReadMark, dropDecisionOp, latestDecisionHandedMark, markLatestDecisionNotApplied, openDecisionOp, planDecisionKey, readMarkOf, settleDecisionOp, settleDecisionOpsOnGateLeft, } from './decisionLedger.js';
|
|
17
16
|
import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
|
|
18
|
-
import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
|
|
17
|
+
import { freshPlanReviewQuestionId, planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
|
|
18
|
+
import { isPlanReviewPark, mintPlanReviewCardFacts, notePlanReviewCardIssued, pausedCheckpointIdOf, planReviewCardFactsFrom, planReviewFactsCurrent, registeredPlanReviewCardFacts, samePlanReviewCardFacts, unknownBody, _resetPlanReviewRunsForTest, } from '../planReviewCardFacts.js';
|
|
19
|
+
import { planReviewAskAgainKey, planReviewCardNoteRef, planReviewDecisionReasonRef, planReviewUnreadableNoteRef, takePlanReviewFacts, _resetPlanReviewFactsFlightsForTest, } from './planReviewCard.js';
|
|
20
|
+
import { EN_MESSAGES } from '../i18n/messages.en.js';
|
|
21
|
+
import { msg, renderMessage, DATA_FACE_LANGUAGE } from '../i18n/translate.js';
|
|
19
22
|
import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
|
|
20
23
|
import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
|
|
21
|
-
import {
|
|
24
|
+
import { durableDecideLegOf } from '../adapter/activeRunSelfHeal.js';
|
|
22
25
|
import { readRunTerminal, runTerminalGateKind } from '../runTerminal.js';
|
|
23
26
|
const PLAN_REVIEW_RESUME_TIMEOUT_MS = 6 * 60 * 60_000;
|
|
24
27
|
export const PLAN_REVIEW_APPROVE_LABEL = 'Yes, approve and run the plan';
|
|
@@ -27,17 +30,33 @@ export const PLAN_REVIEW_APPROVE_AUTO_LABEL = 'Yes, and auto-accept edits';
|
|
|
27
30
|
export const PLAN_REVIEW_APPROVE_MANUAL_LABEL = 'Yes, and manually approve edits';
|
|
28
31
|
const APPROVE_LABEL = PLAN_REVIEW_APPROVE_LABEL;
|
|
29
32
|
const REJECT_LABEL = PLAN_REVIEW_REJECT_LABEL;
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
+
const ASK_AGAIN_LABEL = EN_MESSAGES['approval.planReview.option.askAgain.label'];
|
|
34
|
+
const REJECT_OPTION = Object.freeze({ label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it', choice: 'reject' });
|
|
35
|
+
const OPTIONS_BY_SHAPE = Object.freeze({
|
|
36
|
+
two_choice: Object.freeze([
|
|
37
|
+
Object.freeze({ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)', choice: 'approve' }),
|
|
38
|
+
REJECT_OPTION,
|
|
39
|
+
]),
|
|
40
|
+
three_choice: Object.freeze([
|
|
41
|
+
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' }),
|
|
42
|
+
Object.freeze({ label: PLAN_REVIEW_APPROVE_MANUAL_LABEL, description: 'Resume and execute the plan; every edit still asks for approval', choice: 'approve_manual' }),
|
|
43
|
+
REJECT_OPTION,
|
|
44
|
+
]),
|
|
45
|
+
withheld: Object.freeze([REJECT_OPTION, Object.freeze({ label: ASK_AGAIN_LABEL, description: EN_MESSAGES['approval.planReview.option.askAgain.description'], choice: 'ask_again' })]),
|
|
46
|
+
});
|
|
47
|
+
const LEGACY_TWO_CHOICE_LABELS = Object.freeze(OPTIONS_BY_SHAPE.two_choice.map((o) => o.label));
|
|
48
|
+
const THREE_CHOICE_LABELS = Object.freeze(OPTIONS_BY_SHAPE.three_choice.map((o) => o.label));
|
|
49
|
+
const WITHHELD_LABELS = Object.freeze(OPTIONS_BY_SHAPE.withheld.map((o) => o.label));
|
|
50
|
+
const LABELS_BY_SHAPE = Object.freeze({ three_choice: THREE_CHOICE_LABELS, two_choice: LEGACY_TWO_CHOICE_LABELS, withheld: WITHHELD_LABELS });
|
|
51
|
+
const ANY_CARD_SHAPE_LABELS = Object.freeze([...new Set([...THREE_CHOICE_LABELS, ...LEGACY_TWO_CHOICE_LABELS, ...WITHHELD_LABELS])]);
|
|
33
52
|
export function planReviewDecisionFromAnswer(answer, shownLabels = LEGACY_TWO_CHOICE_LABELS) {
|
|
34
53
|
const { choice } = readPlanReviewAnswer(answer, shownLabels);
|
|
35
|
-
if (choice === '
|
|
36
|
-
return
|
|
54
|
+
if (typeof choice === 'string')
|
|
55
|
+
return choice;
|
|
37
56
|
return choice.decision;
|
|
38
57
|
}
|
|
39
|
-
function
|
|
40
|
-
return { choice
|
|
58
|
+
function readNonDecision(choice) {
|
|
59
|
+
return { choice, noteIgnoredOnApprove: false };
|
|
41
60
|
}
|
|
42
61
|
function freeTextNoteOf(entry) {
|
|
43
62
|
try {
|
|
@@ -50,23 +69,25 @@ function freeTextNoteOf(entry) {
|
|
|
50
69
|
}
|
|
51
70
|
function readPlanReviewAnswer(answer, shownLabels) {
|
|
52
71
|
const answers = answer?.answers;
|
|
53
|
-
if (!Array.isArray(answers) || answers.length
|
|
54
|
-
return
|
|
72
|
+
if (!Array.isArray(answers) || answers.length === 0)
|
|
73
|
+
return readNonDecision('dismissed');
|
|
74
|
+
if (answers.length !== 1)
|
|
75
|
+
return readNonDecision('inadmissible');
|
|
55
76
|
const entry = answers[0];
|
|
56
77
|
const picked = entry?.selected;
|
|
57
78
|
if (!Array.isArray(picked))
|
|
58
|
-
return
|
|
79
|
+
return readNonDecision('dismissed');
|
|
59
80
|
const note = freeTextNoteOf(entry);
|
|
60
81
|
if (picked.length === 0) {
|
|
61
82
|
if (note === undefined || !shownLabels.includes(REJECT_LABEL))
|
|
62
|
-
return
|
|
83
|
+
return readNonDecision('dismissed');
|
|
63
84
|
return { choice: { decision: 'reject', reason: note }, noteIgnoredOnApprove: false };
|
|
64
85
|
}
|
|
65
86
|
if (picked.length !== 1)
|
|
66
|
-
return
|
|
87
|
+
return readNonDecision('inadmissible');
|
|
67
88
|
const selected = picked[0];
|
|
68
89
|
if (typeof selected !== 'string' || !shownLabels.includes(selected))
|
|
69
|
-
return
|
|
90
|
+
return readNonDecision('inadmissible');
|
|
70
91
|
const ignored = note !== undefined;
|
|
71
92
|
if (selected === PLAN_REVIEW_APPROVE_AUTO_LABEL)
|
|
72
93
|
return { choice: { decision: 'approve', permissionModeAfter: 'acceptEdits' }, noteIgnoredOnApprove: ignored };
|
|
@@ -76,7 +97,9 @@ function readPlanReviewAnswer(answer, shownLabels) {
|
|
|
76
97
|
return { choice: { decision: 'approve' }, noteIgnoredOnApprove: ignored };
|
|
77
98
|
if (selected === REJECT_LABEL)
|
|
78
99
|
return { choice: note === undefined ? { decision: 'reject' } : { decision: 'reject', reason: note }, noteIgnoredOnApprove: false };
|
|
79
|
-
|
|
100
|
+
if (selected === ASK_AGAIN_LABEL)
|
|
101
|
+
return { choice: note === undefined ? { decision: 'ask_again' } : { decision: 'ask_again', reason: note }, noteIgnoredOnApprove: false };
|
|
102
|
+
return readNonDecision('inadmissible');
|
|
80
103
|
}
|
|
81
104
|
export function planReviewChoiceFromAnswer(answer, shownLabels = LEGACY_TWO_CHOICE_LABELS) {
|
|
82
105
|
const { choice, noteIgnoredOnApprove } = readPlanReviewAnswer(answer, shownLabels);
|
|
@@ -85,21 +108,53 @@ export function planReviewChoiceFromAnswer(answer, shownLabels = LEGACY_TWO_CHOI
|
|
|
85
108
|
return choice;
|
|
86
109
|
}
|
|
87
110
|
export function planReviewCardOptions(offerModeAfter) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
return OPTIONS_BY_SHAPE[offerModeAfter ? 'three_choice' : 'two_choice'].map(({ label, description }) => ({ label, description }));
|
|
112
|
+
}
|
|
113
|
+
export function planReviewOptionsOf(facts, opts) {
|
|
114
|
+
return OPTIONS_BY_SHAPE[cardShapeOf(facts, submittedInPlanModeOf(opts))];
|
|
115
|
+
}
|
|
116
|
+
function submittedInPlanModeOf(opts) {
|
|
117
|
+
try {
|
|
118
|
+
return readOwnDataValue(opts, 'submittedInPlanMode') === true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function cardShapeOf(facts, submittedInPlanMode) {
|
|
125
|
+
try {
|
|
126
|
+
if (planReviewCardFactsFrom(facts)?.body.kind !== 'shown')
|
|
127
|
+
return 'withheld';
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return 'withheld';
|
|
131
|
+
}
|
|
132
|
+
return submittedInPlanMode ? 'three_choice' : 'two_choice';
|
|
133
|
+
}
|
|
134
|
+
export function planReviewCardViewOf(frame, lang) {
|
|
135
|
+
try {
|
|
136
|
+
const questionId = readOwnDataNonEmptyString(frame ?? undefined, 'questionId');
|
|
137
|
+
const taskId = questionId === undefined ? undefined : taskIdFromPlanReviewQuestionId(questionId);
|
|
138
|
+
if (taskId === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
const facts = planReviewCardFactsFrom(readOwnDataValue(frame ?? undefined, '_sema_planReview'));
|
|
141
|
+
if (facts === undefined || facts.taskId !== taskId) {
|
|
142
|
+
return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang) });
|
|
143
|
+
}
|
|
144
|
+
const shape = cardShapeOf(facts, frame !== null && frame !== undefined && offeredModeFromLabels(frame) === 'three_choice');
|
|
145
|
+
const noteRef = planReviewCardNoteRef(facts);
|
|
146
|
+
return Object.freeze({ facts, options: OPTIONS_BY_SHAPE[shape], ...(noteRef !== undefined ? { note: renderMessage(noteRef, lang) } : {}) });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang) });
|
|
91
150
|
}
|
|
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
151
|
}
|
|
98
152
|
export function notePlanReviewAnsweredIfDecisive(questionId, answer) {
|
|
99
153
|
notePlanReviewAnsweredIfDecisiveFor(DEFAULT_SESSION_KEY, questionId, answer);
|
|
100
154
|
}
|
|
101
155
|
function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
|
|
102
|
-
|
|
156
|
+
const decided = planReviewDecisionFromAnswer(answer ?? { answers: [] }, planReviewShownLabelsOf(questionId) ?? ANY_CARD_SHAPE_LABELS);
|
|
157
|
+
if (decided === 'dismissed' || decided === 'inadmissible')
|
|
103
158
|
return;
|
|
104
159
|
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
105
160
|
const taskId = typeof questionId === 'string' ? taskIdFromPlanReviewQuestionId(questionId) : undefined;
|
|
@@ -110,27 +165,7 @@ function notePlanReviewAnsweredIfDecisiveFor(sessionKey, questionId, answer) {
|
|
|
110
165
|
return;
|
|
111
166
|
notePlanReviewDecisiveAnswerFor(sessionKey, taskId);
|
|
112
167
|
}
|
|
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
|
-
}
|
|
168
|
+
export { isPlanReviewPark };
|
|
134
169
|
function pausedGateKindOf(carrier) {
|
|
135
170
|
try {
|
|
136
171
|
const read = readRunTerminal(carrier);
|
|
@@ -143,15 +178,16 @@ function pausedGateKindOf(carrier) {
|
|
|
143
178
|
return undefined;
|
|
144
179
|
}
|
|
145
180
|
}
|
|
146
|
-
const
|
|
147
|
-
const offerModeAfterByTask = new Map();
|
|
181
|
+
const armedPlanReviews = new Map();
|
|
148
182
|
const offeredModeByQuestionId = new Map();
|
|
149
183
|
const OFFERED_MODE_LEDGER_MAX = 64;
|
|
150
184
|
export function _resetArmedPlanReviewsForTest() {
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
engineRefusedGateIds.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,
|
|
522
|
+
void decidePlanReviewFor(taskId, decided, modeAfter, cardDelivery, 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) {
|
|
@@ -441,7 +534,7 @@ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
|
|
|
441
534
|
switch (effect) {
|
|
442
535
|
case 'took_effect':
|
|
443
536
|
return decision === 'approve'
|
|
444
|
-
? `
|
|
537
|
+
? `The result of the resumed task ${JSON.stringify(taskId)} arrives in its completion notification; when it arrives, give the user a brief report of what was done.`
|
|
445
538
|
: 'Briefly acknowledge to the user that the plan was rejected and continue planning.';
|
|
446
539
|
case 'advanced':
|
|
447
540
|
return `Tell the user the ${decision} went through and that another approval is now waiting for them; do not report the task as finished.`;
|
|
@@ -450,9 +543,9 @@ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
|
|
|
450
543
|
case 'not_sent':
|
|
451
544
|
return `Nothing was sent to the engine. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}; the plan is still waiting for a decision.`;
|
|
452
545
|
case 'not_applied':
|
|
453
|
-
return `The ${decision} did not take effect. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'};
|
|
546
|
+
return `The ${decision} did not take effect. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}; find out where task ${JSON.stringify(taskId)} stands (the background task list or its next notification shows it) before saying anything about the plan.`;
|
|
454
547
|
case 'unconfirmed':
|
|
455
|
-
return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed;
|
|
548
|
+
return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed; the background task list or the next notification of task ${JSON.stringify(taskId)} shows where it stands.`;
|
|
456
549
|
}
|
|
457
550
|
}
|
|
458
551
|
function planReviewDecisionTimedOutOutcome(decision) {
|
|
@@ -539,12 +632,39 @@ export function latestPlanReviewDecisionMark(taskId) {
|
|
|
539
632
|
export function __resetPlanReviewDecisionLatchForTests() {
|
|
540
633
|
__resetDecisionLedgerForTests('plan_review');
|
|
541
634
|
}
|
|
635
|
+
const engineRefusedGateIds = new Set();
|
|
636
|
+
const ENGINE_REFUSED_GATE_IDS_MAX = 64;
|
|
637
|
+
function noteEngineRefusedGate(id) {
|
|
638
|
+
if (engineRefusedGateIds.has(id))
|
|
639
|
+
return;
|
|
640
|
+
engineRefusedGateIds.add(id);
|
|
641
|
+
while (engineRefusedGateIds.size > ENGINE_REFUSED_GATE_IDS_MAX) {
|
|
642
|
+
const oldest = engineRefusedGateIds.values().next();
|
|
643
|
+
if (oldest.done === true)
|
|
644
|
+
break;
|
|
645
|
+
engineRefusedGateIds.delete(oldest.value);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function planReviewDecisionReasonSegment(reason, card) {
|
|
649
|
+
if (reason === undefined)
|
|
650
|
+
return '';
|
|
651
|
+
try {
|
|
652
|
+
return ` ${renderMessage(planReviewDecisionReasonRef(reason, card), DATA_FACE_LANGUAGE)}`;
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return '';
|
|
656
|
+
}
|
|
657
|
+
}
|
|
542
658
|
export async function decidePlanReview(taskId, decision, permissionModeAfter, opts) {
|
|
543
|
-
return decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
|
|
659
|
+
return decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, decisionCardOf(taskId, opts));
|
|
544
660
|
}
|
|
545
|
-
function
|
|
661
|
+
function decisionCardOf(taskId, opts) {
|
|
546
662
|
try {
|
|
547
|
-
|
|
663
|
+
if (opts === null || typeof opts !== 'object')
|
|
664
|
+
return undefined;
|
|
665
|
+
const raw = readOwnDataValue(opts, 'card');
|
|
666
|
+
const facts = raw !== null && typeof raw === 'object' ? planReviewCardFactsFrom(raw) : undefined;
|
|
667
|
+
return facts !== undefined && facts.taskId === taskId ? facts : undefined;
|
|
548
668
|
}
|
|
549
669
|
catch {
|
|
550
670
|
return undefined;
|
|
@@ -588,7 +708,17 @@ function bodySaysReparked(body) {
|
|
|
588
708
|
return false;
|
|
589
709
|
}
|
|
590
710
|
}
|
|
591
|
-
|
|
711
|
+
function askAgainReasonOf(card, opts) {
|
|
712
|
+
try {
|
|
713
|
+
const sentence = renderMessage(msg(planReviewAskAgainKey(card === undefined ? undefined : planReviewCardFactsFrom(card))), DATA_FACE_LANGUAGE);
|
|
714
|
+
const note = opts !== null && typeof opts === 'object' ? opts.reason : undefined;
|
|
715
|
+
return typeof note === 'string' && note.trim() !== '' ? renderMessage(msg('approval.planReview.askAgain.withNote', { sentence, note }), DATA_FACE_LANGUAGE) : sentence;
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
return renderMessage(msg('approval.planReview.askAgain.unknown'), DATA_FACE_LANGUAGE);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, card) {
|
|
592
722
|
if (typeof taskId === 'string' && planReviewOpsOf(taskId).some((op) => op.phase === 'posting')) {
|
|
593
723
|
hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
|
|
594
724
|
return;
|
|
@@ -619,7 +749,9 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
|
|
|
619
749
|
const dispatchNo = ++decideDispatchSeq;
|
|
620
750
|
hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
|
|
621
751
|
try {
|
|
622
|
-
|
|
752
|
+
const wireDecision = decision === 'ask_again' ? 'reject' : decision;
|
|
753
|
+
const wireOpts = decision === 'ask_again' ? { ...opts, reason: askAgainReasonOf(card, opts) } : opts;
|
|
754
|
+
await decidePlanReviewInner(taskId, wireDecision, permissionModeAfter, dispatchNo, releaseLatch, settle, wireOpts, card, decision);
|
|
623
755
|
}
|
|
624
756
|
finally {
|
|
625
757
|
releaseLatch();
|
|
@@ -640,8 +772,11 @@ function planReviewClientsFor(resolved) {
|
|
|
640
772
|
repull: () => resolved.make({ timeoutMs: 15_000 }),
|
|
641
773
|
};
|
|
642
774
|
}
|
|
643
|
-
async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts,
|
|
775
|
+
async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, card, semanticDecision) {
|
|
644
776
|
let outcome;
|
|
777
|
+
let decisionReason;
|
|
778
|
+
const gateBefore = card?.checkpointId;
|
|
779
|
+
const approvalWithheld = decision !== 'approve' ? undefined : card === undefined ? 'card_missing' : card.body.kind !== 'shown' || card.checkpointId === undefined ? 'card_withholds' : undefined;
|
|
645
780
|
let effect = 'unconfirmed';
|
|
646
781
|
let stillParkedCause;
|
|
647
782
|
let statusSeen;
|
|
@@ -653,6 +788,12 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
653
788
|
effect = 'not_sent';
|
|
654
789
|
outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid (it must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
|
|
655
790
|
}
|
|
791
|
+
else if (approvalWithheld !== undefined) {
|
|
792
|
+
hostLog('debug', `planReviewWire: not-sent decision=${decision} reason=${approvalWithheld}`);
|
|
793
|
+
effect = 'not_sent';
|
|
794
|
+
decisionReason = approvalWithheld;
|
|
795
|
+
outcome = renderMessage(msg('approval.planReview.decision.notSent', { decision }), DATA_FACE_LANGUAGE);
|
|
796
|
+
}
|
|
656
797
|
else if (resolved.kind === 'no-target') {
|
|
657
798
|
hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
|
|
658
799
|
effect = 'not_sent';
|
|
@@ -672,11 +813,24 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
672
813
|
}
|
|
673
814
|
else {
|
|
674
815
|
const client = clients.decide;
|
|
816
|
+
const checkpointId = gateBefore !== undefined && !engineRefusedGateIds.has(gateBefore) ? gateBefore : undefined;
|
|
817
|
+
const repullRow = async () => {
|
|
818
|
+
const probe = clients.repull();
|
|
819
|
+
if (!probe)
|
|
820
|
+
return undefined;
|
|
821
|
+
if (wire !== undefined) {
|
|
822
|
+
const got = await settleWithinDeadline(PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS, (signal) => probe.runs.get(taskId, { signal }));
|
|
823
|
+
if (got.kind === 'error')
|
|
824
|
+
throw new Error(got.detail);
|
|
825
|
+
if (got.kind === 'deadline')
|
|
826
|
+
throw new Error(`post-decide status re-pull passed its ${PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS} ms deadline`);
|
|
827
|
+
return got.value;
|
|
828
|
+
}
|
|
829
|
+
return (await probe.runs.get(taskId));
|
|
830
|
+
};
|
|
675
831
|
try {
|
|
676
832
|
const mode = isPlanReviewModeAfter(permissionModeAfter) ? permissionModeAfter : undefined;
|
|
677
833
|
const reason = denyReasonForWire(opts?.reason, `plan-review ${taskId}`);
|
|
678
|
-
const capsBase = resolved.kind === 'target' ? resolved.target.baseUrl : resolved.capsBaseUrl;
|
|
679
|
-
const checkpointId = gateBefore !== undefined && engineCapTrue(capsBase, 'planReviewCheckpointId') ? gateBefore : undefined;
|
|
680
834
|
const reqBody = { decision };
|
|
681
835
|
if (reason !== undefined)
|
|
682
836
|
reqBody.reason = reason;
|
|
@@ -707,20 +861,8 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
707
861
|
let gateAfter;
|
|
708
862
|
let gateKindAfter;
|
|
709
863
|
try {
|
|
710
|
-
const
|
|
711
|
-
if (
|
|
712
|
-
let post;
|
|
713
|
-
if (wire !== undefined) {
|
|
714
|
-
const got = await settleWithinDeadline(PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS, (signal) => probe.runs.get(taskId, { signal }));
|
|
715
|
-
if (got.kind === 'error')
|
|
716
|
-
throw new Error(got.detail);
|
|
717
|
-
if (got.kind === 'deadline')
|
|
718
|
-
throw new Error(`post-decide status re-pull passed its ${PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS} ms deadline`);
|
|
719
|
-
post = got.value;
|
|
720
|
-
}
|
|
721
|
-
else {
|
|
722
|
-
post = (await probe.runs.get(taskId));
|
|
723
|
-
}
|
|
864
|
+
const post = await repullRow();
|
|
865
|
+
if (post !== undefined) {
|
|
724
866
|
postStatus = bodyWordOf(post, 'status');
|
|
725
867
|
gateAfter = pausedCheckpointIdOf(post?.result);
|
|
726
868
|
gateKindAfter = pausedGateKindOf(post?.result);
|
|
@@ -793,6 +935,36 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
793
935
|
`This ${decision} was not applied: the engine no longer has a pending plan review for this task — an earlier decision most likely already went through (or the review expired). ` +
|
|
794
936
|
'This is NOT a rejection of the plan and NOT a failure of the task; the answer just given simply had nothing left to act on.';
|
|
795
937
|
}
|
|
938
|
+
else if (status === 409 && refusedCode === APPROVAL_STALE) {
|
|
939
|
+
let untrusted = false;
|
|
940
|
+
if (checkpointId !== undefined) {
|
|
941
|
+
const bag = readThrownExtra(e);
|
|
942
|
+
const noCurrentPending = bag.state === 'object' &&
|
|
943
|
+
(() => {
|
|
944
|
+
try {
|
|
945
|
+
return Object.getOwnPropertyDescriptor(bag.value, 'currentPending') === undefined;
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
})();
|
|
951
|
+
if (noCurrentPending) {
|
|
952
|
+
try {
|
|
953
|
+
const row = await repullRow();
|
|
954
|
+
untrusted = row !== undefined && pausedCheckpointIdOf(row?.result) === checkpointId;
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
untrusted = false;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (untrusted && checkpointId !== undefined)
|
|
962
|
+
noteEngineRefusedGate(checkpointId);
|
|
963
|
+
hostLog('debug', `planReviewWire: approval-stale decision=${decision} reason=${untrusted ? 'gate_untrusted' : 'gate_moved'}`);
|
|
964
|
+
effect = 'not_applied';
|
|
965
|
+
decisionReason = untrusted ? 'gate_untrusted' : 'gate_moved';
|
|
966
|
+
outcome = renderMessage(msg('approval.planReview.decision.refused', { decision }), DATA_FACE_LANGUAGE);
|
|
967
|
+
}
|
|
796
968
|
else if (checkpointUnsupportedVersionFromError(e) !== null) {
|
|
797
969
|
const cu = checkpointUnsupportedVersionFromError(e);
|
|
798
970
|
hostLog('debug', `planReviewWire: ${decision} → 409 checkpoint.unsupported_version (the server cannot continue this parked review in its stored form; still pending)`);
|
|
@@ -834,12 +1006,13 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
834
1006
|
}
|
|
835
1007
|
}
|
|
836
1008
|
}
|
|
1009
|
+
outcome = `${outcome}${planReviewDecisionReasonSegment(decisionReason, card)}`;
|
|
837
1010
|
const tail = planReviewOutcomeTail(taskId, decision, effect, stillParkedCause);
|
|
838
1011
|
const prompt = `<plan-review-outcome>\n${outcome}\n${tail}\n</plan-review-outcome>`;
|
|
839
1012
|
const onOutcome = opts?.onOutcome;
|
|
840
1013
|
settle(effect === 'not_sent' || effect === 'still_parked');
|
|
841
1014
|
try {
|
|
842
|
-
const delivered = enqueuePlanReviewOutcome(prompt, { taskId, dispatchNo, decision, effect, ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}) });
|
|
1015
|
+
const delivered = enqueuePlanReviewOutcome(prompt, { taskId, dispatchNo, decision: semanticDecision, effect, ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}) });
|
|
843
1016
|
if (!delivered) {
|
|
844
1017
|
if (onOutcome !== undefined) {
|
|
845
1018
|
hostLog('debug', `planReviewWire: outcome queue port not installed for task ${taskId} — outcome handed to the host's onOutcome callback only`);
|
|
@@ -857,7 +1030,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
|
|
|
857
1030
|
onOutcome({
|
|
858
1031
|
taskId,
|
|
859
1032
|
dispatchNo,
|
|
860
|
-
decision,
|
|
1033
|
+
decision: semanticDecision,
|
|
861
1034
|
effect,
|
|
862
1035
|
...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}),
|
|
863
1036
|
detail: outcome,
|
|
@@ -907,8 +1080,8 @@ function joinReopenFlight(flight, taskId, receiptMs) {
|
|
|
907
1080
|
return refusedReopen('not_presented');
|
|
908
1081
|
});
|
|
909
1082
|
}
|
|
910
|
-
function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
|
|
911
|
-
const flightKey = `${sessionKey}\u0000${taskId}`;
|
|
1083
|
+
function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs, taken) {
|
|
1084
|
+
const flightKey = `${sessionKey}\u0000${taskId}\u0000${taken.facts.checkpointId ?? ''}`;
|
|
912
1085
|
const inFlight = reopenFlights.get(flightKey);
|
|
913
1086
|
if (inFlight !== undefined)
|
|
914
1087
|
return joinReopenFlight(inFlight, taskId, receiptMs);
|
|
@@ -926,7 +1099,7 @@ function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
|
|
|
926
1099
|
};
|
|
927
1100
|
let verdict;
|
|
928
1101
|
try {
|
|
929
|
-
verdict = mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs);
|
|
1102
|
+
verdict = mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken);
|
|
930
1103
|
}
|
|
931
1104
|
catch (e) {
|
|
932
1105
|
settle(false);
|
|
@@ -940,7 +1113,12 @@ function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
|
|
|
940
1113
|
throw e;
|
|
941
1114
|
});
|
|
942
1115
|
}
|
|
943
|
-
function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
1116
|
+
function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
|
|
1117
|
+
if (!planReviewFactsCurrent(taken)) {
|
|
1118
|
+
hostLog('debug', `planReviewWire: reopen-superseded task=${taskId}`);
|
|
1119
|
+
return Promise.resolve(refusedReopen('not_presented'));
|
|
1120
|
+
}
|
|
1121
|
+
const facts = taken.facts;
|
|
944
1122
|
const markBefore = dismissalEntryFor(sessionKey, taskId);
|
|
945
1123
|
const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => {
|
|
946
1124
|
if (!presented)
|
|
@@ -952,58 +1130,72 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
|
|
|
952
1130
|
const armedKey = planReviewArmedKeyFor(sessionKey, taskId);
|
|
953
1131
|
const firstSight = !wasGateArmedFor(sessionKey, armedKey);
|
|
954
1132
|
const mintFresh = opts?.mintFreshQuestionId !== false;
|
|
955
|
-
|
|
1133
|
+
const armed = armedPlanReviews.get(taskId);
|
|
1134
|
+
if (!mintFresh &&
|
|
1135
|
+
armed !== undefined &&
|
|
1136
|
+
hasLocalQuestionResponder(armed.questionId) &&
|
|
1137
|
+
armed.checkpointId !== undefined &&
|
|
1138
|
+
armed.checkpointId === facts.checkpointId &&
|
|
1139
|
+
samePlanReviewCardFacts(armed.facts, facts)) {
|
|
956
1140
|
hostLog('debug', `planReviewWire: reopen for task ${taskId} reusing the canonical responder (path=reopen-canonical)`);
|
|
957
1141
|
retireActiveReopen(sessionKey, canonicalId);
|
|
958
|
-
const receipt = waitForGateArmedFor(sessionKey, [
|
|
959
|
-
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(
|
|
1142
|
+
const receipt = waitForGateArmedFor(sessionKey, [armed.questionId], receiptMs);
|
|
1143
|
+
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(armed.questionId, ARM_QUESTION, armed.shape, armed.facts));
|
|
960
1144
|
return settleOnReceipt(receipt, firstSight);
|
|
961
1145
|
}
|
|
962
1146
|
retireActiveReopen(sessionKey, canonicalId);
|
|
963
|
-
|
|
964
|
-
|
|
1147
|
+
const armedId = armed?.questionId ?? canonicalId;
|
|
1148
|
+
if (unregisterLocalQuestionResponder(armedId)) {
|
|
1149
|
+
hostLog('debug', `planReviewWire: reopen-retire-armed task=${taskId} card=${armedId}`);
|
|
965
1150
|
}
|
|
966
|
-
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId:
|
|
967
|
-
const questionId =
|
|
968
|
-
hostLog('debug', `planReviewWire: reopen
|
|
1151
|
+
publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: armedId });
|
|
1152
|
+
const questionId = freshPlanReviewQuestionId(taskId, reopenIdSuffix());
|
|
1153
|
+
hostLog('debug', `planReviewWire: path=reopen-fresh task=${taskId} card=${questionId}`);
|
|
969
1154
|
const delivery = opts?.wire !== undefined || opts?.onOutcome !== undefined
|
|
970
1155
|
? { ...(opts.wire !== undefined ? { wire: opts.wire } : {}), ...(opts.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
|
|
971
1156
|
: undefined;
|
|
972
1157
|
const deliver = opts?.deliverDecision ??
|
|
973
|
-
((tid, decision, reason) => {
|
|
1158
|
+
((tid, decision, reason, card) => {
|
|
974
1159
|
hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
|
|
975
1160
|
const withReason = reason === undefined ? delivery : { ...delivery, reason };
|
|
976
|
-
void (
|
|
1161
|
+
void decidePlanReviewFor(tid, decision, undefined, withReason, card);
|
|
977
1162
|
});
|
|
978
1163
|
const activeKey = `${sessionKey}\u0000${canonicalId}`;
|
|
979
|
-
|
|
1164
|
+
let unregister = () => { };
|
|
1165
|
+
const responder = async (_id, answer) => {
|
|
1166
|
+
const choice = planReviewAnswerChoiceOf(questionId, answer);
|
|
1167
|
+
if (choice === 'inadmissible') {
|
|
1168
|
+
if (activeReopenResponders.get(activeKey)?.questionId === questionId && !hasLocalQuestionResponder(questionId)) {
|
|
1169
|
+
unregister = registerLocalQuestionResponder(questionId, responder);
|
|
1170
|
+
}
|
|
1171
|
+
hostLog('debug', `planReviewWire: answer-inadmissible card=${questionId}`);
|
|
1172
|
+
return { ok: false, inadmissible: true };
|
|
1173
|
+
}
|
|
980
1174
|
unregister();
|
|
981
1175
|
if (activeReopenResponders.get(activeKey)?.questionId === questionId) {
|
|
982
1176
|
activeReopenResponders.delete(activeKey);
|
|
983
1177
|
}
|
|
984
|
-
const choice = planReviewAnswerChoiceOf(questionId, answer);
|
|
985
1178
|
if (choice === 'dismissed') {
|
|
986
1179
|
notePlanReviewUserDismissalFor(sessionKey, taskId, opts?.readGateInstanceKeys, questionId);
|
|
987
1180
|
return { ok: true };
|
|
988
1181
|
}
|
|
989
1182
|
const decided = choice.decision;
|
|
990
|
-
const cardReason = choice.decision === '
|
|
1183
|
+
const cardReason = choice.decision === 'approve' ? undefined : choice.reason;
|
|
991
1184
|
notePlanReviewDecisiveAnswerFor(sessionKey, taskId);
|
|
992
1185
|
notePlanReviewAnsweredFor(sessionKey, questionId);
|
|
993
1186
|
try {
|
|
994
|
-
|
|
995
|
-
await deliver(taskId, decided);
|
|
996
|
-
else
|
|
997
|
-
await deliver(taskId, decided, cardReason);
|
|
1187
|
+
await deliver(taskId, decided, cardReason, facts);
|
|
998
1188
|
}
|
|
999
1189
|
catch (e) {
|
|
1000
1190
|
hostLog('error', `planReviewWire: reopen decision delivery threw for task ${taskId}: ${String(e)}`);
|
|
1001
1191
|
}
|
|
1002
1192
|
return { ok: true };
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1193
|
+
};
|
|
1194
|
+
const shape = cardShapeOf(facts, false);
|
|
1195
|
+
unregister = registerLocalQuestionResponder(questionId, responder);
|
|
1196
|
+
activeReopenResponders.set(activeKey, { questionId, unregister: () => unregister(), shape });
|
|
1005
1197
|
const receipt = waitForGateArmedFor(sessionKey, [questionId], receiptMs);
|
|
1006
|
-
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(questionId, REOPEN_QUESTION,
|
|
1198
|
+
publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(questionId, REOPEN_QUESTION, shape, facts));
|
|
1007
1199
|
return settleOnReceipt(receipt, firstSight);
|
|
1008
1200
|
}
|
|
1009
1201
|
function refusedReopen(cause) {
|
|
@@ -1015,7 +1207,7 @@ function noPresentationSurfaceVerdict(taskId, sessionKey) {
|
|
|
1015
1207
|
`${JSON.stringify(sessionKey)} (onQuestionFrameFor), so the card has nowhere to render`);
|
|
1016
1208
|
return verdict;
|
|
1017
1209
|
}
|
|
1018
|
-
async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart) {
|
|
1210
|
+
async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart, taken) {
|
|
1019
1211
|
const refusedByUser = { reopened: false, dismissedByUser: true };
|
|
1020
1212
|
try {
|
|
1021
1213
|
let fresh = false;
|
|
@@ -1061,7 +1253,7 @@ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, ma
|
|
|
1061
1253
|
putDismissalEntry(sessionKey, taskId, reopening);
|
|
1062
1254
|
let verdict = { reopened: false };
|
|
1063
1255
|
try {
|
|
1064
|
-
verdict = await mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
|
|
1256
|
+
verdict = await mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs, taken);
|
|
1065
1257
|
}
|
|
1066
1258
|
finally {
|
|
1067
1259
|
const presented = verdict.reopened === true;
|
|
@@ -1140,15 +1332,110 @@ export function reopenPlanReviewCard(taskId, opts) {
|
|
|
1140
1332
|
'此处拒绝而不是照发 —— 照发会把这条会话的决断发到另一个槽上。');
|
|
1141
1333
|
return Promise.resolve(refusedReopen('no_delivery_path'));
|
|
1142
1334
|
}
|
|
1335
|
+
const taking = takeReopenPlanReviewFacts(taskId, opts, sessionKey);
|
|
1336
|
+
if (taking.kind === 'now')
|
|
1337
|
+
return reopenWithPlanReviewFacts(taskId, opts, sessionKey, receiptMs, recheckReadStart, taking.taken);
|
|
1338
|
+
return taking.pending.then((taken) => {
|
|
1339
|
+
if (taken === undefined) {
|
|
1340
|
+
hostLog('debug', `planReviewWire: reopen-superseded task=${taskId}`);
|
|
1341
|
+
return refusedReopen('not_presented');
|
|
1342
|
+
}
|
|
1343
|
+
if (decisionInFlightOf({ gate: 'plan_review', taskId }, recheckReadStart)) {
|
|
1344
|
+
hostLog('debug', `planReviewWire: reopen-refused-in-flight-after-read task=${taskId}`);
|
|
1345
|
+
return { reopened: false, _sema_decisionInFlight: true };
|
|
1346
|
+
}
|
|
1347
|
+
return reopenWithPlanReviewFacts(taskId, opts, sessionKey, receiptMs, recheckReadStart, taken);
|
|
1348
|
+
}, () => refusedReopen('internal_error'));
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
return Promise.resolve(refusedReopen('internal_error'));
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function takeReopenPlanReviewFacts(taskId, opts, sessionKey) {
|
|
1355
|
+
const raw = opts.checkpointId;
|
|
1356
|
+
const checkpointId = typeof raw === 'string' && raw.length > 0 ? raw : undefined;
|
|
1357
|
+
const wire = opts.mintFreshQuestionId === false ? undefined : opts.wire;
|
|
1358
|
+
return takePlanReviewFacts(sessionKey, taskId, checkpointId, () => planReviewLedgerSourceFor(wire, sessionKey));
|
|
1359
|
+
}
|
|
1360
|
+
function reopenWithPlanReviewFacts(taskId, opts, sessionKey, receiptMs, recheckReadStart, taken) {
|
|
1361
|
+
try {
|
|
1143
1362
|
const trigger = opts.trigger === undefined ? undefined : opts.trigger === 'user' ? 'user' : 'automatic';
|
|
1144
1363
|
if (trigger === 'automatic') {
|
|
1145
1364
|
const mark = planReviewUserDismissalMarkFor(sessionKey, taskId);
|
|
1146
1365
|
if (mark !== undefined)
|
|
1147
|
-
return reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart);
|
|
1366
|
+
return reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, mark, recheckReadStart, taken);
|
|
1148
1367
|
}
|
|
1149
|
-
return mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
|
|
1368
|
+
return mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs, taken);
|
|
1150
1369
|
}
|
|
1151
1370
|
catch {
|
|
1152
1371
|
return Promise.resolve(refusedReopen('internal_error'));
|
|
1153
1372
|
}
|
|
1154
1373
|
}
|
|
1374
|
+
const PLAN_REVIEW_LEDGER_CLIENT_TIMEOUT_MS = 15_000;
|
|
1375
|
+
function usableLedgerClient(client) {
|
|
1376
|
+
return clientHasVerbs(client, [['runs', 'get'], ['runs', 'events']]);
|
|
1377
|
+
}
|
|
1378
|
+
function planReviewLedgerSourceFor(wire, sessionKey) {
|
|
1379
|
+
try {
|
|
1380
|
+
const resolved = engineWireFor(wire, sessionKey, usableLedgerClient);
|
|
1381
|
+
let client;
|
|
1382
|
+
if (resolved.kind === 'injected')
|
|
1383
|
+
client = resolved.client;
|
|
1384
|
+
else if (resolved.kind === 'target')
|
|
1385
|
+
client = resolved.make({ timeoutMs: PLAN_REVIEW_LEDGER_CLIENT_TIMEOUT_MS });
|
|
1386
|
+
else
|
|
1387
|
+
client = null;
|
|
1388
|
+
if (client === null)
|
|
1389
|
+
return undefined;
|
|
1390
|
+
const c = client;
|
|
1391
|
+
return {
|
|
1392
|
+
runRow: (taskId, signal) => c.runs.get(taskId, { signal }),
|
|
1393
|
+
frames: (taskId, signal) => c.runs.events(taskId, { signal }),
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
catch {
|
|
1397
|
+
return undefined;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function abortSignalOf(v) {
|
|
1401
|
+
if (typeof v !== 'object' || v === null)
|
|
1402
|
+
return undefined;
|
|
1403
|
+
const s = v;
|
|
1404
|
+
return typeof s.aborted === 'boolean' && typeof s.addEventListener === 'function' && typeof s.removeEventListener === 'function' ? v : undefined;
|
|
1405
|
+
}
|
|
1406
|
+
export async function readPlanReviewCardFacts(taskId, opts) {
|
|
1407
|
+
if (typeof taskId !== 'string' || taskId.length === 0)
|
|
1408
|
+
return mintPlanReviewCardFacts('', undefined, unknownBody('gate_unidentified'));
|
|
1409
|
+
let checkpointId;
|
|
1410
|
+
try {
|
|
1411
|
+
checkpointId = readOwnDataNonEmptyString(opts, 'checkpointId');
|
|
1412
|
+
const wire = readOwnDataValue(opts, 'wire');
|
|
1413
|
+
const signal = abortSignalOf(readOwnDataValue(opts, 'signal'));
|
|
1414
|
+
if (signal?.aborted === true)
|
|
1415
|
+
return mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable'));
|
|
1416
|
+
const taking = takePlanReviewFacts(DEFAULT_SESSION_KEY, taskId, checkpointId, () => planReviewLedgerSourceFor(wire, DEFAULT_SESSION_KEY), signal);
|
|
1417
|
+
if (taking.kind === 'now')
|
|
1418
|
+
return taking.taken.facts;
|
|
1419
|
+
let taken;
|
|
1420
|
+
if (signal === undefined)
|
|
1421
|
+
taken = await taking.pending;
|
|
1422
|
+
else {
|
|
1423
|
+
let onAbort;
|
|
1424
|
+
const aborted = new Promise((resolve) => {
|
|
1425
|
+
onAbort = () => resolve(undefined);
|
|
1426
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1427
|
+
});
|
|
1428
|
+
try {
|
|
1429
|
+
taken = await Promise.race([taking.pending, aborted]);
|
|
1430
|
+
}
|
|
1431
|
+
finally {
|
|
1432
|
+
if (onAbort !== undefined)
|
|
1433
|
+
signal.removeEventListener('abort', onAbort);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return taken?.facts ?? mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable'));
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
return mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable'));
|
|
1440
|
+
}
|
|
1441
|
+
}
|