@sema-agent/client-core 0.112.0 → 0.113.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 (182) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +37 -24
  3. package/dist/adapt/arms.js +7 -1
  4. package/dist/adapt/textStream.d.ts +2 -0
  5. package/dist/adapt/textStream.js +27 -1
  6. package/dist/adapter/activeRunSelfHeal.d.ts +7 -5
  7. package/dist/adapter/activeRunSelfHeal.js +38 -66
  8. package/dist/adapter/downstream/eventToSdkMessage.js +4 -0
  9. package/dist/adapter/downstream/terminalToSdkResult.d.ts +8 -0
  10. package/dist/adapter/downstream/terminalToSdkResult.js +36 -1
  11. package/dist/adapter/runStream.d.ts +2 -12
  12. package/dist/adapter/runStream.js +46 -19
  13. package/dist/agentSession/engineAgentRegistryRead.js +2 -1
  14. package/dist/agentSession/engineSessionBackgroundStop.js +2 -2
  15. package/dist/attachmentOutcome.d.ts +35 -0
  16. package/dist/attachmentOutcome.js +192 -0
  17. package/dist/attachmentPathPlan.d.ts +67 -0
  18. package/dist/attachmentPathPlan.js +203 -0
  19. package/dist/attachmentRoute.d.ts +9 -1
  20. package/dist/attachmentRoute.js +23 -2
  21. package/dist/declared/supportFloorDeclared.d.ts +1 -1
  22. package/dist/declared/supportFloorDeclared.js +1 -0
  23. package/dist/deviceRebind.js +2 -3
  24. package/dist/displayUntrusted.js +126 -19
  25. package/dist/engineAddress.d.ts +1 -1
  26. package/dist/engineAddress.js +9 -6
  27. package/dist/engineErrorCodes.d.ts +3 -0
  28. package/dist/engineErrorCodes.js +4 -0
  29. package/dist/engineNoticeCodes.d.ts +34 -4
  30. package/dist/engineNoticeCodes.js +133 -6
  31. package/dist/engineUnavailable.d.ts +4 -2
  32. package/dist/engineUnavailable.js +99 -17
  33. package/dist/fleet/fleetLedger.d.ts +9 -3
  34. package/dist/fleet/fleetLedger.js +112 -16
  35. package/dist/fleet/fleetProjection.d.ts +5 -0
  36. package/dist/fleet/fleetProjection.js +9 -1
  37. package/dist/gateVocabulary.d.ts +7 -0
  38. package/dist/gateVocabulary.js +40 -14
  39. package/dist/generated/engineFactTables.d.ts +4 -2
  40. package/dist/generated/engineFactTables.js +198 -2
  41. package/dist/generated/engineNoticeTables.js +56 -8
  42. package/dist/generated/settingsSchemaFns.d.ts +7 -0
  43. package/dist/generated/settingsSchemaFns.js +11 -0
  44. package/dist/generated/settingsSchemaHash.d.ts +1 -0
  45. package/dist/generated/settingsSchemaHash.js +58 -0
  46. package/dist/generated/settingsSchemaOwnKeyRead.js +6 -1
  47. package/dist/generated/settingsSchemaTables.d.ts +3 -2
  48. package/dist/generated/settingsSchemaTables.js +14 -0
  49. package/dist/generated/settingsSchemaTightenVerdict.d.ts +1 -0
  50. package/dist/generated/settingsSchemaTightenVerdict.js +125 -0
  51. package/dist/headlessPermissionModeWire.d.ts +7 -2
  52. package/dist/headlessPermissionModeWire.js +77 -8
  53. package/dist/hitl/approvalDecisionDoor.d.ts +24 -0
  54. package/dist/hitl/approvalDecisionDoor.js +77 -0
  55. package/dist/hitl/approvalDelivery.d.ts +6 -0
  56. package/dist/hitl/approvalDelivery.js +57 -7
  57. package/dist/hitl/approvalResolution.d.ts +6 -3
  58. package/dist/hitl/approvalResolution.js +24 -2
  59. package/dist/hitl/approvalsFeed.js +1 -1
  60. package/dist/hitl/automaticAnswerVerdict.d.ts +1 -1
  61. package/dist/hitl/decisionSend.d.ts +7 -4
  62. package/dist/hitl/decisionSend.js +90 -41
  63. package/dist/hitl/frameRouter.d.ts +3 -3
  64. package/dist/hitl/frameRouter.js +5 -0
  65. package/dist/hitl/hitlBridge.d.ts +1 -0
  66. package/dist/hitl/hitlBridge.js +17 -4
  67. package/dist/hitl/hitlHostSurface.d.ts +3 -2
  68. package/dist/hitl/hitlHostSurface.js +5 -3
  69. package/dist/hitl/legacyApprovalShapes.d.ts +142 -0
  70. package/dist/hitl/legacyApprovalShapes.js +1 -0
  71. package/dist/hitl/livePendingAsk.d.ts +4 -4
  72. package/dist/hitl/livePendingAsk.js +5 -3
  73. package/dist/hitl/parkResolver.js +1 -1
  74. package/dist/hitl/persistedRulesWire.js +2 -2
  75. package/dist/hitl/planReviewCard.d.ts +4 -2
  76. package/dist/hitl/planReviewCard.js +66 -1
  77. package/dist/hitl/planReviewWire.d.ts +34 -2
  78. package/dist/hitl/planReviewWire.js +326 -150
  79. package/dist/hitl/sessionPolicyWire.js +16 -7
  80. package/dist/hitl/subagentSource.d.ts +1 -0
  81. package/dist/hitl/subagentSource.js +11 -0
  82. package/dist/hitl/toolApprovalWire.d.ts +34 -10
  83. package/dist/hitl/toolApprovalWire.js +182 -33
  84. package/dist/hookMatcherVerdict.d.ts +2 -1
  85. package/dist/hookMatcherVerdict.js +3 -2
  86. package/dist/hooksReaderGenerations.d.ts +8 -0
  87. package/dist/hooksReaderGenerations.js +49 -0
  88. package/dist/hooksWireCaps.js +43 -18
  89. package/dist/hostSettingsSource.d.ts +1 -1
  90. package/dist/hostSettingsSource.js +2 -2
  91. package/dist/hostTurnSettings.d.ts +2 -2
  92. package/dist/hostTurnSettings.js +87 -47
  93. package/dist/humanTurn.d.ts +2 -1
  94. package/dist/humanTurn.js +44 -13
  95. package/dist/humanTurnRead.d.ts +7 -0
  96. package/dist/humanTurnRead.js +23 -14
  97. package/dist/humanTurnSentences.js +3 -0
  98. package/dist/i18n/messages.en.d.ts +93 -5
  99. package/dist/i18n/messages.en.js +100 -12
  100. package/dist/i18n/messages.zh.js +110 -15
  101. package/dist/index.d.ts +29 -15
  102. package/dist/index.js +17 -9
  103. package/dist/liveModelCatalog.js +1 -0
  104. package/dist/liveQuestionStore.d.ts +2 -0
  105. package/dist/liveQuestionStore.js +7 -2
  106. package/dist/localConfig/kernel.d.ts +2 -1
  107. package/dist/localConfig/kernel.js +12 -18
  108. package/dist/localConfig/mcp/edit.js +25 -27
  109. package/dist/localConfig/mcp/servers.js +5 -8
  110. package/dist/localConfig/modelPool.d.ts +6 -1
  111. package/dist/localConfig/modelPool.js +104 -27
  112. package/dist/localConfig/modelPoolWords.d.ts +2 -0
  113. package/dist/localConfig/modelPoolWords.js +1 -0
  114. package/dist/localConfig/settingsWrite.js +1 -14
  115. package/dist/localConfig/skills.js +9 -16
  116. package/dist/localEngineSpawn.d.ts +29 -1
  117. package/dist/localEngineSpawn.js +62 -1
  118. package/dist/mcpLiveness.d.ts +1 -1
  119. package/dist/mcpProbeWire.js +2 -2
  120. package/dist/mcpReconnect.js +2 -2
  121. package/dist/model/catalogLoader.js +3 -17
  122. package/dist/model/onboardJudgement.d.ts +8 -0
  123. package/dist/model/onboardJudgement.js +8 -3
  124. package/dist/model/onboardSteps.d.ts +19 -0
  125. package/dist/model/onboardSteps.js +76 -0
  126. package/dist/notifications.js +20 -12
  127. package/dist/ownKey.d.ts +4 -0
  128. package/dist/ownKey.js +61 -0
  129. package/dist/pathShape.d.ts +14 -0
  130. package/dist/pathShape.js +44 -0
  131. package/dist/planReviewCardFacts.d.ts +3 -0
  132. package/dist/planReviewCardFacts.js +20 -3
  133. package/dist/planReviewOutcomeCause.d.ts +2 -0
  134. package/dist/planReviewOutcomeCause.js +12 -0
  135. package/dist/pluginHooksWire.d.ts +20 -8
  136. package/dist/pluginHooksWire.js +204 -86
  137. package/dist/prepareRefusal.d.ts +2 -0
  138. package/dist/prepareRefusal.js +11 -1
  139. package/dist/request/taskRequest.js +3 -1
  140. package/dist/resultFrameKeys.d.ts +1 -0
  141. package/dist/resultFrameKeys.js +3 -0
  142. package/dist/resumeRefusalCopy.js +3 -1
  143. package/dist/retryStatus.d.ts +11 -1
  144. package/dist/retryStatus.js +15 -4
  145. package/dist/runTerminal.d.ts +1 -0
  146. package/dist/runTerminal.js +15 -1
  147. package/dist/sdkWireTransit.d.ts +1 -1
  148. package/dist/seatContract.d.ts +12 -3
  149. package/dist/seatContract.js +24 -13
  150. package/dist/sessionEngine.js +1 -1
  151. package/dist/sessionHooksRoster.js +8 -6
  152. package/dist/sessionIdSegment.d.ts +0 -1
  153. package/dist/sessionIdSegment.js +1 -3
  154. package/dist/sessionMemoryErase.js +2 -2
  155. package/dist/sessionMemoryStatus.js +2 -1
  156. package/dist/sessionOccupant.d.ts +29 -0
  157. package/dist/sessionOccupant.js +78 -0
  158. package/dist/settingsHooksSalvage.d.ts +4 -1
  159. package/dist/settingsHooksSalvage.js +22 -9
  160. package/dist/skillOverrideChain.d.ts +3 -0
  161. package/dist/skillOverrideChain.js +31 -4
  162. package/dist/skillsWireCaps.d.ts +16 -2
  163. package/dist/skillsWireCaps.js +47 -20
  164. package/dist/subagent/agentStateDetail.js +32 -8
  165. package/dist/subagent/engineCompactWire.js +48 -36
  166. package/dist/subagent/engineDelegatedPrompt.d.ts +3 -2
  167. package/dist/subagent/engineDelegatedPrompt.js +65 -28
  168. package/dist/subagentEditOutcomes.d.ts +9 -0
  169. package/dist/subagentEditOutcomes.js +43 -0
  170. package/dist/supportFloor.d.ts +2 -1
  171. package/dist/supportFloor.js +1 -1
  172. package/dist/toolResult.js +4 -2
  173. package/dist/toolRoster.d.ts +1 -0
  174. package/dist/toolRoster.js +1 -0
  175. package/dist/usageAnalytics.js +2 -1
  176. package/dist/userAttachmentEnvelope.d.ts +11 -0
  177. package/dist/userAttachmentEnvelope.js +7 -2
  178. package/dist/userAttachmentSeat.d.ts +7 -1
  179. package/dist/userAttachmentSeat.js +44 -7
  180. package/dist/wiringInstructionsAttribution.js +8 -2
  181. package/docs/INTEGRATION-CLIENTS.md +1654 -16
  182. package/package.json +5 -5
@@ -1,5 +1,6 @@
1
- import { publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionResponder, unregisterLocalQuestionResponder, hasLocalQuestionResponder, hasQuestionOverlayFor, } from '../liveQuestionStore.js';
1
+ import { deliverQuestionFrameFor, publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionResponder, unregisterLocalQuestionResponder, hasLocalQuestionResponder, hasQuestionOverlayFor, } from '../liveQuestionStore.js';
2
2
  import { hostLog } from '../host.js';
3
+ import { displayUntrusted } from '../displayUntrusted.js';
3
4
  import { invokeHostCallbackIsolated } from '../hostCallbackGuard.js';
4
5
  import { abortableSleep, settleWithinDeadline } from '../abortableSleep.js';
5
6
  import { clientHasVerbs, engineWireFor, noAnswerKindOf, sendOnce } from '../engineWireFor.js';
@@ -8,16 +9,18 @@ import { enqueuePlanReviewOutcome } from '../notifications.js';
8
9
  import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
9
10
  import { decideFailureNote } from '../decideFailureNote.js';
10
11
  import { readThrownExtra, wireFailureTextsOf } from '../wireFailureShape.js';
11
- import { appendOwnElement, readOwnDataNonEmptyString, readOwnDataValue } from '../ownKey.js';
12
+ import { appendOwnElement, guardedRead, hostPresenceOf, readOwnDataNonEmptyString, readOwnDataValue } from '../ownKey.js';
12
13
  import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError, resumeContextUnavailableContent, resumeContextUnavailableFromError } from '../resumeRefusalCopy.js';
13
14
  import { APPROVAL_STALE, REQUEST_FIELD_CONFLICT } from '../engineErrorCodes.js';
14
- import { refusalDeliveryOf } from './decisionRefusal.js';
15
+ import { readDecisionRefusalReading, refusalDeliveryOf } from './decisionRefusal.js';
16
+ import { decisionRefusalWayOutContent } from '../checkpointRowVerdict.js';
15
17
  import { directFailureVerdictOf, directSafetyStopOf } from './approvalResolution.js';
16
18
  import { __resetDecisionLedgerForTests, decisionInFlightOf, decisionKeyInFlight, decisionLedgerKeysOf, decisionOpsWithKey, decisionReadMark, dropDecisionOp, latestDecisionHandedMark, markLatestDecisionNotApplied, openDecisionOp, planDecisionKey, readMarkOf, settleDecisionOp, settleDecisionOpsOnGateLeft, } from './decisionLedger.js';
17
19
  import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
18
20
  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';
21
+ import { isPlanReviewPark, mintPlanReviewCardFacts, noteCardGateHandedOver, notePlanReviewCardIssued, pausedCheckpointIdOf, planReviewCardFactsFrom, planReviewCardReplacesEarlier, planReviewFactsCurrent, registeredPlanReviewCardFacts, samePlanReviewCardFacts, unknownBody, _resetPlanReviewRunsForTest, } from '../planReviewCardFacts.js';
20
22
  import { planReviewAskAgainKey, planReviewCardNoteRef, planReviewDecisionReasonRef, planReviewUnreadableNoteRef, takePlanReviewFacts, _resetPlanReviewFactsFlightsForTest, } from './planReviewCard.js';
23
+ import { planReviewOutcomeCauseOf } from '../planReviewOutcomeCause.js';
21
24
  import { EN_MESSAGES } from '../i18n/messages.en.js';
22
25
  import { msg, renderMessage, DATA_FACE_LANGUAGE } from '../i18n/translate.js';
23
26
  import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
@@ -132,6 +135,14 @@ function cardShapeOf(facts, submittedInPlanMode) {
132
135
  }
133
136
  return submittedInPlanMode ? 'three_choice' : 'two_choice';
134
137
  }
138
+ const PLAN_REVIEW_HEADING_GATE_MAX = 120;
139
+ function planReviewHeadingOf(facts, replaces, lang) {
140
+ const gate = facts?.checkpointId === undefined ? '' : displayUntrusted(facts.checkpointId, { max: PLAN_REVIEW_HEADING_GATE_MAX });
141
+ if (gate === '')
142
+ return renderMessage(msg('approval.planReview.heading.plain'), lang);
143
+ const head = msg('approval.planReview.heading.gate', { gate });
144
+ return renderMessage(replaces ? msg('approval.planReview.heading.replaces', { head }) : head, lang);
145
+ }
135
146
  export function planReviewCardViewOf(frame, lang) {
136
147
  try {
137
148
  const questionId = readOwnDataNonEmptyString(frame ?? undefined, 'questionId');
@@ -140,14 +151,20 @@ export function planReviewCardViewOf(frame, lang) {
140
151
  return undefined;
141
152
  const facts = planReviewCardFactsFrom(readOwnDataValue(frame ?? undefined, '_sema_planReview'));
142
153
  if (facts === undefined || facts.taskId !== taskId) {
143
- return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang) });
154
+ return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang), heading: planReviewHeadingOf(undefined, false, lang) });
144
155
  }
145
156
  const shape = cardShapeOf(facts, frame !== null && frame !== undefined && offeredModeFromLabels(frame) === 'three_choice');
146
157
  const noteRef = planReviewCardNoteRef(facts);
147
- return Object.freeze({ facts, options: OPTIONS_BY_SHAPE[shape], ...(noteRef !== undefined ? { note: renderMessage(noteRef, lang) } : {}) });
158
+ const replaces = readOwnDataValue(frame ?? undefined, '_sema_planReviewReplaces') === true;
159
+ return Object.freeze({
160
+ facts,
161
+ options: OPTIONS_BY_SHAPE[shape],
162
+ ...(noteRef !== undefined ? { note: renderMessage(noteRef, lang) } : {}),
163
+ heading: planReviewHeadingOf(facts, replaces, lang),
164
+ });
148
165
  }
149
166
  catch {
150
- return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang) });
167
+ return Object.freeze({ options: OPTIONS_BY_SHAPE.withheld, note: renderMessage(planReviewUnreadableNoteRef(), lang), heading: planReviewHeadingOf(undefined, false, lang) });
151
168
  }
152
169
  }
153
170
  export function planReviewEvidenceMovedOf(shown, latest) {
@@ -304,7 +321,7 @@ function planReviewQuestions(question, shape) {
304
321
  },
305
322
  ];
306
323
  }
307
- function planReviewQuestionFrame(questionId, question, shape, facts) {
324
+ function planReviewQuestionFrame(questionId, question, shape, facts, sessionKey = DEFAULT_SESSION_KEY) {
308
325
  offeredModeByQuestionId.delete(questionId);
309
326
  offeredModeByQuestionId.set(questionId, shape);
310
327
  while (offeredModeByQuestionId.size > OFFERED_MODE_LEDGER_MAX) {
@@ -318,10 +335,17 @@ function planReviewQuestionFrame(questionId, question, shape, facts) {
318
335
  questionId,
319
336
  questions: planReviewQuestions(question, shape),
320
337
  ...(facts !== undefined ? { _sema_planReview: facts } : {}),
338
+ ...(facts !== undefined && planReviewCardReplacesEarlier(sessionKey, facts) ? { _sema_planReviewReplaces: true } : {}),
321
339
  };
322
340
  }
341
+ function deliverPlanReviewCard(sessionKey, frame, facts) {
342
+ if (deliverQuestionFrameFor(sessionKey, frame) !== 'delivered')
343
+ return;
344
+ if (facts !== undefined)
345
+ noteCardGateHandedOver(sessionKey, facts.taskId, facts.checkpointId);
346
+ }
323
347
  function publishArmedPlanReviewCard(inst) {
324
- publishQuestionFrame(planReviewQuestionFrame(inst.questionId, ARM_QUESTION, inst.shape, inst.facts));
348
+ deliverPlanReviewCard(DEFAULT_SESSION_KEY, planReviewQuestionFrame(inst.questionId, ARM_QUESTION, inst.shape, inst.facts, DEFAULT_SESSION_KEY), inst.facts);
325
349
  }
326
350
  function retireArmedPlanReview(taskId, inst) {
327
351
  if (armedPlanReviews.get(taskId) === inst)
@@ -445,9 +469,7 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
445
469
  else if (!engineWireTarget()) {
446
470
  return false;
447
471
  }
448
- const delivery = wire !== undefined || opts?.onOutcome !== undefined
449
- ? { ...(wire !== undefined ? { wire } : {}), ...(opts?.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
450
- : undefined;
472
+ const delivery = planReviewDeliveryFrom(wire, opts?.onOutcome, opts?.onAccepted);
451
473
  const { taskId } = result;
452
474
  const gateBefore = pausedCheckpointIdOf(result);
453
475
  if (gateBefore === undefined) {
@@ -463,12 +485,12 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
463
485
  void taking.pending.then((taken) => {
464
486
  try {
465
487
  if (taken !== undefined && planReviewFactsCurrent(taken)) {
466
- presentArmedPlanReview(taken.facts, delivery);
488
+ presentAfterAwait(taken.facts, delivery);
467
489
  return;
468
490
  }
469
491
  const current = registeredPlanReviewCardFacts(DEFAULT_SESSION_KEY, taskId, gateBefore);
470
492
  if (current !== undefined) {
471
- presentArmedPlanReview(current, delivery);
493
+ presentAfterAwait(current, delivery);
472
494
  return;
473
495
  }
474
496
  hostLog('debug', `planReviewWire: arm-superseded task=${taskId}`);
@@ -487,7 +509,17 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
487
509
  hostLog('debug', `planReviewWire: arm failed (fail-soft): ${String(e)}`);
488
510
  return false;
489
511
  }
490
- function presentArmedPlanReview(facts, delivery) {
512
+ function presentAfterAwait(facts, delivery) {
513
+ if (!isPlanReviewPark(result))
514
+ return;
515
+ const presence = publishPresenceOf(opts);
516
+ if (presence !== 'here') {
517
+ hostLog('debug', `planReviewWire: arm-withheld-${presence} task=${result.taskId}`);
518
+ return;
519
+ }
520
+ presentArmedPlanReview(facts, delivery, () => publishPresenceOf(opts) === 'here');
521
+ }
522
+ function presentArmedPlanReview(facts, delivery, recheckHere) {
491
523
  if (!isPlanReviewPark(result))
492
524
  return;
493
525
  const { taskId } = result;
@@ -501,6 +533,10 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
501
533
  prev.checkpointId !== undefined &&
502
534
  prev.checkpointId === gateBefore &&
503
535
  samePlanReviewCardFacts(prev.facts, facts)) {
536
+ if (recheckHere !== undefined && !recheckHere()) {
537
+ hostLog('debug', `planReviewWire: arm-withheld-after-retire task=${taskId}`);
538
+ return;
539
+ }
504
540
  hostLog('debug', `planReviewWire: arm replayed — re-presenting the still-armed card for task ${taskId}`);
505
541
  publishArmedPlanReviewCard(prev);
506
542
  return;
@@ -508,6 +544,10 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
508
544
  hostLog('debug', `planReviewWire: arm-retire task=${taskId} card=${prev.questionId}`);
509
545
  retireArmedPlanReview(taskId, prev);
510
546
  }
547
+ if (recheckHere !== undefined && !recheckHere()) {
548
+ hostLog('debug', `planReviewWire: arm-withheld-after-retire task=${taskId}`);
549
+ return;
550
+ }
511
551
  const issuedBefore = notePlanReviewCardIssued(DEFAULT_SESSION_KEY, taskId);
512
552
  const questionId = issuedBefore || hadArmedInstance ? freshPlanReviewQuestionId(taskId, reopenIdSuffix()) : planReviewQuestionId(taskId);
513
553
  const instance = Object.freeze({ questionId, ...(gateBefore !== undefined ? { checkpointId: gateBefore } : {}), facts, shape });
@@ -569,6 +609,11 @@ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
569
609
  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.`;
570
610
  }
571
611
  }
612
+ function planReviewDeliveryFrom(wire, onOutcome, onAccepted) {
613
+ if (wire === undefined && onOutcome === undefined && onAccepted === undefined)
614
+ return undefined;
615
+ return { ...(wire !== undefined ? { wire } : {}), ...(onOutcome !== undefined ? { onOutcome } : {}), ...(onAccepted !== undefined ? { onAccepted } : {}) };
616
+ }
572
617
  function planReviewDecisionTimedOutOutcome(decision) {
573
618
  return (`The plan_review ${decision} was sent, but no answer came back from the engine within the time limit this host allows for it — ` +
574
619
  `it may have taken effect (${decision === 'approve' ? 'an approval keeps the engine busy until the resumed work finishes' : 'the engine may still be applying it'}).`);
@@ -666,11 +711,34 @@ function noteEngineRefusedGate(id) {
666
711
  engineRefusedGateIds.delete(oldest.value);
667
712
  }
668
713
  }
714
+ function wireGateOf(decision, gate) {
715
+ if (gate === undefined)
716
+ return undefined;
717
+ if (decision === 'approve')
718
+ return gate;
719
+ return engineRefusedGateIds.has(gate) ? undefined : gate;
720
+ }
721
+ function planReviewWireBodyOf(decision, reason, mode, gate) {
722
+ const withReason = reason !== undefined ? { reason } : {};
723
+ const withMode = mode !== undefined ? { permissionModeAfter: mode } : {};
724
+ if (decision === 'approve')
725
+ return gate === undefined ? undefined : Object.freeze({ decision, ...withReason, ...withMode, checkpointId: gate });
726
+ return Object.freeze({ decision, ...withReason, ...withMode, ...(gate !== undefined ? { checkpointId: gate } : {}) });
727
+ }
669
728
  function planReviewDecisionReasonSegment(reason, card) {
670
729
  if (reason === undefined)
671
730
  return '';
672
731
  try {
673
- return ` ${renderMessage(planReviewDecisionReasonRef(reason, card), DATA_FACE_LANGUAGE)}`;
732
+ const ref = planReviewDecisionReasonRef(reason, card);
733
+ return ref === undefined ? '' : ` ${renderMessage(ref, DATA_FACE_LANGUAGE)}`;
734
+ }
735
+ catch {
736
+ return '';
737
+ }
738
+ }
739
+ function planReviewSentWithoutGateSegment() {
740
+ try {
741
+ return ` ${renderMessage(msg('approval.planReview.decision.sentWithoutGateId'), DATA_FACE_LANGUAGE)}`;
674
742
  }
675
743
  catch {
676
744
  return '';
@@ -723,13 +791,43 @@ function bodyWordOf(body, key) {
723
791
  return undefined;
724
792
  }
725
793
  }
726
- function bodySaysReparked(body) {
727
- try {
728
- return body !== null && typeof body === 'object' && body.retriable === true;
729
- }
730
- catch {
731
- return false;
732
- }
794
+ function planReviewAcceptanceOf(body) {
795
+ const obj = body !== null && typeof body === 'object' ? body : undefined;
796
+ const str = (key) => (obj === undefined ? undefined : guardedRead(() => readOwnDataNonEmptyString(obj, key)));
797
+ const isTrue = (key) => obj !== undefined && guardedRead(() => readOwnDataValue(obj, key)) === true;
798
+ const taskId = str('taskId');
799
+ const sessionId = str('sessionId');
800
+ const status = str('status');
801
+ const checkpointId = str('checkpointId');
802
+ const engine = Object.freeze({
803
+ ...(taskId !== undefined ? { taskId } : {}),
804
+ ...(sessionId !== undefined ? { sessionId } : {}),
805
+ ...(status !== undefined ? { status } : {}),
806
+ ...(isTrue('decisionCommitted') ? { decisionCommitted: true } : {}),
807
+ ...(checkpointId !== undefined ? { checkpointId } : {}),
808
+ });
809
+ const errorCode = str('errorCode');
810
+ return Object.freeze({ engine, reparked: isTrue('retriable'), ...(errorCode !== undefined ? { errorCode } : {}) });
811
+ }
812
+ function acceptanceSettled(a) {
813
+ return a !== undefined && a.engine.decisionCommitted === true && !a.reparked;
814
+ }
815
+ function planReviewAcceptedPayloadOf(taskId, dispatchNo, semanticDecision, cardId, a, wireGate, card) {
816
+ const body = card?.body;
817
+ const decided = acceptanceSettled(a) && semanticDecision !== 'ask_again' && wireGate !== undefined && body !== undefined && body.kind === 'shown'
818
+ ? Object.freeze({ verdict: semanticDecision === 'approve' ? 'approved' : 'rejected', plan: body.markdown })
819
+ : undefined;
820
+ return Object.freeze({
821
+ taskId,
822
+ dispatchNo,
823
+ decision: semanticDecision,
824
+ ...(cardId !== undefined ? { questionId: cardId } : {}),
825
+ engine: a.engine,
826
+ ...(decided !== undefined ? { decided } : {}),
827
+ });
828
+ }
829
+ function planReviewDeliveryOf(a, semanticDecision) {
830
+ return acceptanceSettled(a) && semanticDecision !== 'approve' ? 'follow' : 'submit';
733
831
  }
734
832
  function askAgainReasonOf(card, opts) {
735
833
  try {
@@ -741,9 +839,17 @@ function askAgainReasonOf(card, opts) {
741
839
  return renderMessage(msg('approval.planReview.askAgain.unknown'), DATA_FACE_LANGUAGE);
742
840
  }
743
841
  }
842
+ function planReviewDecisionWordOf(raw) {
843
+ return raw === 'approve' || raw === 'reject' || raw === 'ask_again' ? raw : undefined;
844
+ }
744
845
  async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, card, cardId) {
846
+ const word = planReviewDecisionWordOf(decision);
847
+ if (word === undefined) {
848
+ hostLog('error', `planReviewWire: decision-word-refused typeof=${typeof decision}`);
849
+ return;
850
+ }
745
851
  if (typeof taskId === 'string' && planReviewOpsOf(taskId).some((op) => op.phase === 'posting')) {
746
- hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
852
+ hostLog('error', `planReviewWire: decidePlanReview(${word}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
747
853
  return;
748
854
  }
749
855
  let op = typeof taskId === 'string' ? claimablePlanReviewDecisionOp(taskId) : undefined;
@@ -770,11 +876,11 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
770
876
  settleDecisionOp(mine, 'package');
771
877
  };
772
878
  const dispatchNo = ++decideDispatchSeq;
773
- hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
879
+ hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${word} mode=${permissionModeAfter ?? '-'}`);
774
880
  try {
775
- const wireDecision = decision === 'ask_again' ? 'reject' : decision;
776
- const wireOpts = decision === 'ask_again' ? { ...opts, reason: askAgainReasonOf(card, opts) } : opts;
777
- await decidePlanReviewInner(taskId, wireDecision, permissionModeAfter, dispatchNo, releaseLatch, settle, wireOpts, card, decision, cardId);
881
+ const wireDecision = word === 'ask_again' ? 'reject' : word;
882
+ const wireOpts = word === 'ask_again' ? { ...opts, reason: askAgainReasonOf(card, opts) } : opts;
883
+ await decidePlanReviewInner(taskId, wireDecision, permissionModeAfter, dispatchNo, releaseLatch, settle, wireOpts, card, word, cardId);
778
884
  }
779
885
  finally {
780
886
  releaseLatch();
@@ -797,14 +903,17 @@ function planReviewClientsFor(resolved) {
797
903
  }
798
904
  async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, card, semanticDecision, cardId) {
799
905
  let outcome;
800
- let decisionReason;
906
+ let cause;
907
+ let unsentProven = false;
908
+ let acceptance;
909
+ let wayOut;
801
910
  const gateBefore = card?.checkpointId;
802
911
  const approvalWithheld = decision !== 'approve' ? undefined : card === undefined ? 'card_missing' : card.body.kind !== 'shown' || card.checkpointId === undefined ? 'card_withholds' : undefined;
803
912
  let effect = 'unconfirmed';
804
- let stillParkedCause;
805
913
  let statusSeen;
806
914
  const wire = opts?.wire;
807
915
  const resolved = engineWireFor(wire, DEFAULT_SESSION_KEY, usablePlanReviewWireClient);
916
+ const wireGate = wireGateOf(decision, gateBefore);
808
917
  const badMode = permissionModeAfter !== undefined && !isPlanReviewModeAfter(permissionModeAfter);
809
918
  if (badMode) {
810
919
  hostLog('error', `planReviewWire: ${decision} NOT sent — permissionModeAfter must be "default" or "acceptEdits"`);
@@ -814,7 +923,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
814
923
  else if (approvalWithheld !== undefined) {
815
924
  hostLog('debug', `planReviewWire: not-sent decision=${decision} reason=${approvalWithheld}`);
816
925
  effect = 'not_sent';
817
- decisionReason = approvalWithheld;
926
+ cause = approvalWithheld;
818
927
  outcome = renderMessage(msg('approval.planReview.decision.notSent', { decision }), DATA_FACE_LANGUAGE);
819
928
  }
820
929
  else if (resolved.kind === 'no-target') {
@@ -836,7 +945,6 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
836
945
  }
837
946
  else {
838
947
  const client = clients.decide;
839
- const checkpointId = gateBefore !== undefined && !engineRefusedGateIds.has(gateBefore) ? gateBefore : undefined;
840
948
  const repullRow = async () => {
841
949
  const probe = clients.repull();
842
950
  if (!probe)
@@ -854,87 +962,97 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
854
962
  try {
855
963
  const mode = isPlanReviewModeAfter(permissionModeAfter) ? permissionModeAfter : undefined;
856
964
  const reason = denyReasonForWire(opts?.reason, `plan-review ${taskId}`);
857
- const reqBody = { decision };
858
- if (reason !== undefined)
859
- reqBody.reason = reason;
860
- if (mode !== undefined)
861
- reqBody.permissionModeAfter = mode;
862
- if (checkpointId !== undefined)
863
- reqBody.checkpointId = checkpointId;
864
- let body;
865
- try {
866
- body = await sendOnce(undefined, () => client.assistant.planReview(taskId, reqBody));
965
+ const wireBody = planReviewWireBodyOf(decision, reason, mode, wireGate);
966
+ if (wireBody === undefined) {
967
+ effect = 'not_sent';
968
+ cause = 'card_withholds';
969
+ outcome = renderMessage(msg('approval.planReview.decision.notSent', { decision }), DATA_FACE_LANGUAGE);
867
970
  }
868
- catch (e1) {
869
- const conflict = e1;
870
- const defaultConflict = mode === 'default' && conflict?.status === 400 && conflict?.errorCode === REQUEST_FIELD_CONFLICT;
871
- if (defaultConflict && !directFailureVerdictOf(e1, directSafetyStopOf(e1)).yields) {
872
- hostLog('debug', 'planReviewWire: permissionModeAfter "default" refused as not applicable — re-sending the approval without the key (same meaning)');
873
- body = await sendOnce(undefined, () => client.assistant.planReview(taskId, { decision, ...(reason !== undefined ? { reason } : {}), ...(checkpointId !== undefined ? { checkpointId } : {}) }));
971
+ else {
972
+ const firstSent = { ...wireBody };
973
+ let body;
974
+ try {
975
+ body = await sendOnce(undefined, () => client.assistant.planReview(taskId, firstSent));
874
976
  }
875
- else {
876
- throw e1;
977
+ catch (e1) {
978
+ const conflict = e1;
979
+ const defaultConflict = mode === 'default' && conflict?.status === 400 && conflict?.errorCode === REQUEST_FIELD_CONFLICT;
980
+ if (defaultConflict && !directFailureVerdictOf(e1, directSafetyStopOf(e1)).yields) {
981
+ hostLog('debug', 'planReviewWire: permissionModeAfter "default" refused as not applicable — re-sending the approval without the key (same meaning)');
982
+ const { permissionModeAfter: _modeDropped, ...resend } = wireBody;
983
+ body = await sendOnce(undefined, () => client.assistant.planReview(taskId, resend));
984
+ }
985
+ else {
986
+ throw e1;
987
+ }
877
988
  }
878
- }
879
- finally {
880
- releaseLatch();
881
- }
882
- hostLog('debug', `planReviewWire: ${decision} → ok (dispatch #${dispatchNo}) ${JSON.stringify(body).slice(0, 200)}`);
883
- let postStatus;
884
- let gateAfter;
885
- let gateKindAfter;
886
- try {
887
- const post = await repullRow();
888
- if (post !== undefined) {
889
- postStatus = bodyWordOf(post, 'status');
890
- gateAfter = pausedCheckpointIdOf(post?.result);
891
- gateKindAfter = pausedGateKindOf(post?.result);
989
+ finally {
990
+ releaseLatch();
991
+ }
992
+ const accepted = planReviewAcceptanceOf(body);
993
+ acceptance = accepted;
994
+ hostLog('debug', `planReviewWire: ${decision} → ok (dispatch #${dispatchNo}) ${JSON.stringify(accepted).slice(0, 200)}`);
995
+ const onAccepted = opts?.onAccepted;
996
+ if (onAccepted !== undefined) {
997
+ const payload = planReviewAcceptedPayloadOf(taskId, dispatchNo, semanticDecision, cardId, accepted, wireGate, card);
998
+ invokeHostCallbackIsolated(`planReviewWire: onAccepted task=${taskId}`, () => onAccepted(payload));
999
+ }
1000
+ let postStatus;
1001
+ let gateAfter;
1002
+ let gateKindAfter;
1003
+ try {
1004
+ const post = await repullRow();
1005
+ if (post !== undefined) {
1006
+ postStatus = bodyWordOf(post, 'status');
1007
+ gateAfter = pausedCheckpointIdOf(post?.result);
1008
+ gateKindAfter = pausedGateKindOf(post?.result);
1009
+ }
1010
+ }
1011
+ catch (e) {
1012
+ hostLog('debug', `planReviewWire: post-decide status re-pull failed (fail-soft): ${String(e)}`);
1013
+ }
1014
+ const bodyStatus = accepted.engine.status;
1015
+ const effective = postStatus ?? bodyStatus;
1016
+ statusSeen = effective;
1017
+ effect = planReviewSettledVerdict({ decision, postStatus, bodyStatus, reparked: accepted.reparked, gateBefore, gateAfter, gateKindAfter });
1018
+ const code = accepted.errorCode;
1019
+ if (effect === 'still_parked' && postStatus === 'needs_review' && !accepted.reparked) {
1020
+ outcome =
1021
+ `The plan_review ${decision} returned HTTP 200 but the session is STILL locked on the same review gate ` +
1022
+ `(post-decide status: needs_review). The decision did NOT take effect (known engine issue, RB-471 family). ` +
1023
+ `Tell the user plainly that the ${decision} did not go through; the reliable exits today are approving the plan or cancelling the task.`;
1024
+ }
1025
+ else if (effect === 'still_parked') {
1026
+ outcome =
1027
+ `The plan_review ${decision} was NOT applied: the engine could not continue the task and put the plan back to wait for a decision` +
1028
+ `${code !== undefined ? ` (engine code: ${code})` : ''}. Tell the user plainly that the ${decision} did not go through.`;
1029
+ }
1030
+ else if (effect === 'advanced') {
1031
+ outcome = planReviewAdvancedOutcome(decision, effective === 'needs_review' ? 'needs_review' : 'suspended');
1032
+ }
1033
+ else if (effect === 'took_effect') {
1034
+ outcome =
1035
+ `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — ${effective === 'completed' ? 'final status' : 'status'}: ${effective} ` +
1036
+ `(${postStatus !== undefined ? 'post-decide re-checked: task left the review gate' : "from the decision's own reply; the post-decide re-check was unavailable"}).`;
1037
+ }
1038
+ else if (effective === 'needs_review' && (gateBefore === undefined || gateAfter === undefined || postStatus !== 'needs_review')) {
1039
+ outcome =
1040
+ `The plan_review ${decision} returned HTTP 200 and the task is waiting at a plan review gate again (post-decide status: needs_review), ` +
1041
+ `but it could not be told whether this is the same review (the ${decision} did not take effect) or a new plan written after it (it did).`;
1042
+ }
1043
+ else if (effective === 'suspended' || effective === 'needs_review') {
1044
+ outcome =
1045
+ `The plan_review ${decision} was accepted by the engine (HTTP 200) and the task is paused again (post-decide status: ${effective}), ` +
1046
+ `${gateKindAfter !== undefined ? `at a '${gateKindAfter}' pause that is not an approval card` : 'but the pause it stopped at was not reported'} — what the ${decision} led to could not be confirmed.`;
1047
+ }
1048
+ else if (effective !== undefined) {
1049
+ outcome =
1050
+ `The plan_review ${decision} was accepted by the engine (HTTP 200), but the task then reported status ${effective}` +
1051
+ `${code !== undefined ? ` (engine code: ${code})` : ''} — not a completion; what the ${decision} led to could not be confirmed.`;
1052
+ }
1053
+ else {
1054
+ outcome = `The plan_review ${decision} was accepted by the engine (HTTP 200) but its effect is unconfirmed — final status: unknown (post-decide verification unavailable). Do not treat this as ${decision === 'approve' ? 'an approval that resumed the task' : 'a discarded plan'}.`;
892
1055
  }
893
- }
894
- catch (e) {
895
- hostLog('debug', `planReviewWire: post-decide status re-pull failed (fail-soft): ${String(e)}`);
896
- }
897
- const bodyStatus = bodyWordOf(body, 'status');
898
- const effective = postStatus ?? bodyStatus;
899
- statusSeen = effective;
900
- effect = planReviewSettledVerdict({ decision, postStatus, bodyStatus, reparked: bodySaysReparked(body), gateBefore, gateAfter, gateKindAfter });
901
- const code = bodyWordOf(body, 'errorCode');
902
- if (effect === 'still_parked' && postStatus === 'needs_review' && !bodySaysReparked(body)) {
903
- outcome =
904
- `The plan_review ${decision} returned HTTP 200 but the session is STILL locked on the same review gate ` +
905
- `(post-decide status: needs_review). The decision did NOT take effect (known engine issue, RB-471 family). ` +
906
- `Tell the user plainly that the ${decision} did not go through; the reliable exits today are approving the plan or cancelling the task.`;
907
- }
908
- else if (effect === 'still_parked') {
909
- outcome =
910
- `The plan_review ${decision} was NOT applied: the engine could not continue the task and put the plan back to wait for a decision` +
911
- `${code !== undefined ? ` (engine code: ${code})` : ''}. Tell the user plainly that the ${decision} did not go through.`;
912
- }
913
- else if (effect === 'advanced') {
914
- outcome = planReviewAdvancedOutcome(decision, effective === 'needs_review' ? 'needs_review' : 'suspended');
915
- }
916
- else if (effect === 'took_effect') {
917
- outcome =
918
- `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — ${effective === 'completed' ? 'final status' : 'status'}: ${effective} ` +
919
- `(${postStatus !== undefined ? 'post-decide re-checked: task left the review gate' : "from the decision's own reply; the post-decide re-check was unavailable"}).`;
920
- }
921
- else if (effective === 'needs_review' && (gateBefore === undefined || gateAfter === undefined || postStatus !== 'needs_review')) {
922
- outcome =
923
- `The plan_review ${decision} returned HTTP 200 and the task is waiting at a plan review gate again (post-decide status: needs_review), ` +
924
- `but it could not be told whether this is the same review (the ${decision} did not take effect) or a new plan written after it (it did).`;
925
- }
926
- else if (effective === 'suspended' || effective === 'needs_review') {
927
- outcome =
928
- `The plan_review ${decision} was accepted by the engine (HTTP 200) and the task is paused again (post-decide status: ${effective}), ` +
929
- `${gateKindAfter !== undefined ? `at a '${gateKindAfter}' pause that is not an approval card` : 'but the pause it stopped at was not reported'} — what the ${decision} led to could not be confirmed.`;
930
- }
931
- else if (effective !== undefined) {
932
- outcome =
933
- `The plan_review ${decision} was accepted by the engine (HTTP 200), but the task then reported status ${effective}` +
934
- `${code !== undefined ? ` (engine code: ${code})` : ''} — not a completion; what the ${decision} led to could not be confirmed.`;
935
- }
936
- else {
937
- outcome = `The plan_review ${decision} was accepted by the engine (HTTP 200) but its effect is unconfirmed — final status: unknown (post-decide verification unavailable). Do not treat this as ${decision === 'approve' ? 'an approval that resumed the task' : 'a discarded plan'}.`;
938
1056
  }
939
1057
  }
940
1058
  catch (e) {
@@ -959,40 +1077,34 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
959
1077
  '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.';
960
1078
  }
961
1079
  else if (status === 409 && refusedCode === APPROVAL_STALE) {
962
- let untrusted = false;
963
- if (checkpointId !== undefined) {
964
- const bag = readThrownExtra(e);
965
- const noCurrentPending = bag.state === 'object' &&
966
- (() => {
967
- try {
968
- return Object.getOwnPropertyDescriptor(bag.value, 'currentPending') === undefined;
969
- }
970
- catch {
971
- return false;
972
- }
973
- })();
974
- if (noCurrentPending) {
975
- try {
976
- const row = await repullRow();
977
- untrusted = row !== undefined && pausedCheckpointIdOf(row?.result) === checkpointId;
978
- }
979
- catch {
980
- untrusted = false;
981
- }
1080
+ let verdict = 'gate_moved';
1081
+ if (wireGate !== undefined && planReviewCurrentGateReadingOf(readThrownExtra(e)) === 'absent') {
1082
+ try {
1083
+ const row = await repullRow();
1084
+ const rowGate = pausedCheckpointIdOf(row?.result);
1085
+ verdict =
1086
+ row !== undefined && rowGate === wireGate
1087
+ ? 'gate_untrusted'
1088
+ : row !== undefined && rowGate === undefined && bodyWordOf(row, 'status') === 'needs_review'
1089
+ ? 'unreadable'
1090
+ : 'gate_moved';
1091
+ }
1092
+ catch {
1093
+ verdict = 'gate_moved';
982
1094
  }
983
1095
  }
984
- if (untrusted && checkpointId !== undefined)
985
- noteEngineRefusedGate(checkpointId);
986
- hostLog('debug', `planReviewWire: approval-stale decision=${decision} reason=${untrusted ? 'gate_untrusted' : 'gate_moved'}`);
1096
+ if (verdict === 'gate_untrusted' && wireGate !== undefined)
1097
+ noteEngineRefusedGate(wireGate);
1098
+ hostLog('debug', `planReviewWire: approval-stale decision=${decision} reason=${verdict}`);
987
1099
  effect = 'not_applied';
988
- decisionReason = untrusted ? 'gate_untrusted' : 'gate_moved';
1100
+ cause = verdict === 'unreadable' ? undefined : verdict;
989
1101
  outcome = renderMessage(msg('approval.planReview.decision.refused', { decision }), DATA_FACE_LANGUAGE);
990
1102
  }
991
1103
  else if (checkpointUnsupportedVersionFromError(e) !== null) {
992
1104
  const cu = checkpointUnsupportedVersionFromError(e);
993
1105
  hostLog('debug', `planReviewWire: ${decision} → 409 checkpoint.unsupported_version (the server cannot continue this parked review in its stored form; still pending)`);
994
1106
  effect = 'still_parked';
995
- stillParkedCause = 'stored_form_unsupported';
1107
+ cause = 'stored_form_unsupported';
996
1108
  outcome = `This ${decision} was not applied. ${checkpointUnsupportedVersionContent(cu, { parkKind: 'plan_review', runId: taskId })}`;
997
1109
  }
998
1110
  else if (resumeContextUnavailableFromError(e) !== null) {
@@ -1011,7 +1123,11 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
1011
1123
  const failureTexts = wireFailureTextsOf(e);
1012
1124
  hostLog('debug', `planReviewWire: ${decision} → ${status} ${failureTexts.log.slice(0, 200)}`);
1013
1125
  effect = refusalDeliveryOf(e, { route: 'plan_review' }).delivery === 'refused' ? 'not_applied' : 'unconfirmed';
1014
- outcome = `${`The plan_review decision failed: HTTP ${status} ${failureTexts.reason}`.trim()}${decideFailureNote(e)}`;
1126
+ const engineWayOut = status === 409 ? readDecisionRefusalReading(e, { route: 'plan_review' }).wayOut : undefined;
1127
+ const wayOutClause = engineWayOut === undefined ? undefined : decisionRefusalWayOutContent(engineWayOut);
1128
+ if (wayOutClause !== undefined)
1129
+ wayOut = engineWayOut;
1130
+ outcome = `${`The plan_review decision failed: HTTP ${status} ${failureTexts.reason}`.trim()}${decideFailureNote(e)}${wayOutClause === undefined ? '' : ` — ${wayOutClause}.`}`;
1015
1131
  }
1016
1132
  else if (noAnswer === 'time-limit' || noAnswer === 'deadline') {
1017
1133
  hostLog('debug', `planReviewWire: ${decision} sent but no answer within the request time limit (task ${taskId})`);
@@ -1024,18 +1140,32 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
1024
1140
  else {
1025
1141
  const failureTexts = wireFailureTextsOf(e);
1026
1142
  hostLog('debug', `planReviewWire: ${decision} could not reach the engine: ${failureTexts.log}`);
1143
+ unsentProven = true;
1027
1144
  outcome = `The plan_review decision could not reach the engine: ${failureTexts.reason}`;
1028
1145
  }
1029
1146
  }
1030
1147
  }
1031
1148
  }
1032
- outcome = `${outcome}${planReviewDecisionReasonSegment(decisionReason, card)}`;
1033
- const tail = planReviewOutcomeTail(taskId, decision, effect, stillParkedCause);
1149
+ const outcomeCause = planReviewOutcomeCauseOf(effect, cause);
1150
+ const sentWithoutGate = gateBefore !== undefined && wireGate === undefined && effect !== 'not_sent' && !unsentProven;
1151
+ outcome = `${outcome}${planReviewDecisionReasonSegment(outcomeCause, card)}${sentWithoutGate ? planReviewSentWithoutGateSegment() : ''}`;
1152
+ const tail = planReviewOutcomeTail(taskId, decision, effect, outcomeCause);
1034
1153
  const prompt = `<plan-review-outcome>\n${outcome}\n${tail}\n</plan-review-outcome>`;
1035
1154
  const onOutcome = opts?.onOutcome;
1155
+ const meta = {
1156
+ taskId,
1157
+ dispatchNo,
1158
+ decision: semanticDecision,
1159
+ effect,
1160
+ ...(outcomeCause !== undefined ? { cause: outcomeCause } : {}),
1161
+ ...(cardId !== undefined ? { questionId: cardId } : {}),
1162
+ delivery: planReviewDeliveryOf(acceptance, semanticDecision),
1163
+ ...(sentWithoutGate ? { gateIdOmitted: true } : {}),
1164
+ ...(wayOut !== undefined ? { wayOut } : {}),
1165
+ };
1036
1166
  settle(effect === 'not_sent' || effect === 'still_parked');
1037
1167
  try {
1038
- const delivered = enqueuePlanReviewOutcome(prompt, { taskId, dispatchNo, decision: semanticDecision, effect, ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}), ...(cardId !== undefined ? { questionId: cardId } : {}) });
1168
+ const delivered = enqueuePlanReviewOutcome(prompt, meta);
1039
1169
  if (!delivered) {
1040
1170
  if (onOutcome !== undefined) {
1041
1171
  hostLog('debug', `planReviewWire: outcome queue port not installed for task ${taskId} — outcome handed to the host's onOutcome callback only`);
@@ -1050,12 +1180,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
1050
1180
  }
1051
1181
  if (onOutcome !== undefined) {
1052
1182
  invokeHostCallbackIsolated(`planReviewWire: onOutcome callback (task ${taskId})`, () => onOutcome({
1053
- taskId,
1054
- dispatchNo,
1055
- decision: semanticDecision,
1056
- effect,
1057
- ...(effect === 'still_parked' && stillParkedCause !== undefined ? { cause: stillParkedCause } : {}),
1058
- ...(cardId !== undefined ? { questionId: cardId } : {}),
1183
+ ...meta,
1059
1184
  detail: outcome,
1060
1185
  tail,
1061
1186
  prompt,
@@ -1137,6 +1262,9 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
1137
1262
  hostLog('debug', `planReviewWire: reopen-superseded task=${taskId}`);
1138
1263
  return Promise.resolve(refusedReopen('not_presented'));
1139
1264
  }
1265
+ const early = reopenPresenceRefusal(taskId, opts, 'before');
1266
+ if (early !== undefined)
1267
+ return Promise.resolve(early);
1140
1268
  const facts = taken.facts;
1141
1269
  const markBefore = dismissalEntryFor(sessionKey, taskId);
1142
1270
  const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => {
@@ -1158,8 +1286,11 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
1158
1286
  samePlanReviewCardFacts(armed.facts, facts)) {
1159
1287
  hostLog('debug', `planReviewWire: reopen for task ${taskId} reusing the canonical responder (path=reopen-canonical)`);
1160
1288
  retireActiveReopen(sessionKey, canonicalId);
1289
+ const lateReuse = reopenPresenceRefusal(taskId, opts, 'after-retire');
1290
+ if (lateReuse !== undefined)
1291
+ return Promise.resolve(lateReuse);
1161
1292
  const receipt = waitForGateArmedFor(sessionKey, [armed.questionId], receiptMs);
1162
- publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(armed.questionId, ARM_QUESTION, armed.shape, armed.facts));
1293
+ deliverPlanReviewCard(sessionKey, planReviewQuestionFrame(armed.questionId, ARM_QUESTION, armed.shape, armed.facts, sessionKey), armed.facts);
1163
1294
  return settleOnReceipt(receipt, firstSight);
1164
1295
  }
1165
1296
  retireActiveReopen(sessionKey, canonicalId);
@@ -1168,11 +1299,12 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
1168
1299
  hostLog('debug', `planReviewWire: reopen-retire-armed task=${taskId} card=${armedId}`);
1169
1300
  }
1170
1301
  publishQuestionFrameFor(sessionKey, { type: 'question_complete', questionId: armedId });
1302
+ const lateFresh = reopenPresenceRefusal(taskId, opts, 'after-retire');
1303
+ if (lateFresh !== undefined)
1304
+ return Promise.resolve(lateFresh);
1171
1305
  const questionId = freshPlanReviewQuestionId(taskId, reopenIdSuffix());
1172
1306
  hostLog('debug', `planReviewWire: path=reopen-fresh task=${taskId} card=${questionId}`);
1173
- const delivery = opts?.wire !== undefined || opts?.onOutcome !== undefined
1174
- ? { ...(opts.wire !== undefined ? { wire: opts.wire } : {}), ...(opts.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
1175
- : undefined;
1307
+ const delivery = planReviewDeliveryFrom(opts?.wire, opts?.onOutcome, opts?.onAccepted);
1176
1308
  const deliver = opts?.deliverDecision ??
1177
1309
  ((tid, decision, reason, card) => {
1178
1310
  hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
@@ -1220,12 +1352,32 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
1220
1352
  unregister = registerLocalQuestionResponder(questionId, responder, onWithdraw);
1221
1353
  activeReopenResponders.set(activeKey, { questionId, unregister: () => unregister(), shape });
1222
1354
  const receipt = waitForGateArmedFor(sessionKey, [questionId], receiptMs);
1223
- publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(questionId, REOPEN_QUESTION, shape, facts));
1355
+ deliverPlanReviewCard(sessionKey, planReviewQuestionFrame(questionId, REOPEN_QUESTION, shape, facts, sessionKey), facts);
1224
1356
  return settleOnReceipt(receipt, firstSight);
1225
1357
  }
1226
1358
  function refusedReopen(cause) {
1227
1359
  return { reopened: false, _sema_reopenRefusal: cause };
1228
1360
  }
1361
+ function publishPresenceOf(opts) {
1362
+ return hostPresenceOf(opts, (ask) => {
1363
+ const got = { answered: false };
1364
+ invokeHostCallbackIsolated('planReviewWire: stillHere', () => {
1365
+ got.answer = ask();
1366
+ got.answered = true;
1367
+ return got.answer;
1368
+ });
1369
+ if (!got.answered)
1370
+ throw new Error('stillHere did not answer');
1371
+ return got.answer;
1372
+ });
1373
+ }
1374
+ function reopenPresenceRefusal(taskId, opts, stage) {
1375
+ const presence = publishPresenceOf(opts);
1376
+ if (presence === 'here')
1377
+ return undefined;
1378
+ hostLog('debug', `planReviewWire: reopen-withheld-${presence} task=${taskId} at=${stage}`);
1379
+ return refusedReopen(presence === 'left' ? 'conversation_left' : 'conversation_unconfirmed');
1380
+ }
1229
1381
  function noPresentationSurfaceVerdict(taskId, sessionKey) {
1230
1382
  const verdict = { reopened: false, _sema_noPresentationSurface: true };
1231
1383
  hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — nothing is subscribed to question frames for session ` +
@@ -1333,7 +1485,8 @@ export function reopenPlanReviewCard(taskId, opts) {
1333
1485
  return Promise.resolve(refusedReopen('invalid_call'));
1334
1486
  }
1335
1487
  const injected = opts;
1336
- if (opts?.mintFreshQuestionId === false && (injected?.deliverDecision !== undefined || injected?.wire !== undefined || injected?.onOutcome !== undefined)) {
1488
+ if (opts?.mintFreshQuestionId === false &&
1489
+ (injected?.deliverDecision !== undefined || injected?.wire !== undefined || injected?.onOutcome !== undefined || injected?.onAccepted !== undefined)) {
1337
1490
  hostLog('error', `planReviewWire: plan_review reopen for task ${taskId} refused — mintFreshQuestionId:false reuses the armed card's responder and its delivery, ` +
1338
1491
  'so deliverDecision / wire / onOutcome cannot be given with it; drop mintFreshQuestionId:false to deliver through the injected path');
1339
1492
  return Promise.resolve(refusedReopen('invalid_call'));
@@ -1464,3 +1617,26 @@ export async function readPlanReviewCardFacts(taskId, opts) {
1464
1617
  return mintPlanReviewCardFacts(taskId, checkpointId, unknownBody('unreadable'));
1465
1618
  }
1466
1619
  }
1620
+ function planReviewCurrentGateReadingOf(bag) {
1621
+ if (bag.state !== 'object')
1622
+ return 'malformed';
1623
+ try {
1624
+ const d = Object.getOwnPropertyDescriptor(bag.value, 'currentPending');
1625
+ if (d === undefined)
1626
+ return 'absent';
1627
+ if (!Object.hasOwn(d, 'value'))
1628
+ return 'malformed';
1629
+ const slot = d.value;
1630
+ if (typeof slot !== 'object' || slot === null || Array.isArray(slot))
1631
+ return 'malformed';
1632
+ const checkpointId = readOwnDataNonEmptyString(slot, 'checkpointId');
1633
+ return checkpointId === undefined ? 'malformed' : { checkpointId };
1634
+ }
1635
+ catch {
1636
+ return 'malformed';
1637
+ }
1638
+ }
1639
+ export function readPlanReviewCurrentGate(e) {
1640
+ const reading = planReviewCurrentGateReadingOf(readThrownExtra(e));
1641
+ return typeof reading === 'object' ? reading.checkpointId : undefined;
1642
+ }