@sema-agent/client-core 0.105.0 → 0.107.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 (152) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/README.md +40 -21
  3. package/dist/adapt/arms.js +10 -1
  4. package/dist/adapter/activeRunSelfHeal.d.ts +21 -2
  5. package/dist/adapter/activeRunSelfHeal.js +129 -28
  6. package/dist/adapter/downstream/eventToSdkMessage.d.ts +0 -5
  7. package/dist/adapter/downstream/eventToSdkMessage.js +20 -19
  8. package/dist/adapter/downstream/terminalToSdkResult.js +2 -3
  9. package/dist/adapter/downstream/wiringManifestView.d.ts +1 -3
  10. package/dist/adapter/downstream/wiringManifestView.js +0 -1
  11. package/dist/adapter/runStream.js +3 -4
  12. package/dist/approvalReachWire.d.ts +1 -0
  13. package/dist/approvalReachWire.js +4 -1
  14. package/dist/approvalSeat.d.ts +21 -0
  15. package/dist/approvalSeat.js +73 -0
  16. package/dist/assistantInbox.d.ts +2 -0
  17. package/dist/assistantInbox.js +3 -0
  18. package/dist/checkpointRowVerdict.d.ts +29 -0
  19. package/dist/checkpointRowVerdict.js +98 -0
  20. package/dist/coreValuePorts.d.ts +12 -1
  21. package/dist/coreValuePorts.js +10 -0
  22. package/dist/decideFailureNote.js +3 -1
  23. package/dist/decideReceipt.d.ts +9 -0
  24. package/dist/decideReceipt.js +29 -1
  25. package/dist/declared/supportFloorDeclared.js +2 -2
  26. package/dist/deviceRebind.d.ts +7 -0
  27. package/dist/deviceRebind.js +39 -5
  28. package/dist/displayUntrusted.d.ts +5 -3
  29. package/dist/displayUntrusted.js +50 -41
  30. package/dist/engineAddress.d.ts +3 -0
  31. package/dist/engineAddress.js +25 -0
  32. package/dist/engineDirectory.js +9 -17
  33. package/dist/engineErrorCodes.d.ts +8 -0
  34. package/dist/engineErrorCodes.js +8 -0
  35. package/dist/engineNoticeCodes.d.ts +38 -3
  36. package/dist/engineNoticeCodes.js +167 -11
  37. package/dist/engineRowRef.js +1 -1
  38. package/dist/errorSupplementRoles.js +4 -1
  39. package/dist/eventLeg.d.ts +2 -2
  40. package/dist/eventLeg.js +2 -1
  41. package/dist/executionLaneCapability.js +3 -2
  42. package/dist/executionLaneWords.d.ts +1 -0
  43. package/dist/executionLaneWords.js +3 -0
  44. package/dist/fleet/fleetProjection.js +26 -5
  45. package/dist/generated/engineFactTables.d.ts +2 -1
  46. package/dist/generated/engineFactTables.js +3 -1
  47. package/dist/generated/engineNoticeTables.js +28 -7
  48. package/dist/generated/sdkFactTables.d.ts +3 -0
  49. package/dist/generated/sdkFactTables.js +15 -0
  50. package/dist/generated/settingsSchemaFns.d.ts +11 -0
  51. package/dist/generated/settingsSchemaFns.js +2 -1
  52. package/dist/generated/settingsSchemaSafetyMergeSpec.js +1 -1
  53. package/dist/generated/settingsSchemaSettingsSources.d.ts +1 -1
  54. package/dist/generated/settingsSchemaSettingsSources.js +22 -2
  55. package/dist/hitl/approvalLiveFace.d.ts +17 -0
  56. package/dist/hitl/approvalLiveFace.js +35 -0
  57. package/dist/hitl/approvalQueueRow.d.ts +21 -0
  58. package/dist/hitl/approvalQueueRow.js +69 -0
  59. package/dist/hitl/approvalResolution.d.ts +5 -1
  60. package/dist/hitl/approvalResolution.js +18 -1
  61. package/dist/hitl/approvalsFeed.d.ts +5 -4
  62. package/dist/hitl/approvalsFeed.js +39 -6
  63. package/dist/hitl/askParkRowRouting.d.ts +4 -3
  64. package/dist/hitl/askParkRowRouting.js +27 -3
  65. package/dist/hitl/automaticAnswerVerdict.js +13 -10
  66. package/dist/hitl/crashConverged.d.ts +5 -1
  67. package/dist/hitl/decisionConflict.js +2 -1
  68. package/dist/hitl/decisionRefusal.js +1 -1
  69. package/dist/hitl/decisionSend.d.ts +3 -0
  70. package/dist/hitl/decisionSend.js +32 -6
  71. package/dist/hitl/frameRouter.js +4 -5
  72. package/dist/hitl/hitlBridge.d.ts +6 -2
  73. package/dist/hitl/hitlBridge.js +53 -7
  74. package/dist/hitl/livePendingAsk.d.ts +5 -0
  75. package/dist/hitl/livePendingAsk.js +10 -3
  76. package/dist/hitl/parkOwnership.js +4 -5
  77. package/dist/hitl/parkResolver.js +9 -5
  78. package/dist/hitl/parkRowBirthWait.d.ts +7 -0
  79. package/dist/hitl/parkRowBirthWait.js +1 -1
  80. package/dist/hitl/parkRowDelegation.d.ts +6 -0
  81. package/dist/hitl/parkRowDelegation.js +33 -0
  82. package/dist/hitl/persistedRulesWire.d.ts +15 -3
  83. package/dist/hitl/persistedRulesWire.js +102 -20
  84. package/dist/hitl/planReviewCard.d.ts +2 -0
  85. package/dist/hitl/planReviewCard.js +21 -0
  86. package/dist/hitl/planReviewWire.d.ts +2 -2
  87. package/dist/hitl/planReviewWire.js +105 -38
  88. package/dist/hitl/sessionAllowRoute.d.ts +7 -0
  89. package/dist/hitl/sessionAllowRoute.js +49 -0
  90. package/dist/hitl/sessionPolicyWire.d.ts +1 -7
  91. package/dist/hitl/sessionPolicyWire.js +3 -43
  92. package/dist/hitl/standingRows.js +10 -10
  93. package/dist/hitl/subagentSource.d.ts +1 -0
  94. package/dist/hitl/subagentSource.js +13 -0
  95. package/dist/hitl/toolApprovalWire.d.ts +3 -2
  96. package/dist/hitl/toolApprovalWire.js +100 -44
  97. package/dist/hostSettingsSource.d.ts +4 -1
  98. package/dist/hostSettingsSource.js +11 -6
  99. package/dist/hostTurnSettings.d.ts +4 -5
  100. package/dist/hostTurnSettings.js +80 -19
  101. package/dist/i18n/messages.en.d.ts +41 -12
  102. package/dist/i18n/messages.en.js +42 -13
  103. package/dist/i18n/messages.zh.js +45 -13
  104. package/dist/index.d.ts +22 -2
  105. package/dist/index.js +12 -2
  106. package/dist/injectionLaneNotice.js +2 -1
  107. package/dist/liveInitToolFace.js +0 -1
  108. package/dist/liveModelCatalog.d.ts +15 -0
  109. package/dist/liveModelCatalog.js +40 -0
  110. package/dist/mcpWireCaps.js +1 -1
  111. package/dist/modelCapabilityProbe.js +66 -33
  112. package/dist/noticePresentation.js +2 -0
  113. package/dist/ownKey.d.ts +0 -1
  114. package/dist/ownKey.js +0 -2
  115. package/dist/permissionsDeclaration.d.ts +2 -2
  116. package/dist/permissionsDeclaration.js +12 -14
  117. package/dist/projectSeed.d.ts +39 -0
  118. package/dist/projectSeed.js +80 -0
  119. package/dist/promptDelivery.js +1 -1
  120. package/dist/replayBody.d.ts +7 -6
  121. package/dist/replayBody.js +62 -7
  122. package/dist/request/taskRequest.js +3 -4
  123. package/dist/ruleCompileVerdict.d.ts +0 -2
  124. package/dist/ruleCompileVerdict.js +0 -31
  125. package/dist/ruleRefusalSentence.js +0 -3
  126. package/dist/runReattachPolicy.d.ts +3 -0
  127. package/dist/runReattachPolicy.js +15 -2
  128. package/dist/runTerminal.js +30 -8
  129. package/dist/sandboxWire.d.ts +0 -3
  130. package/dist/sandboxWire.js +0 -9
  131. package/dist/sdkWireTransit.d.ts +1 -1
  132. package/dist/seam.d.ts +14 -7
  133. package/dist/seam.js +12 -0
  134. package/dist/seatContract.d.ts +58 -7
  135. package/dist/seatContract.js +135 -4
  136. package/dist/serverSupportRange.d.ts +18 -0
  137. package/dist/serverSupportRange.js +25 -0
  138. package/dist/sessionEngine.d.ts +156 -0
  139. package/dist/sessionEngine.js +464 -0
  140. package/dist/sessionMap.d.ts +11 -1
  141. package/dist/sessionMap.js +35 -9
  142. package/dist/sessionMemorySync.d.ts +31 -0
  143. package/dist/sessionMemorySync.js +95 -0
  144. package/dist/sessionRowTag.d.ts +4 -0
  145. package/dist/sessionRowTag.js +18 -0
  146. package/dist/settingsLayerAdmission.d.ts +1 -0
  147. package/dist/settingsLayerAdmission.js +4 -1
  148. package/dist/toolResult.js +1 -1
  149. package/dist/wireRefusalCopy.d.ts +6 -1
  150. package/dist/wireRefusalCopy.js +9 -7
  151. package/docs/INTEGRATION-CLIENTS.md +1393 -18
  152. package/package.json +5 -5
@@ -1,11 +1,11 @@
1
1
  import { displayUntrusted, quoteUntrustedForSentence } from '../displayUntrusted.js';
2
2
  import { engineCapTrue } from '../engineCapsCache.js';
3
3
  import { CORE_RULE_BEHAVIORS } from '../generated/engineFactTables.js';
4
- import { wireFailureShapeOf } from '../wireFailureShape.js';
4
+ import { slotReadOf, wireFailureShapeOf } from '../wireFailureShape.js';
5
5
  import { CAPABILITY_OPERATOR_PRINCIPALS_REQUIRED } from '../engineErrorCodes.js';
6
6
  import { hostLog } from '../host.js';
7
7
  import { msg, renderMessage } from '../i18n/translate.js';
8
- import { guardedRead, readOwnDataValue } from '../ownKey.js';
8
+ import { appendOwnElement, guardedRead, readOwnDataValue } from '../ownKey.js';
9
9
  import { observedPermissionRulesWrite, permissionRulesWriteAvailable } from '../permissionRulesWriteCapability.js';
10
10
  import { isToolApprovalRespondReplayed, readToolApprovalRespondAck } from './toolApprovalWire.js';
11
11
  export function persistedRulesLaneAvailable(baseUrl) {
@@ -17,27 +17,109 @@ export function persistedRulesGovernanceAvailable(baseUrl) {
17
17
  export function persistedRulesWriteAvailable(baseUrl) {
18
18
  return permissionRulesWriteAvailable(observedPermissionRulesWrite(baseUrl ?? ''));
19
19
  }
20
- export function readCcImportRedeemCounts(result) {
21
- const o = (result ?? {});
22
- if (Array.isArray(o.members)) {
23
- let persisted = 0;
24
- let deduped = 0;
25
- for (const m of o.members) {
26
- const status = m?.status;
27
- if (status === 'persisted')
28
- persisted += 1;
29
- else if (status === 'deduped')
30
- deduped += 1;
20
+ const NO_INDETERMINATE = Object.freeze([]);
21
+ const INDETERMINATE_DETAIL_SHOWN_MAX = 400;
22
+ function ownArrayItems(v) {
23
+ try {
24
+ if (typeof v !== 'object' || v === null || !Array.isArray(v))
25
+ return undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ const len = slotReadOf(v, 'length');
31
+ if (len.presence !== 'present' || typeof len.value !== 'number' || !Number.isSafeInteger(len.value) || len.value < 0)
32
+ return undefined;
33
+ const out = [];
34
+ for (let i = 0; i < len.value; i++) {
35
+ const item = slotReadOf(v, String(i));
36
+ if (item.presence !== 'present')
37
+ return undefined;
38
+ appendOwnElement(out, item.value);
39
+ }
40
+ return out;
41
+ }
42
+ function ownNonEmpty(o, key) {
43
+ const r = slotReadOf(o, key);
44
+ return r.presence === 'present' && typeof r.value === 'string' && r.value !== '' ? r.value : undefined;
45
+ }
46
+ function indeterminateRowOf(row) {
47
+ const k = slotReadOf(row, 'key');
48
+ const keyObj = k.presence === 'present' && typeof k.value === 'object' && k.value !== null ? k.value : undefined;
49
+ const behavior = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'behavior');
50
+ const rule = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'rule');
51
+ const scope = keyObj === undefined ? undefined : ownNonEmpty(keyObj, 'scope');
52
+ const reason = ownNonEmpty(row, 'reason');
53
+ const d = slotReadOf(row, 'detail');
54
+ const shown = d.presence === 'present' && typeof d.value === 'string' ? displayUntrusted(d.value, { max: INDETERMINATE_DETAIL_SHOWN_MAX }) : undefined;
55
+ return Object.freeze({
56
+ ...(behavior !== undefined && rule !== undefined && scope !== undefined ? { key: Object.freeze({ behavior, rule, scope }) } : {}),
57
+ ...(reason !== undefined ? { reason } : {}),
58
+ ...(shown !== undefined && shown.trim() !== '' ? { detail: shown } : {}),
59
+ });
60
+ }
61
+ const RECEIPT_ROW_ARMS = {
62
+ persisted: (acc) => {
63
+ acc.persisted += 1;
64
+ },
65
+ 'no-op': (acc) => {
66
+ acc.deduped += 1;
67
+ },
68
+ indeterminate: (acc, row) => {
69
+ appendOwnElement(acc.indeterminate, indeterminateRowOf(row));
70
+ },
71
+ };
72
+ function receiptCountsOf(rows) {
73
+ const acc = { persisted: 0, deduped: 0, indeterminate: [] };
74
+ for (const row of rows) {
75
+ if (typeof row !== 'object' || row === null)
76
+ return null;
77
+ const o = slotReadOf(row, 'outcome');
78
+ const word = o.presence === 'present' ? o.value : undefined;
79
+ if (typeof word !== 'string' || !Object.hasOwn(RECEIPT_ROW_ARMS, word))
80
+ return null;
81
+ RECEIPT_ROW_ARMS[word](acc, row);
82
+ }
83
+ return { persisted: acc.persisted, deduped: acc.deduped, indeterminate: acc.indeterminate.length === 0 ? NO_INDETERMINATE : Object.freeze(acc.indeterminate) };
84
+ }
85
+ function legacyEnvelopeCountsOf(result) {
86
+ if (typeof result !== 'object' || result === null)
87
+ return null;
88
+ const m = slotReadOf(result, 'members');
89
+ const rows = m.presence === 'present' ? ownArrayItems(m.value) : undefined;
90
+ if (rows === undefined)
91
+ return null;
92
+ let persisted = 0;
93
+ let deduped = 0;
94
+ for (const row of rows) {
95
+ if (typeof row !== 'object' || row === null)
96
+ continue;
97
+ const st = slotReadOf(row, 'status');
98
+ const status = st.presence === 'present' ? st.value : undefined;
99
+ if (status === 'persisted')
100
+ persisted += 1;
101
+ else if (status === 'deduped')
102
+ deduped += 1;
103
+ }
104
+ return { persisted, deduped, indeterminate: NO_INDETERMINATE };
105
+ }
106
+ export function readCcImportRedeemCounts(body) {
107
+ try {
108
+ if (typeof body !== 'object' || body === null)
109
+ return null;
110
+ const top = slotReadOf(body, 'members');
111
+ if (top.presence !== 'absent') {
112
+ if (top.presence !== 'present')
113
+ return null;
114
+ const rows = ownArrayItems(top.value);
115
+ return rows === undefined ? null : receiptCountsOf(rows);
31
116
  }
32
- return { persisted, deduped };
117
+ const legacy = slotReadOf(body, 'result');
118
+ return legacy.presence === 'present' ? legacyEnvelopeCountsOf(legacy.value) : null;
33
119
  }
34
- if (Array.isArray(o.persisted) || Array.isArray(o.deduped)) {
35
- return {
36
- persisted: Array.isArray(o.persisted) ? o.persisted.length : 0,
37
- deduped: Array.isArray(o.deduped) ? o.deduped.length : 0,
38
- };
120
+ catch {
121
+ return null;
39
122
  }
40
- return null;
41
123
  }
42
124
  export function classifyRulesFailure(e, context) {
43
125
  const { status, errorCode, message, retryAfterSec, committed } = wireFailureShapeOf(e);
@@ -6,6 +6,8 @@ export declare function planReviewCardNoteRef(facts: PlanReviewCardFacts): Messa
6
6
  export declare function planReviewUnreadableNoteRef(): MessageRef;
7
7
  export declare function planReviewCardNote(facts: PlanReviewCardFacts, lang?: UiLanguage): string | undefined;
8
8
  export declare function planReviewAskAgainKey(facts: PlanReviewCardFacts | undefined): MessageKey;
9
+ export type PlanReviewDecisionReason = 'card_missing' | 'card_withholds' | 'gate_moved' | 'gate_untrusted';
10
+ export declare function planReviewDecisionReasonRef(reason: PlanReviewDecisionReason, card?: PlanReviewCardFacts): MessageRef;
9
11
  export interface PlanReviewLedgerSource {
10
12
  readonly runRow: (taskId: string, signal: AbortSignal) => Promise<object>;
11
13
  readonly frames: (taskId: string, signal: AbortSignal) => AsyncIterable<object>;
@@ -46,6 +46,27 @@ export function planReviewAskAgainKey(facts) {
46
46
  return body.cause === 'not_carried' ? 'approval.planReview.askAgain.notCarried' : 'approval.planReview.askAgain.unknown';
47
47
  }
48
48
  }
49
+ export function planReviewDecisionReasonRef(reason, card) {
50
+ switch (reason) {
51
+ case 'card_missing':
52
+ return msg('approval.planReview.decision.cardMissing');
53
+ case 'card_withholds': {
54
+ let ref;
55
+ try {
56
+ const read = card === undefined ? undefined : planReviewCardFactsFrom(card);
57
+ ref = read === undefined ? undefined : planReviewCardNoteRef(read);
58
+ }
59
+ catch {
60
+ ref = undefined;
61
+ }
62
+ return ref ?? planReviewUnreadableNoteRef();
63
+ }
64
+ case 'gate_moved':
65
+ return msg('approval.planReview.decision.gateMoved');
66
+ case 'gate_untrusted':
67
+ return msg('approval.planReview.decision.gateUntrusted');
68
+ }
69
+ }
49
70
  const PLAN_REVIEW_LEDGER_BUDGET_MS = 3_000;
50
71
  const planReviewFactsFlights = new Map();
51
72
  export function _resetPlanReviewFactsFlightsForTest() {
@@ -100,7 +100,7 @@ export interface PlanReviewDeliveryOptions {
100
100
  }
101
101
  export interface DecidePlanReviewOptions extends PlanReviewDeliveryOptions {
102
102
  reason?: string;
103
- parkedResult?: unknown;
103
+ card?: PlanReviewCardFacts;
104
104
  }
105
105
  export interface PlanReviewDecisionHandle {
106
106
  settle(): void;
@@ -124,7 +124,7 @@ export interface ReopenPlanReviewSharedOpts {
124
124
  }
125
125
  export interface ReopenPlanReviewFreshOpts extends ReopenPlanReviewSharedOpts {
126
126
  mintFreshQuestionId?: true;
127
- deliverDecision?: (taskId: string, decision: Parameters<typeof decidePlanReview>[1], reason?: string) => void | Promise<void>;
127
+ deliverDecision?: (taskId: string, decision: Parameters<typeof decidePlanReview>[1], reason: string | undefined, card: PlanReviewCardFacts) => void | Promise<void>;
128
128
  wire?: PlanReviewWire;
129
129
  onOutcome?: PlanReviewDeliveryOptions['onOutcome'];
130
130
  }
@@ -2,22 +2,21 @@ import { publishQuestionFrame, publishQuestionFrameFor, registerLocalQuestionRes
2
2
  import { hostLog } from '../host.js';
3
3
  import { abortableSleep, settleWithinDeadline } from '../abortableSleep.js';
4
4
  import { clientHasVerbs, engineWireFor, noAnswerKindOf, sendOnce } from '../engineWireFor.js';
5
- import { engineCapTrue } from '../engineCapsCache.js';
6
5
  import { engineWireTarget } from '../engineWireTarget.js';
7
6
  import { enqueuePlanReviewOutcome } from '../notifications.js';
8
7
  import { corruptStoredRowContent, corruptStoredRowWhere } from '../wireErrorTriage.js';
9
8
  import { decideFailureNote } from '../decideFailureNote.js';
10
- import { wireFailureTextsOf } from '../wireFailureShape.js';
9
+ import { readThrownExtra, wireFailureTextsOf } from '../wireFailureShape.js';
11
10
  import { appendOwnElement, readOwnDataNonEmptyString, readOwnDataValue } from '../ownKey.js';
12
11
  import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError, resumeContextUnavailableContent, resumeContextUnavailableFromError } from '../resumeRefusalCopy.js';
13
- import { REQUEST_FIELD_CONFLICT } from '../engineErrorCodes.js';
12
+ import { APPROVAL_STALE, REQUEST_FIELD_CONFLICT } from '../engineErrorCodes.js';
14
13
  import { refusalDeliveryOf } from './decisionRefusal.js';
15
14
  import { directFailureVerdictOf, directSafetyStopOf } from './approvalResolution.js';
16
15
  import { __resetDecisionLedgerForTests, decisionInFlightOf, decisionKeyInFlight, decisionLedgerKeysOf, decisionOpsWithKey, decisionReadMark, dropDecisionOp, latestDecisionHandedMark, markLatestDecisionNotApplied, openDecisionOp, planDecisionKey, readMarkOf, settleDecisionOp, settleDecisionOpsOnGateLeft, } from './decisionLedger.js';
17
16
  import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
18
17
  import { freshPlanReviewQuestionId, planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
19
18
  import { isPlanReviewPark, mintPlanReviewCardFacts, notePlanReviewCardIssued, pausedCheckpointIdOf, planReviewCardFactsFrom, planReviewFactsCurrent, registeredPlanReviewCardFacts, samePlanReviewCardFacts, unknownBody, _resetPlanReviewRunsForTest, } from '../planReviewCardFacts.js';
20
- import { planReviewAskAgainKey, planReviewCardNoteRef, planReviewUnreadableNoteRef, takePlanReviewFacts, _resetPlanReviewFactsFlightsForTest, } from './planReviewCard.js';
19
+ import { planReviewAskAgainKey, planReviewCardNoteRef, planReviewDecisionReasonRef, planReviewUnreadableNoteRef, takePlanReviewFacts, _resetPlanReviewFactsFlightsForTest, } from './planReviewCard.js';
21
20
  import { EN_MESSAGES } from '../i18n/messages.en.js';
22
21
  import { msg, renderMessage, DATA_FACE_LANGUAGE } from '../i18n/translate.js';
23
22
  import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
@@ -183,6 +182,7 @@ const armedPlanReviews = new Map();
183
182
  const offeredModeByQuestionId = new Map();
184
183
  const OFFERED_MODE_LEDGER_MAX = 64;
185
184
  export function _resetArmedPlanReviewsForTest() {
185
+ engineRefusedGateIds.clear();
186
186
  armedPlanReviews.clear();
187
187
  offeredModeByQuestionId.clear();
188
188
  planReviewDismissalsByKey.clear();
@@ -519,7 +519,7 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
519
519
  const cardDelivery = cardReason !== undefined ? { ...delivery, reason: cardReason } : delivery;
520
520
  notePlanReviewAnswered(questionId);
521
521
  hostLog('debug', `planReviewWire: arm responder delivering ${decided} for task ${taskId} (path=arm)`);
522
- void decidePlanReviewFor(taskId, decided, modeAfter, cardDelivery, gateBefore, facts);
522
+ void decidePlanReviewFor(taskId, decided, modeAfter, cardDelivery, facts);
523
523
  return { ok: true };
524
524
  };
525
525
  unregister = registerLocalQuestionResponder(questionId, responder);
@@ -534,7 +534,7 @@ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
534
534
  switch (effect) {
535
535
  case 'took_effect':
536
536
  return decision === 'approve'
537
- ? `Use TaskOutput("${taskId}") if you need the resumed task's full output, then give the user a brief report of what was done.`
537
+ ? `The result of the resumed task ${JSON.stringify(taskId)} arrives in its completion notification; when it arrives, give the user a brief report of what was done.`
538
538
  : 'Briefly acknowledge to the user that the plan was rejected and continue planning.';
539
539
  case 'advanced':
540
540
  return `Tell the user the ${decision} went through and that another approval is now waiting for them; do not report the task as finished.`;
@@ -543,9 +543,9 @@ export function planReviewOutcomeTail(taskId, decision, effect, cause) {
543
543
  case 'not_sent':
544
544
  return `Nothing was sent to the engine. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}; the plan is still waiting for a decision.`;
545
545
  case 'not_applied':
546
- return `The ${decision} did not take effect. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}; check the task's current state with TaskOutput("${taskId}") before saying anything about the plan.`;
546
+ return `The ${decision} did not take effect. Do not tell the user the plan was ${decision === 'approve' ? 'approved' : 'rejected'}; find out where task ${JSON.stringify(taskId)} stands (the background task list or its next notification shows it) before saying anything about the plan.`;
547
547
  case 'unconfirmed':
548
- return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed; TaskOutput("${taskId}") shows where the task stands.`;
548
+ return `Do not state the result as fact: tell the user the ${decision} was submitted but its effect could not be confirmed; the background task list or the next notification of task ${JSON.stringify(taskId)} shows where it stands.`;
549
549
  }
550
550
  }
551
551
  function planReviewDecisionTimedOutOutcome(decision) {
@@ -632,12 +632,39 @@ export function latestPlanReviewDecisionMark(taskId) {
632
632
  export function __resetPlanReviewDecisionLatchForTests() {
633
633
  __resetDecisionLedgerForTests('plan_review');
634
634
  }
635
+ const engineRefusedGateIds = new Set();
636
+ const ENGINE_REFUSED_GATE_IDS_MAX = 64;
637
+ function noteEngineRefusedGate(id) {
638
+ if (engineRefusedGateIds.has(id))
639
+ return;
640
+ engineRefusedGateIds.add(id);
641
+ while (engineRefusedGateIds.size > ENGINE_REFUSED_GATE_IDS_MAX) {
642
+ const oldest = engineRefusedGateIds.values().next();
643
+ if (oldest.done === true)
644
+ break;
645
+ engineRefusedGateIds.delete(oldest.value);
646
+ }
647
+ }
648
+ function planReviewDecisionReasonSegment(reason, card) {
649
+ if (reason === undefined)
650
+ return '';
651
+ try {
652
+ return ` ${renderMessage(planReviewDecisionReasonRef(reason, card), DATA_FACE_LANGUAGE)}`;
653
+ }
654
+ catch {
655
+ return '';
656
+ }
657
+ }
635
658
  export async function decidePlanReview(taskId, decision, permissionModeAfter, opts) {
636
- return decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, parkedGateIdentityOf(opts));
659
+ return decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, decisionCardOf(taskId, opts));
637
660
  }
638
- function parkedGateIdentityOf(opts) {
661
+ function decisionCardOf(taskId, opts) {
639
662
  try {
640
- return opts !== null && typeof opts === 'object' ? pausedCheckpointIdOf(opts.parkedResult) : undefined;
663
+ if (opts === null || typeof opts !== 'object')
664
+ return undefined;
665
+ const raw = readOwnDataValue(opts, 'card');
666
+ const facts = raw !== null && typeof raw === 'object' ? planReviewCardFactsFrom(raw) : undefined;
667
+ return facts !== undefined && facts.taskId === taskId ? facts : undefined;
641
668
  }
642
669
  catch {
643
670
  return undefined;
@@ -650,6 +677,8 @@ function planReviewSettledVerdict(r) {
650
677
  return status !== 'completed' && !gateMoved ? 'still_parked' : 'unconfirmed';
651
678
  if (status === 'completed')
652
679
  return 'took_effect';
680
+ if (status === 'running' && r.decision === 'reject')
681
+ return 'took_effect';
653
682
  const newGateHasCard = () => {
654
683
  const leg = durableDecideLegOf({ gateKind: r.gateKindAfter ?? null });
655
684
  return leg === 'approval' || leg === 'plan_review';
@@ -691,7 +720,7 @@ function askAgainReasonOf(card, opts) {
691
720
  return renderMessage(msg('approval.planReview.askAgain.unknown'), DATA_FACE_LANGUAGE);
692
721
  }
693
722
  }
694
- async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, gateBefore, card) {
723
+ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, card) {
695
724
  if (typeof taskId === 'string' && planReviewOpsOf(taskId).some((op) => op.phase === 'posting')) {
696
725
  hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
697
726
  return;
@@ -724,7 +753,7 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
724
753
  try {
725
754
  const wireDecision = decision === 'ask_again' ? 'reject' : decision;
726
755
  const wireOpts = decision === 'ask_again' ? { ...opts, reason: askAgainReasonOf(card, opts) } : opts;
727
- await decidePlanReviewInner(taskId, wireDecision, permissionModeAfter, dispatchNo, releaseLatch, settle, wireOpts, gateBefore, decision);
756
+ await decidePlanReviewInner(taskId, wireDecision, permissionModeAfter, dispatchNo, releaseLatch, settle, wireOpts, card, decision);
728
757
  }
729
758
  finally {
730
759
  releaseLatch();
@@ -745,8 +774,11 @@ function planReviewClientsFor(resolved) {
745
774
  repull: () => resolved.make({ timeoutMs: 15_000 }),
746
775
  };
747
776
  }
748
- async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, gateBefore, semanticDecision) {
777
+ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, dispatchNo, releaseLatch, settle, opts, card, semanticDecision) {
749
778
  let outcome;
779
+ let decisionReason;
780
+ const gateBefore = card?.checkpointId;
781
+ const approvalWithheld = decision !== 'approve' ? undefined : card === undefined ? 'card_missing' : card.body.kind !== 'shown' || card.checkpointId === undefined ? 'card_withholds' : undefined;
750
782
  let effect = 'unconfirmed';
751
783
  let stillParkedCause;
752
784
  let statusSeen;
@@ -758,6 +790,12 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
758
790
  effect = 'not_sent';
759
791
  outcome = `The plan_review ${decision} could NOT be sent: the post-approval permission mode given with it is not valid (it must be "default" or "acceptEdits"). Tell the user plainly that the decision did not go through; the plan is still waiting.`;
760
792
  }
793
+ else if (approvalWithheld !== undefined) {
794
+ hostLog('debug', `planReviewWire: not-sent decision=${decision} reason=${approvalWithheld}`);
795
+ effect = 'not_sent';
796
+ decisionReason = approvalWithheld;
797
+ outcome = renderMessage(msg('approval.planReview.decision.notSent', { decision }), DATA_FACE_LANGUAGE);
798
+ }
761
799
  else if (resolved.kind === 'no-target') {
762
800
  hostLog('error', `planReviewWire: ${decision} NOT sent — engineWireTarget() unavailable`);
763
801
  effect = 'not_sent';
@@ -777,11 +815,24 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
777
815
  }
778
816
  else {
779
817
  const client = clients.decide;
818
+ const checkpointId = gateBefore !== undefined && !engineRefusedGateIds.has(gateBefore) ? gateBefore : undefined;
819
+ const repullRow = async () => {
820
+ const probe = clients.repull();
821
+ if (!probe)
822
+ return undefined;
823
+ if (wire !== undefined) {
824
+ const got = await settleWithinDeadline(PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS, (signal) => probe.runs.get(taskId, { signal }));
825
+ if (got.kind === 'error')
826
+ throw new Error(got.detail);
827
+ if (got.kind === 'deadline')
828
+ throw new Error(`post-decide status re-pull passed its ${PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS} ms deadline`);
829
+ return got.value;
830
+ }
831
+ return (await probe.runs.get(taskId));
832
+ };
780
833
  try {
781
834
  const mode = isPlanReviewModeAfter(permissionModeAfter) ? permissionModeAfter : undefined;
782
835
  const reason = denyReasonForWire(opts?.reason, `plan-review ${taskId}`);
783
- const capsBase = resolved.kind === 'target' ? resolved.target.baseUrl : resolved.capsBaseUrl;
784
- const checkpointId = gateBefore !== undefined && engineCapTrue(capsBase, 'planReviewCheckpointId') ? gateBefore : undefined;
785
836
  const reqBody = { decision };
786
837
  if (reason !== undefined)
787
838
  reqBody.reason = reason;
@@ -812,20 +863,8 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
812
863
  let gateAfter;
813
864
  let gateKindAfter;
814
865
  try {
815
- const probe = clients.repull();
816
- if (probe) {
817
- let post;
818
- if (wire !== undefined) {
819
- const got = await settleWithinDeadline(PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS, (signal) => probe.runs.get(taskId, { signal }));
820
- if (got.kind === 'error')
821
- throw new Error(got.detail);
822
- if (got.kind === 'deadline')
823
- throw new Error(`post-decide status re-pull passed its ${PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS} ms deadline`);
824
- post = got.value;
825
- }
826
- else {
827
- post = (await probe.runs.get(taskId));
828
- }
866
+ const post = await repullRow();
867
+ if (post !== undefined) {
829
868
  postStatus = bodyWordOf(post, 'status');
830
869
  gateAfter = pausedCheckpointIdOf(post?.result);
831
870
  gateKindAfter = pausedGateKindOf(post?.result);
@@ -837,7 +876,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
837
876
  const bodyStatus = bodyWordOf(body, 'status');
838
877
  const effective = postStatus ?? bodyStatus;
839
878
  statusSeen = effective;
840
- effect = planReviewSettledVerdict({ postStatus, bodyStatus, reparked: bodySaysReparked(body), gateBefore, gateAfter, gateKindAfter });
879
+ effect = planReviewSettledVerdict({ decision, postStatus, bodyStatus, reparked: bodySaysReparked(body), gateBefore, gateAfter, gateKindAfter });
841
880
  const code = bodyWordOf(body, 'errorCode');
842
881
  if (effect === 'still_parked' && postStatus === 'needs_review' && !bodySaysReparked(body)) {
843
882
  outcome =
@@ -855,7 +894,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
855
894
  }
856
895
  else if (effect === 'took_effect') {
857
896
  outcome =
858
- `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — final status: ${effective} ` +
897
+ `The plan was ${decision === 'approve' ? 'approved and the parked task resumed to completion' : 'rejected (plan discarded)'} — ${effective === 'completed' ? 'final status' : 'status'}: ${effective} ` +
859
898
  `(${postStatus !== undefined ? 'post-decide re-checked: task left the review gate' : "from the decision's own reply; the post-decide re-check was unavailable"}).`;
860
899
  }
861
900
  else if (effective === 'needs_review' && (gateBefore === undefined || gateAfter === undefined || postStatus !== 'needs_review')) {
@@ -898,6 +937,36 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
898
937
  `This ${decision} was not applied: the engine no longer has a pending plan review for this task — an earlier decision most likely already went through (or the review expired). ` +
899
938
  '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.';
900
939
  }
940
+ else if (status === 409 && refusedCode === APPROVAL_STALE) {
941
+ let untrusted = false;
942
+ if (checkpointId !== undefined) {
943
+ const bag = readThrownExtra(e);
944
+ const noCurrentPending = bag.state === 'object' &&
945
+ (() => {
946
+ try {
947
+ return Object.getOwnPropertyDescriptor(bag.value, 'currentPending') === undefined;
948
+ }
949
+ catch {
950
+ return false;
951
+ }
952
+ })();
953
+ if (noCurrentPending) {
954
+ try {
955
+ const row = await repullRow();
956
+ untrusted = row !== undefined && pausedCheckpointIdOf(row?.result) === checkpointId;
957
+ }
958
+ catch {
959
+ untrusted = false;
960
+ }
961
+ }
962
+ }
963
+ if (untrusted && checkpointId !== undefined)
964
+ noteEngineRefusedGate(checkpointId);
965
+ hostLog('debug', `planReviewWire: approval-stale decision=${decision} reason=${untrusted ? 'gate_untrusted' : 'gate_moved'}`);
966
+ effect = 'not_applied';
967
+ decisionReason = untrusted ? 'gate_untrusted' : 'gate_moved';
968
+ outcome = renderMessage(msg('approval.planReview.decision.refused', { decision }), DATA_FACE_LANGUAGE);
969
+ }
901
970
  else if (checkpointUnsupportedVersionFromError(e) !== null) {
902
971
  const cu = checkpointUnsupportedVersionFromError(e);
903
972
  hostLog('debug', `planReviewWire: ${decision} → 409 checkpoint.unsupported_version (the server cannot continue this parked review in its stored form; still pending)`);
@@ -939,6 +1008,7 @@ async function decidePlanReviewInner(taskId, decision, permissionModeAfter, disp
939
1008
  }
940
1009
  }
941
1010
  }
1011
+ outcome = `${outcome}${planReviewDecisionReasonSegment(decisionReason, card)}`;
942
1012
  const tail = planReviewOutcomeTail(taskId, decision, effect, stillParkedCause);
943
1013
  const prompt = `<plan-review-outcome>\n${outcome}\n${tail}\n</plan-review-outcome>`;
944
1014
  const onOutcome = opts?.onOutcome;
@@ -1087,10 +1157,10 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
1087
1157
  ? { ...(opts.wire !== undefined ? { wire: opts.wire } : {}), ...(opts.onOutcome !== undefined ? { onOutcome: opts.onOutcome } : {}) }
1088
1158
  : undefined;
1089
1159
  const deliver = opts?.deliverDecision ??
1090
- ((tid, decision, reason) => {
1160
+ ((tid, decision, reason, card) => {
1091
1161
  hostLog('debug', `planReviewWire: reopen responder delivering ${decision} for task ${tid} (path=reopen-default)`);
1092
1162
  const withReason = reason === undefined ? delivery : { ...delivery, reason };
1093
- void decidePlanReviewFor(tid, decision, undefined, withReason, parkedGateIdentityOf(withReason), facts);
1163
+ void decidePlanReviewFor(tid, decision, undefined, withReason, card);
1094
1164
  });
1095
1165
  const activeKey = `${sessionKey}\u0000${canonicalId}`;
1096
1166
  let unregister = () => { };
@@ -1116,10 +1186,7 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs, taken) {
1116
1186
  notePlanReviewDecisiveAnswerFor(sessionKey, taskId);
1117
1187
  notePlanReviewAnsweredFor(sessionKey, questionId);
1118
1188
  try {
1119
- if (cardReason === undefined)
1120
- await deliver(taskId, decided);
1121
- else
1122
- await deliver(taskId, decided, cardReason);
1189
+ await deliver(taskId, decided, cardReason, facts);
1123
1190
  }
1124
1191
  catch (e) {
1125
1192
  hostLog('error', `planReviewWire: reopen decision delivery threw for task ${taskId}: ${String(e)}`);
@@ -0,0 +1,7 @@
1
+ export type SessionAllowLeg = 'frame' | 'v1' | 'generic';
2
+ export declare function sessionAllowCarried(leg: SessionAllowLeg, approvalId: unknown): boolean;
3
+ export type SessionAllowWhy = 'given' | 'route_cannot_carry' | 'evidence_unreadable' | 'card_unreadable';
4
+ export type CardUnreadableEvidence = readonly string[] | 'absent' | 'unreadable';
5
+ export declare function readCardUnreadableEvidence(card: object): CardUnreadableEvidence;
6
+ export declare function sessionAllowCardReadable(card: object | null | undefined): Exclude<SessionAllowWhy, 'route_cannot_carry'>;
7
+ export declare function sessionAllowEligible(card: object | null | undefined): SessionAllowWhy;
@@ -0,0 +1,49 @@
1
+ export function sessionAllowCarried(leg, approvalId) {
2
+ if (leg === 'frame')
3
+ return true;
4
+ if (leg === 'v1')
5
+ return typeof approvalId === 'string' && approvalId !== '';
6
+ return false;
7
+ }
8
+ export function readCardUnreadableEvidence(card) {
9
+ try {
10
+ if (!('unreadableEvidence' in card))
11
+ return 'absent';
12
+ const d = Object.getOwnPropertyDescriptor(card, 'unreadableEvidence');
13
+ const list = d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
14
+ return Array.isArray(list) && list.every((k) => typeof k === 'string') ? list : 'unreadable';
15
+ }
16
+ catch {
17
+ return 'unreadable';
18
+ }
19
+ }
20
+ export function sessionAllowCardReadable(card) {
21
+ if (typeof card !== 'object' || card === null)
22
+ return 'card_unreadable';
23
+ try {
24
+ if ('cardPayloadUnreadable' in card)
25
+ return 'card_unreadable';
26
+ }
27
+ catch {
28
+ return 'card_unreadable';
29
+ }
30
+ const evidence = readCardUnreadableEvidence(card);
31
+ if (evidence === 'unreadable')
32
+ return 'card_unreadable';
33
+ return evidence !== 'absent' && evidence.length > 0 ? 'evidence_unreadable' : 'given';
34
+ }
35
+ export function sessionAllowEligible(card) {
36
+ if (typeof card !== 'object' || card === null)
37
+ return 'card_unreadable';
38
+ let carried;
39
+ try {
40
+ const d = Object.getOwnPropertyDescriptor(card, 'allowSessionCarried');
41
+ carried = d !== undefined && Object.hasOwn(d, 'value') ? d.value : undefined;
42
+ }
43
+ catch {
44
+ return 'card_unreadable';
45
+ }
46
+ if (carried !== true)
47
+ return 'route_cannot_carry';
48
+ return sessionAllowCardReadable(card);
49
+ }
@@ -128,17 +128,11 @@ export interface SessionPolicyInertEntryRefusal {
128
128
  export declare function tightenSessionPolicy(facade: SessionPolicyFacade, req: SessionPolicyTightenRequest, opts?: {
129
129
  signal?: AbortSignal;
130
130
  }): Promise<SessionPolicyTightenOutcome>;
131
- export interface SessionPolicyTightenFacts {
132
- readonly agentsStartedBeforeFirstRule?: number | undefined;
133
- readonly workflowsRunning?: number | undefined;
134
- readonly untrackedBackgroundItems?: number | undefined;
135
- readonly inFlightUnreadable?: true | undefined;
136
- }
137
131
  export interface SessionPolicyDeploymentFacts {
138
132
  readonly operator?: 'present' | 'absent';
139
133
  readonly identityAttach?: 'available' | 'unavailable';
140
134
  }
141
- export declare function sessionPolicyTightenNotice(outcome: SessionPolicyTightenOutcome, lang?: UiLanguage, facts?: SessionPolicyTightenFacts, deployment?: SessionPolicyDeploymentFacts): string;
135
+ export declare function sessionPolicyTightenNotice(outcome: SessionPolicyTightenOutcome, lang?: UiLanguage, deployment?: SessionPolicyDeploymentFacts): string;
142
136
  export declare function sessionPolicyReadNotice(outcome: Exclude<SessionPolicyReadOutcome, {
143
137
  kind: 'present';
144
138
  }> | {
@@ -414,29 +414,6 @@ export async function tightenSessionPolicy(facade, req, opts) {
414
414
  return { kind: 'conflict', rev: second.retryAtRev };
415
415
  }
416
416
  const earlierGrammarNote = (lang) => renderMessage(msg('rules.sessionPolicy.tighten.earlierGrammar'), lang);
417
- function factSlot(facts, key) {
418
- try {
419
- return ownRead(facts, key);
420
- }
421
- catch {
422
- return undefined;
423
- }
424
- }
425
- function factCount(facts, key) {
426
- const n = factSlot(facts, key);
427
- return typeof n === 'number' && Number.isSafeInteger(n) && n >= 1 ? n : undefined;
428
- }
429
- function firstRuleWorkFactsOf(facts) {
430
- const none = { agents: undefined, workflows: undefined, untracked: undefined, unreadable: false };
431
- if (typeof facts !== 'object' || facts === null)
432
- return none;
433
- return {
434
- agents: factCount(facts, 'agentsStartedBeforeFirstRule'),
435
- workflows: factCount(facts, 'workflowsRunning'),
436
- untracked: factCount(facts, 'untrackedBackgroundItems'),
437
- unreadable: factSlot(facts, 'inFlightUnreadable') === true,
438
- };
439
- }
440
417
  function deploymentHasNoOperator(d) {
441
418
  try {
442
419
  return typeof d === 'object' && d !== null && ownRead(d, 'operator') === 'absent';
@@ -453,28 +430,11 @@ function deploymentHasNoIdentity(d) {
453
430
  return false;
454
431
  }
455
432
  }
456
- export function sessionPolicyTightenNotice(outcome, lang, facts, deployment) {
433
+ export function sessionPolicyTightenNotice(outcome, lang, deployment) {
457
434
  const say = (key, params) => renderMessage(msg(key, params), lang);
458
435
  switch (outcome.kind) {
459
- case 'written': {
460
- const head = say('rules.sessionPolicy.tighten.written', { rev: outcome.rev });
461
- const w = firstRuleWorkFactsOf(facts);
462
- const unseen = msg('rules.sessionPolicy.tighten.writtenBeforeFirstRule.unseen');
463
- if (w.unreadable)
464
- return `${head} ${renderMessage(msg('rules.sessionPolicy.tighten.writtenBeforeFirstRule.unreadable'), lang)} ${renderMessage(unseen, lang)}`;
465
- const parts = [];
466
- if (w.agents !== undefined)
467
- appendOwnElement(parts, msg(w.agents === 1 ? 'rules.sessionPolicy.tighten.writtenBeforeFirstRule.agents.one' : 'rules.sessionPolicy.tighten.writtenBeforeFirstRule.agents.many', { n: w.agents }));
468
- if (w.workflows !== undefined)
469
- appendOwnElement(parts, msg(w.workflows === 1 ? 'rules.sessionPolicy.tighten.writtenBeforeFirstRule.workflows.one' : 'rules.sessionPolicy.tighten.writtenBeforeFirstRule.workflows.many', { n: w.workflows }));
470
- if (w.untracked !== undefined)
471
- appendOwnElement(parts, msg(w.untracked === 1 ? 'rules.sessionPolicy.tighten.writtenBeforeFirstRule.untracked.one' : 'rules.sessionPolicy.tighten.writtenBeforeFirstRule.untracked.many', { n: w.untracked }));
472
- const [p0, p1, p2] = parts;
473
- if (p0 === undefined)
474
- return head;
475
- const list = p1 === undefined ? p0 : p2 === undefined ? msg('rules.sessionPolicy.tighten.writtenBeforeFirstRule.list.two', { a: p0, b: p1 }) : msg('rules.sessionPolicy.tighten.writtenBeforeFirstRule.list.three', { a: p0, b: p1, c: p2 });
476
- return `${head} ${renderMessage(msg('rules.sessionPolicy.tighten.writtenBeforeFirstRule.counts', { list }), lang)} ${renderMessage(unseen, lang)}`;
477
- }
436
+ case 'written':
437
+ return say('rules.sessionPolicy.tighten.written', { rev: outcome.rev });
478
438
  case 'conflict':
479
439
  return say('rules.sessionPolicy.tighten.conflict');
480
440
  case 'loosen_forbidden':