@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
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The request-hook contract of hosted model-key custody (row CK21; custody design 4.9, round-10 fold section 15).
3
+ *
4
+ * `HookRequestInput` is the serializable projection of the fused `before_provider_request` input: the provider payload
5
+ * and the effective request model, without the request headers or the model's own headers, which can carry a
6
+ * credential and never leave the side that dials. A step call has this input; an aux call has none. A hand serves
7
+ * `hook.model_request` by running its user's request hooks on the input, only after the request's placement claim
8
+ * verified, and replies with the payload they produced, with their failures and the messages and notices they sent.
9
+ */
10
+ import type { Api, Model } from "@earendil-works/pi-ai";
11
+ import type { ExecutionScope } from "@earendil-works/pi-agent-core";
12
+ import { type ClaimVerdict } from "../egress/placement-claim.ts";
13
+ import type { HookModelRequest, HookModelRequestReply, ProviderCallErrorCode, ProviderCallKind, WireJson } from "../egress/wire.ts";
14
+ import type { BeforeProviderRequestEvent } from "../extensions/types.ts";
15
+ /** The pinned shape of `HookModelRequestInput` (and of the reply's `output`). */
16
+ export type HookRequestInput = {
17
+ readonly payload: WireJson;
18
+ readonly model?: {
19
+ readonly [key: string]: WireJson;
20
+ };
21
+ };
22
+ /** The input or output cannot cross the wire: it is not JSON, or not the closed envelope. */
23
+ export declare class HookContractError extends Error {
24
+ readonly name = "HookContractError";
25
+ }
26
+ /** The request's placement claim did not verify, so no hook ran; `code`/`retryable` are the claim's wire answer. */
27
+ export declare class HookRequestRefusedError extends Error {
28
+ readonly name = "HookRequestRefusedError";
29
+ readonly verdict: Exclude<ClaimVerdict, "valid">;
30
+ readonly code: ProviderCallErrorCode;
31
+ readonly retryable: boolean;
32
+ constructor(verdict: Exclude<ClaimVerdict, "valid">);
33
+ }
34
+ /** The projection of a fused `before_provider_request` input. */
35
+ export declare function hookRequestInputOf(event: Omit<BeforeProviderRequestEvent, "type"> & {
36
+ type?: string;
37
+ }): HookRequestInput;
38
+ /** A step call's hook input; an aux call runs no request hooks on the hand and has none. */
39
+ export declare function hookRequestInputForCall(call: ProviderCallKind, event: Omit<BeforeProviderRequestEvent, "type"> & {
40
+ type?: string;
41
+ }): HookRequestInput | undefined;
42
+ /** The reply's `output`: the payload the hooks produced, with the model the request carried. */
43
+ export declare function hookRequestOutputOf(input: HookRequestInput, payload: unknown): HookRequestInput;
44
+ /** Reads a wire value back as the closed envelope `{ payload, model? }`; the model never carries headers. */
45
+ export declare function parseHookRequestInput(value: unknown): HookRequestInput;
46
+ /** The effective model a hand hands its hooks, as the loop sent it. */
47
+ export declare function hookModelOf(input: HookRequestInput): Model<Api> | undefined;
48
+ /** What the user's request hooks produced: the reply's `output`, their failures and what they sent. */
49
+ export type HookRequestHooksResult = Omit<HookModelRequestReply, "t" | "sessionId" | "stepId" | "output"> & {
50
+ readonly output: HookRequestInput;
51
+ };
52
+ export interface HookServePorts {
53
+ /** The hand's placement-claim check (CK7's verifier with the hand's trust and clock). */
54
+ verifyClaim(claim: unknown): Promise<ClaimVerdict>;
55
+ /** Runs the user's request hooks of `sessionId` on `input` and returns what they produced. */
56
+ runRequestHooks(sessionId: string, input: HookRequestInput): Promise<HookRequestHooksResult>;
57
+ }
58
+ /** Serves one `hook.model_request`: claim first, then the closed input, then the hooks. */
59
+ export declare function serveHookModelRequest(request: HookModelRequest, ports: HookServePorts): Promise<HookModelRequestReply>;
60
+ /**
61
+ * `runRequestHooks` over a hand's loop-hook runner (`LooplessHand.loopHooks`): the session's hooks run on the runtime of
62
+ * `scope`, inside a loop-hook call bound to that session, with the loop busy (a model request is mid-turn). The hooks'
63
+ * failures and what they sent travel with the output, as they do on every loop-hook reply.
64
+ */
65
+ export declare function loopHookRequestHooks(loopHooks: (event: string, payload: WireJson) => Promise<unknown>, scope: ExecutionScope): HookServePorts["runRequestHooks"];
66
+ //# sourceMappingURL=hook-contract.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { CLAIM_VERDICT_ERRORS } from "../egress/placement-claim.js";
2
+ /** The input or output cannot cross the wire: it is not JSON, or not the closed envelope. */
3
+ export class HookContractError extends Error {
4
+ constructor() {
5
+ super(...arguments);
6
+ this.name = "HookContractError";
7
+ }
8
+ }
9
+ /** The request's placement claim did not verify, so no hook ran; `code`/`retryable` are the claim's wire answer. */
10
+ export class HookRequestRefusedError extends Error {
11
+ constructor(verdict) {
12
+ super(`hook.model_request refused: placement claim ${verdict}`);
13
+ this.name = "HookRequestRefusedError";
14
+ this.verdict = verdict;
15
+ this.code = CLAIM_VERDICT_ERRORS[verdict].code;
16
+ this.retryable = CLAIM_VERDICT_ERRORS[verdict].retryable;
17
+ }
18
+ }
19
+ const isPlainObject = (value) => {
20
+ if (typeof value !== "object" || value === null || Array.isArray(value))
21
+ return false;
22
+ const prototype = Object.getPrototypeOf(value);
23
+ return prototype === Object.prototype || prototype === null;
24
+ };
25
+ /** `value` as JSON, exactly; anything JSON would drop, coerce or reject is refused (an `undefined` member is absent). */
26
+ function wireJson(value, at) {
27
+ if (value === null || typeof value === "string" || typeof value === "boolean")
28
+ return value;
29
+ if (typeof value === "number") {
30
+ if (Number.isFinite(value))
31
+ return value;
32
+ throw new HookContractError(`${at} is not a finite number`);
33
+ }
34
+ if (Array.isArray(value))
35
+ return value.map((item, index) => wireJson(item, `${at}[${index}]`));
36
+ if (isPlainObject(value)) {
37
+ const out = {};
38
+ for (const [key, member] of Object.entries(value))
39
+ if (member !== undefined)
40
+ out[key] = wireJson(member, `${at}.${key}`);
41
+ return out;
42
+ }
43
+ throw new HookContractError(`${at} is not JSON`);
44
+ }
45
+ function wireModel(model) {
46
+ const { headers: _headers, ...rest } = model;
47
+ return wireJson(rest, "model");
48
+ }
49
+ /** The projection of a fused `before_provider_request` input. */
50
+ export function hookRequestInputOf(event) {
51
+ const payload = wireJson(event.payload, "payload");
52
+ return event.model === undefined ? { payload } : { payload, model: wireModel(event.model) };
53
+ }
54
+ /** A step call's hook input; an aux call runs no request hooks on the hand and has none. */
55
+ export function hookRequestInputForCall(call, event) {
56
+ return call.kind === "step" ? hookRequestInputOf(event) : undefined;
57
+ }
58
+ /** The reply's `output`: the payload the hooks produced, with the model the request carried. */
59
+ export function hookRequestOutputOf(input, payload) {
60
+ const out = wireJson(payload, "payload");
61
+ return input.model === undefined ? { payload: out } : { payload: out, model: input.model };
62
+ }
63
+ const MODEL_STRING_FIELDS = ["id", "name", "api", "provider", "baseUrl"];
64
+ /** Reads a wire value back as the closed envelope `{ payload, model? }`; the model never carries headers. */
65
+ export function parseHookRequestInput(value) {
66
+ if (!isPlainObject(value) || !("payload" in value))
67
+ throw new HookContractError("hook input is not an object with a payload");
68
+ const extra = Object.keys(value).filter((key) => key !== "payload" && key !== "model");
69
+ if (extra.length > 0)
70
+ throw new HookContractError(`hook input has unknown members: ${extra.join(", ")}`);
71
+ const payload = wireJson(value.payload, "payload");
72
+ if (value.model === undefined)
73
+ return { payload };
74
+ const model = value.model;
75
+ if (!isPlainObject(model) || MODEL_STRING_FIELDS.some((field) => typeof model[field] !== "string"))
76
+ throw new HookContractError("hook input model is not a model");
77
+ if ("headers" in model)
78
+ throw new HookContractError("hook input model carries headers");
79
+ return { payload, model: wireJson(model, "model") };
80
+ }
81
+ /** The effective model a hand hands its hooks, as the loop sent it. */
82
+ export function hookModelOf(input) {
83
+ return input.model;
84
+ }
85
+ /** Serves one `hook.model_request`: claim first, then the closed input, then the hooks. */
86
+ export async function serveHookModelRequest(request, ports) {
87
+ const verdict = await ports.verifyClaim(request.claim);
88
+ if (verdict !== "valid")
89
+ throw new HookRequestRefusedError(verdict);
90
+ const input = parseHookRequestInput(request.input);
91
+ const produced = await ports.runRequestHooks(request.sessionId, input);
92
+ return { t: "hook.model_request.reply", sessionId: request.sessionId, stepId: request.stepId, ...produced };
93
+ }
94
+ const isHookFailure = (value) => isPlainObject(value) &&
95
+ typeof value.extensionPath === "string" &&
96
+ typeof value.event === "string" &&
97
+ typeof value.error === "string";
98
+ function hookFailures(value) {
99
+ if (!Array.isArray(value) || !value.every(isHookFailure))
100
+ throw new HookContractError("the hand's request-hook reply has malformed errors");
101
+ return value.map(({ extensionPath, event, error }) => ({ extensionPath, event, error }));
102
+ }
103
+ function wireList(value, at) {
104
+ if (!Array.isArray(value))
105
+ throw new HookContractError(`the hand's request-hook reply has malformed ${at}`);
106
+ return value.map((item, index) => wireJson(item, `${at}[${index}]`));
107
+ }
108
+ function droppedCount(value) {
109
+ if (typeof value === "number" && Number.isSafeInteger(value) && value > 0)
110
+ return value;
111
+ throw new HookContractError("the hand's request-hook reply has a malformed droppedLate");
112
+ }
113
+ /**
114
+ * `runRequestHooks` over a hand's loop-hook runner (`LooplessHand.loopHooks`): the session's hooks run on the runtime of
115
+ * `scope`, inside a loop-hook call bound to that session, with the loop busy (a model request is mid-turn). The hooks'
116
+ * failures and what they sent travel with the output, as they do on every loop-hook reply.
117
+ */
118
+ export function loopHookRequestHooks(loopHooks, scope) {
119
+ return async (sessionId, input) => {
120
+ const reply = await loopHooks("before_provider_request", {
121
+ scope: { id: scope.id, root: scope.root },
122
+ sessionId,
123
+ idle: false,
124
+ input,
125
+ });
126
+ if (!isPlainObject(reply))
127
+ throw new HookContractError("the hand's request-hook reply is not an object");
128
+ const { errors, sent, notices, droppedLate } = reply;
129
+ return {
130
+ output: parseHookRequestInput(reply.output),
131
+ ...(errors === undefined ? {} : { errors: hookFailures(errors) }),
132
+ ...(sent === undefined ? {} : { sent: wireList(sent, "sent") }),
133
+ ...(notices === undefined ? {} : { notices: wireList(notices, "notices") }),
134
+ ...(droppedLate === undefined ? {} : { droppedLate: droppedCount(droppedLate) }),
135
+ };
136
+ };
137
+ }
138
+ //# sourceMappingURL=hook-contract.js.map
@@ -1,6 +1,8 @@
1
1
  export interface OpenTimer {
2
2
  mark(phase: string): void;
3
3
  }
4
+ /** Speculative builds have no caller to charge; drop their timing rather than report a misleading open. */
5
+ export declare function withoutOpenTiming<T>(build: () => T): T;
4
6
  export declare function openTimingEnabled(): boolean;
5
7
  export declare function startOpenTimer(scope: string): OpenTimer;
6
8
  //# sourceMappingURL=open-timing.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { envValue } from "./brand.js";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  /**
3
4
  * Per-phase timing of a session open (senpi#5707), for naming the await that holds a slow open.
4
5
  * Enable with SENPI_OPEN_TIMING=1. Off, every call returns before reading a clock or touching
@@ -11,11 +12,16 @@ import { envValue } from "./brand.js";
11
12
  */
12
13
  const ENABLED = envValue("OPEN_TIMING") === "1";
13
14
  const DISABLED = { mark: () => { } };
15
+ const speculative = new AsyncLocalStorage();
16
+ /** Speculative builds have no caller to charge; drop their timing rather than report a misleading open. */
17
+ export function withoutOpenTiming(build) {
18
+ return speculative.run(true, build);
19
+ }
14
20
  export function openTimingEnabled() {
15
21
  return ENABLED;
16
22
  }
17
23
  export function startOpenTimer(scope) {
18
- if (!ENABLED)
24
+ if (!ENABLED || speculative.getStore())
19
25
  return DISABLED;
20
26
  let last = performance.now();
21
27
  return {
@@ -569,6 +569,8 @@ export class DefaultResourceLoader {
569
569
  cwd: this.cwd,
570
570
  agentDir: this.agentDir,
571
571
  ...this.settingsManager.getSettings().contextFiles,
572
+ // senpi#5567: user settings only; a project's settings never redirect the global file.
573
+ globalPath: this.settingsManager.getGlobalContextFilePath(),
572
574
  }),
573
575
  };
574
576
  const resolvedAgentsFiles = this.agentsFilesOverride ? this.agentsFilesOverride(agentsFiles) : agentsFiles;
@@ -6,5 +6,11 @@ export interface FallbackLogger {
6
6
  export interface FallbackLoggerOptions {
7
7
  maxBytes?: number;
8
8
  }
9
- export declare function createFallbackLogger(agentDir: string, options?: FallbackLoggerOptions): FallbackLogger;
9
+ export interface SettledFallbackLogger extends FallbackLogger {
10
+ /** Resolves once the one-time removal of earlier log generations has finished and every line logged before it was written. */
11
+ settled(): Promise<void>;
12
+ }
13
+ /** Resolves once every fallback logger created so far has finished its first-start removal and flushed its queue. */
14
+ export declare function settledFallbackLoggers(): Promise<void>;
15
+ export declare function createFallbackLogger(agentDir: string, options?: FallbackLoggerOptions): SettledFallbackLogger;
10
16
  //# sourceMappingURL=log.d.ts.map
@@ -1,19 +1,81 @@
1
1
  import { closeSync, fchmodSync, mkdirSync, openSync, writeSync } from "node:fs";
2
+ import { access, mkdir, rm, writeFile } from "node:fs/promises";
2
3
  import { dirname, join } from "node:path";
3
4
  import { rotateLogIfNeeded } from "../log-file-rotation.js";
4
- import { redactSecrets } from "../sensitive-output.js";
5
+ import { redactSecrets, redactSensitiveTokenValues } from "../sensitive-output.js";
5
6
  const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
6
7
  const MAX_STRING_LENGTH = 200;
8
+ const MAX_QUEUED_LINES = 256;
7
9
  const BLOCKED_KEY = /^(?:__proto__|constructor|prototype|headers?|env(?:ironment)?|authorization|credential(?:s)?|password|secret|token|api_?key|client_?secret)$/i;
8
10
  const ALLOWED_DATA_KEY = /^(?:selector|candidate|currentSelector|originalSelector|from|to|model|provider|chainKey|reason|skipReason|classification|thinkingLevel|durationMs|retryAfterMs|error|errorMessage|warning|trigger)$/;
9
11
  const SENSITIVE_TEXT = /((?:authorization\s*[:=]\s*(?:bearer|basic)\s+)|(?:bearer\s+)|(?:[?&](?:api[_-]?key|token|secret|password|auth(?:orization)?)=))[^\s&,"'}\]]+/gi;
12
+ const unsettledLoggers = new Set();
13
+ /** Resolves once every fallback logger created so far has finished its first-start removal and flushed its queue. */
14
+ export async function settledFallbackLoggers() {
15
+ await Promise.all([...unsettledLoggers]);
16
+ }
17
+ function errorCode(failure) {
18
+ if (typeof failure === "object" && failure !== null && "code" in failure && typeof failure.code === "string") {
19
+ return failure.code;
20
+ }
21
+ return "unknown";
22
+ }
10
23
  export function createFallbackLogger(agentDir, options = {}) {
11
24
  const filePath = join(agentDir, "logs", "fallback.log");
12
25
  const maxBytes = validMaxBytes(options.maxBytes);
13
26
  let reportedWriteFailure = false;
27
+ // Lines logged while the earlier generations are still being removed wait here, so the removal can never delete them.
28
+ // They are redacted before they enter the queue and the queue is bounded: the newest lines win, the drops are counted.
29
+ let queued = [];
30
+ let dropped = 0;
31
+ function write(line) {
32
+ try {
33
+ writeLine(filePath, line, maxBytes);
34
+ }
35
+ catch (error) {
36
+ if (!reportedWriteFailure) {
37
+ reportedWriteFailure = true;
38
+ console.error("Unable to write retry fallback debug log", error);
39
+ }
40
+ }
41
+ }
42
+ const settled = retireUnsharedRegistryGenerations(filePath)
43
+ .then(() => undefined)
44
+ .catch((error) => {
45
+ reportedWriteFailure = true;
46
+ console.error("Unable to remove earlier retry fallback debug log generations", error);
47
+ return error;
48
+ })
49
+ .then((failure) => {
50
+ const lines = queued ?? [];
51
+ queued = undefined;
52
+ for (const line of lines)
53
+ write(line);
54
+ if (dropped > 0) {
55
+ write(formatLine("warn", "queued_lines_dropped", {
56
+ reason: `${dropped} lines dropped while earlier log generations were removed`,
57
+ }));
58
+ }
59
+ if (failure !== undefined) {
60
+ // The code only: the error text names the agent dir's absolute path.
61
+ write(formatLine("warn", "retire_generations_failed", {
62
+ errorMessage: `removal failed: ${errorCode(failure)}`,
63
+ }));
64
+ }
65
+ });
66
+ unsettledLoggers.add(settled);
67
+ void settled.then(() => unsettledLoggers.delete(settled));
14
68
  function log(level, event, data) {
15
69
  try {
16
70
  const line = formatLine(level, event, data);
71
+ if (queued !== undefined) {
72
+ queued.push(redactSensitiveTokenValues(line, "[redacted]"));
73
+ if (queued.length > MAX_QUEUED_LINES) {
74
+ queued.shift();
75
+ dropped += 1;
76
+ }
77
+ return;
78
+ }
17
79
  writeLine(filePath, line, maxBytes);
18
80
  }
19
81
  catch (error) {
@@ -27,8 +89,23 @@ export function createFallbackLogger(agentDir, options = {}) {
27
89
  debug: (event, data) => log("debug", event, data),
28
90
  info: (event, data) => log("info", event, data),
29
91
  warn: (event, data) => log("warn", event, data),
92
+ settled: () => settled,
30
93
  };
31
94
  }
95
+ /**
96
+ * Generations written before every engine copy in a process shared one resolved-value registry (omo#12965) are
97
+ * removed once per agent dir, the first time an engine with the shared registry starts on it; the marker makes every
98
+ * later start leave the logs alone.
99
+ */
100
+ async function retireUnsharedRegistryGenerations(filePath) {
101
+ const marker = join(dirname(filePath), ".fallback-log-v2");
102
+ if (await access(marker).then(() => true, () => false))
103
+ return;
104
+ await rm(`${filePath}.1`, { force: true });
105
+ await rm(filePath, { force: true });
106
+ await mkdir(dirname(filePath), { recursive: true, mode: 0o700 });
107
+ await writeFile(marker, "", { mode: 0o600 });
108
+ }
32
109
  function validMaxBytes(value) {
33
110
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_MAX_BYTES;
34
111
  }
@@ -51,7 +128,7 @@ function formatLine(level, event, data) {
51
128
  }
52
129
  function writeLine(filePath, line, maxBytes) {
53
130
  mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
54
- const text = `${line}\n`;
131
+ const text = `${redactSensitiveTokenValues(line, "[redacted]")}\n`;
55
132
  rotateLogIfNeeded(filePath, Buffer.byteLength(text), maxBytes);
56
133
  const descriptor = openSync(filePath, "a", 0o600);
57
134
  try {
@@ -66,13 +143,13 @@ function serializeValue(value, seen) {
66
143
  if (typeof value === "string")
67
144
  return safeText(value);
68
145
  if (typeof value === "bigint")
69
- return value.toString();
146
+ return safeText(value.toString());
70
147
  if (typeof value === "number" || typeof value === "boolean" || value === null)
71
148
  return value;
72
149
  if (typeof value === "undefined")
73
150
  return undefined;
74
151
  if (typeof value === "function" || typeof value === "symbol")
75
- return String(value);
152
+ return safeText(String(value));
76
153
  if (seen.has(value))
77
154
  return "[Circular]";
78
155
  seen.add(value);
@@ -83,7 +160,7 @@ function serializeValue(value, seen) {
83
160
  if (!BLOCKED_KEY.test(key)) {
84
161
  const safeValue = serializeValue(item, seen);
85
162
  if (safeValue !== undefined)
86
- result[key] = safeValue;
163
+ result[safeText(key)] = safeValue;
87
164
  }
88
165
  }
89
166
  return result;
@@ -1,3 +1,4 @@
1
+ import { redactSecrets } from "../sensitive-output.js";
1
2
  const EXCERPT_LIMIT = 500;
2
3
  /**
3
4
  * A model configuration error is never retried or fallen back: another model would answer a request the caller did
@@ -16,7 +17,7 @@ export function createModelConfigErrorReporter(logger, emit) {
16
17
  if (reported.has(key))
17
18
  return;
18
19
  reported.add(key);
19
- const errorMessage = message.errorMessage ?? "";
20
+ const errorMessage = redactSecrets(message.errorMessage ?? "");
20
21
  logger.warn("model_config_error", { model, provider: message.provider, errorMessage });
21
22
  emit({
22
23
  type: "model_config_error",
@@ -440,6 +440,8 @@ export declare class SessionManager {
440
440
  private _transcriptEntryBytes;
441
441
  private _serializedFileEntries;
442
442
  isPersisted(): boolean;
443
+ /** Give an unclaimed in-memory session its first file without rebuilding or changing its identity. */
444
+ enablePersistence(): Promise<void>;
443
445
  getCwd(): string;
444
446
  getSessionDir(): string;
445
447
  usesDefaultSessionDir(): boolean;
@@ -1,7 +1,7 @@
1
1
  import { getCurrentSystemMessage, normalizeProviderId } from "@earendil-works/pi-ai";
2
2
  import { randomBytes, randomUUID } from "crypto";
3
3
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readSync, rmSync, statSync, writeFileSync, } from "fs";
4
- import { appendFile, open, readdir, rm } from "fs/promises";
4
+ import { appendFile, mkdir, open, readdir, rm } from "fs/promises";
5
5
  import { basename, join, resolve } from "path";
6
6
  import { StringDecoder } from "string_decoder";
7
7
  import { APP_NAME, getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
@@ -1230,6 +1230,20 @@ export class SessionManager {
1230
1230
  isPersisted() {
1231
1231
  return this.persist;
1232
1232
  }
1233
+ /** Give an unclaimed in-memory session its first file without rebuilding or changing its identity. */
1234
+ async enablePersistence() {
1235
+ if (this.persist)
1236
+ return;
1237
+ const dir = getDefaultSessionDirPath(this.cwd);
1238
+ await mkdir(dir, { recursive: true });
1239
+ const header = this.fileEntries[0];
1240
+ const path = join(dir, `${header.timestamp.replace(/[:.]/g, "-")}_${this.sessionId}.jsonl`);
1241
+ reserveSessionWrite(path);
1242
+ this.sessionDir = dir;
1243
+ this.sessionFile = path;
1244
+ this.persist = true;
1245
+ registerSessionWriter(this);
1246
+ }
1233
1247
  getCwd() {
1234
1248
  return this.cwd;
1235
1249
  }
@@ -302,6 +302,12 @@ export declare class SettingsManager {
302
302
  getSettings(): Settings;
303
303
  getGlobalSettings(): Settings;
304
304
  getProjectSettings(): Settings;
305
+ /**
306
+ * `contextFiles.globalPath` from the user's own layers only: session overrides, then global
307
+ * settings. A project's `.senpi/settings.json` never redirects the user's global context file
308
+ * (senpi#5567), so the project layer is deliberately skipped.
309
+ */
310
+ getGlobalContextFilePath(): string | undefined;
305
311
  /** The project settings file at `path` as stored: parsed and migrated like a load, but never gated by trust. */
306
312
  readProjectSettingsFile(path: string): ProjectSettingsFile;
307
313
  /**
@@ -697,6 +697,14 @@ export class SettingsManager {
697
697
  getProjectSettings() {
698
698
  return structuredClone(this.projectSettings);
699
699
  }
700
+ /**
701
+ * `contextFiles.globalPath` from the user's own layers only: session overrides, then global
702
+ * settings. A project's `.senpi/settings.json` never redirects the user's global context file
703
+ * (senpi#5567), so the project layer is deliberately skipped.
704
+ */
705
+ getGlobalContextFilePath() {
706
+ return this.runtimeOverrides.contextFiles?.globalPath ?? this.globalSettings.contextFiles?.globalPath;
707
+ }
700
708
  /** The project settings file at `path` as stored: parsed and migrated like a load, but never gated by trust. */
701
709
  readProjectSettingsFile(path) {
702
710
  let content;
@@ -1,17 +1,15 @@
1
- import type { SessionManager } from "./session-manager.ts";
2
1
  /**
3
- * Who started a turn, for the delegated-default permission rule (remote approval-mode work):
4
- * - `local`: a local connection's or the TUI's own input;
5
- * - `delegated`: input that arrived on a delegated (remote device) connection;
6
- * - `peer`: an agent's relayed delivery on a delegated connection the forwarder granted `peer_agent` (senpi#6305): it
7
- * runs under the receiving session's own preset, as the session's own work does;
2
+ * Who started a turn, for the remote approval-mode rule (senpi#6342: a turn follows the permission setting of the machine
3
+ * it runs on, and no origin outlives the turn it started):
4
+ * - `local`: this machine's own input, or input from a device the forwarder stated is the same account;
5
+ * - `delegated`: input from a device the forwarder did not state is the same account, or the one run such a device's
6
+ * record started;
8
7
  * - `engine`: a turn no client record started (an extension's message, a goal or loop turn, restored input).
9
8
  * A steer joins the running turn (`join`) and never changes its origin.
10
9
  */
11
- export type TurnOrigin = "local" | "delegated" | "peer" | "engine";
10
+ export type TurnOrigin = "local" | "delegated" | "engine";
12
11
  export type InputTurnMark = TurnOrigin | "join";
13
12
  export type ConnectionOrigin = "local" | "delegated";
14
- type OriginSessionManager = Pick<SessionManager, "getEntries" | "appendCustomEntry">;
15
13
  /** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
16
14
  export declare function inputTurnMark(input: {
17
15
  readonly mode?: "steer" | "followUp";
@@ -23,11 +21,19 @@ export declare function markInputTurnOrigin(message: object, mark: InputTurnMark
23
21
  export declare function copyInputTurnMark(from: object, to: object): void;
24
22
  /** Whether a message carries an origin mark: a user message always opens a turn's origin, any other only when marked. */
25
23
  export declare function hasInputTurnMark(message: object): boolean;
26
- /** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
24
+ /** Runs `handle`, and every async task it starts, as the record `origin` describes. */
25
+ export declare function runInRecordScope<T>(origin: ConnectionOrigin, handle: () => Promise<T>): Promise<T>;
26
+ /** Whether the work being created belongs to an open cross-account record's own handling. */
27
+ export declare function recordIsDelegated(): boolean;
28
+ /**
29
+ * A run starts as engine work, or as delegated work when a cross-account record's own handling starts it; a retry keeps
30
+ * the turn it continues. The user message that opens a turn sets its origin, a steer keeps it.
31
+ */
27
32
  export declare function noteRunStart(sessionManager: object): void;
33
+ /** A retry or recovery run repeats the turn it continues, so it keeps that turn's origin until the continuation ends. */
34
+ export declare function noteTurnContinuation(sessionManager: object): void;
35
+ export declare function endTurnContinuation(sessionManager: object): void;
28
36
  export declare function noteUserMessageStart(sessionManager: object, message: object): void;
29
- export declare function recordDelegatedOrigin(sessionManager: OriginSessionManager): void;
30
- /** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
31
- export declare function decidingTurnIsDelegated(sessionManager: Pick<SessionManager, "getEntries">): boolean;
32
- export {};
37
+ /** Whether the deciding turn counts as delegated-origin: a cross-account device started this turn or this run. */
38
+ export declare function decidingTurnIsDelegated(sessionManager: object): boolean;
33
39
  //# sourceMappingURL=turn-origin.d.ts.map
@@ -1,13 +1,16 @@
1
- const DELEGATED_ORIGIN_ENTRY = "delegated_origin";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  const inputMarks = new WeakMap();
3
3
  const currentOrigins = new WeakMap();
4
- const delegatedSessions = new WeakSet();
4
+ const recordScopes = new AsyncLocalStorage();
5
+ const continuingTurns = new WeakSet();
5
6
  /** The mark an input gets where it is created: explicit for client records, `local` for the TUI, else `engine`. */
6
7
  export function inputTurnMark(input) {
7
8
  if (input.mode === "steer")
8
9
  return "join";
9
10
  if (input.connectionOrigin !== undefined)
10
11
  return input.connectionOrigin;
12
+ if (recordIsDelegated())
13
+ return "delegated";
11
14
  return input.source === undefined || input.source === "interactive" ? "local" : "engine";
12
15
  }
13
16
  export function markInputTurnOrigin(message, mark) {
@@ -23,9 +26,34 @@ export function copyInputTurnMark(from, to) {
23
26
  export function hasInputTurnMark(message) {
24
27
  return inputMarks.has(message);
25
28
  }
26
- /** A run starts as engine work; the user message that opens a turn sets its origin, a steer keeps it. */
29
+ /** Runs `handle`, and every async task it starts, as the record `origin` describes. */
30
+ export function runInRecordScope(origin, handle) {
31
+ return recordScopes.run({ origin, open: true }, handle);
32
+ }
33
+ /** Whether the work being created belongs to an open cross-account record's own handling. */
34
+ export function recordIsDelegated() {
35
+ const scope = recordScopes.getStore();
36
+ return scope?.open === true && scope.origin === "delegated";
37
+ }
38
+ /**
39
+ * A run starts as engine work, or as delegated work when a cross-account record's own handling starts it; a retry keeps
40
+ * the turn it continues. The user message that opens a turn sets its origin, a steer keeps it.
41
+ */
27
42
  export function noteRunStart(sessionManager) {
28
- currentOrigins.set(sessionManager, "engine");
43
+ if (continuingTurns.has(sessionManager))
44
+ return;
45
+ const delegated = recordIsDelegated();
46
+ const scope = recordScopes.getStore();
47
+ if (scope !== undefined)
48
+ scope.open = false;
49
+ currentOrigins.set(sessionManager, delegated ? "delegated" : "engine");
50
+ }
51
+ /** A retry or recovery run repeats the turn it continues, so it keeps that turn's origin until the continuation ends. */
52
+ export function noteTurnContinuation(sessionManager) {
53
+ continuingTurns.add(sessionManager);
54
+ }
55
+ export function endTurnContinuation(sessionManager) {
56
+ continuingTurns.delete(sessionManager);
29
57
  }
30
58
  export function noteUserMessageStart(sessionManager, message) {
31
59
  const mark = inputMarks.get(message);
@@ -33,32 +61,8 @@ export function noteUserMessageStart(sessionManager, message) {
33
61
  return;
34
62
  currentOrigins.set(sessionManager, mark ?? "engine");
35
63
  }
36
- export function recordDelegatedOrigin(sessionManager) {
37
- if (delegatedOriginOf(sessionManager))
38
- return;
39
- delegatedSessions.add(sessionManager);
40
- sessionManager.appendCustomEntry(DELEGATED_ORIGIN_ENTRY, { delegatedOrigin: true }, { startupBookkeeping: true });
41
- }
42
- // How many entries a session's scan has already found free of the marker: each gated decision reads only new entries.
43
- const scannedEntries = new WeakMap();
44
- function delegatedOriginOf(sessionManager) {
45
- if (delegatedSessions.has(sessionManager))
46
- return true;
47
- const entries = sessionManager.getEntries();
48
- const persisted = entries
49
- .slice(scannedEntries.get(sessionManager) ?? 0)
50
- .some((entry) => entry.type === "custom" && entry.customType === DELEGATED_ORIGIN_ENTRY);
51
- if (persisted)
52
- delegatedSessions.add(sessionManager);
53
- else
54
- scannedEntries.set(sessionManager, entries.length);
55
- return persisted;
56
- }
57
- /** Whether the deciding turn counts as delegated-origin: a remote device started it, or nobody did on a remote-driven session. */
64
+ /** Whether the deciding turn counts as delegated-origin: a cross-account device started this turn or this run. */
58
65
  export function decidingTurnIsDelegated(sessionManager) {
59
- const origin = currentOrigins.get(sessionManager) ?? "engine";
60
- if (origin === "delegated")
61
- return true;
62
- return origin === "engine" && delegatedOriginOf(sessionManager);
66
+ return currentOrigins.get(sessionManager) === "delegated";
63
67
  }
64
68
  //# sourceMappingURL=turn-origin.js.map
@@ -4,5 +4,6 @@ export declare function serviceTierForProvider(provider: string | undefined, ser
4
4
  export declare function ultrafastSelectionWarning(model: {
5
5
  readonly provider: string;
6
6
  readonly id: string;
7
+ readonly upstreamModelId?: string;
7
8
  }, serviceTier: ServiceTier | undefined): string | undefined;
8
9
  //# sourceMappingURL=ultrafast-lanes.d.ts.map