@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +11 -4
  3. package/dist/adapt/arms.js +14 -6
  4. package/dist/adapter/activeRunSelfHeal.d.ts +1 -0
  5. package/dist/adapter/activeRunSelfHeal.js +10 -5
  6. package/dist/adapter/downstream/eventToSdkMessage.js +4 -4
  7. package/dist/adapter/downstream/wiringManifestView.d.ts +6 -0
  8. package/dist/adapter/downstream/wiringManifestView.js +25 -0
  9. package/dist/adapter/runStream.js +5 -1
  10. package/dist/cloudConfigWireCaps.js +3 -0
  11. package/dist/compensations.js +6 -6
  12. package/dist/coreValuePorts.d.ts +3 -1
  13. package/dist/coreValuePorts.js +4 -0
  14. package/dist/displayUntrusted.d.ts +4 -2
  15. package/dist/displayUntrusted.js +45 -22
  16. package/dist/engineGenerationPort.d.ts +11 -2
  17. package/dist/engineGenerationPort.js +63 -9
  18. package/dist/engineHealthFacts.d.ts +5 -0
  19. package/dist/engineHealthFacts.js +11 -0
  20. package/dist/eventLeg.d.ts +1 -1
  21. package/dist/eventLeg.js +23 -3
  22. package/dist/generated/engineFactTables.d.ts +1 -0
  23. package/dist/generated/engineFactTables.js +1 -0
  24. package/dist/generated/sdkFactTables.d.ts +2 -0
  25. package/dist/generated/sdkFactTables.js +9 -0
  26. package/dist/headlessReconnectWire.js +31 -4
  27. package/dist/hitl/askGateWire.js +26 -2
  28. package/dist/hitl/frameRouter.d.ts +4 -0
  29. package/dist/hitl/frameRouter.js +14 -0
  30. package/dist/hitl/gateIdentity.d.ts +1 -0
  31. package/dist/hitl/gateIdentity.js +3 -0
  32. package/dist/hitl/planReviewCard.d.ts +21 -0
  33. package/dist/hitl/planReviewCard.js +229 -0
  34. package/dist/hitl/planReviewWire.d.ts +38 -13
  35. package/dist/hitl/planReviewWire.js +355 -133
  36. package/dist/hostSettingsSource.d.ts +1 -1
  37. package/dist/hostSettingsSource.js +8 -4
  38. package/dist/hostTurnSettings.d.ts +1 -1
  39. package/dist/hostTurnSettings.js +29 -10
  40. package/dist/i18n/messages.en.d.ts +50 -0
  41. package/dist/i18n/messages.en.js +50 -0
  42. package/dist/i18n/messages.zh.js +51 -0
  43. package/dist/index.d.ts +15 -3
  44. package/dist/index.js +9 -1
  45. package/dist/mcpTransportKindsCapability.d.ts +18 -0
  46. package/dist/mcpTransportKindsCapability.js +75 -0
  47. package/dist/mcpWireCaps.d.ts +4 -1
  48. package/dist/mcpWireCaps.js +24 -13
  49. package/dist/notifications.d.ts +9 -0
  50. package/dist/notifications.js +11 -2
  51. package/dist/ownKey.d.ts +4 -0
  52. package/dist/ownKey.js +8 -0
  53. package/dist/permissionModeWriteCapability.d.ts +13 -0
  54. package/dist/permissionModeWriteCapability.js +35 -0
  55. package/dist/permissionsAllowGrantCapability.d.ts +3 -0
  56. package/dist/permissionsAllowGrantCapability.js +26 -3
  57. package/dist/permissionsDeclaration.d.ts +23 -1
  58. package/dist/permissionsDeclaration.js +60 -30
  59. package/dist/planReviewCardFacts.d.ts +116 -0
  60. package/dist/planReviewCardFacts.js +469 -0
  61. package/dist/reattachWindow.d.ts +8 -1
  62. package/dist/reattachWindow.js +44 -12
  63. package/dist/runFollow.d.ts +30 -0
  64. package/dist/runFollow.js +117 -0
  65. package/dist/runLedgerSnapshot.d.ts +20 -0
  66. package/dist/runLedgerSnapshot.js +272 -0
  67. package/dist/runPermissionModeSwitch.d.ts +57 -0
  68. package/dist/runPermissionModeSwitch.js +135 -0
  69. package/dist/runReattachPolicy.d.ts +13 -5
  70. package/dist/runReattachPolicy.js +58 -23
  71. package/dist/seam.d.ts +3 -0
  72. package/dist/seam.js +1 -1
  73. package/dist/seatContract.js +1 -0
  74. package/dist/sessionMemoryErase.d.ts +1 -1
  75. package/dist/sessionMemoryErase.js +5 -0
  76. package/dist/sessionMemoryStatus.d.ts +27 -1
  77. package/dist/sessionMemoryStatus.js +117 -0
  78. package/dist/sseIdleTriage.d.ts +3 -1
  79. package/dist/sseIdleTriage.js +19 -4
  80. package/dist/subagent/engineSubagentTail.js +2 -0
  81. package/dist/subagentContentStore.d.ts +3 -0
  82. package/dist/subagentContentStore.js +10 -1
  83. package/dist/turnFailurePresentation.d.ts +5 -2
  84. package/dist/turnFailurePresentation.js +12 -4
  85. package/docs/INTEGRATION-CLIENTS.md +493 -10
  86. 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 { PLAN_REVIEW_GATE_KIND, durableDecideLegOf } from '../adapter/activeRunSelfHeal.js';
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 LEGACY_TWO_CHOICE_LABELS = Object.freeze([PLAN_REVIEW_APPROVE_LABEL, PLAN_REVIEW_REJECT_LABEL]);
31
- const THREE_CHOICE_LABELS = Object.freeze([PLAN_REVIEW_APPROVE_AUTO_LABEL, PLAN_REVIEW_APPROVE_MANUAL_LABEL, PLAN_REVIEW_REJECT_LABEL]);
32
- const ANY_CARD_SHAPE_LABELS = Object.freeze([PLAN_REVIEW_APPROVE_AUTO_LABEL, PLAN_REVIEW_APPROVE_MANUAL_LABEL, PLAN_REVIEW_APPROVE_LABEL, PLAN_REVIEW_REJECT_LABEL]);
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 === 'dismissed')
36
- return 'dismissed';
55
+ if (typeof choice === 'string')
56
+ return choice;
37
57
  return choice.decision;
38
58
  }
39
- function readDismissed() {
40
- return { choice: 'dismissed', noteIgnoredOnApprove: false };
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 !== 1)
54
- return readDismissed();
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 readDismissed();
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 readDismissed();
84
+ return readNonDecision('dismissed');
63
85
  return { choice: { decision: 'reject', reason: note }, noteIgnoredOnApprove: false };
64
86
  }
65
87
  if (picked.length !== 1)
66
- return readDismissed();
88
+ return readNonDecision('inadmissible');
67
89
  const selected = picked[0];
68
90
  if (typeof selected !== 'string' || !shownLabels.includes(selected))
69
- return readDismissed();
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
- return readDismissed();
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
- const reject = { label: REJECT_LABEL, description: 'Discard this plan and stay in plan mode to refine it' };
89
- if (!offerModeAfter) {
90
- return [{ label: APPROVE_LABEL, description: 'Resume the task now and execute the plan (runs to completion engine-side)' }, reject];
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
- if (planReviewDecisionFromAnswer(answer ?? { answers: [] }, planReviewShownLabelsOf(questionId) ?? ANY_CARD_SHAPE_LABELS) === 'dismissed')
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 function isPlanReviewPark(result) {
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 armedPlanReviewTaskIds = new Set();
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
- armedPlanReviewTaskIds.clear();
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, offerModeAfter = false) {
280
+ function planReviewQuestions(question, shape) {
245
281
  return [
246
282
  {
247
283
  header: 'Plan review',
248
284
  question,
249
- options: planReviewCardOptions(offerModeAfter),
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, offerModeAfter, reopened = false) {
255
- if (!reopened) {
256
- offeredModeByQuestionId.delete(questionId);
257
- offeredModeByQuestionId.set(questionId, offerModeAfter);
258
- while (offeredModeByQuestionId.size > OFFERED_MODE_LEDGER_MAX) {
259
- const oldest = offeredModeByQuestionId.keys().next();
260
- if (oldest.done === true)
261
- break;
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 { type: 'question', questionId, questions: planReviewQuestions(question, offerModeAfter) };
299
+ return {
300
+ type: 'question',
301
+ questionId,
302
+ questions: planReviewQuestions(question, shape),
303
+ ...(facts !== undefined ? { _sema_planReview: facts } : {}),
304
+ };
266
305
  }
267
- function publishPlanReviewCard(taskId) {
268
- publishQuestionFrame(planReviewQuestionFrame(planReviewQuestionId(taskId), ARM_QUESTION, offerModeAfterByTask.get(taskId) === true));
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
- const recorded = offeredModeByQuestionId.get(questionId);
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 ? THREE_CHOICE_LABELS : LEGACY_TWO_CHOICE_LABELS;
341
- for (const t of armedPlanReviewTaskIds) {
342
- if (planReviewQuestionId(t) === questionId)
343
- return offerModeAfterByTask.get(t) === true ? THREE_CHOICE_LABELS : LEGACY_TWO_CHOICE_LABELS;
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
- if (taskIdFromPlanReviewQuestionId(questionId) === undefined)
346
- return undefined;
347
- return questionId.includes(REOPEN_ID_TAIL) ? LEGACY_TWO_CHOICE_LABELS : undefined;
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
- if (armedPlanReviewTaskIds.has(taskId)) {
389
- if (hasLocalQuestionResponder(planReviewQuestionId(taskId))) {
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
- publishPlanReviewCard(taskId);
392
- return true;
488
+ publishArmedPlanReviewCard(prev);
489
+ return;
393
490
  }
394
- hostLog('debug', `planReviewWire: stale armed state for task ${taskId} (no local responder) — re-arming from scratch`);
395
- armedPlanReviewTaskIds.delete(taskId);
491
+ hostLog('debug', `planReviewWire: arm-retire task=${taskId} card=${prev.questionId}`);
492
+ retireArmedPlanReview(taskId, prev);
396
493
  }
397
- armedPlanReviewTaskIds.add(taskId);
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
- const offerModeAfter = opts?.submittedInPlanMode === true;
400
- if (offerModeAfter)
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
- armedPlanReviewTaskIds.delete(taskId);
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 === 'reject' ? choice.reason : undefined;
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
- publishPlanReviewCard(taskId);
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
- async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, gateBefore) {
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
- await decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, gateBefore);
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
- if (!mintFresh && hasLocalQuestionResponder(canonicalId)) {
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, [canonicalId], receiptMs);
959
- publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(canonicalId, ARM_QUESTION, offerModeAfterByTask.get(taskId) === true));
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
- if (unregisterLocalQuestionResponder(canonicalId)) {
964
- hostLog('debug', `planReviewWire: reopen retired the canonical responder for task ${taskId} (single-active discipline)`);
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: canonicalId });
967
- const questionId = `${canonicalId}${REOPEN_ID_TAIL}${reopenIdSuffix()}`;
968
- hostLog('debug', `planReviewWire: reopen for task ${taskId} minting a fresh card ${questionId} (path=reopen-fresh)`);
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 (withReason === undefined ? decidePlanReview(tid, decision) : decidePlanReview(tid, decision, undefined, withReason));
1093
+ void decidePlanReviewFor(tid, decision, undefined, withReason, parkedGateIdentityOf(withReason), facts);
977
1094
  });
978
1095
  const activeKey = `${sessionKey}\u0000${canonicalId}`;
979
- const unregister = registerLocalQuestionResponder(questionId, async (_id, answer) => {
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 === 'reject' ? choice.reason : undefined;
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
- activeReopenResponders.set(activeKey, { questionId, unregister });
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, false, true));
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
+ }