@code-yeongyu/senpi 2026.11.10-1 → 2026.11.10-3

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 (209) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/bundle/chunks/{args-77GYOPEO.js → args-HFYB6URH.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-YFZ734T3.js → azure-openai-responses-MNN2TINI.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-ZUQCYIRY.js → chunk-2ODGHBUD.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-YPAQXRP3.js → chunk-47T6YMI2.js} +2 -2
  6. package/dist/bundle/chunks/{chunk-I5ASHSLZ.js → chunk-4JMGWVRH.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-MSDBBOB2.js → chunk-5H43O4MA.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-3IYWH6RO.js → chunk-6Q7Y2G5A.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-X4XWAB3J.js → chunk-AC6MDRAE.js} +81 -81
  10. package/dist/bundle/chunks/{chunk-33MZJTQ3.js → chunk-B4WD5XHP.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-V5FSENW6.js → chunk-COKX65TE.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-LHEP3ACI.js → chunk-DEQLHN2J.js} +2 -2
  13. package/dist/bundle/chunks/{chunk-EJB5E2JK.js → chunk-DHAAWL2H.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-2EJ4BGBM.js → chunk-DHORIIVQ.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-GPXZTSEX.js → chunk-DIHIEJFV.js} +3 -3
  16. package/dist/bundle/chunks/{chunk-NH7SYIGN.js → chunk-DLPV4B3R.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-RCHIMH7N.js → chunk-DQY26AX3.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-Y7Z7XPUF.js → chunk-DZQA5LVN.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-2CNSCMYC.js → chunk-E5O4SP5I.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-3Q5ZENSD.js → chunk-E6ZBTSC6.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-HI5JSLG5.js → chunk-H3VJVKUK.js} +4 -4
  22. package/dist/bundle/chunks/{chunk-L6NRDUWJ.js → chunk-HZDKQOWB.js} +1 -1
  23. package/dist/bundle/chunks/chunk-IREUIMFE.js +11 -0
  24. package/dist/bundle/chunks/{chunk-FAFIGBUD.js → chunk-JSD6O3RJ.js} +2 -2
  25. package/dist/bundle/chunks/{chunk-BELNOPAI.js → chunk-L3KVP7VJ.js} +3 -3
  26. package/dist/bundle/chunks/{chunk-7MN7YNO5.js → chunk-L7DGTA3L.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-J2NGJKXL.js → chunk-LXINMDMX.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-H33A33M4.js → chunk-M2VJVJMK.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-ARUV3US4.js → chunk-NN7NESW2.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-2U6HXWXS.js → chunk-NNQ34ZDW.js} +1 -1
  31. package/dist/bundle/chunks/chunk-NQKCJQHB.js +2 -0
  32. package/dist/bundle/chunks/{chunk-TGW4R7SI.js → chunk-O5WJVIEU.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-O3FG6HML.js → chunk-PJI5GT7U.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-BPNSOZ2C.js → chunk-QUVSIW2R.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-WXYMAUFX.js → chunk-QX7WD3IM.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-Y4ONXQMR.js → chunk-RGHVPUUX.js} +1 -1
  37. package/dist/bundle/chunks/{chunk-RYPM5BZQ.js → chunk-S7NAJQB3.js} +1 -1
  38. package/dist/bundle/chunks/{chunk-LT353NSF.js → chunk-T4CUD7RF.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-J7FW7MED.js → chunk-TCZORL3V.js} +1 -1
  40. package/dist/bundle/chunks/{chunk-KU72IKV5.js → chunk-VGSWOLIY.js} +1 -1
  41. package/dist/bundle/chunks/{chunk-DBQBJTMF.js → chunk-VUY4BZDI.js} +1 -1
  42. package/dist/bundle/chunks/chunk-XI6THO4F.js +2 -0
  43. package/dist/bundle/chunks/{chunk-THYEZBUG.js → chunk-Z2C5N2LM.js} +2 -2
  44. package/dist/bundle/chunks/{cli-main-VG3C2EXI.js → cli-main-ZTT3N4DO.js} +1 -1
  45. package/dist/bundle/chunks/{credential-accounts-LVVPKFEY.js → credential-accounts-5KHVGSCY.js} +1 -1
  46. package/dist/bundle/chunks/{dispatch-EP35KKJ3.js → dispatch-YUYZQXRP.js} +1 -1
  47. package/dist/bundle/chunks/{hand-entry-3LQKUTYS.js → hand-entry-UV5XPMBF.js} +3 -3
  48. package/dist/bundle/chunks/{headless-host-HKGNJGYT.js → headless-host-53UMJMM6.js} +1 -1
  49. package/dist/bundle/chunks/{health-write-FBF74QJ7.js → health-write-JLSF5EVV.js} +1 -1
  50. package/dist/bundle/chunks/{help-fast-path-5SCPY3OH.js → help-fast-path-552QLMWT.js} +1 -1
  51. package/dist/bundle/chunks/{host-command-CCASS7W4.js → host-command-BZN7WYYV.js} +1 -1
  52. package/dist/bundle/chunks/{host-lifecycle-WWC7HXQ6.js → host-lifecycle-TNHASSFJ.js} +1 -1
  53. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  54. package/dist/bundle/chunks/{host-session-command-YJB2FLKU.js → host-session-command-O5WYHZXH.js} +1 -1
  55. package/dist/bundle/chunks/{host-socket-request-DMYXC56U.js → host-socket-request-QN4Q2P5E.js} +1 -1
  56. package/dist/bundle/chunks/{interactive-mode-CUN3WMUF.js → interactive-mode-OCU4NDPC.js} +1 -1
  57. package/dist/bundle/chunks/{login-health-EVXVLHB2.js → login-health-TOSQT5RG.js} +1 -1
  58. package/dist/bundle/chunks/{main-P7XRC2OI.js → main-CNDP6HCF.js} +1 -1
  59. package/dist/bundle/chunks/{multi-session-host-244X3P6L.js → multi-session-host-ODGJAGJP.js} +11 -8
  60. package/dist/bundle/chunks/openai-codex-responses-VRGOAFEG.js +10 -0
  61. package/dist/bundle/chunks/{openai-responses-NIVBWE2H.js → openai-responses-I7PXRWCM.js} +2 -2
  62. package/dist/bundle/chunks/{package-manager-cli-UVWJOQA3.js → package-manager-cli-HSPUG4RJ.js} +1 -1
  63. package/dist/bundle/chunks/{role-HQUN4XXK.js → role-AEOLJRTP.js} +1 -1
  64. package/dist/bundle/chunks/{rotation-stream-F3BEMXBE.js → rotation-stream-E3FJFHXJ.js} +1 -1
  65. package/dist/bundle/chunks/rpc-mode-KOZPXU3U.js +2 -0
  66. package/dist/bundle/chunks/{rpc-monitor-source-C6VQ3BC4.js → rpc-monitor-source-PGTDSGWP.js} +1 -1
  67. package/dist/bundle/chunks/{run-attached-tui-R3RB5P7G.js → run-attached-tui-4AENRP7P.js} +1 -1
  68. package/dist/bundle/chunks/{schedule-command-5FG2OKSR.js → schedule-command-SG44FE23.js} +1 -1
  69. package/dist/bundle/chunks/session-control-endpoint-5A24JYPE.js +3 -0
  70. package/dist/bundle/chunks/{session-picker-2FZSNFS7.js → session-picker-73YRS3GE.js} +1 -1
  71. package/dist/bundle/chunks/{session-slot-CZGQQCOC.js → session-slot-RH7JLO3F.js} +1 -1
  72. package/dist/bundle/chunks/session-worker.js +199 -196
  73. package/dist/bundle/chunks/{state-store-S7QAESIF.js → state-store-IECVTYXC.js} +1 -1
  74. package/dist/bundle/cli.js +2 -2
  75. package/dist/bundle/host-supervisor.js +1 -1
  76. package/dist/bundle/index.js +1 -1
  77. package/dist/bundle/rpc-entry.js +1 -1
  78. package/dist/bundle/runtime-manifest.json +1 -1
  79. package/dist/core/agent-session-runtime.d.ts +2 -0
  80. package/dist/core/agent-session-runtime.js +5 -0
  81. package/dist/core/agent-session.d.ts +13 -3
  82. package/dist/core/agent-session.js +89 -18
  83. package/dist/core/assistant-error-redaction.d.ts +9 -0
  84. package/dist/core/assistant-error-redaction.js +27 -0
  85. package/dist/core/config-reference-redaction.js +90 -10
  86. package/dist/core/context-files.d.ts +10 -0
  87. package/dist/core/context-files.js +58 -12
  88. package/dist/core/egress/wire.d.ts +13 -0
  89. package/dist/core/extensions/builtin/config-reload/change-groups.js +6 -2
  90. package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +32 -8
  91. package/dist/core/extensions/builtin/mcp/auth/oauth-grant.d.ts +43 -0
  92. package/dist/core/extensions/builtin/mcp/auth/oauth-grant.js +196 -0
  93. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +4 -17
  94. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +64 -75
  95. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.d.ts +1 -1
  96. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +25 -4
  97. package/dist/core/extensions/builtin/mcp/auth/oauth.js +10 -7
  98. package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +18 -0
  99. package/dist/core/extensions/builtin/mcp/auth/token-store.js +3 -0
  100. package/dist/core/extensions/builtin/mcp/catalog-cache.js +7 -6
  101. package/dist/core/extensions/builtin/mcp/catalog.js +3 -2
  102. package/dist/core/extensions/builtin/mcp/invocation.js +6 -0
  103. package/dist/core/extensions/builtin/mcp/service-register.js +16 -2
  104. package/dist/core/extensions/builtin/mcp/service-tools-changed.js +61 -54
  105. package/dist/core/extensions/builtin/mcp/startup-race.d.ts +2 -0
  106. package/dist/core/extensions/builtin/mcp/startup-race.js +25 -7
  107. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-2.js +6 -6
  108. package/dist/core/extensions/loader.js +24 -4
  109. package/dist/core/extensions/runner.d.ts +9 -1
  110. package/dist/core/extensions/runner.js +9 -4
  111. package/dist/core/extensions/types.d.ts +17 -3
  112. package/dist/core/hand/hook-advertise.d.ts +41 -0
  113. package/dist/core/hand/hook-advertise.js +98 -0
  114. package/dist/core/hand/hook-contract.d.ts +66 -0
  115. package/dist/core/hand/hook-contract.js +138 -0
  116. package/dist/core/open-timing.d.ts +2 -0
  117. package/dist/core/open-timing.js +7 -1
  118. package/dist/core/resource-loader.js +2 -0
  119. package/dist/core/retry-fallback/log.d.ts +7 -1
  120. package/dist/core/retry-fallback/log.js +82 -5
  121. package/dist/core/retry-fallback/model-config-error.js +2 -1
  122. package/dist/core/session-manager.d.ts +2 -0
  123. package/dist/core/session-manager.js +15 -1
  124. package/dist/core/settings-manager.d.ts +6 -0
  125. package/dist/core/settings-manager.js +8 -0
  126. package/dist/core/turn-origin.d.ts +19 -13
  127. package/dist/core/turn-origin.js +34 -30
  128. package/dist/core/ultrafast-lanes.d.ts +1 -0
  129. package/dist/core/ultrafast-lanes.js +4 -2
  130. package/dist/modes/interactive/components/footer.d.ts +23 -5
  131. package/dist/modes/interactive/components/footer.js +45 -16
  132. package/dist/modes/interactive/interactive-mode.js +9 -1
  133. package/dist/modes/interactive/session-control-rpc-commands.d.ts +1 -1
  134. package/dist/modes/interactive/session-control-rpc-commands.js +5 -0
  135. package/dist/modes/rpc/admit-external-message-command.d.ts +2 -2
  136. package/dist/modes/rpc/admit-external-message-command.js +2 -10
  137. package/dist/modes/rpc/compact-admission-validation.js +4 -1
  138. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  139. package/dist/modes/rpc/connection-handler.js +20 -8
  140. package/dist/modes/rpc/connection-roles.d.ts +2 -0
  141. package/dist/modes/rpc/connection-roles.js +4 -0
  142. package/dist/modes/rpc/custom-capability.d.ts +4 -0
  143. package/dist/modes/rpc/custom-capability.js +4 -0
  144. package/dist/modes/rpc/delegated-drive-origin.d.ts +6 -6
  145. package/dist/modes/rpc/delegated-drive-origin.js +7 -8
  146. package/dist/modes/rpc/delegation-declaration.d.ts +2 -0
  147. package/dist/modes/rpc/delegation-declaration.js +5 -1
  148. package/dist/modes/rpc/effect-commands.d.ts +21 -0
  149. package/dist/modes/rpc/host-capabilities.d.ts +1 -0
  150. package/dist/modes/rpc/host-capabilities.js +5 -1
  151. package/dist/modes/rpc/host-observe-request.d.ts +2 -0
  152. package/dist/modes/rpc/host-observe-request.js +6 -1
  153. package/dist/modes/rpc/host-park-ledger.d.ts +19 -0
  154. package/dist/modes/rpc/host-park-ledger.js +110 -0
  155. package/dist/modes/rpc/lifecycle-delivery.js +4 -1
  156. package/dist/modes/rpc/loopless-tool-handlers.d.ts +6 -0
  157. package/dist/modes/rpc/loopless-tool-handlers.js +14 -1
  158. package/dist/modes/rpc/media-placeholders.js +1 -0
  159. package/dist/modes/rpc/method-classes-schema-host.d.ts +42 -1
  160. package/dist/modes/rpc/method-classes-schema-host.js +24 -21
  161. package/dist/modes/rpc/method-classes-schema.d.ts +42 -1
  162. package/dist/modes/rpc/method-classes-schema.js +5 -2
  163. package/dist/modes/rpc/method-classes.d.ts +2 -0
  164. package/dist/modes/rpc/method-classes.js +3 -0
  165. package/dist/modes/rpc/multi-session-host.js +1 -1
  166. package/dist/modes/rpc/rpc-host-command-types.d.ts +14 -1
  167. package/dist/modes/rpc/rpc-host-response-types.d.ts +21 -0
  168. package/dist/modes/rpc/session-attachment-gate.js +2 -0
  169. package/dist/modes/rpc/session-binding.d.ts +1 -0
  170. package/dist/modes/rpc/session-binding.js +6 -1
  171. package/dist/modes/rpc/session-connection-context.d.ts +2 -0
  172. package/dist/modes/rpc/session-connection-context.js +4 -0
  173. package/dist/modes/rpc/session-registry-attach.d.ts +2 -0
  174. package/dist/modes/rpc/session-registry-attach.js +15 -11
  175. package/dist/modes/rpc/session-registry-claims.js +2 -0
  176. package/dist/modes/rpc/session-registry-open.js +13 -7
  177. package/dist/modes/rpc/session-registry-opening.js +2 -1
  178. package/dist/modes/rpc/session-registry-types.d.ts +4 -0
  179. package/dist/modes/rpc/session-registry.d.ts +3 -0
  180. package/dist/modes/rpc/session-registry.js +27 -3
  181. package/dist/modes/rpc/session-router-close.js +1 -0
  182. package/dist/modes/rpc/session-router-context.d.ts +4 -2
  183. package/dist/modes/rpc/session-router-context.js +3 -0
  184. package/dist/modes/rpc/session-router-host-reads.js +1 -0
  185. package/dist/modes/rpc/session-router-occupancy.js +10 -1
  186. package/dist/modes/rpc/session-router-open-result.d.ts +12 -0
  187. package/dist/modes/rpc/session-router-open-result.js +63 -0
  188. package/dist/modes/rpc/session-router-open.d.ts +3 -0
  189. package/dist/modes/rpc/session-router-open.js +15 -63
  190. package/dist/modes/rpc/session-router-park.d.ts +15 -0
  191. package/dist/modes/rpc/session-router-park.js +176 -0
  192. package/dist/modes/rpc/session-router-routing.d.ts +1 -1
  193. package/dist/modes/rpc/session-router-routing.js +7 -0
  194. package/dist/modes/rpc/session-status-source.js +2 -2
  195. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  196. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +22 -2
  197. package/docs/extensions.md +10 -0
  198. package/docs/rpc.md +50 -0
  199. package/docs/settings.md +1 -0
  200. package/docs/usage.md +1 -1
  201. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  202. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  203. package/package.json +6 -6
  204. package/dist/bundle/chunks/chunk-436DNN4Z.js +0 -2
  205. package/dist/bundle/chunks/chunk-DLAY7CIU.js +0 -2
  206. package/dist/bundle/chunks/chunk-XH2MWTXA.js +0 -11
  207. package/dist/bundle/chunks/openai-codex-responses-ABB2J43U.js +0 -10
  208. package/dist/bundle/chunks/rpc-mode-G4I3HHPB.js +0 -2
  209. package/dist/bundle/chunks/session-control-endpoint-XWCFLJYC.js +0 -3
@@ -96,7 +96,7 @@ import { deliveryIdOf, ExternalAdmission } from "./external-admission.js";
96
96
  import { HELD_INPUT_ENTRY, HeldInputs, heldInputHistory } from "./held-inputs.js";
97
97
  import { shouldWarnHighReasoning } from "./high-reasoning-warning.js";
98
98
  import { customTurnOrigin, inputAttributionOf, inputOriginOf } from "./input-origin.js";
99
- import { copyInputTurnMark, hasInputTurnMark, inputTurnMark, markInputTurnOrigin, noteRunStart, noteUserMessageStart, } from "./turn-origin.js";
99
+ import { copyInputTurnMark, endTurnContinuation, hasInputTurnMark, inputTurnMark, markInputTurnOrigin, noteRunStart, noteTurnContinuation, recordIsDelegated, noteUserMessageStart, } from "./turn-origin.js";
100
100
  import { releaseDecisionPreset } from "./permission-decision.js";
101
101
  import { markInterrupted } from "./interrupted-tool-result.js";
102
102
  import { deliveredReceipts, itemNoticeMessage, QueuedDeliveryCleared, } from "./itemized-send.js";
@@ -134,6 +134,7 @@ import { followProviderAccountChanges, RetryFallbackController, } from "./retry-
134
134
  import { SelectorCooldowns } from "./retry-fallback/cooldown.js";
135
135
  import { classifyRateLimitedWait, degradeWithoutFallback, nextInTurnDelayMs, probeBackSchedule, } from "./retry-fallback/hint-policy.js";
136
136
  import { createFallbackLogger } from "./retry-fallback/log.js";
137
+ import { redactAssistantErrorText } from "./assistant-error-redaction.js";
137
138
  import { createModelConfigErrorReporter, isModelConfigFailure, } from "./retry-fallback/model-config-error.js";
138
139
  import { ProbeBackScheduler } from "./retry-fallback/probe-scheduler.js";
139
140
  import { REQUEST_SHAPE_FAILURE_TEXT, REQUEST_SHAPE_RECOVERY_ENTRY_TYPE, REQUEST_SHAPE_RECOVERY_NOTE, RequestShapeRecovery, } from "./retry-fallback/request-shape.js";
@@ -619,8 +620,9 @@ export class AgentSession {
619
620
  this._abortDuringBeforeSettle = false;
620
621
  this._baseToolDefinitions = new Map();
621
622
  this._toolChildHostTargets = new Map();
622
- /** Settles once dispose() has stopped the tool child, swept what it launched, and closed its in-process offload jobs. */
623
+ /** Settles once dispose() has stopped the tool child, swept what it launched, closed its in-process offload jobs, and the fallback log's first-start removal has finished. */
623
624
  this._toolChildClosed = Promise.resolve();
625
+ this._fallbackLogSettled = Promise.resolve();
624
626
  this._usesDefaultTools = false;
625
627
  /** Policy tools withheld from the model, retained so disarming can restore direct access. */
626
628
  this._withheldEvalOnlyToolNames = new Set();
@@ -669,6 +671,7 @@ export class AgentSession {
669
671
  this._endSelectedModels = new WeakMap();
670
672
  /** Internal handler for agent events - shared by subscribe and reconnect */
671
673
  this._handleAgentEvent = (event, signal) => {
674
+ redactAssistantErrorText(event);
672
675
  let queuedEvent = event;
673
676
  let pendingMessage;
674
677
  // Lifecycle events this event raises are delivered just before it, in the same queue (LifecycleEventSequencer).
@@ -844,6 +847,7 @@ export class AgentSession {
844
847
  this._modelRegistry = config.modelRegistry ?? new ModelRegistry(modelRuntime);
845
848
  this._agentDir = config.agentDir ?? getAgentDir();
846
849
  const fallbackLogger = createFallbackLogger(this._agentDir);
850
+ this._fallbackLogSettled = fallbackLogger.settled();
847
851
  this._modelConfigErrors = createModelConfigErrorReporter(fallbackLogger, (event) => this._emit(event));
848
852
  this._sessionLogger = createSessionLogger(this._agentDir, {
849
853
  context: () => ({
@@ -2064,7 +2068,7 @@ export class AgentSession {
2064
2068
  get agentDir() {
2065
2069
  return this._agentDir;
2066
2070
  }
2067
- /** Resolves after dispose() once the tool child has exited and its launched groups are swept; awaited by disposers. */
2071
+ /** Resolves after dispose() once the tool child has exited, its launched groups are swept and the fallback log has stopped writing; awaited by disposers. */
2068
2072
  whenToolChildClosed() {
2069
2073
  return this._toolChildClosed;
2070
2074
  }
@@ -3519,7 +3523,7 @@ export class AgentSession {
3519
3523
  ?.close()
3520
3524
  .catch((error) => this._sessionLogger.warn("tool_child_close_failed", { error: String(error) })) ??
3521
3525
  Promise.resolve();
3522
- this._toolChildClosed = Promise.all([inProcessClosed, childClosed]).then(() => undefined);
3526
+ this._toolChildClosed = Promise.all([inProcessClosed, childClosed, this._fallbackLogSettled]).then(() => undefined);
3523
3527
  this._disconnectFromAgent();
3524
3528
  this._unsubscribeSettingsSource?.();
3525
3529
  this._unsubscribeSettingsSource = undefined;
@@ -5510,6 +5514,8 @@ export class AgentSession {
5510
5514
  // Restored input with no source keeps no mark: an unmarked turn is engine work, never a trusted origin.
5511
5515
  if (input.source !== undefined)
5512
5516
  markInputTurnOrigin(message, inputTurnMark(input));
5517
+ else if (recordIsDelegated())
5518
+ markInputTurnOrigin(message, "delegated");
5513
5519
  if (input.inputId !== undefined)
5514
5520
  this._queuedInputDispositions.track(message, input.inputId);
5515
5521
  this._queuedMessages.set(queuedId, message);
@@ -5900,6 +5906,10 @@ export class AgentSession {
5900
5906
  };
5901
5907
  // A delivery that found a run already streaming becomes a steer or follow-up: it keeps the origin it was admitted with.
5902
5908
  copyInputTurnMark(message, appMessage);
5909
+ // senpi#6342: a custom message a cross-account record's own handling sends carries that origin wherever it waits
5910
+ // (joining a streaming run, parked behind a compaction, held for the next turn), so the turn it opens still asks.
5911
+ if (!hasInputTurnMark(appMessage) && recordIsDelegated())
5912
+ markInputTurnOrigin(appMessage, "delegated");
5903
5913
  if (this._sizesOnlyStartup && options?.triggerTurn === true) {
5904
5914
  // Size inspection makes no model call (#3483): keep the message for a turn that never starts.
5905
5915
  this._pendingNextTurnMessages.push(appMessage);
@@ -6574,7 +6584,7 @@ export class AgentSession {
6574
6584
  }
6575
6585
  }
6576
6586
  /**
6577
- * A refused engine turn still records its message and settles a cross-session delivery it carries. A delivery
6587
+ * A refused engine turn still records its message and settles a cross-session delivery it carries. A message
6578
6588
  * whose entry the file refuses leaves the context too: its sender redelivers it, and it must not appear twice.
6579
6589
  */
6580
6590
  _recordRefusedEngineTurnMessage(appMessage) {
@@ -6583,12 +6593,17 @@ export class AgentSession {
6583
6593
  this.externalAdmission.observePersisted(appMessage);
6584
6594
  }
6585
6595
  catch (error) {
6586
- if (deliveryIdOf(appMessage) !== undefined) {
6587
- const index = this.agent.state.messages.lastIndexOf(appMessage);
6588
- if (index >= 0)
6589
- this.agent.state.messages.splice(index, 1);
6590
- }
6591
- this.externalAdmission.observeRefused(appMessage, error instanceof Error ? error.message : String(error));
6596
+ // The session file refused the entry: the message leaves the context too, so the context matches the
6597
+ // file, and the failure follows the same contract as the main persistence path — it is recorded,
6598
+ // logged, and emitted (senpi#2972). This holds for non-delivery messages as well as deliveries.
6599
+ const errorMessage = error instanceof Error ? error.message : String(error);
6600
+ const index = this.agent.state.messages.lastIndexOf(appMessage);
6601
+ if (index >= 0)
6602
+ this.agent.state.messages.splice(index, 1);
6603
+ this._transcriptWriteFailures.record(appMessage, error);
6604
+ this._sessionLogger.warn("transcript_write_failed", { role: appMessage.role, error: errorMessage });
6605
+ this._emit({ type: "transcript_write_failed", role: appMessage.role, errorMessage });
6606
+ this.externalAdmission.observeRefused(appMessage, errorMessage);
6592
6607
  this._takePersistWatcher(appMessage)?.failed(error);
6593
6608
  }
6594
6609
  }
@@ -6674,12 +6689,27 @@ export class AgentSession {
6674
6689
  const start = { type: "message_start", message: appMessage };
6675
6690
  this._recoveryIdentity?.observe(start);
6676
6691
  const entryId = this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details, { recoveryOwner: recoveryCarrier(start) });
6677
- this._emitEntryAppended(entryId);
6692
+ // The entry is written: only a failure of the write itself counts as refused. Bookkeeping (ticket,
6693
+ // watcher settlement, revision) runs before any notification, and each notification is guarded on its
6694
+ // own, so a throwing listener can neither leave a persisted delivery unsettled nor suppress a later
6695
+ // notification (senpi#2972).
6678
6696
  this._recordWrittenTicket(appMessage, entryId);
6679
6697
  this._takePersistWatcher(appMessage)?.persisted(entryId);
6680
6698
  this._incrementMessageRevision();
6681
- this._emit(start);
6682
- this._emit({ type: "message_end", message: appMessage });
6699
+ const notify = (emit) => {
6700
+ try {
6701
+ emit();
6702
+ }
6703
+ catch (error) {
6704
+ this._sessionLogger.warn("custom_message_listener_failed", {
6705
+ customType: appMessage.customType,
6706
+ error: error instanceof Error ? error.message : String(error),
6707
+ });
6708
+ }
6709
+ };
6710
+ notify(() => this._emitEntryAppended(entryId));
6711
+ notify(() => this._emit(start));
6712
+ notify(() => this._emit({ type: "message_end", message: appMessage }));
6683
6713
  }
6684
6714
  _flushPendingCustomMessages() {
6685
6715
  const pending = this._pendingCustomMessages;
@@ -9357,7 +9387,7 @@ export class AgentSession {
9357
9387
  }
9358
9388
  this._scheduledContinuationRecompacted = true;
9359
9389
  }
9360
- async _continueAgentAfterCurrentRun(options = {}, retryTimeoutMs) {
9390
+ async _continueAgentAfterCurrentRun(options = {}, retryTimeoutMs, keepsTurnOrigin = false) {
9361
9391
  await this.agent.waitForIdle();
9362
9392
  try {
9363
9393
  if (this.agent.state.isStreaming)
@@ -9384,6 +9414,8 @@ export class AgentSession {
9384
9414
  this._transcriptWriteFailures.startRun();
9385
9415
  await runBoundedRetryContinuation({
9386
9416
  continueRun: async () => {
9417
+ if (keepsTurnOrigin)
9418
+ noteTurnContinuation(this.sessionManager);
9387
9419
  if (this._continueWithQueuedInput) {
9388
9420
  // Joined input of a start that never ran: the transcript tail may be anything, even empty.
9389
9421
  this._continueWithQueuedInput = false;
@@ -9428,6 +9460,7 @@ export class AgentSession {
9428
9460
  throw error;
9429
9461
  }
9430
9462
  finally {
9463
+ endTurnContinuation(this.sessionManager);
9431
9464
  this._scheduledContinuationRecompacted = false;
9432
9465
  this._continueWithQueuedInput = false;
9433
9466
  }
@@ -9459,13 +9492,13 @@ export class AgentSession {
9459
9492
  this._continueWithQueuedInput = true;
9460
9493
  this._scheduleContinuationAfterCurrentEvent();
9461
9494
  }
9462
- _scheduleContinuationAfterCurrentEvent(options = {}, retryContinuation = false, retryTimeoutMs) {
9495
+ _scheduleContinuationAfterCurrentEvent(options = {}, retryContinuation = false, retryTimeoutMs, keepsTurnOrigin = retryContinuation) {
9463
9496
  // Tool hooks wait for queued message persistence, so continue() cannot run inside this event promise.
9464
9497
  const currentEventQueue = this._agentEventQueue;
9465
9498
  const finishContinuationWork = this._sessionWorkBarrier.begin();
9466
9499
  const continueAfterEvent = async () => {
9467
9500
  try {
9468
- const outcome = await this._continueAgentAfterCurrentRun(options, retryTimeoutMs);
9501
+ const outcome = await this._continueAgentAfterCurrentRun(options, retryTimeoutMs, keepsTurnOrigin);
9469
9502
  if (retryContinuation && outcome === "taken-over") {
9470
9503
  DEFERRED_RETRY_QUEUE_OWNERS.delete(this);
9471
9504
  }
@@ -9607,7 +9640,8 @@ export class AgentSession {
9607
9640
  // kept tail still covers (one per rung climbed so far); the retry must
9608
9641
  // continue from the turn they failed to answer, so strip the whole run.
9609
9642
  this._stripTrailingFailedAssistants();
9610
- this._scheduleContinuationAfterCurrentEvent();
9643
+ // The recovery run repeats this turn, so it keeps the turn's origin.
9644
+ this._scheduleContinuationAfterCurrentEvent({}, false, undefined, true);
9611
9645
  return true;
9612
9646
  }
9613
9647
  else if (this.pendingMessageCount > 0) {
@@ -11947,8 +11981,45 @@ export class AgentSession {
11947
11981
  const estimate = branchEntries.some((entry) => entry.type === "context_edit")
11948
11982
  ? estimateProjectedContextTokens(this.sessionManager.buildSessionProjection(), branchEntries)
11949
11983
  : estimateContextTokens(messages);
11984
+ // A model switch after the last reported usage invalidates that usage: it was
11985
+ // counted with the previous model's tokenizer, so showing it against the new
11986
+ // model's window misstates the context (oh-my-openagent#11684). Fall back to
11987
+ // the content estimate, like the compaction and context-edit paths above.
11988
+ if (estimate.lastUsageIndex !== null && this._isUsageStaleAfterModelChange(branchEntries)) {
11989
+ return { tokens: messages.reduce((sum, message) => sum + estimateTokens(message), 0), reported: false };
11990
+ }
11950
11991
  return { tokens: estimate.tokens, reported: estimate.usageTokens > 0 };
11951
11992
  }
11993
+ /**
11994
+ * Whether the latest reported usage predates the latest model switch: no
11995
+ * assistant message after the switch reported usage, so the last reported
11996
+ * figure belongs to the previous model's tokenizer. Assistant messages are
11997
+ * appended to the branch only once they stop streaming (a pending assistant
11998
+ * message is rejected), so a switch that lands mid-turn still precedes the
11999
+ * message entry carrying the new model's usage.
12000
+ */
12001
+ _isUsageStaleAfterModelChange(branchEntries) {
12002
+ let latestModelChangeIndex = -1;
12003
+ for (let i = branchEntries.length - 1; i >= 0; i--) {
12004
+ if (branchEntries[i].type === "model_change") {
12005
+ latestModelChangeIndex = i;
12006
+ break;
12007
+ }
12008
+ }
12009
+ if (latestModelChangeIndex === -1)
12010
+ return false;
12011
+ for (let i = branchEntries.length - 1; i > latestModelChangeIndex; i--) {
12012
+ const entry = branchEntries[i];
12013
+ if (entry.type === "message" && entry.message.role === "assistant") {
12014
+ const assistant = entry.message;
12015
+ if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error") {
12016
+ if (calculateContextTokens(assistant.usage) > 0)
12017
+ return false;
12018
+ }
12019
+ }
12020
+ }
12021
+ return true;
12022
+ }
11952
12023
  /**
11953
12024
  * Export session to HTML.
11954
12025
  * @param outputPath Optional output path (defaults to session directory)
@@ -0,0 +1,9 @@
1
+ import type { AgentEvent } from "@earendil-works/pi-agent-core";
2
+ /**
3
+ * Provider error text can echo a value a config reference resolved to (omo#12965). An assistant message carries that
4
+ * text from its first streamed partial to its final form, and the final object is the one agent state keeps and the
5
+ * transcript persists, so every message event is redacted in place before a listener, the transcript or the state
6
+ * reads it.
7
+ */
8
+ export declare function redactAssistantErrorText(event: AgentEvent): void;
9
+ //# sourceMappingURL=assistant-error-redaction.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { redactSensitiveTokenValues } from "./sensitive-output.js";
2
+ function redactErrorText(message) {
3
+ if (message.role === "assistant" && message.errorMessage) {
4
+ message.errorMessage = redactSensitiveTokenValues(message.errorMessage);
5
+ }
6
+ }
7
+ /**
8
+ * Provider error text can echo a value a config reference resolved to (omo#12965). An assistant message carries that
9
+ * text from its first streamed partial to its final form, and the final object is the one agent state keeps and the
10
+ * transcript persists, so every message event is redacted in place before a listener, the transcript or the state
11
+ * reads it.
12
+ */
13
+ export function redactAssistantErrorText(event) {
14
+ if (event.type !== "message_start" && event.type !== "message_update" && event.type !== "message_end")
15
+ return;
16
+ redactErrorText(event.message);
17
+ if (event.type !== "message_update")
18
+ return;
19
+ const update = event.assistantMessageEvent;
20
+ if ("partial" in update)
21
+ redactErrorText(update.partial);
22
+ else if (update.type === "done")
23
+ redactErrorText(update.message);
24
+ else
25
+ redactErrorText(update.error);
26
+ }
27
+ //# sourceMappingURL=assistant-error-redaction.js.map
@@ -1,21 +1,101 @@
1
+ /**
2
+ * Resolved-value registry shared by every copy of this module in one JavaScript realm (omo#12965).
3
+ *
4
+ * A process can evaluate this module more than once (the bundle's shared chunk, its self-contained session-worker
5
+ * entry, and the SDK graph an in-process child loads), so the values are not kept in a module-local set: the first
6
+ * copy pins a frozen registry on `globalThis` under `SLOT_KEY` (non-writable, non-configurable, non-enumerable) and
7
+ * every later copy adopts it. The backing `Set` lives only in the installing copy's closure; the registry offers no
8
+ * delete, clear or setter, so in-realm code cannot remove a value, replace the slot or turn redaction off. Any
9
+ * in-realm code can `add` a string, which can only over-redact. This is a stopgap until one engine instance serves
10
+ * the whole process; other realms (workers, `vm` contexts, child processes) keep their own registry.
11
+ *
12
+ * Compatibility contract, permanent: the key string never changes. The v1 members `version` (1), `add(value)`,
13
+ * `size` (an accessor reporting the live count) and `values()` (a fresh array) are never removed or repurposed; a
14
+ * later contract keeps writing through this same slot and only adds members, and adopters ignore members they do not
15
+ * know. A copy adopts only a slot pinned the way `pin` pins it, frozen, with those four members; anything else (a
16
+ * slot planted before the first copy loaded, an accessor, another version) makes the import throw
17
+ * `ConfigReferenceRedactionSlotError`, because a copy running with a private set is the leak this registry removes.
18
+ */
19
+ const SLOT_KEY_NAME = "@code-yeongyu/senpi/config-reference-redaction/v1";
20
+ const SLOT_KEY = Symbol.for(SLOT_KEY_NAME);
1
21
  const MIN_REDACTED_LENGTH = 4;
2
- const resolvedValues = new Set();
3
- let redactionForms;
22
+ /** The largest `$file` reference value (`config-reference-file.ts`); longer strings are never remembered. */
23
+ const MAX_REMEMBERED_LENGTH = 65_536;
24
+ class ConfigReferenceRedactionSlotError extends Error {
25
+ constructor(cause) {
26
+ super(`The process-wide redaction registry ${SLOT_KEY_NAME} was not installed by senpi or is incompatible; restart the process.`, cause === undefined ? undefined : { cause });
27
+ this.name = "ConfigReferenceRedactionSlotError";
28
+ this.code = "SENPI_REDACTION_SLOT_INCOMPATIBLE";
29
+ }
30
+ }
31
+ function createRegistry() {
32
+ const values = new Set();
33
+ return Object.freeze({
34
+ version: 1,
35
+ add(value) {
36
+ if (typeof value !== "string" || value.length < MIN_REDACTED_LENGTH || value.length > MAX_REMEMBERED_LENGTH)
37
+ return;
38
+ values.add(value);
39
+ },
40
+ get size() {
41
+ return values.size;
42
+ },
43
+ values: () => [...values],
44
+ });
45
+ }
46
+ function isFrozenV1Registry(value) {
47
+ return (typeof value === "object" &&
48
+ value !== null &&
49
+ Object.isFrozen(value) &&
50
+ Reflect.get(value, "version") === 1 &&
51
+ typeof Reflect.get(value, "add") === "function" &&
52
+ typeof Reflect.get(value, "values") === "function" &&
53
+ typeof Reflect.get(value, "size") === "number");
54
+ }
55
+ function pin(registry) {
56
+ try {
57
+ Object.defineProperty(globalThis, SLOT_KEY, {
58
+ value: registry,
59
+ writable: false,
60
+ configurable: false,
61
+ enumerable: false,
62
+ });
63
+ }
64
+ catch (error) {
65
+ throw new ConfigReferenceRedactionSlotError(error);
66
+ }
67
+ return registry;
68
+ }
69
+ function sharedRegistry() {
70
+ const slot = Reflect.getOwnPropertyDescriptor(globalThis, SLOT_KEY);
71
+ if (slot === undefined)
72
+ return pin(createRegistry());
73
+ const value = slot.value;
74
+ if (slot.writable !== false || slot.configurable !== false || !isFrozenV1Registry(value)) {
75
+ throw new ConfigReferenceRedactionSlotError();
76
+ }
77
+ return value;
78
+ }
79
+ const registry = sharedRegistry();
80
+ let redactionForms = [];
81
+ let formsSize = 0;
4
82
  /** Every value a reference resolves to is kept for the process lifetime, including rotated ones: no cap, no eviction. */
5
83
  export function rememberResolvedReferenceValue(value) {
6
- if (value.length < MIN_REDACTED_LENGTH || resolvedValues.has(value))
7
- return;
8
- resolvedValues.add(value);
9
- redactionForms = undefined;
84
+ registry.add(value);
10
85
  }
11
86
  function encodings(value) {
12
87
  const form = new URLSearchParams([["", value]]).toString().slice(1);
13
88
  return [value, JSON.stringify(value).slice(1, -1), encodeURIComponent(value), encodeURI(value), form];
14
89
  }
90
+ /** The registry is append-only, so a changed size is the only way its content can change. */
15
91
  function forms() {
16
- redactionForms ??= [...new Set([...resolvedValues].flatMap(encodings))]
17
- .filter((form) => form.length >= MIN_REDACTED_LENGTH)
18
- .sort((left, right) => right.length - left.length);
92
+ const size = registry.size;
93
+ if (size !== formsSize) {
94
+ redactionForms = [...new Set(registry.values().flatMap(encodings))]
95
+ .filter((form) => form.length >= MIN_REDACTED_LENGTH)
96
+ .sort((left, right) => right.length - left.length);
97
+ formsSize = size;
98
+ }
19
99
  return redactionForms;
20
100
  }
21
101
  /** Replaces every resolved reference value, raw or encoded, before text reaches a sink outside the process. */
@@ -134,7 +214,7 @@ function isProtocolValue(key, value) {
134
214
  return Object.hasOwn(PROTOCOL_VALUES, key) && PROTOCOL_VALUES[key]?.has(value) === true;
135
215
  }
136
216
  export function redactResolvedReferenceData(value) {
137
- if (resolvedValues.size === 0)
217
+ if (registry.size === 0)
138
218
  return value;
139
219
  const copies = new WeakMap();
140
220
  const pending = [];
@@ -16,6 +16,16 @@ export type ContextFiles = ContextFile[] & {
16
16
  export interface ContextFileLimits {
17
17
  maxBytesPerFile?: number;
18
18
  maxTotalBytes?: number;
19
+ /**
20
+ * The global (user-scope) context file to load instead of the agent-dir probe (senpi#5567).
21
+ * Honored from the user's own settings only (`SettingsManager.getGlobalContextFilePath`); a
22
+ * project's settings never set it. A leading `~` is expanded; a relative path resolves against
23
+ * the agent dir, like other paths in global settings; a directory runs the same candidate probe
24
+ * as the agent dir. Unset or blank keeps the agent-dir file. A path that is set but missing,
25
+ * unreadable, a special file, or a directory with no candidate falls back to the agent-dir probe
26
+ * and reports one warning diagnostic instead of erroring.
27
+ */
28
+ globalPath?: string;
19
29
  }
20
30
  export declare const DEFAULT_CONTEXT_FILE_MAX_BYTES: number;
21
31
  export declare const DEFAULT_CONTEXT_FILES_MAX_TOTAL_BYTES: number;
@@ -29,6 +29,47 @@ function loadContextFileFromDir(dir) {
29
29
  }
30
30
  return null;
31
31
  }
32
+ /**
33
+ * The global user-scope context file (senpi#5567). `contextFiles.globalPath` names a file (or a
34
+ * directory to probe for the usual candidates) to load in place of the agent-dir file. Precedence
35
+ * is defined and never errors: a blank or unset path keeps the agent-dir probe silently; a
36
+ * configured target that is missing, unreadable, a special file, or a directory with no candidates
37
+ * falls back to the agent-dir probe and returns one warning naming the configured path.
38
+ */
39
+ function loadGlobalContextFile(agentDir, globalPath) {
40
+ const explicit = typeof globalPath === "string" ? globalPath.trim() : "";
41
+ if (explicit === "")
42
+ return { file: loadContextFileFromDir(agentDir) };
43
+ const resolved = resolvePath(explicit, agentDir);
44
+ const fallBack = (problem) => ({
45
+ file: loadContextFileFromDir(agentDir),
46
+ diagnostic: {
47
+ type: "warning",
48
+ path: resolved,
49
+ message: `contextFiles.globalPath ${problem}; using the global context file from ${agentDir} instead.`,
50
+ },
51
+ });
52
+ let stats;
53
+ try {
54
+ stats = statSync(resolved);
55
+ }
56
+ catch (error) {
57
+ const code = error.code;
58
+ return fallBack(code === "ENOENT" || code === "ENOTDIR" ? "does not exist" : `could not be read (${String(error)})`);
59
+ }
60
+ if (stats.isDirectory()) {
61
+ const file = loadContextFileFromDir(resolved);
62
+ return file ? { file } : fallBack("is a directory with no AGENTS.md or CLAUDE.md");
63
+ }
64
+ if (!stats.isFile())
65
+ return fallBack("is not a regular file or directory");
66
+ try {
67
+ return { file: { path: resolved, content: stripBom(readFileSync(resolved, "utf-8")) } };
68
+ }
69
+ catch (error) {
70
+ return fallBack(`could not be read (${String(error)})`);
71
+ }
72
+ }
32
73
  /**
33
74
  * The main repo's context file that a nested linked worktree's own copy shadows: both
34
75
  * occupy the same logical repository scope, so loading both applies that context twice. Returns
@@ -142,21 +183,22 @@ function applyContextFileLimits(files, limits) {
142
183
  }
143
184
  return Object.assign(kept, { diagnostics });
144
185
  }
145
- /** Compares directories by their real path, ignoring case where the filesystem does (Windows). */
146
- function directoryKey(dir) {
147
- const real = canonicalizePath(resolvePath(dir));
186
+ /** Compares files and directories by their real path, ignoring case where the filesystem does (Windows). */
187
+ function pathKey(path) {
188
+ const real = canonicalizePath(resolvePath(path));
148
189
  return process.platform === "win32" ? real.toLowerCase() : real;
149
190
  }
150
191
  export function loadProjectContextFiles(options) {
151
192
  const resolvedCwd = resolvePath(options.cwd);
152
193
  const resolvedAgentDir = resolvePath(options.agentDir);
153
- const homeKey = directoryKey(options.homeDir ?? homedir());
194
+ const homeKey = pathKey(options.homeDir ?? homedir());
154
195
  const contextFiles = [];
196
+ // Real-path keys: a symlinked ancestor file that points at an already loaded file is the same file.
155
197
  const seenPaths = new Set();
156
- const globalContext = loadContextFileFromDir(resolvedAgentDir);
157
- if (globalContext) {
158
- contextFiles.push({ ...globalContext, scope: "user" });
159
- seenPaths.add(globalContext.path);
198
+ const globalContext = loadGlobalContextFile(resolvedAgentDir, options.globalPath);
199
+ if (globalContext.file) {
200
+ contextFiles.push({ ...globalContext.file, scope: "user" });
201
+ seenPaths.add(pathKey(globalContext.file.path));
160
202
  }
161
203
  const ancestorContextFiles = [];
162
204
  const shadowedContextFile = findShadowedContextFile(resolvedCwd);
@@ -164,9 +206,10 @@ export function loadProjectContextFiles(options) {
164
206
  while (true) {
165
207
  const contextFile = loadContextFileFromDir(currentDir);
166
208
  const isShadowed = shadowedContextFile !== undefined && canonicalizePath(contextFile?.path ?? "") === shadowedContextFile;
167
- if (contextFile && !isShadowed && !seenPaths.has(contextFile.path)) {
168
- ancestorContextFiles.unshift(directoryKey(currentDir) === homeKey ? { ...contextFile, scope: "user" } : contextFile);
169
- seenPaths.add(contextFile.path);
209
+ const contextFileKey = contextFile ? pathKey(contextFile.path) : undefined;
210
+ if (contextFile && contextFileKey !== undefined && !isShadowed && !seenPaths.has(contextFileKey)) {
211
+ ancestorContextFiles.unshift(pathKey(currentDir) === homeKey ? { ...contextFile, scope: "user" } : contextFile);
212
+ seenPaths.add(contextFileKey);
170
213
  }
171
214
  const parentDir = dirname(currentDir);
172
215
  if (parentDir === currentDir)
@@ -174,6 +217,9 @@ export function loadProjectContextFiles(options) {
174
217
  currentDir = parentDir;
175
218
  }
176
219
  contextFiles.push(...ancestorContextFiles);
177
- return applyContextFileLimits(contextFiles, options);
220
+ const limited = applyContextFileLimits(contextFiles, options);
221
+ if (globalContext.diagnostic)
222
+ limited.diagnostics.unshift(globalContext.diagnostic);
223
+ return limited;
178
224
  }
179
225
  //# sourceMappingURL=context-files.js.map
@@ -168,11 +168,24 @@ export interface HookModelRequest {
168
168
  readonly claim: SignedPlacementClaim;
169
169
  readonly input: HookModelRequestInput;
170
170
  }
171
+ /** A user hook's failure on the hand: reported with the reply, as a fused session reports it, never thrown. */
172
+ export interface HookFailure {
173
+ readonly extensionPath: string;
174
+ readonly event: string;
175
+ readonly error: string;
176
+ }
171
177
  export interface HookModelRequestReply {
172
178
  readonly t: "hook.model_request.reply";
173
179
  readonly sessionId: string;
174
180
  readonly stepId: string;
175
181
  readonly output: HookModelRequestInput;
182
+ /** The hooks that failed; `output` is what the others produced. Absent: none failed. */
183
+ readonly errors?: readonly HookFailure[];
184
+ /** The messages and notices the hooks sent during the call, as on every loop-hook reply (S33). Absent: none. */
185
+ readonly sent?: readonly WireJson[];
186
+ readonly notices?: readonly WireJson[];
187
+ /** Late messages and notices the hand dropped past its per-session bound. Absent: none. */
188
+ readonly droppedLate?: number;
176
189
  }
177
190
  export interface HandSessionAdvertisement {
178
191
  readonly t: "hand.session.advertise";
@@ -7,8 +7,12 @@ export function groupChangedPaths(paths, targets, rearmedContainers) {
7
7
  if (!targetMatchesPath(activeTarget.target, path))
8
8
  continue;
9
9
  matched = true;
10
- // The project container only rearms discovery; newly discovered config files carry the change.
11
- if (activeTarget.target.id === "builtin-project-presence" && rearmedContainers.includes(resolve(path)))
10
+ // Discovery containers only rearm their watches; discovered files carry the change.
11
+ // An extensions directory may contain only runtime state (e.g. a park's goal/no-session).
12
+ if (rearmedContainers.includes(resolve(path)) &&
13
+ (activeTarget.target.id === "builtin-project-presence" ||
14
+ activeTarget.target.id === "builtin-global-extensions" ||
15
+ activeTarget.target.id === "builtin-project-extensions"))
12
16
  continue;
13
17
  const group = groups.get(activeTarget.registrationId) ?? [];
14
18
  if (!group.includes(path))
@@ -1,15 +1,22 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
+ import { Type } from "typebox";
4
+ import { Check } from "typebox/value";
3
5
  import { keyedIdentity } from "../config-identity-key.js";
4
6
  import { resolveAuthMode } from "./context.js";
7
+ import { oauthGrantIdentity } from "./oauth-grant.js";
5
8
  import { McpTokenStore } from "./token-store.js";
6
9
  /** Local snapshot only: never migrates credentials, acquires a lock, or refreshes authentication. */
7
10
  export function mcpCredentialIdentity(config, serverName, agentDir, env) {
8
11
  const material = credentialMaterial(config, serverName, agentDir, env);
9
- return material === undefined || material === "none" ? material : keyedIdentity(agentDir, material);
12
+ return material === undefined || material === "none" || resolveAuthMode(config) === "oauth"
13
+ ? material
14
+ : keyedIdentity(agentDir, material);
10
15
  }
11
16
  /** The unkeyed identity earlier builds persisted; read only to adopt a cache entry for the same credential. */
12
17
  export function legacyMcpCredentialIdentity(config, serverName, agentDir, env) {
18
+ if (resolveAuthMode(config) === "oauth")
19
+ return undefined;
13
20
  const material = credentialMaterial(config, serverName, agentDir, env);
14
21
  return material === undefined || material === "none"
15
22
  ? material
@@ -31,14 +38,15 @@ function credentialMaterial(config, serverName, agentDir, env) {
31
38
  try {
32
39
  const store = new McpTokenStore({ serverName, serverUrl: config.url ?? "", agentDir });
33
40
  const value = JSON.parse(readFileSync(store.tokensPath, "utf8"));
34
- if (typeof value !== "object" || value === null || !("accessToken" in value))
35
- return undefined;
36
- if (typeof value.accessToken !== "string" || value.accessToken.length === 0)
37
- return undefined;
38
- const refreshToken = "refreshToken" in value ? value.refreshToken : undefined;
39
- if (refreshToken !== undefined && typeof refreshToken !== "string")
41
+ if (!Check(storedGrantSchema, value))
40
42
  return undefined;
41
- return JSON.stringify(["mcp-catalog-oauth", value.accessToken, refreshToken]);
43
+ return oauthGrantIdentity(value, {
44
+ serverName,
45
+ serverUrl: store.serverUrl,
46
+ agentDir: store.agentDir,
47
+ clientId: config.oauth?.clientId,
48
+ clientMetadataUrl: config.oauth?.clientMetadataUrl,
49
+ });
42
50
  }
43
51
  catch (error) {
44
52
  if (error instanceof Error)
@@ -46,4 +54,20 @@ function credentialMaterial(config, serverName, agentDir, env) {
46
54
  throw error;
47
55
  }
48
56
  }
57
+ // Parse exactly the stored fields the identity reader consumes; authorization is parsed by its proof owner.
58
+ const storedGrantSchema = Type.Object({
59
+ accessToken: Type.String({ minLength: 1 }),
60
+ refreshToken: Type.Optional(Type.String()),
61
+ issuer: Type.Optional(Type.String()),
62
+ resource: Type.Optional(Type.String()),
63
+ scope: Type.Optional(Type.String()),
64
+ refreshScope: Type.Optional(Type.String()),
65
+ clientInfo: Type.Optional(Type.Object({
66
+ client_id: Type.String(),
67
+ client_secret: Type.Optional(Type.String()),
68
+ token_endpoint_auth_method: Type.Optional(Type.String()),
69
+ issuer: Type.Optional(Type.String()),
70
+ })),
71
+ discoveryState: Type.Optional(Type.Object({ authorizationServerUrl: Type.String() })),
72
+ });
49
73
  //# sourceMappingURL=catalog-identity.js.map