@sema-agent/client-core 0.98.0 → 0.100.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 (236) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +61 -35
  3. package/dist/adapt/arms.js +120 -55
  4. package/dist/adapt/panelTasks.d.ts +3 -2
  5. package/dist/adapt/panelTasks.js +34 -11
  6. package/dist/adapt/textStream.d.ts +1 -1
  7. package/dist/adapt/textStream.js +51 -28
  8. package/dist/adapter/activeRunSelfHeal.d.ts +11 -3
  9. package/dist/adapter/activeRunSelfHeal.js +210 -294
  10. package/dist/adapter/downstream/eventToSdkMessage.d.ts +25 -6
  11. package/dist/adapter/downstream/eventToSdkMessage.js +176 -12
  12. package/dist/adapter/downstream/terminalToSdkResult.js +10 -0
  13. package/dist/adapter/downstream/wiringManifestView.d.ts +3 -1
  14. package/dist/adapter/downstream/wiringManifestView.js +1 -0
  15. package/dist/adapter/runStream.js +82 -40
  16. package/dist/adapter/types.d.ts +2 -0
  17. package/dist/adapter/types.js +2 -1
  18. package/dist/agentSession/engineSessionBackgroundStop.js +16 -8
  19. package/dist/agentsWireCaps.d.ts +7 -3
  20. package/dist/agentsWireCaps.js +133 -39
  21. package/dist/cloudConfigWireCaps.d.ts +10 -1
  22. package/dist/cloudConfigWireCaps.js +181 -65
  23. package/dist/compactionStatus.d.ts +34 -0
  24. package/dist/compactionStatus.js +196 -0
  25. package/dist/controlRouter.d.ts +4 -2
  26. package/dist/controlRouter.js +39 -51
  27. package/dist/coreValuePorts.d.ts +26 -1
  28. package/dist/coreValuePorts.js +10 -0
  29. package/dist/corruptSessionCopy.d.ts +2 -1
  30. package/dist/corruptSessionCopy.js +3 -3
  31. package/dist/decideFailureNote.js +3 -3
  32. package/dist/declared/supportFloorDeclared.js +1 -1
  33. package/dist/detachWire.js +3 -2
  34. package/dist/devicesFace.d.ts +34 -0
  35. package/dist/devicesFace.js +90 -0
  36. package/dist/displayUntrusted.d.ts +1 -0
  37. package/dist/displayUntrusted.js +153 -5
  38. package/dist/engineAgentPanelStore.d.ts +14 -4
  39. package/dist/engineAgentPanelStore.js +14 -0
  40. package/dist/engineCapReader.d.ts +2 -1
  41. package/dist/engineCapReader.js +7 -5
  42. package/dist/engineCapsCache.d.ts +8 -0
  43. package/dist/engineCapsCache.js +59 -6
  44. package/dist/engineCapsChange.d.ts +2 -0
  45. package/dist/engineCapsChange.js +9 -0
  46. package/dist/engineErrorCodes.d.ts +11 -2
  47. package/dist/engineErrorCodes.js +11 -12
  48. package/dist/engineRowRef.js +4 -0
  49. package/dist/engineSessionMessages.d.ts +1 -0
  50. package/dist/engineSessionMessages.js +7 -1
  51. package/dist/engineTargetSeat.d.ts +27 -0
  52. package/dist/engineTargetSeat.js +199 -0
  53. package/dist/engineToolDirectory.d.ts +3 -0
  54. package/dist/engineToolDirectory.js +4 -2
  55. package/dist/engineWireFor.js +3 -8
  56. package/dist/env/uiLanguage.d.ts +2 -1
  57. package/dist/env/uiLanguage.js +3 -2
  58. package/dist/eventLeg.d.ts +7 -5
  59. package/dist/eventLeg.js +68 -49
  60. package/dist/fileHistoryCaptureCapability.d.ts +2 -4
  61. package/dist/fileHistoryCaptureCapability.js +12 -22
  62. package/dist/fileHistoryLegNotice.d.ts +2 -1
  63. package/dist/fileHistoryLegNotice.js +3 -3
  64. package/dist/frameBinding.d.ts +3 -0
  65. package/dist/frameBinding.js +12 -0
  66. package/dist/gateOutcome.js +16 -2
  67. package/dist/gateVocabulary.d.ts +8 -7
  68. package/dist/gateVocabulary.js +71 -73
  69. package/dist/generated/engineFactTables.d.ts +18 -1
  70. package/dist/generated/engineFactTables.js +56 -0
  71. package/dist/generated/engineNoticeTables.js +12 -3
  72. package/dist/generated/settingsSchemaFns.d.ts +18 -0
  73. package/dist/generated/settingsSchemaFns.js +70 -0
  74. package/dist/generated/settingsSchemaHooksClassifier.d.ts +2 -0
  75. package/dist/generated/settingsSchemaHooksClassifier.js +254 -0
  76. package/dist/generated/settingsSchemaMcpRequestEntry.d.ts +1 -0
  77. package/dist/generated/settingsSchemaMcpRequestEntry.js +162 -0
  78. package/dist/generated/settingsSchemaTables.d.ts +2 -0
  79. package/dist/generated/settingsSchemaTables.js +9 -0
  80. package/dist/generated/settingsSchemaTypes.d.ts +133 -0
  81. package/dist/generated/settingsSchemaTypes.js +1 -0
  82. package/dist/hitl/allowRuleShadow.d.ts +2 -1
  83. package/dist/hitl/allowRuleShadow.js +18 -15
  84. package/dist/hitl/approvalCardReasons.d.ts +3 -2
  85. package/dist/hitl/approvalCardReasons.js +29 -30
  86. package/dist/hitl/approvalHolds.d.ts +17 -0
  87. package/dist/hitl/approvalHolds.js +114 -29
  88. package/dist/hitl/askClassPair.d.ts +2 -2
  89. package/dist/hitl/askClassPair.js +16 -0
  90. package/dist/hitl/askGateWire.js +2 -1
  91. package/dist/hitl/askSettlementLine.d.ts +14 -4
  92. package/dist/hitl/askSettlementLine.js +67 -47
  93. package/dist/hitl/attributionLadder.d.ts +23 -0
  94. package/dist/hitl/attributionLadder.js +67 -0
  95. package/dist/hitl/automaticAnswerVerdict.d.ts +2 -1
  96. package/dist/hitl/automaticAnswerVerdict.js +8 -8
  97. package/dist/hitl/crashConvergedNotice.d.ts +5 -4
  98. package/dist/hitl/crashConvergedNotice.js +28 -41
  99. package/dist/hitl/decisionLedger.d.ts +55 -0
  100. package/dist/hitl/decisionLedger.js +156 -0
  101. package/dist/hitl/decisionRefusal.d.ts +1 -0
  102. package/dist/hitl/decisionRefusal.js +1 -1
  103. package/dist/hitl/decisionSend.d.ts +113 -0
  104. package/dist/hitl/decisionSend.js +579 -0
  105. package/dist/hitl/frameRouter.js +7 -8
  106. package/dist/hitl/hitlBridge.d.ts +1 -0
  107. package/dist/hitl/hitlBridge.js +21 -9
  108. package/dist/hitl/persistedRulesWire.d.ts +6 -5
  109. package/dist/hitl/persistedRulesWire.js +55 -73
  110. package/dist/hitl/planReviewWire.d.ts +5 -0
  111. package/dist/hitl/planReviewWire.js +53 -76
  112. package/dist/hitl/sessionPolicyDeliverable.d.ts +2 -0
  113. package/dist/hitl/sessionPolicyDeliverable.js +49 -9
  114. package/dist/hitl/sessionPolicyWire.d.ts +2 -1
  115. package/dist/hitl/sessionPolicyWire.js +23 -24
  116. package/dist/hitl/suspendedReopen.d.ts +2 -1
  117. package/dist/hitl/suspendedReopen.js +7 -10
  118. package/dist/hitl/toolApprovalWire.d.ts +17 -5
  119. package/dist/hitl/toolApprovalWire.js +120 -147
  120. package/dist/hooksWireCaps.js +145 -56
  121. package/dist/host.d.ts +21 -0
  122. package/dist/hostTurnSettings.d.ts +110 -0
  123. package/dist/hostTurnSettings.js +692 -0
  124. package/dist/humanTurn.d.ts +15 -1
  125. package/dist/humanTurn.js +100 -12
  126. package/dist/humanTurnRead.d.ts +2 -0
  127. package/dist/humanTurnRead.js +22 -1
  128. package/dist/i18n/messages.en.d.ts +713 -0
  129. package/dist/i18n/messages.en.js +713 -0
  130. package/dist/i18n/messages.zh.js +854 -1
  131. package/dist/index.d.ts +45 -13
  132. package/dist/index.js +27 -9
  133. package/dist/inheritEnvCapability.d.ts +4 -2
  134. package/dist/inheritEnvCapability.js +12 -5
  135. package/dist/inheritEnvWire.d.ts +3 -2
  136. package/dist/inheritEnvWire.js +14 -16
  137. package/dist/injectionLaneNotice.d.ts +5 -0
  138. package/dist/injectionLaneNotice.js +46 -0
  139. package/dist/interactiveHalt.js +17 -6
  140. package/dist/liveModelCatalog.d.ts +2 -1
  141. package/dist/liveModelCatalog.js +14 -8
  142. package/dist/mcpPanel.d.ts +10 -1
  143. package/dist/mcpPanel.js +42 -13
  144. package/dist/mcpProbeCapability.d.ts +2 -1
  145. package/dist/mcpProbeCapability.js +7 -11
  146. package/dist/mcpProbeWire.d.ts +3 -1
  147. package/dist/mcpProbeWire.js +5 -0
  148. package/dist/mcpReconnect.js +2 -2
  149. package/dist/mcpToolToggles.d.ts +6 -0
  150. package/dist/mcpToolToggles.js +46 -0
  151. package/dist/mcpWireCaps.d.ts +43 -3
  152. package/dist/mcpWireCaps.js +194 -59
  153. package/dist/mcpWireTransport.d.ts +4 -0
  154. package/dist/mcpWireTransport.js +18 -0
  155. package/dist/memoryEntriesWire.js +5 -10
  156. package/dist/model/onboardJudgement.d.ts +93 -0
  157. package/dist/model/onboardJudgement.js +415 -0
  158. package/dist/model/tierVocabulary.d.ts +2 -0
  159. package/dist/model/tierVocabulary.js +17 -5
  160. package/dist/modelCapabilityProbe.d.ts +65 -0
  161. package/dist/modelCapabilityProbe.js +601 -0
  162. package/dist/modelIdentityResolvability.d.ts +5 -0
  163. package/dist/modelIdentityResolvability.js +49 -22
  164. package/dist/ownKey.d.ts +33 -0
  165. package/dist/ownKey.js +109 -0
  166. package/dist/panelReduce.d.ts +11 -1
  167. package/dist/panelReduce.js +95 -16
  168. package/dist/panelStateHolder.d.ts +1 -0
  169. package/dist/panelStateHolder.js +10 -1
  170. package/dist/permissionRuleIssue.d.ts +2 -1
  171. package/dist/permissionRuleIssue.js +23 -20
  172. package/dist/permissionRulesWriteCapability.d.ts +2 -1
  173. package/dist/permissionRulesWriteCapability.js +8 -6
  174. package/dist/pluginHooksWire.d.ts +46 -3
  175. package/dist/pluginHooksWire.js +220 -12
  176. package/dist/prepareRefusal.d.ts +8 -0
  177. package/dist/prepareRefusal.js +72 -0
  178. package/dist/printModelStatus.d.ts +63 -0
  179. package/dist/printModelStatus.js +155 -0
  180. package/dist/request/taskRequest.d.ts +16 -10
  181. package/dist/request/taskRequest.js +195 -58
  182. package/dist/resumeRefusalCopy.d.ts +14 -9
  183. package/dist/resumeRefusalCopy.js +89 -144
  184. package/dist/retryStatus.d.ts +55 -0
  185. package/dist/retryStatus.js +155 -19
  186. package/dist/ruleRefusalSentence.d.ts +4 -0
  187. package/dist/ruleRefusalSentence.js +58 -0
  188. package/dist/sandboxWire.d.ts +2 -1
  189. package/dist/sandboxWire.js +55 -21
  190. package/dist/scenarioRefusal.d.ts +10 -0
  191. package/dist/scenarioRefusal.js +111 -0
  192. package/dist/seam.d.ts +48 -9
  193. package/dist/seam.js +54 -5
  194. package/dist/seatContract.d.ts +272 -14
  195. package/dist/seatContract.js +711 -23
  196. package/dist/selfOrchestrationDenial.js +7 -19
  197. package/dist/sessionIdSegment.d.ts +6 -0
  198. package/dist/sessionIdSegment.js +24 -0
  199. package/dist/sessionMemoryStatus.js +6 -11
  200. package/dist/sessionPolicyCapability.d.ts +2 -1
  201. package/dist/sessionPolicyCapability.js +8 -6
  202. package/dist/settingsCredentialKeyPaths.d.ts +8 -0
  203. package/dist/settingsCredentialKeyPaths.js +219 -0
  204. package/dist/settingsHooksSalvage.d.ts +44 -0
  205. package/dist/settingsHooksSalvage.js +211 -0
  206. package/dist/skillToolListsCapability.d.ts +2 -1
  207. package/dist/skillToolListsCapability.js +8 -6
  208. package/dist/skillsWireCaps.d.ts +0 -3
  209. package/dist/skillsWireCaps.js +1 -2
  210. package/dist/sseIdleTriage.js +3 -2
  211. package/dist/subagent/engineSubagentResume.js +15 -9
  212. package/dist/subagentContentStore.d.ts +4 -0
  213. package/dist/subagentContentStore.js +157 -27
  214. package/dist/toolHistoryMismatch.js +3 -2
  215. package/dist/toolRoster.d.ts +3 -2
  216. package/dist/toolRoster.js +21 -20
  217. package/dist/toolsFlagNames.d.ts +8 -0
  218. package/dist/toolsFlagNames.js +110 -0
  219. package/dist/turnFailurePresentation.d.ts +12 -1
  220. package/dist/turnFailurePresentation.js +220 -15
  221. package/dist/usageAnalytics.js +6 -11
  222. package/dist/userAttachmentEnvelope.d.ts +1 -0
  223. package/dist/userAttachmentEnvelope.js +37 -7
  224. package/dist/userAttachmentSeat.d.ts +21 -0
  225. package/dist/userAttachmentSeat.js +75 -0
  226. package/dist/userInvokedSkillsCapability.d.ts +11 -1
  227. package/dist/userInvokedSkillsCapability.js +31 -7
  228. package/dist/wireErrorTriage.d.ts +7 -0
  229. package/dist/wireErrorTriage.js +23 -1
  230. package/dist/wireFailureShape.js +2 -1
  231. package/dist/wireRefusalCopy.d.ts +62 -16
  232. package/dist/wireRefusalCopy.js +307 -148
  233. package/dist/workflowsJournalCapability.d.ts +2 -1
  234. package/dist/workflowsJournalCapability.js +8 -6
  235. package/docs/INTEGRATION-CLIENTS.md +951 -42
  236. package/package.json +5 -5
@@ -7,8 +7,9 @@ import { ownDataField, readOwnDataValue } from '../ownKey.js';
7
7
  import { PROTOCOL_ERROR_NAME, THROWN_SLOT_UNREADABLE, protocolErrorNameReading, readBlockedBy, readThrownSlot, thrownIsInstanceOf, wireFailureTextsOf } from '../wireFailureShape.js';
8
8
  import { APPROVAL_BINDING_MISMATCH, CHECKPOINT_INVALID_OUTCOME, RESUME_POLICY_FOLD_CODE } from '../engineErrorCodes.js';
9
9
  import { suspendedReopenOf } from './suspendedReopen.js';
10
+ import { closeDecisionOp, decisionLedgerKeysOf, openDecisionOp } from './decisionLedger.js';
10
11
  export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
11
- const MAX_DENY_REASON_CHARS = 4096;
12
+ export const MAX_DENY_REASON_CHARS = 4096;
12
13
  export function denyReasonForWire(reason, tag) {
13
14
  if (typeof reason !== 'string')
14
15
  return undefined;
@@ -146,13 +147,13 @@ function isTransientDecideTransportFailure(e, depth = 0) {
146
147
  return false;
147
148
  if (thrownIsInstanceOf(e, HitlSafetyError))
148
149
  return false;
149
- const status = readThrownSlot(e, 'status');
150
+ const status = readThrownSlot(e, 'status', 'data');
150
151
  if (status === THROWN_SLOT_UNREADABLE || typeof status === 'number')
151
152
  return false;
152
153
  const name = readThrownSlot(e, 'name');
153
154
  if (name === 'TimeoutError' || name === 'AbortError')
154
155
  return true;
155
- const code = readThrownSlot(e, 'code');
156
+ const code = readThrownSlot(e, 'code', 'data');
156
157
  if (typeof code === 'string' && TRANSIENT_NETWORK_CODES.has(code))
157
158
  return true;
158
159
  const message = readThrownSlot(e, 'message');
@@ -172,6 +173,7 @@ class DecideSpan {
172
173
  owner;
173
174
  observer;
174
175
  identity;
176
+ ledgerOp;
175
177
  attempts = 0;
176
178
  ok = false;
177
179
  retryExhausted = false;
@@ -189,9 +191,11 @@ class DecideSpan {
189
191
  return mine === undefined || (mine.sessionId === identity.sessionId && mine.boundCallId === identity.boundCallId);
190
192
  }
191
193
  async run(identity, send) {
192
- if (this.observer !== undefined && this.identity === undefined) {
194
+ if (this.identity === undefined) {
193
195
  this.identity = identity;
194
- notifyDecideObserver(this.observer, Object.freeze({ ...this.identity, phase: 'start' }));
196
+ this.ledgerOp = openDecideLedgerOp(identity);
197
+ if (this.observer !== undefined)
198
+ notifyDecideObserver(this.observer, Object.freeze({ ...this.identity, phase: 'start' }));
195
199
  }
196
200
  this.inFlight++;
197
201
  const tally = { attempts: 0 };
@@ -224,11 +228,16 @@ class DecideSpan {
224
228
  if (this.ended)
225
229
  return;
226
230
  this.ended = true;
231
+ if (this.ledgerOp !== undefined)
232
+ closeDecisionOp(this.ledgerOp, 'package');
227
233
  if (this.observer === undefined || this.identity === undefined)
228
234
  return;
229
235
  notifyDecideObserver(this.observer, Object.freeze({ ...this.identity, phase: 'end', ok: this.ok, attempts: this.attempts, retryExhausted: this.retryExhausted }));
230
236
  }
231
237
  }
238
+ function openDecideLedgerOp(identity) {
239
+ return openDecisionOp('tool_approval', decisionLedgerKeysOf({ taskId: identity.taskId, callId: identity.boundCallId }), 'posting', true);
240
+ }
232
241
  function wireOptsOf(opts) {
233
242
  if (opts === undefined || opts.operation === undefined)
234
243
  return opts;
@@ -408,10 +417,10 @@ export class HitlBridge {
408
417
  }
409
418
  const observer = this.decideObserver;
410
419
  const tally = { attempts: 0 };
411
- if (observer === undefined)
412
- return this.decideRawAttempts(sessionId, decision, opts, tally);
413
420
  const identity = this.decideIdentityOf(sessionId, decision);
414
- notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'start' }));
421
+ const ledgerOp = openDecideLedgerOp(identity);
422
+ if (observer !== undefined)
423
+ notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'start' }));
415
424
  let ok = false;
416
425
  let retryExhausted = false;
417
426
  try {
@@ -424,7 +433,10 @@ export class HitlBridge {
424
433
  throw e;
425
434
  }
426
435
  finally {
427
- notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'end', ok, attempts: tally.attempts, retryExhausted }));
436
+ closeDecisionOp(ledgerOp, 'package');
437
+ if (observer !== undefined) {
438
+ notifyDecideObserver(observer, Object.freeze({ ...identity, phase: 'end', ok, attempts: tally.attempts, retryExhausted }));
439
+ }
428
440
  }
429
441
  }
430
442
  decideIdentityOf(sessionId, decision) {
@@ -1,4 +1,5 @@
1
1
  import type { CcImportLayer, CcImportPrepareResult, CcImportRedeemResult, PersistedRule, RuleBehavior, RuleListParams, RuleListResult, RuleRevokeRequest, RemovalLiveness, RuleRevokeResult, RuleWriteRequest, RuleWriteResult } from '@sema-agent/sdk';
2
+ import type { UiLanguage } from '../env/uiLanguage.js';
2
3
  export interface RulesFacade {
3
4
  list(params?: RuleListParams, opts?: {
4
5
  signal?: AbortSignal;
@@ -93,9 +94,9 @@ export interface PersistedRuleOriginsRow {
93
94
  readonly origin?: string | undefined;
94
95
  }[] | undefined;
95
96
  }
96
- export declare function persistedRuleAddOriginLabel(origin: string | undefined): string;
97
- export declare function persistedRuleOriginsLabel(rule: PersistedRuleOriginsRow): string;
98
- export declare function persistedRuleScopeLabel(scope: string | undefined): string;
97
+ export declare function persistedRuleAddOriginLabel(origin: string | undefined, lang?: UiLanguage): string;
98
+ export declare function persistedRuleOriginsLabel(rule: PersistedRuleOriginsRow, lang?: UiLanguage): string;
99
+ export declare function persistedRuleScopeLabel(scope: string | undefined, lang?: UiLanguage): string;
99
100
  export type PersistedRuleRevokeOutcomeWord = 'removed' | 'nothing-written' | 'unknown-status';
100
101
  export interface PersistedRuleRevokeResultBody {
101
102
  readonly status?: string | undefined;
@@ -107,11 +108,11 @@ export interface PersistedRuleRevokeOutcome {
107
108
  readonly notice: string;
108
109
  readonly change: string;
109
110
  }
110
- export declare function persistedRuleRevokeOutcome(ruleText: string | undefined, result: PersistedRuleRevokeResultBody | undefined): PersistedRuleRevokeOutcome;
111
+ export declare function persistedRuleRevokeOutcome(ruleText: string | undefined, result: PersistedRuleRevokeResultBody | undefined, lang?: UiLanguage): PersistedRuleRevokeOutcome;
111
112
  export interface PersistedRuleMatchRow {
112
113
  readonly match?: string | undefined;
113
114
  }
114
- export declare function persistedRuleMatchLabel(ruleOrMatch: string | PersistedRuleMatchRow | undefined): string;
115
+ export declare function persistedRuleMatchLabel(ruleOrMatch: string | PersistedRuleMatchRow | undefined, lang?: UiLanguage): string;
115
116
  export declare function revokeTargetFromPersistedRule(rule: unknown, opts?: {
116
117
  principal?: string;
117
118
  }): RuleRevokeRequest | undefined;
@@ -4,6 +4,7 @@ import { CORE_RULE_BEHAVIORS } from '../generated/engineFactTables.js';
4
4
  import { wireFailureShapeOf } from '../wireFailureShape.js';
5
5
  import { CAPABILITY_OPERATOR_PRINCIPALS_REQUIRED } from '../engineErrorCodes.js';
6
6
  import { hostLog } from '../host.js';
7
+ import { msg, renderMessage } from '../i18n/translate.js';
7
8
  import { guardedRead, readOwnDataValue } from '../ownKey.js';
8
9
  import { observedPermissionRulesWrite, permissionRulesWriteAvailable } from '../permissionRulesWriteCapability.js';
9
10
  import { isToolApprovalRespondReplayed, readToolApprovalRespondAck } from './toolApprovalWire.js';
@@ -114,123 +115,104 @@ export const PERSISTED_RULE_BEHAVIOR_UNKNOWN = '(unknown)';
114
115
  export function persistedRuleBehaviorLabel(rule) {
115
116
  return rowStr(rule, 'behavior') ?? PERSISTED_RULE_BEHAVIOR_UNKNOWN;
116
117
  }
117
- const PERSISTED_RULE_ORIGIN_PHRASES = Object.freeze({
118
- user: 'added by you',
119
- 'imported-cc': 'imported',
120
- starter: 'preset',
118
+ const PERSISTED_RULE_ORIGIN_KEYS = Object.freeze({
119
+ user: 'rules.origin.user',
120
+ 'imported-cc': 'rules.origin.importedCc',
121
+ starter: 'rules.origin.starter',
121
122
  });
122
- const UNKNOWN_ORIGIN_PHRASE = 'an origin this build does not know';
123
- const NO_ORIGIN_PHRASE = 'unknown origin';
123
+ const UNKNOWN_ORIGIN_KEY = 'rules.origin.unknownWord';
124
+ const NO_ORIGIN_KEY = 'rules.origin.none';
125
+ function originKeyOf(origin) {
126
+ return typeof origin === 'string' && Object.hasOwn(PERSISTED_RULE_ORIGIN_KEYS, origin) ? PERSISTED_RULE_ORIGIN_KEYS[origin] : UNKNOWN_ORIGIN_KEY;
127
+ }
124
128
  function ownSlot(o, key) {
125
129
  if (typeof o !== 'object' || o === null)
126
130
  return undefined;
127
131
  return guardedRead(() => readOwnDataValue(o, key));
128
132
  }
129
133
  const ORIGINS_READ_MAX = 256;
130
- export function persistedRuleAddOriginLabel(origin) {
131
- return typeof origin === 'string' && Object.hasOwn(PERSISTED_RULE_ORIGIN_PHRASES, origin)
132
- ? PERSISTED_RULE_ORIGIN_PHRASES[origin]
133
- : UNKNOWN_ORIGIN_PHRASE;
134
+ export function persistedRuleAddOriginLabel(origin, lang) {
135
+ return renderMessage(msg(originKeyOf(origin)), lang);
134
136
  }
135
- export function persistedRuleOriginsLabel(rule) {
137
+ export function persistedRuleOriginsLabel(rule, lang) {
136
138
  const adds = ownSlot(rule, 'adds');
137
139
  const isArr = guardedRead(() => (Array.isArray(adds) ? true : undefined));
138
140
  if (isArr !== true)
139
- return NO_ORIGIN_PHRASE;
141
+ return renderMessage(msg(NO_ORIGIN_KEY), lang);
140
142
  const lenRaw = ownSlot(adds, 'length');
141
143
  const len = typeof lenRaw === 'number' && Number.isSafeInteger(lenRaw) && lenRaw > 0 ? Math.min(lenRaw, ORIGINS_READ_MAX) : 0;
142
144
  const seen = [];
143
145
  for (let i = 0; i < len; i++) {
144
146
  const origin = ownSlot(ownSlot(adds, String(i)), 'origin');
145
- const phrase = persistedRuleAddOriginLabel(typeof origin === 'string' ? origin : undefined);
146
- if (!seen.includes(phrase))
147
- seen.push(phrase);
147
+ const key = originKeyOf(typeof origin === 'string' ? origin : undefined);
148
+ if (!seen.includes(key))
149
+ seen.push(key);
148
150
  }
149
- return seen.length === 0 ? NO_ORIGIN_PHRASE : seen.join(' + ');
151
+ return seen.length === 0 ? renderMessage(msg(NO_ORIGIN_KEY), lang) : seen.map((k) => renderMessage(msg(k), lang)).join(renderMessage(msg('rules.origin.join'), lang));
150
152
  }
151
- const UNKNOWN_SCOPE_PHRASE = 'a scope this build does not know';
152
- export function persistedRuleScopeLabel(scope) {
153
+ const UNKNOWN_SCOPE_KEY = 'rules.scope.unknown';
154
+ export function persistedRuleScopeLabel(scope, lang) {
153
155
  if (scope === 'global')
154
- return 'all projects';
156
+ return renderMessage(msg('rules.scope.all'), lang);
155
157
  if (typeof scope === 'string' && scope.startsWith('project:') && scope.length > 'project:'.length) {
156
158
  const shown = displayUntrusted(scope.slice('project:'.length), { max: 400, blanks: true });
157
- return shown === '' ? UNKNOWN_SCOPE_PHRASE : shown;
159
+ return shown === '' ? renderMessage(msg(UNKNOWN_SCOPE_KEY), lang) : shown;
158
160
  }
159
- return UNKNOWN_SCOPE_PHRASE;
161
+ return renderMessage(msg(UNKNOWN_SCOPE_KEY), lang);
160
162
  }
161
- const PERSISTED_RULE_REVOKE_LINES = Object.freeze({
163
+ const PERSISTED_RULE_REVOKE_KEYS = Object.freeze({
162
164
  removed: {
163
- yes: {
164
- notice: '{rule}: a tombstone was written, but the rule is still live — it was approved again during the call.',
165
- change: 'Revoked saved rule {rule} (still live — approved again meanwhile)',
166
- },
167
- unknown: {
168
- notice: '{rule}: a tombstone was written, but whether the rule is still live could not be confirmed — treat it as still in effect and check the list.',
169
- change: 'Revoked saved rule {rule} (liveness could not be confirmed — treated as still in effect)',
170
- },
171
- no: { notice: 'Removed {rule}.', change: 'Revoked saved rule {rule}' },
165
+ yes: { notice: 'rules.revoke.removed.yes.notice', change: 'rules.revoke.removed.yes.change' },
166
+ unknown: { notice: 'rules.revoke.removed.unknown.notice', change: 'rules.revoke.removed.unknown.change' },
167
+ no: { notice: 'rules.revoke.removed.no.notice', change: 'rules.revoke.removed.no.change' },
172
168
  },
173
169
  'nothing-written': {
174
- yes: {
175
- notice: '{rule}: nothing was written, and the rule is still live — it was not removed; check the list.',
176
- change: 'Removing saved rule {rule} wrote nothing — the rule is still live',
177
- },
178
- unknown: {
179
- notice: '{rule}: nothing was written, and whether the rule is still live could not be confirmed — treat it as still in effect and check the list.',
180
- change: 'Removing saved rule {rule} wrote nothing — liveness could not be confirmed',
181
- },
182
- no: { notice: '{rule}: nothing to remove.', change: 'Removing saved rule {rule} wrote nothing (nothing to remove)' },
170
+ yes: { notice: 'rules.revoke.nothingWritten.yes.notice', change: 'rules.revoke.nothingWritten.yes.change' },
171
+ unknown: { notice: 'rules.revoke.nothingWritten.unknown.notice', change: 'rules.revoke.nothingWritten.unknown.change' },
172
+ no: { notice: 'rules.revoke.nothingWritten.no.notice', change: 'rules.revoke.nothingWritten.no.change' },
183
173
  },
184
174
  });
185
- const REVOKE_UNKNOWN_STATUS = Object.freeze({
186
- absent: {
187
- notice: '{rule}: the engine did not say what this removal did, so whether anything was removed is unclear — check the list.',
188
- change: 'Removing saved rule {rule} came back without saying what it did',
189
- },
190
- unrecognised: {
191
- notice: '{rule}: the engine answered with a status this build does not know, so whether anything was removed is unclear — check the list.',
192
- change: 'Removing saved rule {rule} returned a status this build does not know',
193
- },
194
- liveness: { yes: ' It also reports the rule is still live.', unknown: ' Whether the rule is still live could not be confirmed either.', no: '' },
175
+ const REVOKE_UNKNOWN_STATUS_KEYS = Object.freeze({
176
+ absent: { notice: 'rules.revoke.absent.notice', change: 'rules.revoke.absent.change' },
177
+ unrecognised: { notice: 'rules.revoke.unrecognised.notice', change: 'rules.revoke.unrecognised.change' },
178
+ liveness: { yes: 'rules.revoke.liveness.yes', unknown: 'rules.revoke.liveness.unknown', no: undefined },
195
179
  });
196
- const fillRule = (template, rule) => template.split('{rule}').join(rule);
197
- export function persistedRuleRevokeOutcome(ruleText, result) {
180
+ export function persistedRuleRevokeOutcome(ruleText, result, lang) {
198
181
  const quoted = typeof ruleText === 'string' ? quoteUntrustedForSentence(ruleText, 120) : undefined;
199
- const rule = quoted ?? 'this rule';
182
+ const rule = quoted ?? msg('rules.revoke.thisRule');
200
183
  const status = ownSlot(result, 'status');
201
184
  const rawLive = ownSlot(result, 'stillLive');
202
185
  const liveness = rawLive === 'yes' || rawLive === 'no' || rawLive === 'unknown' ? rawLive : 'unknown';
203
186
  if (status === 'removed' || status === 'no-op') {
204
187
  const outcome = status === 'removed' ? 'removed' : 'nothing-written';
205
- const line = PERSISTED_RULE_REVOKE_LINES[outcome][liveness];
206
- return Object.freeze({ outcome, liveness, notice: fillRule(line.notice, rule), change: fillRule(line.change, rule) });
188
+ const line = PERSISTED_RULE_REVOKE_KEYS[outcome][liveness];
189
+ return Object.freeze({ outcome, liveness, notice: renderMessage(msg(line.notice, { rule }), lang), change: renderMessage(msg(line.change, { rule }), lang) });
207
190
  }
208
- const head = typeof status === 'string' && status !== '' ? REVOKE_UNKNOWN_STATUS.unrecognised : REVOKE_UNKNOWN_STATUS.absent;
191
+ const head = typeof status === 'string' && status !== '' ? REVOKE_UNKNOWN_STATUS_KEYS.unrecognised : REVOKE_UNKNOWN_STATUS_KEYS.absent;
192
+ const tailKey = REVOKE_UNKNOWN_STATUS_KEYS.liveness[liveness];
209
193
  return Object.freeze({
210
194
  outcome: 'unknown-status',
211
195
  liveness,
212
- notice: `${fillRule(head.notice, rule)}${REVOKE_UNKNOWN_STATUS.liveness[liveness]}`,
213
- change: fillRule(head.change, rule),
196
+ notice: `${renderMessage(msg(head.notice, { rule }), lang)}${tailKey === undefined ? '' : renderMessage(msg(tailKey), lang)}`,
197
+ change: renderMessage(msg(head.change, { rule }), lang),
214
198
  });
215
199
  }
216
- const PERSISTED_RULE_MATCH_LABELS = Object.freeze({
217
- exact: 'exact match',
218
- prefix: 'prefix match',
219
- wildcard: 'wildcard match',
220
- subpath: 'subpath match',
221
- path: 'path match',
222
- name: 'skill name match',
223
- name_prefix: 'skill name prefix match',
224
- host: 'web domain match (one host)',
225
- subdomain: 'web domain match (every host under the domain)',
200
+ const PERSISTED_RULE_MATCH_KEYS = Object.freeze({
201
+ exact: 'rules.match.exact',
202
+ prefix: 'rules.match.prefix',
203
+ wildcard: 'rules.match.wildcard',
204
+ subpath: 'rules.match.subpath',
205
+ path: 'rules.match.path',
206
+ name: 'rules.match.name',
207
+ name_prefix: 'rules.match.namePrefix',
208
+ host: 'rules.match.host',
209
+ subdomain: 'rules.match.subdomain',
226
210
  });
227
- export function persistedRuleMatchLabel(ruleOrMatch) {
211
+ export function persistedRuleMatchLabel(ruleOrMatch, lang) {
228
212
  const word = typeof ruleOrMatch === 'string' ? ruleOrMatch : ownSlot(ruleOrMatch, 'match');
229
213
  if (typeof word !== 'string' || word === '')
230
- return 'an unreported match form';
231
- return Object.hasOwn(PERSISTED_RULE_MATCH_LABELS, word)
232
- ? PERSISTED_RULE_MATCH_LABELS[word]
233
- : 'a match form this build does not know';
214
+ return renderMessage(msg('rules.match.unreported'), lang);
215
+ return renderMessage(msg(Object.hasOwn(PERSISTED_RULE_MATCH_KEYS, word) ? PERSISTED_RULE_MATCH_KEYS[word] : 'rules.match.unknown'), lang);
234
216
  }
235
217
  export function revokeTargetFromPersistedRule(rule, opts) {
236
218
  const behavior = persistedRuleBehaviorOf(rule);
@@ -37,6 +37,11 @@ export interface PlanReviewCardLike {
37
37
  }>;
38
38
  }>;
39
39
  }
40
+ export interface PlanReviewQuestionIdReading {
41
+ readonly taskId: string;
42
+ readonly reopened: boolean;
43
+ }
44
+ export declare function planReviewTaskIdFromQuestionId(questionId: {} | null | undefined): PlanReviewQuestionIdReading | undefined;
40
45
  export declare function planReviewOfferedModeOf(card: string | PlanReviewCardLike): PlanReviewOfferedMode;
41
46
  export interface ArmPlanReviewOptions {
42
47
  submittedInPlanMode?: boolean;
@@ -11,6 +11,7 @@ import { readOwnDataNonEmptyString, readOwnDataValue } from '../ownKey.js';
11
11
  import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError, resumeContextUnavailableContent, resumeContextUnavailableFromError } from '../resumeRefusalCopy.js';
12
12
  import { REQUEST_FIELD_CONFLICT } from '../engineErrorCodes.js';
13
13
  import { refusalDeliveryOf } from './decisionRefusal.js';
14
+ import { __resetDecisionLedgerForTests, decisionInFlightOf, decisionKeyInFlight, decisionLedgerKeysOf, decisionOpsWithKey, decisionReadMark, dropDecisionOp, latestDecisionHandedMark, markLatestDecisionNotApplied, openDecisionOp, planDecisionKey, readMarkOf, settleDecisionOp, settleDecisionOpsOnGateLeft, } from './decisionLedger.js';
14
15
  import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
15
16
  import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
16
17
  import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
@@ -300,6 +301,14 @@ function offeredModeFromLabels(card) {
300
301
  return 'two_choice';
301
302
  return undefined;
302
303
  }
304
+ export function planReviewTaskIdFromQuestionId(questionId) {
305
+ if (typeof questionId !== 'string')
306
+ return undefined;
307
+ const taskId = taskIdFromPlanReviewQuestionId(questionId);
308
+ if (taskId === undefined)
309
+ return undefined;
310
+ return Object.freeze({ taskId, reopened: questionId.includes(REOPEN_ID_TAIL) });
311
+ }
303
312
  function offeredModeFromQuestionId(questionId) {
304
313
  if (taskIdFromPlanReviewQuestionId(questionId) === undefined)
305
314
  return 'unknown';
@@ -449,42 +458,21 @@ function usablePlanReviewWireClient(client) {
449
458
  return clientHasVerbs(client, [['assistant', 'planReview'], ['runs', 'get']]);
450
459
  }
451
460
  const PLAN_REVIEW_INJECTED_REPULL_TIMEOUT_MS = 15_000;
452
- const planReviewDecisionOps = [];
453
- const MAX_PLAN_REVIEW_DECISION_OPS = 32;
454
- let planReviewLedgerClock = 0;
455
- let decideDispatchSeq = 0;
456
- function planReviewOpsOf(taskId) {
457
- return planReviewDecisionOps.filter((op) => op.taskId === taskId);
458
- }
459
- function settlePlanReviewDecisionOp(op, by) {
460
- if (op.settledAt !== null)
461
- return;
462
- op.settledAt = ++planReviewLedgerClock;
463
- op.settler = by;
464
- }
465
- function pushPlanReviewDecisionOp(op) {
466
- planReviewDecisionOps.push(op);
467
- while (planReviewDecisionOps.length > MAX_PLAN_REVIEW_DECISION_OPS) {
468
- let victim = planReviewDecisionOps.findIndex((o) => o.settledAt !== null && !o.ownCallOpen);
469
- if (victim < 0)
470
- victim = planReviewDecisionOps.findIndex((o) => o !== op && o.phase === 'handed');
471
- if (victim < 0)
472
- break;
473
- planReviewDecisionOps.splice(victim, 1);
474
- }
461
+ function planReviewLedgerKeys(taskId) {
462
+ return taskId === '' ? [planDecisionKey('')] : decisionLedgerKeysOf({ taskId, planTaskId: taskId });
475
463
  }
476
- function dropPlanReviewDecisionOp(op) {
477
- const i = planReviewDecisionOps.indexOf(op);
478
- if (i >= 0)
479
- planReviewDecisionOps.splice(i, 1);
464
+ function planReviewOpsOf(taskId) {
465
+ return decisionOpsWithKey('plan_review', planDecisionKey(taskId));
480
466
  }
467
+ let decideDispatchSeq = 0;
481
468
  function claimablePlanReviewDecisionOp(taskId) {
482
- const open = planReviewDecisionOps.find((o) => o.taskId === taskId && o.phase === 'handed' && o.settledAt === null);
469
+ const ops = planReviewOpsOf(taskId);
470
+ const open = ops.find((o) => o.phase === 'handed' && o.settledAt === null);
483
471
  if (open !== undefined)
484
472
  return open;
485
473
  let leftBehind;
486
- for (const o of planReviewDecisionOps) {
487
- if (o.taskId !== taskId || o.phase !== 'handed' || o.settler !== 'gate_left')
474
+ for (const o of ops) {
475
+ if (o.phase !== 'handed' || o.settler !== 'gate_left')
488
476
  continue;
489
477
  if (leftBehind === undefined || o.handedAt > leftBehind.handedAt)
490
478
  leftBehind = o;
@@ -494,62 +482,47 @@ function claimablePlanReviewDecisionOp(taskId) {
494
482
  export function notePlanReviewDecisionHandedOver(taskId) {
495
483
  if (typeof taskId !== 'string' || taskId === '')
496
484
  return null;
497
- if (planReviewDecisionOps.some((op) => op.taskId === taskId && op.settledAt === null))
485
+ if (planReviewOpsOf(taskId).some((op) => op.settledAt === null))
498
486
  return null;
499
- const op = { taskId, handedAt: ++planReviewLedgerClock, settledAt: null, settler: null, phase: 'handed', notApplied: false, ownCallOpen: false };
500
- pushPlanReviewDecisionOp(op);
487
+ const op = openDecisionOp('plan_review', planReviewLedgerKeys(taskId), 'handed', false);
501
488
  return {
502
489
  settle: () => {
503
490
  if (op.phase !== 'handed')
504
491
  return;
505
- settlePlanReviewDecisionOp(op, 'host');
492
+ settleDecisionOp(op, 'host');
506
493
  },
507
494
  revoke: () => {
508
495
  if (op.phase !== 'handed' || op.settledAt !== null)
509
496
  return;
510
- dropPlanReviewDecisionOp(op);
497
+ dropDecisionOp(op);
511
498
  },
512
499
  };
513
500
  }
514
501
  export function planReviewPendingReadMark() {
515
- return ++planReviewLedgerClock;
502
+ return decisionReadMark();
516
503
  }
517
504
  export function planReviewDecisionInFlight(taskId, pendingReadStartedAt) {
518
505
  if (typeof taskId !== 'string' || taskId === '')
519
506
  return false;
520
- const mark = typeof pendingReadStartedAt === 'number' && Number.isFinite(pendingReadStartedAt) ? pendingReadStartedAt : undefined;
521
- return planReviewDecisionOps.some((op) => op.taskId === taskId && (op.settledAt === null || (mark !== undefined && op.settledAt > mark && !op.notApplied)));
507
+ return decisionKeyInFlight(planDecisionKey(taskId), readMarkOf(pendingReadStartedAt));
522
508
  }
523
509
  export function notePlanReviewDecisionNotApplied(taskId) {
524
510
  if (typeof taskId !== 'string' || taskId === '')
525
511
  return;
526
- const ops = planReviewOpsOf(taskId);
527
- if (ops.some((op) => op.settledAt === null))
528
- return;
529
- let latest;
530
- for (const op of ops)
531
- if (latest === undefined || op.handedAt > latest.handedAt)
532
- latest = op;
533
- if (latest !== undefined)
534
- latest.notApplied = true;
512
+ markLatestDecisionNotApplied('plan_review', planDecisionKey(taskId));
535
513
  }
536
514
  export function notePlanReviewGateLeft(taskId) {
537
515
  if (typeof taskId !== 'string' || taskId === '')
538
516
  return;
539
- for (const op of planReviewOpsOf(taskId))
540
- settlePlanReviewDecisionOp(op, 'gate_left');
517
+ settleDecisionOpsOnGateLeft('plan_review', planDecisionKey(taskId));
541
518
  }
542
519
  export function latestPlanReviewDecisionMark(taskId) {
543
520
  if (typeof taskId !== 'string' || taskId === '')
544
521
  return null;
545
- let mark = null;
546
- for (const op of planReviewOpsOf(taskId))
547
- if (mark === null || op.handedAt > mark)
548
- mark = op.handedAt;
549
- return mark;
522
+ return latestDecisionHandedMark('plan_review', planDecisionKey(taskId));
550
523
  }
551
524
  export function __resetPlanReviewDecisionLatchForTests() {
552
- planReviewDecisionOps.length = 0;
525
+ __resetDecisionLedgerForTests('plan_review');
553
526
  }
554
527
  export async function decidePlanReview(taskId, decision, permissionModeAfter, opts) {
555
528
  return decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, parkedGateIdentityOf(opts));
@@ -601,14 +574,13 @@ function bodySaysReparked(body) {
601
574
  }
602
575
  }
603
576
  async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts, gateBefore) {
604
- if (typeof taskId === 'string' && planReviewDecisionOps.some((op) => op.taskId === taskId && op.phase === 'posting')) {
577
+ if (typeof taskId === 'string' && planReviewOpsOf(taskId).some((op) => op.phase === 'posting')) {
605
578
  hostLog('error', `planReviewWire: decidePlanReview(${decision}) for task ${taskId} REFUSED — a decision for this task is already in flight (duplicate delivery latched)`);
606
579
  return;
607
580
  }
608
- let op = claimablePlanReviewDecisionOp(taskId);
581
+ let op = typeof taskId === 'string' ? claimablePlanReviewDecisionOp(taskId) : undefined;
609
582
  if (op === undefined) {
610
- op = { taskId: typeof taskId === 'string' ? taskId : String(taskId), handedAt: ++planReviewLedgerClock, settledAt: null, settler: null, phase: 'posting', notApplied: false, ownCallOpen: true };
611
- pushPlanReviewDecisionOp(op);
583
+ op = openDecisionOp('plan_review', planReviewLedgerKeys(typeof taskId === 'string' ? taskId : String(taskId)), 'posting', true);
612
584
  }
613
585
  else {
614
586
  if (op.settler === 'gate_left') {
@@ -627,7 +599,7 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
627
599
  const settle = (notApplied) => {
628
600
  if (notApplied)
629
601
  mine.notApplied = true;
630
- settlePlanReviewDecisionOp(mine, 'package');
602
+ settleDecisionOp(mine, 'package');
631
603
  };
632
604
  const dispatchNo = ++decideDispatchSeq;
633
605
  hostLog('debug', `planReviewWire: decidePlanReview dispatch #${dispatchNo} task=${taskId} decision=${decision} mode=${permissionModeAfter ?? '-'}`);
@@ -636,7 +608,7 @@ async function decidePlanReviewFor(taskId, decision, permissionModeAfter, opts,
636
608
  }
637
609
  finally {
638
610
  releaseLatch();
639
- settlePlanReviewDecisionOp(mine, 'package');
611
+ settleDecisionOp(mine, 'package');
640
612
  mine.ownCallOpen = false;
641
613
  }
642
614
  }
@@ -904,10 +876,10 @@ function joinReopenFlight(flight, taskId, receiptMs) {
904
876
  abortableSleep(Math.max(0, receiptMs), ac.signal).then(() => false),
905
877
  ]).then((presented) => {
906
878
  ac.abort();
907
- return presented ? { reopened: true, firstSight: false } : { reopened: false };
879
+ return presented ? { reopened: true, firstSight: false } : refusedReopen('not_presented');
908
880
  }, () => {
909
881
  ac.abort();
910
- return { reopened: false };
882
+ return refusedReopen('not_presented');
911
883
  });
912
884
  }
913
885
  function mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs) {
@@ -947,10 +919,10 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
947
919
  const markBefore = dismissalEntryFor(sessionKey, taskId);
948
920
  const settleOnReceipt = (receipt, firstSight) => receipt.then((presented) => {
949
921
  if (!presented)
950
- return { reopened: false };
922
+ return refusedReopen('not_presented');
951
923
  revokePlanReviewUserDismissal(sessionKey, taskId, markBefore);
952
924
  return { reopened: true, firstSight };
953
- }, () => ({ reopened: false }));
925
+ }, () => refusedReopen('not_presented'));
954
926
  const canonicalId = planReviewQuestionId(taskId);
955
927
  const armedKey = planReviewArmedKeyFor(sessionKey, taskId);
956
928
  const firstSight = !wasGateArmedFor(sessionKey, armedKey);
@@ -1009,6 +981,9 @@ function mintPlanReviewReopenCard(taskId, opts, sessionKey, receiptMs) {
1009
981
  publishQuestionFrameFor(sessionKey, planReviewQuestionFrame(questionId, REOPEN_QUESTION, false, true));
1010
982
  return settleOnReceipt(receipt, firstSight);
1011
983
  }
984
+ function refusedReopen(cause) {
985
+ return { reopened: false, _sema_reopenRefusal: cause };
986
+ }
1012
987
  function noPresentationSurfaceVerdict(taskId, sessionKey) {
1013
988
  const verdict = { reopened: false, _sema_noPresentationSurface: true };
1014
989
  hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — nothing is subscribed to question frames for session ` +
@@ -1031,7 +1006,7 @@ async function reopenAfterDismissalCheck(taskId, opts, sessionKey, receiptMs, ma
1031
1006
  }
1032
1007
  }
1033
1008
  }
1034
- const ledgerSaysInFlight = planReviewDecisionInFlight(taskId, recheckReadStart);
1009
+ const ledgerSaysInFlight = decisionInFlightOf({ gate: 'plan_review', taskId }, recheckReadStart);
1035
1010
  if (ledgerSaysInFlight) {
1036
1011
  hostLog('debug', `planReviewWire: reopen for task ${taskId} refused after the dismissal check — a decision for this task went out while the check was reading`);
1037
1012
  return { reopened: false, _sema_decisionInFlight: true };
@@ -1080,24 +1055,26 @@ async function staleDismissalVerdict(current, sessionKey, taskId, receiptMs) {
1080
1055
  const windowEnd = abortableSleep(Math.max(0, receiptMs), ac.signal).then(() => false);
1081
1056
  try {
1082
1057
  let entry = current;
1058
+ let lastWaitPresented;
1083
1059
  for (let round = 0; round < 8; round++) {
1084
1060
  if (entry?.kind === 'decided')
1085
1061
  return { reopened: false, _sema_decisionInFlight: true };
1086
1062
  if (entry?.kind === 'dismissed')
1087
1063
  return { reopened: false, dismissedByUser: true };
1088
1064
  if (entry?.kind !== 'reopened' || entry.onScreen === undefined)
1089
- return { reopened: false };
1065
+ return refusedReopen(entry === undefined && lastWaitPresented === false ? 'not_presented' : 'gate_record_gone');
1090
1066
  const onScreen = await Promise.race([
1091
1067
  entry.onScreen.then((v) => v === true, () => false),
1092
1068
  windowEnd,
1093
1069
  ]);
1094
1070
  const now = dismissalEntryFor(sessionKey, taskId);
1095
1071
  if (now === entry) {
1096
- return onScreen ? { reopened: true, firstSight: !wasGateArmedFor(sessionKey, planReviewArmedKeyFor(sessionKey, taskId)) } : { reopened: false };
1072
+ return onScreen ? { reopened: true, firstSight: !wasGateArmedFor(sessionKey, planReviewArmedKeyFor(sessionKey, taskId)) } : refusedReopen('not_presented');
1097
1073
  }
1074
+ lastWaitPresented = onScreen;
1098
1075
  entry = now;
1099
1076
  }
1100
- return { reopened: false };
1077
+ return refusedReopen('gate_record_gone');
1101
1078
  }
1102
1079
  finally {
1103
1080
  ac.abort();
@@ -1107,25 +1084,25 @@ export function reopenPlanReviewCard(taskId, opts) {
1107
1084
  const receiptMs = opts?.presentationReceiptMs;
1108
1085
  if (typeof receiptMs !== 'number' || Number.isNaN(receiptMs)) {
1109
1086
  hostLog('debug', `planReviewWire: reopen for task ${String(taskId)} refused — presentationReceiptMs is required (a number of milliseconds); the synchronous form was retired in 0.98.0`);
1110
- return Promise.resolve({ reopened: false });
1087
+ return Promise.resolve(refusedReopen('invalid_call'));
1111
1088
  }
1112
1089
  try {
1113
1090
  if (typeof taskId !== 'string' || taskId.length === 0) {
1114
- return Promise.resolve({ reopened: false });
1091
+ return Promise.resolve(refusedReopen('invalid_call'));
1115
1092
  }
1116
1093
  const injected = opts;
1117
1094
  if (opts?.mintFreshQuestionId === false && (injected?.deliverDecision !== undefined || injected?.wire !== undefined || injected?.onOutcome !== undefined)) {
1118
1095
  hostLog('error', `planReviewWire: plan_review reopen for task ${taskId} refused — mintFreshQuestionId:false reuses the armed card's responder and its delivery, ` +
1119
1096
  'so deliverDecision / wire / onOutcome cannot be given with it; drop mintFreshQuestionId:false to deliver through the injected path');
1120
- return Promise.resolve({ reopened: false });
1097
+ return Promise.resolve(refusedReopen('invalid_call'));
1121
1098
  }
1122
1099
  const readStart = typeof opts.pendingReadStartedAt === 'number' && Number.isFinite(opts.pendingReadStartedAt) ? opts.pendingReadStartedAt : undefined;
1123
- if (planReviewDecisionInFlight(taskId, readStart)) {
1100
+ if (decisionInFlightOf({ gate: 'plan_review', taskId }, readStart)) {
1124
1101
  hostLog('debug', `planReviewWire: reopen for task ${taskId} refused — a decision for this task is in flight (its answer is still being delivered)`);
1125
1102
  const refused = { reopened: false, _sema_decisionInFlight: true };
1126
1103
  return Promise.resolve(refused);
1127
1104
  }
1128
- const guardStartedAt = planReviewPendingReadMark();
1105
+ const guardStartedAt = decisionReadMark();
1129
1106
  const recheckReadStart = readStart === undefined ? guardStartedAt : Math.min(readStart, guardStartedAt);
1130
1107
  const sessionKey = opts.sessionKey ?? DEFAULT_SESSION_KEY;
1131
1108
  if (!hasQuestionOverlayFor(sessionKey)) {
@@ -1136,7 +1113,7 @@ export function reopenPlanReviewCard(taskId, opts) {
1136
1113
  '而缺省决断投递口(decidePlanReview)今天固定打默认槽的 baseUrl/token/principal。' +
1137
1114
  '整条链转 keyed 之前,非默认槽要重开请自带 deliverDecision(按本槽投递);' +
1138
1115
  '此处拒绝而不是照发 —— 照发会把这条会话的决断发到另一个槽上。');
1139
- return Promise.resolve({ reopened: false });
1116
+ return Promise.resolve(refusedReopen('no_delivery_path'));
1140
1117
  }
1141
1118
  const trigger = opts.trigger === undefined ? undefined : opts.trigger === 'user' ? 'user' : 'automatic';
1142
1119
  if (trigger === 'automatic') {
@@ -1147,6 +1124,6 @@ export function reopenPlanReviewCard(taskId, opts) {
1147
1124
  return mintPlanReviewReopen(taskId, opts, sessionKey, receiptMs);
1148
1125
  }
1149
1126
  catch {
1150
- return Promise.resolve({ reopened: false });
1127
+ return Promise.resolve(refusedReopen('internal_error'));
1151
1128
  }
1152
1129
  }
@@ -1,11 +1,13 @@
1
1
  import type { SessionPermissionRules } from '@sema-agent/sdk';
2
2
  import type { ToolRosterView } from '../toolRoster.js';
3
+ import type { SessionRuleInertReason } from './sessionPolicyWire.js';
3
4
  export type SessionPolicyWithheldWhy = 'ask_no_bucket' | 'qualified_deny' | 'peer_wide' | 'wildcard' | 'not_a_tool_name' | 'legacy_tool_name' | 'not_in_roster';
4
5
  export declare const SESSION_POLICY_WITHHELD_WHY: readonly SessionPolicyWithheldWhy[];
5
6
  export interface SessionPolicyWithheldRule {
6
7
  readonly rule: string;
7
8
  readonly why: SessionPolicyWithheldWhy;
8
9
  readonly currentName?: string;
10
+ readonly inertReason?: SessionRuleInertReason;
9
11
  }
10
12
  export interface SessionPolicyDeliverability {
11
13
  readonly deliverable: SessionPermissionRules;