@sema-agent/client-core 0.98.0 → 0.99.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 (192) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +35 -22
  3. package/dist/adapt/arms.js +61 -20
  4. package/dist/adapt/panelTasks.d.ts +1 -1
  5. package/dist/adapt/panelTasks.js +2 -2
  6. package/dist/adapter/activeRunSelfHeal.d.ts +4 -3
  7. package/dist/adapter/activeRunSelfHeal.js +162 -293
  8. package/dist/adapter/downstream/eventToSdkMessage.js +52 -4
  9. package/dist/adapter/runStream.js +52 -37
  10. package/dist/adapter/types.js +2 -1
  11. package/dist/agentSession/engineSessionBackgroundStop.js +16 -8
  12. package/dist/agentsWireCaps.d.ts +0 -3
  13. package/dist/agentsWireCaps.js +33 -24
  14. package/dist/cloudConfigWireCaps.d.ts +1 -1
  15. package/dist/cloudConfigWireCaps.js +26 -4
  16. package/dist/controlRouter.d.ts +4 -2
  17. package/dist/controlRouter.js +39 -51
  18. package/dist/coreValuePorts.d.ts +26 -1
  19. package/dist/coreValuePorts.js +10 -0
  20. package/dist/corruptSessionCopy.d.ts +2 -1
  21. package/dist/corruptSessionCopy.js +3 -3
  22. package/dist/declared/supportFloorDeclared.js +1 -1
  23. package/dist/detachWire.js +3 -2
  24. package/dist/devicesFace.d.ts +34 -0
  25. package/dist/devicesFace.js +90 -0
  26. package/dist/displayUntrusted.d.ts +1 -0
  27. package/dist/displayUntrusted.js +3 -0
  28. package/dist/engineAgentPanelStore.d.ts +7 -0
  29. package/dist/engineAgentPanelStore.js +14 -0
  30. package/dist/engineCapReader.d.ts +2 -1
  31. package/dist/engineCapReader.js +7 -5
  32. package/dist/engineCapsCache.d.ts +8 -0
  33. package/dist/engineCapsCache.js +59 -6
  34. package/dist/engineCapsChange.d.ts +2 -0
  35. package/dist/engineCapsChange.js +9 -0
  36. package/dist/engineErrorCodes.d.ts +2 -1
  37. package/dist/engineErrorCodes.js +2 -7
  38. package/dist/engineRowRef.js +3 -0
  39. package/dist/engineSessionMessages.d.ts +1 -0
  40. package/dist/engineSessionMessages.js +7 -1
  41. package/dist/engineTargetSeat.d.ts +27 -0
  42. package/dist/engineTargetSeat.js +199 -0
  43. package/dist/engineWireFor.js +3 -8
  44. package/dist/env/uiLanguage.d.ts +2 -1
  45. package/dist/env/uiLanguage.js +3 -2
  46. package/dist/eventLeg.d.ts +8 -0
  47. package/dist/eventLeg.js +61 -41
  48. package/dist/fileHistoryCaptureCapability.d.ts +2 -1
  49. package/dist/fileHistoryCaptureCapability.js +12 -18
  50. package/dist/fileHistoryLegNotice.d.ts +2 -1
  51. package/dist/fileHistoryLegNotice.js +3 -3
  52. package/dist/frameBinding.d.ts +3 -0
  53. package/dist/frameBinding.js +12 -0
  54. package/dist/gateOutcome.js +16 -2
  55. package/dist/gateVocabulary.d.ts +8 -7
  56. package/dist/gateVocabulary.js +71 -73
  57. package/dist/generated/engineFactTables.d.ts +17 -1
  58. package/dist/generated/engineFactTables.js +29 -0
  59. package/dist/generated/engineNoticeTables.js +9 -3
  60. package/dist/generated/settingsSchemaFns.d.ts +6 -0
  61. package/dist/generated/settingsSchemaFns.js +57 -0
  62. package/dist/generated/settingsSchemaHooksClassifier.d.ts +2 -0
  63. package/dist/generated/settingsSchemaHooksClassifier.js +254 -0
  64. package/dist/generated/settingsSchemaTables.d.ts +1 -0
  65. package/dist/generated/settingsSchemaTables.js +4 -0
  66. package/dist/generated/settingsSchemaTypes.d.ts +105 -0
  67. package/dist/generated/settingsSchemaTypes.js +1 -0
  68. package/dist/hitl/allowRuleShadow.d.ts +2 -1
  69. package/dist/hitl/allowRuleShadow.js +17 -14
  70. package/dist/hitl/approvalCardReasons.d.ts +3 -2
  71. package/dist/hitl/approvalCardReasons.js +27 -28
  72. package/dist/hitl/approvalHolds.d.ts +3 -0
  73. package/dist/hitl/approvalHolds.js +73 -29
  74. package/dist/hitl/askClassPair.d.ts +1 -1
  75. package/dist/hitl/askClassPair.js +16 -0
  76. package/dist/hitl/askGateWire.js +2 -1
  77. package/dist/hitl/askSettlementLine.d.ts +14 -4
  78. package/dist/hitl/askSettlementLine.js +67 -47
  79. package/dist/hitl/automaticAnswerVerdict.d.ts +2 -1
  80. package/dist/hitl/automaticAnswerVerdict.js +8 -8
  81. package/dist/hitl/crashConvergedNotice.d.ts +5 -4
  82. package/dist/hitl/crashConvergedNotice.js +28 -41
  83. package/dist/hitl/frameRouter.js +7 -8
  84. package/dist/hitl/hitlBridge.d.ts +1 -0
  85. package/dist/hitl/hitlBridge.js +3 -3
  86. package/dist/hitl/persistedRulesWire.d.ts +6 -5
  87. package/dist/hitl/persistedRulesWire.js +55 -73
  88. package/dist/hitl/planReviewWire.d.ts +5 -0
  89. package/dist/hitl/planReviewWire.js +8 -0
  90. package/dist/hitl/sessionPolicyDeliverable.d.ts +2 -0
  91. package/dist/hitl/sessionPolicyDeliverable.js +46 -3
  92. package/dist/hitl/sessionPolicyWire.d.ts +2 -1
  93. package/dist/hitl/sessionPolicyWire.js +23 -24
  94. package/dist/hitl/suspendedReopen.d.ts +2 -1
  95. package/dist/hitl/suspendedReopen.js +7 -10
  96. package/dist/hitl/toolApprovalWire.d.ts +17 -5
  97. package/dist/hitl/toolApprovalWire.js +79 -40
  98. package/dist/hooksWireCaps.js +145 -56
  99. package/dist/host.d.ts +21 -0
  100. package/dist/hostTurnSettings.d.ts +71 -0
  101. package/dist/hostTurnSettings.js +291 -0
  102. package/dist/humanTurn.d.ts +14 -1
  103. package/dist/humanTurn.js +100 -12
  104. package/dist/humanTurnRead.d.ts +2 -0
  105. package/dist/humanTurnRead.js +22 -1
  106. package/dist/i18n/messages.en.d.ts +582 -0
  107. package/dist/i18n/messages.en.js +582 -0
  108. package/dist/i18n/messages.zh.js +718 -1
  109. package/dist/index.d.ts +29 -13
  110. package/dist/index.js +18 -9
  111. package/dist/inheritEnvCapability.d.ts +3 -2
  112. package/dist/inheritEnvCapability.js +3 -3
  113. package/dist/inheritEnvWire.d.ts +3 -2
  114. package/dist/inheritEnvWire.js +12 -14
  115. package/dist/interactiveHalt.js +17 -6
  116. package/dist/liveModelCatalog.d.ts +2 -1
  117. package/dist/liveModelCatalog.js +14 -8
  118. package/dist/mcpPanel.d.ts +10 -1
  119. package/dist/mcpPanel.js +40 -13
  120. package/dist/mcpProbeCapability.d.ts +2 -1
  121. package/dist/mcpProbeCapability.js +7 -11
  122. package/dist/mcpProbeWire.d.ts +3 -1
  123. package/dist/mcpProbeWire.js +5 -0
  124. package/dist/mcpWireCaps.d.ts +34 -0
  125. package/dist/mcpWireCaps.js +117 -17
  126. package/dist/memoryEntriesWire.js +5 -10
  127. package/dist/model/onboardJudgement.d.ts +93 -0
  128. package/dist/model/onboardJudgement.js +415 -0
  129. package/dist/model/tierVocabulary.d.ts +2 -0
  130. package/dist/model/tierVocabulary.js +17 -5
  131. package/dist/modelCapabilityProbe.d.ts +64 -0
  132. package/dist/modelCapabilityProbe.js +600 -0
  133. package/dist/modelIdentityResolvability.d.ts +3 -0
  134. package/dist/modelIdentityResolvability.js +26 -17
  135. package/dist/ownKey.d.ts +7 -0
  136. package/dist/ownKey.js +34 -0
  137. package/dist/panelReduce.d.ts +6 -1
  138. package/dist/panelReduce.js +50 -3
  139. package/dist/permissionRuleIssue.js +2 -0
  140. package/dist/permissionRulesWriteCapability.d.ts +2 -1
  141. package/dist/permissionRulesWriteCapability.js +8 -6
  142. package/dist/pluginHooksWire.d.ts +46 -3
  143. package/dist/pluginHooksWire.js +217 -11
  144. package/dist/printModelStatus.d.ts +63 -0
  145. package/dist/printModelStatus.js +155 -0
  146. package/dist/request/taskRequest.d.ts +11 -5
  147. package/dist/request/taskRequest.js +112 -16
  148. package/dist/resumeRefusalCopy.d.ts +12 -9
  149. package/dist/resumeRefusalCopy.js +70 -143
  150. package/dist/retryStatus.d.ts +37 -0
  151. package/dist/retryStatus.js +79 -0
  152. package/dist/scenarioRefusal.d.ts +9 -0
  153. package/dist/scenarioRefusal.js +111 -0
  154. package/dist/seam.d.ts +25 -1
  155. package/dist/seam.js +10 -1
  156. package/dist/seatContract.d.ts +260 -10
  157. package/dist/seatContract.js +692 -19
  158. package/dist/selfOrchestrationDenial.js +7 -19
  159. package/dist/sessionIdSegment.d.ts +6 -0
  160. package/dist/sessionIdSegment.js +24 -0
  161. package/dist/sessionMemoryStatus.js +6 -11
  162. package/dist/sessionPolicyCapability.d.ts +2 -1
  163. package/dist/sessionPolicyCapability.js +8 -6
  164. package/dist/settingsHooksSalvage.d.ts +44 -0
  165. package/dist/settingsHooksSalvage.js +211 -0
  166. package/dist/skillToolListsCapability.d.ts +2 -1
  167. package/dist/skillToolListsCapability.js +8 -6
  168. package/dist/sseIdleTriage.js +3 -2
  169. package/dist/subagent/engineSubagentResume.js +15 -9
  170. package/dist/subagentContentStore.d.ts +3 -0
  171. package/dist/subagentContentStore.js +22 -0
  172. package/dist/toolHistoryMismatch.js +3 -2
  173. package/dist/toolRoster.d.ts +3 -2
  174. package/dist/toolRoster.js +21 -20
  175. package/dist/turnFailurePresentation.d.ts +12 -1
  176. package/dist/turnFailurePresentation.js +212 -15
  177. package/dist/usageAnalytics.js +6 -11
  178. package/dist/userAttachmentEnvelope.d.ts +1 -0
  179. package/dist/userAttachmentEnvelope.js +37 -7
  180. package/dist/userAttachmentSeat.d.ts +21 -0
  181. package/dist/userAttachmentSeat.js +75 -0
  182. package/dist/userInvokedSkillsCapability.d.ts +11 -1
  183. package/dist/userInvokedSkillsCapability.js +31 -7
  184. package/dist/wireErrorTriage.d.ts +7 -0
  185. package/dist/wireErrorTriage.js +23 -1
  186. package/dist/wireFailureShape.js +2 -1
  187. package/dist/wireRefusalCopy.d.ts +13 -1
  188. package/dist/wireRefusalCopy.js +24 -28
  189. package/dist/workflowsJournalCapability.d.ts +2 -1
  190. package/dist/workflowsJournalCapability.js +8 -6
  191. package/docs/INTEGRATION-CLIENTS.md +542 -30
  192. package/package.json +5 -5
@@ -1,4 +1,6 @@
1
1
  import { abortableSleep } from '../abortableSleep.js';
2
+ import { EN_MESSAGES } from '../i18n/messages.en.js';
3
+ import { msg, renderMessage } from '../i18n/translate.js';
2
4
  import { corruptStoredRowContent, corruptStoredRowWhere, effectiveWireErrorCode } from '../wireErrorTriage.js';
3
5
  import { readThrownStatusSlot } from '../wireFailureShape.js';
4
6
  import { steerBodyOf, UNOBSERVED_STEER_PRIORITY } from '../controlRouter.js';
@@ -832,38 +834,34 @@ async function routeFreshNonRunning(fresh, taskId, busy, runs, deps) {
832
834
  return { kind: 'running-settled', taskId, status: fresh };
833
835
  return { kind: 'not-parked', taskId, status: fresh };
834
836
  }
835
- export const INTERACTIVE_WAY_OUT = 'run /clear to keep working in a fresh session';
836
- const DEFAULT_WAY_OUT = INTERACTIVE_WAY_OUT;
837
- const DEFAULT_FRESH_SESSION = 'start a new session (drop --resume/--continue)';
838
- function gateKindPhrase(kind) {
839
- return kind ? `a ${kind} decision` : 'a decision';
837
+ export const INTERACTIVE_WAY_OUT = EN_MESSAGES['session.selfHeal.wayOut.interactive'];
838
+ const say = (lang, key, params) => renderMessage(msg(key, params), lang);
839
+ function gateKindPhrase(kind, lang) {
840
+ return kind ? say(lang, 'session.selfHeal.gate.kind', { kind }) : say(lang, 'session.selfHeal.gate.generic');
840
841
  }
841
- function decidePathClause(signal) {
842
+ function decidePathClause(signal, lang) {
842
843
  const decidePath = signal?.pendingGate?.decidePath;
843
844
  if (!decidePath)
844
845
  return '';
845
- return (` The engine says that run is waiting on ${gateKindPhrase(signal?.pendingGate?.kind)} and that it is` +
846
- ` decided at POST ${decidePath} — deciding it releases the session.`);
846
+ return say(lang, 'session.selfHeal.decidePathClause', { gate: gateKindPhrase(signal?.pendingGate?.kind, lang), decidePath });
847
847
  }
848
- function governanceOriginClause(signal) {
849
- return signal?.pendingGate?.governanceForced === true
850
- ? ' That gate is enforced by this deployment\'s governance policy, not by a permission rule you set.'
851
- : '';
848
+ function governanceOriginClause(signal, lang) {
849
+ return signal?.pendingGate?.governanceForced === true ? say(lang, 'session.selfHeal.governanceOrigin') : '';
852
850
  }
853
- function lastActivityClause(signal) {
851
+ function lastActivityClause(signal, lang) {
854
852
  const ms = signal?.msSinceLastActivity;
855
853
  if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
856
854
  return '';
857
855
  const human = ms < 120_000 ? `${String(Math.max(1, Math.round(ms / 1000)))}s` : `${String(Math.round(ms / 60_000))}m`;
858
- return ` The engine last recorded activity on that run ${human} ago.`;
856
+ return say(lang, 'session.selfHeal.lastActivity', { ago: human });
859
857
  }
860
- export function activeRunSelfHealRow(outcome, signal, copy, origin, follow) {
858
+ export function activeRunSelfHealRow(outcome, signal, copy, origin, follow, lang) {
861
859
  const override = copy?.rowFor?.(outcome, signal);
862
860
  if (typeof override === 'string')
863
861
  return override;
864
862
  const following = follow?.following === true && steerFollowIntent(outcome)?.kind === 'tail';
865
863
  if (origin === 'injected')
866
- return injectedSubmissionRow(outcome, following);
864
+ return injectedSubmissionRow(outcome, following, lang);
867
865
  const engineDecide = copy?.engineDecidePath !== false;
868
866
  const keep = (outcome.kind === 'plan-review-reopen-failed' || outcome.kind === 'ask-reopen-failed') &&
869
867
  outcome.decisionInFlight !== true &&
@@ -871,101 +869,77 @@ export function activeRunSelfHealRow(outcome, signal, copy, origin, follow) {
871
869
  (outcome.kind !== 'plan-review-reopen-failed' || outcome.dismissedByUser !== true)
872
870
  ? keepSessionWayOutOf(copy)
873
871
  : null;
874
- const base = activeRunSelfHealBaseRow(outcome, copy?.wayOut ?? DEFAULT_WAY_OUT, following, keep, engineDecide);
875
- const viaEngine = engineDecide ? decidePathClause(signal) : '';
872
+ const base = activeRunSelfHealBaseRow(outcome, copy?.wayOut ?? say(lang, 'session.selfHeal.wayOut.interactive'), following, keep, engineDecide, lang);
873
+ const viaEngine = engineDecide ? decidePathClause(signal, lang) : '';
876
874
  switch (outcome.kind) {
877
875
  case 'not-parked':
878
- return base + lastActivityClause(signal) + (outcome.pendingGateKind !== undefined ? viaEngine + governanceOriginClause(signal) : '');
876
+ return base + lastActivityClause(signal, lang) + (outcome.pendingGateKind !== undefined ? viaEngine + governanceOriginClause(signal, lang) : '');
879
877
  case 'state-unknown':
880
- return base + viaEngine + governanceOriginClause(signal);
878
+ return base + viaEngine + governanceOriginClause(signal, lang);
881
879
  default:
882
- return base + governanceOriginClause(signal);
880
+ return base + governanceOriginClause(signal, lang);
883
881
  }
884
882
  }
885
- const INJECTED_LEAD = 'A follow-up message sema sent on its own (not one you typed)';
886
- const USER_CLOSED_CARD_NOT_PUT_BACK = 'sema does not put a card you closed back on screen by itself';
887
- const NO_CARD_VIEW_ATTACHED = 'this session has no view attached that can show decision cards';
888
- function injectedSubmissionRow(outcome, following = false) {
889
- const lead = INJECTED_LEAD;
890
- const id = 'taskId' in outcome && outcome.taskId ? ` (id ${outcome.taskId})` : '';
891
- if (outcome.kind === 'running-attach') {
892
- return `${lead} was not delivered: you chose to watch the reply already in progress${id} instead, and sema is replaying that reply here, so the part already shown above will appear again below. sema did not retry, so the model has not seen it.`;
893
- }
883
+ function injectedSubmissionRow(outcome, following = false, lang) {
884
+ const lead = msg('session.selfHeal.injected.lead');
885
+ const id = 'taskId' in outcome && outcome.taskId ? msg('session.selfHeal.injected.id', { id: outcome.taskId }) : '';
886
+ if (outcome.kind === 'running-attach')
887
+ return say(lang, 'session.selfHeal.injected.attach', { lead, id });
894
888
  if (outcome.kind === 'running-steer-failed') {
895
889
  return outcome.delivery === 'unknown'
896
- ? `${lead} was passed to the reply already in progress${id}, but its delivery could not be confirmed ` +
897
- `(${outcome.detail}) — that reply may or may not have received it. sema did not retry, because sending it twice ` +
898
- `is not safe; watch that reply for what it does next.`
899
- : `${lead} was not delivered: passing it to the reply already in progress${id} was refused (${outcome.detail}). ` +
900
- `sema did not retry, so the model has not seen it.`;
890
+ ? say(lang, 'session.selfHeal.injected.steerUnknown', { lead, id, detail: outcome.detail })
891
+ : say(lang, 'session.selfHeal.injected.steerRejected', { lead, id, detail: outcome.detail });
901
892
  }
902
893
  if ((outcome.kind === 'ask-reopen-failed' || outcome.kind === 'plan-review-reopen-failed') && outcome.decisionInFlight === true) {
903
- return (`${lead} was not delivered: this session is still busy with an earlier reply${id}, and your answer to the decision ` +
904
- `it is waiting on is still on its way. sema did not retry, so the model has not seen it.`);
894
+ return say(lang, 'session.selfHeal.injected.decisionInFlight', { lead, id });
905
895
  }
906
896
  if (outcome.kind === 'plan-review-reopen-failed' && outcome.dismissedByUser === true) {
907
- return (`${lead} was not delivered: this session is held by an earlier reply${id} whose plan is waiting for your review, and ` +
908
- `you closed that approval card — ${USER_CLOSED_CARD_NOT_PUT_BACK}. Send a message when you want the card back; ` +
909
- `sema did not retry, so the model has not seen it.`);
897
+ return say(lang, 'session.selfHeal.injected.dismissedPlan', { lead, id, closed: msg('session.selfHeal.userClosedCardNotPutBack') });
910
898
  }
911
899
  if (outcome.kind === 'running-steered' && outcome.delivery === 'queued') {
912
- const queuedOn = `${lead} is queued on an earlier reply${id} that is paused waiting for a decision`;
900
+ const queuedOn = msg('session.selfHeal.injected.queuedOn', { lead, id });
913
901
  const reopened = reopenedOfOutcome(outcome);
914
- if (reopened !== null && reopenDelivered(reopened)) {
915
- return `${queuedOn}; sema has shown that decision card, and the message is picked up once you answer it.`;
916
- }
917
- if (reopenRefusedForDecisionInFlight(reopened)) {
918
- return `${queuedOn}; your answer to it is still on its way, and the message is picked up once that answer is applied.`;
919
- }
920
- if (reopenRefusedForUserDismissal(reopened)) {
921
- return (`${queuedOn}; you closed that card yourself, so sema did not put it back on screen — send a message to bring it ` +
922
- `back, and the message is picked up once that decision is made.`);
923
- }
924
- const noView = reopenRefusedForNoPresentationSurface(reopened) ? ` (${NO_CARD_VIEW_ATTACHED})` : '';
925
- return `${queuedOn} sema could not show here${noView}; the message is picked up only once that decision is made.`;
902
+ if (reopened !== null && reopenDelivered(reopened))
903
+ return say(lang, 'session.selfHeal.injected.queuedShown', { queuedOn });
904
+ if (reopenRefusedForDecisionInFlight(reopened))
905
+ return say(lang, 'session.selfHeal.injected.queuedInFlight', { queuedOn });
906
+ if (reopenRefusedForUserDismissal(reopened))
907
+ return say(lang, 'session.selfHeal.injected.queuedDismissed', { queuedOn });
908
+ const noView = reopenRefusedForNoPresentationSurface(reopened) ? msg('session.selfHeal.noCardView.inParens') : '';
909
+ return say(lang, 'session.selfHeal.injected.queuedCannotShow', { queuedOn, noView });
926
910
  }
927
911
  if (outcome.kind === 'running-steered' && outcome.delivery === 'parked_for_wake') {
928
- return (`${lead} has not reached the model: the earlier reply${id} had already finished, so the message was set aside on it ` +
929
- `instead of starting a new reply, and sema did not retry it.`);
912
+ return say(lang, 'session.selfHeal.injected.parkedForWake', { lead, id });
930
913
  }
931
914
  if (outcome.kind === 'running-steered' && outcome.delivery !== 'applied') {
932
- const unknown = `${lead} was accepted for the reply already in progress${id}, but how it will be delivered was not reported, ` +
933
- `so sema cannot tell whether that reply will pick it up`;
915
+ const unknown = msg('session.selfHeal.injected.unknownDelivery', { lead, id });
934
916
  return following
935
- ? `${unknown}. sema is following that reply and will show whatever it asks for next; check what it does ` +
936
- `before deciding whether anything needs to be sent again.`
937
- : `${unknown} — watch that reply before deciding whether anything needs to be sent again.`;
917
+ ? say(lang, 'session.selfHeal.injected.unknownDeliveryFollowing', { unknown })
918
+ : say(lang, 'session.selfHeal.injected.unknownDeliveryWatch', { unknown });
938
919
  }
939
920
  switch (selfHealSubmissionDisposition(outcome)) {
940
921
  case 'held-for-decision':
941
- return (`${lead} is on hold: this session is busy with an earlier reply${id} that is waiting on a decision. ` +
942
- `sema kept the message queued and will send it after you answer the open card.`);
922
+ return say(lang, 'session.selfHeal.injected.heldForDecision', { lead, id });
943
923
  case 'resending':
944
924
  return 'releaseConfirmedByEngine' in outcome && outcome.releaseConfirmedByEngine === true
945
- ? `${lead} was held back because this session was busy with an earlier reply${id}; the session is free again, ` +
946
- `so sema is sending the message now — nothing for you to do.`
947
- : `${lead} was held back because this session was busy with an earlier reply${id}; that reply is now reported ` +
948
- `as finished, so sema is sending the message now — nothing for you to do (if the session has not quite ` +
949
- `freed up yet, it may be held back once more).`;
925
+ ? say(lang, 'session.selfHeal.injected.resendingConfirmed', { lead, id })
926
+ : say(lang, 'session.selfHeal.injected.resendingInferred', { lead, id });
950
927
  case 'handed-off':
951
928
  return following
952
- ? `${lead} was passed to the reply already in progress${id} instead of starting a new one. ` +
953
- `sema is following that reply and will show whatever it asks for next; nothing for you to do now.`
954
- : `${lead} was passed to the reply already in progress${id} instead of starting a new one — ` +
955
- `watch that reply for what it does with it.`;
929
+ ? say(lang, 'session.selfHeal.injected.handedOffFollowing', { lead, id })
930
+ : say(lang, 'session.selfHeal.injected.handedOffWatch', { lead, id });
956
931
  case 'not-delivered': {
957
932
  const noView = (outcome.kind === 'ask-reopen-failed' || outcome.kind === 'plan-review-reopen-failed') && outcome.noPresentationSurface === true
958
- ? ` (${NO_CARD_VIEW_ATTACHED})`
933
+ ? msg('session.selfHeal.noCardView.inParens')
959
934
  : '';
960
- return (`${lead} was not delivered: this session was still busy with an earlier reply${id}, and there was no open card ` +
961
- `you could answer to free it${noView}. sema did not retry, so the model has not seen it.`);
935
+ return say(lang, 'session.selfHeal.injected.notDelivered', { lead, id, noView });
962
936
  }
963
937
  }
964
938
  }
965
- function decisionInFlightRow(parkedOn) {
966
- return (`The previous turn is parked ${parkedOn} and your answer to that card is still on its way to the engine, so ` +
967
- `sema did not show the card again — answering it a second time would send a second decision. It did NOT ` +
968
- `cancel the run. Your message was NOT sent; send it again in a moment, once the engine has applied that decision.`);
939
+ function decisionInFlightRow(parkedOn, taskId, lang) {
940
+ return say(lang, 'session.selfHeal.decisionInFlight', {
941
+ parkedOn: msg(parkedOn === 'ask' ? 'session.selfHeal.parkedOn.ask' : 'session.selfHeal.parkedOn.plan', { taskId }),
942
+ });
969
943
  }
970
944
  function keepSessionWayOutOf(copy) {
971
945
  if (typeof copy?.keepSessionWayOut !== 'function')
@@ -979,287 +953,182 @@ function keepSessionWayOutOf(copy) {
979
953
  return null;
980
954
  }
981
955
  }
982
- function activeRunSelfHealBaseRow(outcome, wayOut, following = false, keepSessionWayOut = null, engineDecide = true) {
956
+ function activeRunSelfHealBaseRow(outcome, wayOut, following = false, keepSessionWayOut = null, engineDecide = true, lang) {
983
957
  switch (outcome.kind) {
984
958
  case 'decision-pending':
985
- return (`This session is held by an earlier turn${outcome.taskId ? ` (run ${outcome.taskId})` : ''} that ` +
986
- `is waiting on a decision you have not made yet, and there is a decision card open for it. ` +
987
- `sema did NOT cancel it — answering that card is what releases the session, and cancelling ` +
988
- `would decide it for you. Your message was NOT sent; answer the open card and send it again.`);
959
+ return say(lang, 'session.selfHeal.row.decisionPending', { handle: outcome.taskId ? msg('session.selfHeal.handle.run', { taskId: outcome.taskId }) : '' });
989
960
  case 'ask-reopened':
990
- if (outcome.firstSight) {
991
- return (`The previous turn advanced to a decision it needs from you (run ${outcome.taskId}), which is ` +
992
- `why nothing new could start — sema has surfaced that decision card. Answer it and send your ` +
993
- `message again. (sema did NOT cancel the run: cancelling would have decided it for you.)`);
994
- }
995
- return (`The previous turn is parked waiting for your decision (run ${outcome.taskId}), which is why ` +
996
- `nothing new could start — that card was closed without being answered, so sema reopened it. ` +
997
- `Answer it and send your message again. (sema did NOT cancel the run: cancelling would have ` +
998
- `decided it for you.)`);
961
+ if (outcome.firstSight)
962
+ return say(lang, 'session.selfHeal.row.askReopened.firstSight', { taskId: outcome.taskId });
963
+ return say(lang, 'session.selfHeal.row.askReopened.reseen', { taskId: outcome.taskId });
999
964
  case 'plan-review-reopened':
1000
- if (outcome.firstSight) {
1001
- return (`The previous turn produced a plan that is waiting for your review (run ${outcome.taskId}), ` +
1002
- `which is why nothing new could start — sema has surfaced the review card. Answer it and send ` +
1003
- `your message again. (sema did NOT cancel the run: cancelling would have thrown the plan away ` +
1004
- `on your behalf.)`);
1005
- }
1006
- return (`The previous turn is parked waiting for you to review its plan (run ${outcome.taskId}), which is ` +
1007
- `why nothing new could start — that approval card was closed without being answered, so sema ` +
1008
- `reopened it. Answer it and send your message again. (sema did NOT cancel the run: cancelling ` +
1009
- `would have thrown the plan away on your behalf.)`);
965
+ if (outcome.firstSight)
966
+ return say(lang, 'session.selfHeal.row.planReviewReopened.firstSight', { taskId: outcome.taskId });
967
+ return say(lang, 'session.selfHeal.row.planReviewReopened.reseen', { taskId: outcome.taskId });
1010
968
  case 'ask-decided-without-card':
1011
- if (outcome.releaseConfirmedByEngine === true) {
1012
- return (`The previous turn was parked on an approval that your permission rules decided on their own (no card was ` +
1013
- `needed); run ${outcome.taskId} resumed and the engine confirmed it is no longer holding this session — ` +
1014
- `sema is re-sending your message.`);
1015
- }
1016
- return (`The previous turn was parked on an approval that your permission rules decided on their own (no card was ` +
1017
- `needed); run ${outcome.taskId} resumed and has since finished — sema is re-sending your message; if that ` +
1018
- `run has not fully let go of the session yet, you may have to send it once more.`);
969
+ if (outcome.releaseConfirmedByEngine === true)
970
+ return say(lang, 'session.selfHeal.row.askDecided.confirmed', { taskId: outcome.taskId });
971
+ return say(lang, 'session.selfHeal.row.askDecided.inferred', { taskId: outcome.taskId });
1019
972
  case 'ask-decided-release-unknown':
1020
- return (`The previous turn was parked on an approval that your permission rules decided on their own (no card was ` +
1021
- `needed), so run ${outcome.taskId} should be resuming` +
1022
- (outcome.lastStatus ? ` (last engine status: ${outcome.lastStatus})` : '') +
1023
- `. sema could not yet confirm it finished, so your message was not sent — wait a moment and send it again.`);
973
+ return say(lang, 'session.selfHeal.row.askDecided.releaseUnknown', {
974
+ taskId: outcome.taskId,
975
+ status: outcome.lastStatus ? msg('session.selfHeal.lastEngineStatus', { status: outcome.lastStatus }) : '',
976
+ });
1024
977
  case 'ask-reopen-failed': {
1025
978
  if (outcome.decisionInFlight === true)
1026
- return decisionInFlightRow(`waiting for your decision (run ${outcome.taskId})`);
1027
- const viaEngine = engineDecide && outcome.decidePath
1028
- ? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
1029
- : '';
1030
- const noView = outcome.noPresentationSurface === true ? `: ${NO_CARD_VIEW_ATTACHED}` : '';
1031
- return (`The previous turn is parked waiting for your decision (run ${outcome.taskId}) and sema could ` +
1032
- `not reopen that card here${noView}. It did NOT cancel the run — that would have decided it for you. ` +
1033
- (keepSessionWayOut !== null
1034
- ? `Your message was NOT sent. To keep this conversation, ${keepSessionWayOut}: the card comes back when the session resumes, if it is still waiting. Or ${wayOut} if you no longer want it.${viaEngine}`
1035
- : `Your message was NOT sent; ${wayOut} if you no longer want it.${viaEngine}`));
979
+ return decisionInFlightRow('ask', outcome.taskId, lang);
980
+ const viaEngine = engineDecide && outcome.decidePath ? msg('session.selfHeal.viaEngine', { decidePath: outcome.decidePath }) : '';
981
+ const noView = outcome.noPresentationSurface === true ? msg('session.selfHeal.noCardView.afterColon') : '';
982
+ return keepSessionWayOut !== null
983
+ ? say(lang, 'session.selfHeal.row.askReopenFailed.withKeep', { taskId: outcome.taskId, noView, keep: keepSessionWayOut, wayOut, viaEngine })
984
+ : say(lang, 'session.selfHeal.row.askReopenFailed.noKeep', { taskId: outcome.taskId, noView, wayOut, viaEngine });
1036
985
  }
1037
986
  case 'plan-review-reopen-failed': {
1038
987
  if (outcome.decisionInFlight === true)
1039
- return decisionInFlightRow(`on a plan review (run ${outcome.taskId})`);
988
+ return decisionInFlightRow('plan', outcome.taskId, lang);
1040
989
  if (outcome.dismissedByUser === true) {
1041
- return (`The previous turn is parked waiting for a plan review (run ${outcome.taskId}), and you closed its approval card — ` +
1042
- `${USER_CLOSED_CARD_NOT_PUT_BACK}. It did NOT cancel the run — that would have discarded the plan for you. ` +
1043
- `Your message was NOT sent; send it again to bring the card back.`);
990
+ return say(lang, 'session.selfHeal.row.planReopenFailed.dismissed', { taskId: outcome.taskId, closed: msg('session.selfHeal.userClosedCardNotPutBack') });
1044
991
  }
1045
- const viaEngine = engineDecide && outcome.decidePath
1046
- ? ` You can also decide it on the engine directly: POST ${outcome.decidePath}.`
1047
- : '';
1048
- const noView = outcome.noPresentationSurface === true ? `: ${NO_CARD_VIEW_ATTACHED}` : '';
1049
- return (`The previous turn is parked waiting for a plan review (run ${outcome.taskId}) and sema could not ` +
1050
- `reopen that approval card${noView}. It did NOT cancel the run — that would have discarded the plan for ` +
1051
- (keepSessionWayOut !== null
1052
- ? `you. Your message was NOT sent. To keep this conversation, ${keepSessionWayOut}: the card comes back when the session resumes, if it is still waiting. Or ${wayOut} if you no longer want that plan.${viaEngine}`
1053
- : `you. Your message was NOT sent; ${wayOut} if you no longer want that plan.${viaEngine}`));
992
+ const viaEngine = engineDecide && outcome.decidePath ? msg('session.selfHeal.viaEngine', { decidePath: outcome.decidePath }) : '';
993
+ const noView = outcome.noPresentationSurface === true ? msg('session.selfHeal.noCardView.afterColon') : '';
994
+ return keepSessionWayOut !== null
995
+ ? say(lang, 'session.selfHeal.row.planReopenFailed.withKeep', { taskId: outcome.taskId, noView, keep: keepSessionWayOut, wayOut, viaEngine })
996
+ : say(lang, 'session.selfHeal.row.planReopenFailed.noKeep', { taskId: outcome.taskId, noView, wayOut, viaEngine });
1054
997
  }
1055
998
  case 'running-steered': {
1056
- const handle = `run ${outcome.taskId}`;
1057
- const textOnly = `Only the TEXT of your message was handed over — a steer carries no attachments, so any images in that submission were NOT sent.`;
999
+ const taskId = outcome.taskId;
1000
+ const textOnly = msg('session.selfHeal.row.steered.textOnly');
1058
1001
  if (outcome.delivery === 'applied') {
1059
- return (`sema handed your message to the run that is already working (${handle}) — the engine applied it to ` +
1060
- `that run, so it is picked up at that run's next step. ${textOnly} Nothing was cancelled, and your ` +
1061
- `message did NOT start a new turn — ` +
1062
- (following
1063
- ? `sema is following that run and will surface whatever it asks for next.`
1064
- : `watch that run for what it does with it.`));
1002
+ return say(lang, 'session.selfHeal.row.steered.applied', {
1003
+ taskId,
1004
+ textOnly,
1005
+ tail: msg(following ? 'session.selfHeal.row.steered.tail.following' : 'session.selfHeal.row.steered.tail.watch'),
1006
+ });
1065
1007
  }
1066
1008
  if (outcome.delivery === 'queued') {
1067
- const parked = outcome.status ? `parked (status ${outcome.status})` : 'parked';
1009
+ const parked = outcome.status ? msg('session.selfHeal.row.steered.parked.withStatus', { status: outcome.status }) : msg('session.selfHeal.row.steered.parked.plain');
1068
1010
  const reopened = reopenedOfOutcome(outcome);
1069
1011
  const next = reopened !== null && reopenDelivered(reopened)
1070
- ? `sema has surfaced that decision card — answering it is what lets that run resume and pick your message up.`
1012
+ ? msg('session.selfHeal.row.steered.next.shown')
1071
1013
  : reopenRefusedForDecisionInFlight(reopened)
1072
- ? `Your answer to that decision is still on its way to the engine, so sema did not show the card again; ` +
1073
- `that run moves on once the engine applies it.`
1014
+ ? msg('session.selfHeal.row.steered.next.inFlight')
1074
1015
  : reopenRefusedForUserDismissal(reopened)
1075
- ? `You closed that decision card yourself, so sema did not put it back on screen; nothing will resume that ` +
1076
- `run until that decision is made — send a message to bring the card back.`
1077
- : `sema could not surface that decision card here` +
1078
- (reopenRefusedForNoPresentationSurface(reopened) ? ` (${NO_CARD_VIEW_ATTACHED})` : '') +
1079
- `, so nothing will resume that run until that decision is made; ` +
1080
- `${wayOut} if you no longer want it.`;
1081
- return (`${handle} is not actually working right now: the engine reports it ${parked} on a decision it needs ` +
1082
- `from you, so it queued your message on that park instead of running it. ${next} ${textOnly} ` +
1083
- `Nothing was cancelled, and your message did NOT start a new turn.`);
1016
+ ? msg('session.selfHeal.row.steered.next.dismissed')
1017
+ : msg('session.selfHeal.row.steered.next.cannotShow', {
1018
+ noView: reopenRefusedForNoPresentationSurface(reopened) ? msg('session.selfHeal.noCardView.inParens') : '',
1019
+ wayOut,
1020
+ });
1021
+ return say(lang, 'session.selfHeal.row.steered.queued', { taskId, parked, next, textOnly });
1084
1022
  }
1085
1023
  if (outcome.delivery === 'parked_for_wake') {
1086
- return (`${handle} had already finished, so the engine parked your message on that finished run instead of ` +
1087
- `running it — a parked message is delivered only if this session is woken on the engine ` +
1088
- `(POST /v1/sessions/:id/wake), which sema does not do here. That run is no longer holding this ` +
1089
- `session: send your message again to run it as a new turn — but note that the parked copy is NOT ` +
1090
- `discarded, so if anything ever wakes this session on the engine that instruction would run a ` +
1091
- `second time. ${textOnly} Nothing was cancelled.`);
1024
+ return say(lang, 'session.selfHeal.row.steered.parkedForWake', { taskId, textOnly });
1092
1025
  }
1093
- return (`sema handed your message to ${handle}, but the engine did not say how it will be delivered` +
1094
- `${outcome.status ? ` (it reported that run as ${outcome.status})` : ''}. ${textOnly} Nothing was ` +
1095
- `cancelled, and your message did NOT start a new turn — ` +
1096
- (following
1097
- ? `sema is following that run and will surface whatever it asks for next before you send it again.`
1098
- : `watch that run before sending it again.`));
1026
+ return say(lang, 'session.selfHeal.row.steered.unknown', {
1027
+ taskId,
1028
+ reported: outcome.status ? msg('session.selfHeal.row.steered.reported', { status: outcome.status }) : '',
1029
+ textOnly,
1030
+ tail: msg(following ? 'session.selfHeal.row.steered.unknownTail.following' : 'session.selfHeal.row.steered.unknownTail.watch'),
1031
+ });
1099
1032
  }
1100
1033
  case 'running-steer-failed':
1101
- if (outcome.delivery === 'rejected') {
1102
- return (`Handing your message to the running run (run ${outcome.taskId}) was refused: ${outcome.detail}. ` +
1103
- `sema did NOT retry it (a steer is not safe to send twice) and it did NOT cancel that run. ` +
1104
- `Your message was NOT sent; send it again if you still want it.`);
1105
- }
1106
- return (`sema sent your message to the running run (run ${outcome.taskId}) but could not confirm what happened ` +
1107
- `to it (${outcome.detail}). A steer is not safe to send twice, so sema did NOT retry it, and it did NOT ` +
1108
- `cancel that run. That run may or may not have received your message — watch what it does next ` +
1109
- `before sending it again.`);
1034
+ return outcome.delivery === 'rejected'
1035
+ ? say(lang, 'session.selfHeal.row.steerFailed.rejected', { taskId: outcome.taskId, detail: outcome.detail })
1036
+ : say(lang, 'session.selfHeal.row.steerFailed.unknown', { taskId: outcome.taskId, detail: outcome.detail });
1110
1037
  case 'running-attach':
1111
- return (`${resumeAttachReplayDisclosure(outcome.taskId)} Your message was NOT sent; send it again once that run has finished.`);
1038
+ return say(lang, 'session.selfHeal.row.attach', { disclosure: resumeAttachReplayDisclosure(outcome.taskId) });
1112
1039
  case 'running-cancelled':
1113
1040
  return outcome.releaseConfirmedByEngine === true
1114
- ? `You chose to cancel run ${outcome.taskId}. The engine confirmed it is no longer holding this session, ` +
1115
- `so sema is sending your message now.`
1116
- : `You chose to cancel run ${outcome.taskId}. The engine now reports that run as finished, so sema is ` +
1117
- `sending your message now; if that run has not fully let go of the session yet, you may have to send ` +
1118
- `it once more.`;
1041
+ ? say(lang, 'session.selfHeal.row.cancelled.confirmed', { taskId: outcome.taskId })
1042
+ : say(lang, 'session.selfHeal.row.cancelled.reported', { taskId: outcome.taskId });
1119
1043
  case 'running-cancel-timeout': {
1120
1044
  const waited = `${String(Math.max(1, Math.round(outcome.waitedMs / 1000)))}s`;
1121
- if (outcome.aborted) {
1122
- return (`sema asked the engine to cancel run ${outcome.taskId} and you interrupted while it was still ` +
1123
- `confirming (${waited} in). Cancelling is asynchronous, so that run may still be winding down and ` +
1124
- `may still hold this session. Your message was NOT sent; send it again in a moment, or ${wayOut}.`);
1125
- }
1045
+ if (outcome.aborted)
1046
+ return say(lang, 'session.selfHeal.row.cancelTimeout.aborted', { taskId: outcome.taskId, waited, wayOut });
1126
1047
  if (!outcome.confirmedHeld) {
1127
- return (`sema asked the engine to cancel run ${outcome.taskId} but could not read that run's state back ` +
1128
- `within ${waited}, so it cannot confirm whether the session was released. Your message was NOT sent; ` +
1129
- `send it again in a moment (if that run really is gone it will just run), or ${wayOut}.`);
1048
+ return say(lang, 'session.selfHeal.row.cancelTimeout.unread', { taskId: outcome.taskId, waited, wayOut });
1130
1049
  }
1131
- return (`sema asked the engine to cancel run ${outcome.taskId}, but that run still held this session ` +
1132
- `${waited} later — cancelling is asynchronous and it may still be winding down. Your message was NOT ` +
1133
- `sent; send it again in a moment, or ${wayOut}.`);
1050
+ return say(lang, 'session.selfHeal.row.cancelTimeout.held', { taskId: outcome.taskId, waited, wayOut });
1134
1051
  }
1135
1052
  case 'running-cancel-failed':
1136
1053
  if (outcome.delivery === 'unknown') {
1137
- return (`sema sent the cancel for run ${outcome.taskId} but could not confirm what happened to it ` +
1138
- `(${outcome.detail}), and it could not read that run back as released either. That run may or may ` +
1139
- `not still be holding this session. Your message was NOT sent; send it again in a moment, or ${wayOut}.`);
1054
+ return say(lang, 'session.selfHeal.row.cancelFailed.unknown', { taskId: outcome.taskId, detail: outcome.detail, wayOut });
1140
1055
  }
1141
- return (`sema could not cancel run ${outcome.taskId} — the engine rejected that request (${outcome.detail}), so ` +
1142
- `nothing was cancelled and sema cannot tell whether that run is still holding this session. Your message ` +
1143
- `was NOT sent; send it again to find out, or ${wayOut}.`);
1056
+ return say(lang, 'session.selfHeal.row.cancelFailed.rejected', { taskId: outcome.taskId, detail: outcome.detail, wayOut });
1144
1057
  case 'ask-run-not-found':
1145
- return (`The previous turn is recorded as parked on a decision (run ${outcome.taskId}), but the approval it ` +
1146
- `was waiting on is no longer on the engine, and asking the engine about that run directly comes back ` +
1147
- `not-found — a lookup that cannot separate a run that is really gone from one this session is simply ` +
1148
- `not allowed to read. Either way sema has nothing it can act on here, so it did NOT cancel anything. ` +
1149
- `Your message was NOT sent; send it again (if the session really is free it will just run), or ${wayOut}.`);
1058
+ return say(lang, 'session.selfHeal.row.askRunNotFound', { taskId: outcome.taskId, wayOut });
1150
1059
  case 'ask-run-settled':
1151
- return (`The previous turn was recorded as parked on a decision (run ${outcome.taskId}), but that run has already finished ` +
1152
- `(status ${outcome.status}) — there is nothing left to answer and nothing to cancel, so sema did NOT cancel anything. ` +
1153
- `Your message was NOT sent; send it again (once that run has let go of the session it will just run), or ${wayOut}.`);
1060
+ return say(lang, 'session.selfHeal.row.askRunSettled', { taskId: outcome.taskId, status: outcome.status, wayOut });
1154
1061
  case 'stale-park-cancelled':
1155
- if (outcome.releaseConfirmedByEngine === true) {
1156
- return (`Run ${outcome.taskId} was parked on an approval that no longer exists on the engine, so there was ` +
1157
- `nothing left for you to answer. You chose to cancel that run, and the engine confirmed it is no longer ` +
1158
- `holding this session, so sema is sending your message again now.`);
1159
- }
1160
- return (`Run ${outcome.taskId} was parked on an approval that no longer exists on the engine, so there was ` +
1161
- `nothing left for you to answer. You chose to cancel that run: the engine now reports it as finished, ` +
1162
- `so it will not resume — whatever that turn had left to do is not coming back. sema is sending your ` +
1163
- `message again now; if that run has not fully let go of the session yet, you may have to send it once ` +
1164
- `more.`);
1062
+ return outcome.releaseConfirmedByEngine === true
1063
+ ? say(lang, 'session.selfHeal.row.stale.cancelled.confirmed', { taskId: outcome.taskId })
1064
+ : say(lang, 'session.selfHeal.row.stale.cancelled.reported', { taskId: outcome.taskId });
1165
1065
  case 'stale-park-cancel-timeout': {
1166
1066
  const waited = `${String(Math.max(1, Math.round(outcome.waitedMs / 1000)))}s`;
1167
- if (outcome.aborted) {
1168
- return (`sema asked the engine to cancel run ${outcome.taskId} (it was parked on an approval that no longer ` +
1169
- `exists there) and you interrupted while it was still confirming (${waited} in). Cancelling is ` +
1170
- `asynchronous, so that run may still be winding down and may still hold this session. Your message ` +
1171
- `was NOT sent; send it again in a moment, or ${wayOut}.`);
1172
- }
1067
+ if (outcome.aborted)
1068
+ return say(lang, 'session.selfHeal.row.stale.cancelTimeout.aborted', { taskId: outcome.taskId, waited, wayOut });
1173
1069
  if (!outcome.confirmedHeld) {
1174
- return (`sema asked the engine to cancel run ${outcome.taskId} (parked on an approval that no longer exists ` +
1175
- `there) but could not read that run's state back within ${waited}, so it cannot confirm whether the ` +
1176
- `session was released. Your message was NOT sent; send it again in a moment (if that run really is ` +
1177
- `gone it will just run), or ${wayOut}.`);
1070
+ return say(lang, 'session.selfHeal.row.stale.cancelTimeout.unread', { taskId: outcome.taskId, waited, wayOut });
1178
1071
  }
1179
- return (`sema asked the engine to cancel run ${outcome.taskId} (parked on an approval that no longer exists ` +
1180
- `there), but that run still held this session ${waited} later — cancelling is asynchronous and it may ` +
1181
- `still be winding down. Your message was NOT sent; send it again in a moment, or ${wayOut}.`);
1072
+ return say(lang, 'session.selfHeal.row.stale.cancelTimeout.held', { taskId: outcome.taskId, waited, wayOut });
1182
1073
  }
1183
1074
  case 'stale-park-cancel-failed':
1184
1075
  if (outcome.delivery === 'unknown') {
1185
- return (`sema sent the cancel for run ${outcome.taskId} (parked on an approval that no longer exists on the ` +
1186
- `engine) but could not confirm what happened to it (${outcome.detail}), and it could not read that ` +
1187
- `run back as released either. That run may or may not still be holding this session. Your message ` +
1188
- `was NOT sent; send it again in a moment, or ${wayOut}.`);
1076
+ return say(lang, 'session.selfHeal.row.stale.cancelFailed.unknown', { taskId: outcome.taskId, detail: outcome.detail, wayOut });
1189
1077
  }
1190
- return (`sema could not cancel run ${outcome.taskId} — the engine rejected that request (${outcome.detail}), ` +
1191
- `so nothing was cancelled and sema cannot tell whether that run is still holding this session. Your ` +
1192
- `message was NOT sent; send it again to find out, or ${wayOut}.`);
1078
+ return say(lang, 'session.selfHeal.row.cancelFailed.rejected', { taskId: outcome.taskId, detail: outcome.detail, wayOut });
1193
1079
  case 'stale-park-wait':
1194
- if (outcome.alreadyOffered === true) {
1195
- return (`Run ${outcome.taskId} is still parked (status ${outcome.status}) on an approval that no longer ` +
1196
- `exists on the engine. sema already asked what to do with it and you chose to leave it alone, so it ` +
1197
- `is not asking again. Your message was NOT sent; cancel that run on the engine ` +
1198
- `(POST /v1/runs/${outcome.taskId}/cancel — that ends it for good), or ${wayOut}.`);
1199
- }
1200
- return (`Run ${outcome.taskId} is still parked (status ${outcome.status}) on an approval that no longer exists ` +
1201
- `on the engine, so there is no card left to answer and that run will not settle on its own. sema did ` +
1202
- `NOT cancel it — you chose to leave it alone. Your message was NOT sent; cancel that run on the engine ` +
1203
- `(POST /v1/runs/${outcome.taskId}/cancel — that ends it for good), or ${wayOut}.`);
1080
+ return outcome.alreadyOffered === true
1081
+ ? say(lang, 'session.selfHeal.row.stale.wait.offered', { taskId: outcome.taskId, status: outcome.status, wayOut })
1082
+ : say(lang, 'session.selfHeal.row.stale.wait.first', { taskId: outcome.taskId, status: outcome.status, wayOut });
1204
1083
  case 'running-not-found':
1205
- return (`The engine reported this session as held by run ${outcome.taskId} (status running), but it no ` +
1206
- `longer recognizes that run when asked directly — that claim looks stale (the run most likely no ` +
1207
- `longer exists). sema did not offer to steer or cancel it: there is nothing left to act on. ` +
1208
- `Your message was NOT sent; send it again (if the session really is free it will just run), or ${wayOut}.`);
1084
+ return say(lang, 'session.selfHeal.row.runNotFound', { taskId: outcome.taskId, wayOut });
1209
1085
  case 'running-settled':
1210
- return (`Run ${outcome.taskId} has already finished (status ${outcome.status}), but the engine still ` +
1211
- `reported it as holding this session — that claim looks stale or mid-release. sema did not offer ` +
1212
- `to steer or cancel it: the run is over. Your message was NOT sent; send it again (once the claim ` +
1213
- `clears it will just run), or ${wayOut}.`);
1086
+ return say(lang, 'session.selfHeal.row.runSettled', { taskId: outcome.taskId, status: outcome.status, wayOut });
1214
1087
  case 'not-parked':
1215
1088
  if (outcome.alreadyOffered === true) {
1216
- return (`Run ${outcome.taskId} is still holding this session (status ${outcome.status}). sema already ` +
1217
- `asked what to do with it and you chose to leave it alone, so it is not asking again. ` +
1218
- `Your message was NOT sent; wait for that run, cancel it on the engine ` +
1219
- `(POST /v1/runs/${outcome.taskId}/cancel), or ${wayOut}.`);
1089
+ return say(lang, 'session.selfHeal.row.notParked.offered', { taskId: outcome.taskId, status: outcome.status, wayOut });
1220
1090
  }
1221
- const parkedOn = outcome.pendingGateKind !== undefined ? gateKindPhrase(outcome.pendingGateKind) : isSdkRunParked(outcome.status) ? gateKindPhrase(null) : undefined;
1091
+ const parkedOn = outcome.pendingGateKind !== undefined
1092
+ ? gateKindPhrase(outcome.pendingGateKind, lang)
1093
+ : isSdkRunParked(outcome.status)
1094
+ ? gateKindPhrase(null, lang)
1095
+ : undefined;
1222
1096
  if (parkedOn !== undefined) {
1223
- return (`This session is held by an earlier run (run ${outcome.taskId}, status ${outcome.status}) that is ` +
1224
- `waiting on ${parkedOn} — it will not finish on its own. sema did ` +
1225
- `NOT cancel it, because cancelling would decide that item for you. Your message was NOT sent; ` +
1226
- `${wayOut} if you no longer want it.`);
1097
+ return say(lang, 'session.selfHeal.row.notParked.parkedOn', { taskId: outcome.taskId, status: outcome.status, parkedOn, wayOut });
1227
1098
  }
1228
- return (`This session already has a run in flight (run ${outcome.taskId}, status ${outcome.status}) — ` +
1229
- `most likely a turn you sent to the background. sema did NOT cancel it, because that would ` +
1230
- `throw away work you asked for. Wait for it to finish, or ${wayOut}.`);
1099
+ return say(lang, 'session.selfHeal.row.notParked.inFlight', { taskId: outcome.taskId, status: outcome.status, wayOut });
1231
1100
  case 'state-unknown':
1232
- return (`This session is locked by an earlier run${outcome.taskId ? ` (run ${outcome.taskId})` : ''} and ` +
1233
- `sema could not read that run's state, so it did not touch it (${outcome.detail}). ` +
1234
- `Your message was NOT sent. This session cannot start a new turn until that run settles — ` +
1235
- `${wayOut}.`);
1101
+ return say(lang, 'session.selfHeal.row.stateUnknown', {
1102
+ handle: outcome.taskId ? msg('session.selfHeal.handle.run', { taskId: outcome.taskId }) : '',
1103
+ detail: outcome.detail,
1104
+ wayOut,
1105
+ });
1236
1106
  }
1237
1107
  }
1238
- export function activeRunBusyHeadlessRow(signal, copy) {
1108
+ export function activeRunBusyHeadlessRow(signal, copy, lang) {
1239
1109
  const override = copy?.headlessRowFor?.(signal);
1240
1110
  if (typeof override === 'string')
1241
1111
  return override;
1242
1112
  const activeTaskId = signal.activeTaskId;
1243
- const handle = activeTaskId ? ` (run ${activeTaskId})` : '';
1113
+ const handle = activeTaskId ? msg('session.selfHeal.handle.run', { taskId: activeTaskId }) : '';
1244
1114
  const cancelPath = `POST /v1/runs/${activeTaskId ?? '<id>'}/cancel`;
1245
1115
  const status = signal.activeTaskStatus;
1246
1116
  const decidePath = signal.pendingGate?.decidePath ?? null;
1247
- const freshSession = copy?.freshSession ?? DEFAULT_FRESH_SESSION;
1117
+ const hostFresh = copy?.freshSession ?? undefined;
1118
+ const freshSession = hostFresh ?? say(lang, 'session.selfHeal.freshSession.default');
1248
1119
  if (decidePath) {
1249
- return (`This session is held by an earlier run${handle}${status ? ` (status ${status})` : ''} that is parked ` +
1250
- `waiting on ${gateKindPhrase(signal.pendingGate?.kind)}, so this message was NOT sent. Decide it and ` +
1251
- `the session unlocks: POST ${decidePath}. Cancelling instead (${cancelPath}) also unlocks it, but it ` +
1252
- `decides that item against you. Or ${freshSession}.` +
1253
- governanceOriginClause(signal));
1254
- }
1255
- if (status) {
1256
- return (`This session is held by an earlier run${handle} with status ${status}, so this message was NOT sent. ` +
1257
- `The engine reported no decision pending on that run: if it is still working, wait for it to finish; ` +
1258
- `if it will never settle, release it with ${cancelPath} (that discards whatever it was doing). ` +
1259
- `Or ${freshSession}.`);
1260
- }
1261
- const startOver = `${freshSession.charAt(0).toUpperCase()}${freshSession.slice(1)}`;
1262
- return (`This session is locked by an earlier run${handle} that was never released, so this message was ` +
1263
- `NOT sent. Nothing here clears on its own. ${startOver}, or ` +
1264
- `release the run on the engine: ${cancelPath}`);
1120
+ return say(lang, 'session.selfHeal.headless.decide', {
1121
+ handle,
1122
+ status: status ? msg('session.selfHeal.headless.status', { status }) : '',
1123
+ gate: gateKindPhrase(signal.pendingGate?.kind, lang),
1124
+ decidePath,
1125
+ cancelPath,
1126
+ freshSession,
1127
+ governance: governanceOriginClause(signal, lang),
1128
+ });
1129
+ }
1130
+ if (status)
1131
+ return say(lang, 'session.selfHeal.headless.statusOnly', { handle, status, cancelPath, freshSession });
1132
+ const startOver = hostFresh !== undefined ? `${hostFresh.charAt(0).toUpperCase()}${hostFresh.slice(1)}` : say(lang, 'session.selfHeal.freshSession.defaultCapitalized');
1133
+ return say(lang, 'session.selfHeal.headless.neverReleased', { handle, startOver, cancelPath });
1265
1134
  }