@code-yeongyu/senpi 2026.11.6-2 → 2026.11.9-2

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 (244) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
  3. package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-3TZQXZKH.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-45MVD7BU.js} +1 -1
  6. package/dist/bundle/chunks/chunk-4JIBEZZE.js +106 -0
  7. package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-4UR6UFBH.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-527Q2UYM.js} +9 -8
  10. package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-622VYTRI.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-7CFORZPE.js} +221 -229
  12. package/dist/bundle/chunks/{chunk-GKAYPJ4E.js → chunk-7PROV6BB.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-4E7GDG4H.js → chunk-7VMEE62L.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-K3UHGDUQ.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-LHSWAJJL.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
  17. package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
  18. package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-M7GF73FO.js} +2 -2
  19. package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-NDSWTAYJ.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-Q6NR6644.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-SGAFIMAU.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-STGOYIXC.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
  24. package/dist/bundle/chunks/chunk-XZDCMDUH.js +2 -0
  25. package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-Y5L4UFMI.js} +2 -2
  26. package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-XBEFQW5A.js} +1 -1
  27. package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
  28. package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-QFEY3JMQ.js} +1 -1
  29. package/dist/bundle/chunks/{headless-host-ES74ZB6M.js → headless-host-YXUGONR3.js} +1 -1
  30. package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-VHX2ILLM.js} +1 -1
  31. package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-P3MRW63Z.js} +1 -1
  32. package/dist/bundle/chunks/interactive-mode-YGL3QHU6.js +2 -0
  33. package/dist/bundle/chunks/main-5SD4DHMC.js +2 -0
  34. package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-PGRHY3JP.js} +20 -17
  35. package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-SADLU7QX.js} +1 -1
  36. package/dist/bundle/chunks/{role-RRFFHZF5.js → role-4BUU46VW.js} +3 -3
  37. package/dist/bundle/chunks/{rotation-stream-BN2YK2MC.js → rotation-stream-TODBIHXJ.js} +1 -1
  38. package/dist/bundle/chunks/rpc-mode-27TC7W6K.js +2 -0
  39. package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-MXCC5IZR.js} +1 -1
  40. package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
  41. package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-MX56D6UE.js} +1 -1
  42. package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-IL6ERD4U.js} +1 -1
  43. package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-6VRC25MJ.js} +1 -1
  44. package/dist/bundle/chunks/session-worker.js +114 -110
  45. package/dist/bundle/cli.js +2 -2
  46. package/dist/bundle/index.js +1 -1
  47. package/dist/bundle/rpc-entry.js +1 -1
  48. package/dist/bundle/runtime-manifest.json +1 -1
  49. package/dist/core/agent-session-runtime.d.ts +2 -0
  50. package/dist/core/agent-session-runtime.js +9 -2
  51. package/dist/core/agent-session-services.d.ts +2 -0
  52. package/dist/core/agent-session-services.js +1 -0
  53. package/dist/core/agent-session.d.ts +32 -3
  54. package/dist/core/agent-session.js +142 -29
  55. package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
  56. package/dist/core/credential-pool/state-store.d.ts +1 -1
  57. package/dist/core/egress/classify.d.ts +8 -3
  58. package/dist/core/egress/classify.js +177 -158
  59. package/dist/core/egress/console-redactor.d.ts +7 -0
  60. package/dist/core/egress/console-redactor.js +74 -0
  61. package/dist/core/egress/credentials.d.ts +73 -0
  62. package/dist/core/egress/credentials.js +20 -0
  63. package/dist/core/egress/device-custody.d.ts +1 -1
  64. package/dist/core/egress/device-custody.js +18 -15
  65. package/dist/core/egress/device-proxy-call.d.ts +59 -0
  66. package/dist/core/egress/device-proxy-call.js +148 -0
  67. package/dist/core/egress/device-proxy-lost-calls.d.ts +27 -0
  68. package/dist/core/egress/device-proxy-lost-calls.js +28 -0
  69. package/dist/core/egress/device-proxy-ports.d.ts +49 -0
  70. package/dist/core/egress/device-proxy-ports.js +2 -0
  71. package/dist/core/egress/device-proxy-request.d.ts +34 -0
  72. package/dist/core/egress/device-proxy-request.js +69 -0
  73. package/dist/core/egress/device-proxy-waits.d.ts +40 -0
  74. package/dist/core/egress/device-proxy-waits.js +67 -0
  75. package/dist/core/egress/device-proxy.d.ts +3 -0
  76. package/dist/core/egress/device-proxy.js +214 -0
  77. package/dist/core/egress/endpoint.js +2 -1
  78. package/dist/core/egress/entry-verify.d.ts +13 -0
  79. package/dist/core/egress/entry-verify.js +33 -0
  80. package/dist/core/egress/hpke.d.ts +63 -0
  81. package/dist/core/egress/hpke.js +192 -0
  82. package/dist/core/egress/key-policy-template.d.ts +7 -1
  83. package/dist/core/egress/ledger-schema.d.ts +80 -0
  84. package/dist/core/egress/ledger-schema.js +117 -0
  85. package/dist/core/egress/ledger.d.ts +55 -0
  86. package/dist/core/egress/ledger.js +159 -0
  87. package/dist/core/egress/pinned-infra.d.ts +23 -2
  88. package/dist/core/egress/pinned-infra.js +9 -2
  89. package/dist/core/egress/placement-claim.d.ts +69 -0
  90. package/dist/core/egress/placement-claim.js +115 -0
  91. package/dist/core/egress/pool-state.d.ts +18 -0
  92. package/dist/core/egress/pool-state.js +5 -0
  93. package/dist/core/egress/redact.d.ts +10 -0
  94. package/dist/core/egress/redact.js +134 -0
  95. package/dist/core/egress/sealed-format.d.ts +101 -0
  96. package/dist/core/egress/sealed-format.js +174 -0
  97. package/dist/core/egress/sealed-source.d.ts +35 -0
  98. package/dist/core/egress/sealed-source.js +140 -0
  99. package/dist/core/egress/verified-keys.js +1 -1
  100. package/dist/core/egress/wire.d.ts +228 -0
  101. package/dist/core/egress/wire.js +49 -0
  102. package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
  103. package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
  104. package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
  105. package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
  106. package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
  107. package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
  108. package/dist/core/extensions/builtin/goal/index.js +46 -0
  109. package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
  110. package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
  111. package/dist/core/extensions/builtin/monitor-state-event.d.ts +26 -0
  112. package/dist/core/extensions/builtin/monitor-state-event.js +2 -0
  113. package/dist/core/extensions/builtin/permission-system/index.js +10 -4
  114. package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
  115. package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
  116. package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
  117. package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
  118. package/dist/core/extensions/builtin/terminal/background-age.d.ts +25 -0
  119. package/dist/core/extensions/builtin/terminal/background-age.js +91 -0
  120. package/dist/core/extensions/builtin/terminal/extension-state.d.ts +1 -1
  121. package/dist/core/extensions/builtin/terminal/extension-state.js +21 -3
  122. package/dist/core/extensions/builtin/terminal/notify.d.ts +6 -0
  123. package/dist/core/extensions/builtin/terminal/notify.js +39 -5
  124. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +9 -2
  125. package/dist/core/extensions/builtin/terminal/session-bundle.js +36 -1
  126. package/dist/core/extensions/builtin/terminal/settings.d.ts +2 -0
  127. package/dist/core/extensions/builtin/terminal/settings.js +8 -0
  128. package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.d.ts +15 -0
  129. package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.js +87 -0
  130. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +3 -1
  131. package/dist/core/extensions/builtin/terminal/tools/bash.js +44 -15
  132. package/dist/core/extensions/builtin/terminal/tools/child-placement.d.ts +8 -0
  133. package/dist/core/extensions/builtin/terminal/tools/child-placement.js +43 -6
  134. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +14 -2
  135. package/dist/core/extensions/bun-extension-importer.js +4 -3
  136. package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
  137. package/dist/core/extensions/bun-extension-load-turns.js +45 -0
  138. package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
  139. package/dist/core/extensions/bun-extension-registry.js +19 -1
  140. package/dist/core/extensions/loader.js +5 -0
  141. package/dist/core/extensions/session-control-types.d.ts +15 -0
  142. package/dist/core/extensions/session-control-types.js +11 -0
  143. package/dist/core/extensions/session-dialog-api.d.ts +5 -0
  144. package/dist/core/extensions/session-dialog-api.js +13 -0
  145. package/dist/core/extensions/types.d.ts +14 -1
  146. package/dist/core/external-admission.d.ts +16 -2
  147. package/dist/core/external-admission.js +22 -4
  148. package/dist/core/permission-decision.d.ts +22 -0
  149. package/dist/core/permission-decision.js +53 -0
  150. package/dist/core/permission-mode.d.ts +56 -0
  151. package/dist/core/permission-mode.js +133 -0
  152. package/dist/core/retry-fallback/controller-types.d.ts +6 -0
  153. package/dist/core/retry-fallback/controller.d.ts +9 -0
  154. package/dist/core/retry-fallback/controller.js +34 -3
  155. package/dist/core/retry-fallback/unusable.d.ts +25 -1
  156. package/dist/core/retry-fallback/unusable.js +87 -2
  157. package/dist/core/retry-fallback/usage-limit.d.ts +6 -1
  158. package/dist/core/retry-fallback/usage-limit.js +12 -4
  159. package/dist/core/sdk.d.ts +6 -1
  160. package/dist/core/sdk.js +3 -0
  161. package/dist/core/session-log.js +1 -1
  162. package/dist/core/stall-warning.d.ts +41 -0
  163. package/dist/core/stall-warning.js +72 -0
  164. package/dist/core/terminal-settings.d.ts +1 -0
  165. package/dist/core/turn-origin.d.ts +31 -0
  166. package/dist/core/turn-origin.js +64 -0
  167. package/dist/modes/interactive/components/remote-delivery-message.d.ts +2 -1
  168. package/dist/modes/interactive/components/remote-delivery-message.js +13 -5
  169. package/dist/modes/rpc/admit-external-message-command.d.ts +37 -0
  170. package/dist/modes/rpc/admit-external-message-command.js +61 -0
  171. package/dist/modes/rpc/admit-external-message-wire.d.ts +42 -0
  172. package/dist/modes/rpc/admit-external-message-wire.js +55 -0
  173. package/dist/modes/rpc/client-input-handler.d.ts +2 -0
  174. package/dist/modes/rpc/client-input-handler.js +2 -0
  175. package/dist/modes/rpc/connection-handler.js +30 -5
  176. package/dist/modes/rpc/connection-roles.d.ts +6 -0
  177. package/dist/modes/rpc/connection-roles.js +13 -0
  178. package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
  179. package/dist/modes/rpc/delegated-drive-origin.js +16 -0
  180. package/dist/modes/rpc/effect-commands.d.ts +7 -1
  181. package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
  182. package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
  183. package/dist/modes/rpc/host-capabilities.d.ts +2 -0
  184. package/dist/modes/rpc/host-capabilities.js +3 -0
  185. package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
  186. package/dist/modes/rpc/host-handover-parked.js +17 -0
  187. package/dist/modes/rpc/host-handover-reopen.js +18 -6
  188. package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
  189. package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
  190. package/dist/modes/rpc/host-idle-handover.js +1 -0
  191. package/dist/modes/rpc/host-warm.d.ts +16 -1
  192. package/dist/modes/rpc/host-warm.js +33 -6
  193. package/dist/modes/rpc/method-classes-schema-delivery.d.ts +32 -0
  194. package/dist/modes/rpc/method-classes-schema-delivery.js +22 -0
  195. package/dist/modes/rpc/method-classes-schema-session.d.ts +36 -0
  196. package/dist/modes/rpc/method-classes-schema-session.js +3 -0
  197. package/dist/modes/rpc/method-classes-schema.d.ts +36 -0
  198. package/dist/modes/rpc/method-classes.d.ts +2 -0
  199. package/dist/modes/rpc/method-classes.js +4 -0
  200. package/dist/modes/rpc/multi-session-host.js +6 -1
  201. package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
  202. package/dist/modes/rpc/permission-preset-command.js +27 -0
  203. package/dist/modes/rpc/rpc-input-validation.js +4 -3
  204. package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
  205. package/dist/modes/rpc/rpc-response-types.d.ts +2 -1
  206. package/dist/modes/rpc/rpc-session-command-types.d.ts +6 -1
  207. package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
  208. package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
  209. package/dist/modes/rpc/rpc-session-state.js +2 -0
  210. package/dist/modes/rpc/session-attachment-commands.js +3 -3
  211. package/dist/modes/rpc/session-event-writer-connections.js +1 -0
  212. package/dist/modes/rpc/session-event-writer.d.ts +1 -1
  213. package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
  214. package/dist/modes/rpc/session-registry-attach.js +16 -3
  215. package/dist/modes/rpc/session-registry-types.d.ts +2 -0
  216. package/dist/modes/rpc/session-registry.d.ts +2 -0
  217. package/dist/modes/rpc/session-registry.js +16 -3
  218. package/dist/modes/rpc/session-router-close.js +43 -6
  219. package/dist/modes/rpc/session-router-context.js +2 -0
  220. package/dist/modes/rpc/session-router-occupancy.js +2 -11
  221. package/dist/modes/rpc/session-router-open-profile.js +4 -0
  222. package/dist/modes/rpc/session-router-open.d.ts +1 -1
  223. package/dist/modes/rpc/session-router-open.js +6 -17
  224. package/dist/modes/rpc/session-router-residency.d.ts +24 -0
  225. package/dist/modes/rpc/session-router-residency.js +90 -0
  226. package/dist/modes/rpc/session-router-routing.js +7 -1
  227. package/dist/modes/rpc/session-status.js +4 -1
  228. package/dist/modes/rpc/session-worker.js +4 -0
  229. package/dist/modes/rpc/tool-child/terminal-jobs.d.ts +1 -0
  230. package/dist/modes/rpc/tool-child/terminal-jobs.js +17 -1
  231. package/dist/modes/rpc/worker-session-registry.js +5 -2
  232. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  233. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +29 -2
  234. package/docs/extensions.md +12 -0
  235. package/docs/rpc.md +129 -12
  236. package/docs/terminal-tools.md +25 -1
  237. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  238. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  239. package/package.json +6 -6
  240. package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
  241. package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
  242. package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
  243. package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
  244. package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The brain-to-hand `model` lane of hosted model-key custody (row CK7; custody design 2.2, 4.3, 4.6, 15.14, 15.15).
3
+ *
4
+ * Message shapes only: the brain's device-proxy egress (CK9), the hand's provider-call executor (CK8), the hook and
5
+ * refresh channels (CK16, CK21) and the publisher (CK12d) speak these. Every request that makes the hand execute
6
+ * something carries a `SignedPlacementClaim`, checked by `verifyPlacementClaim` before anything runs.
7
+ */
8
+ import type { DeviceId, ProviderId, WaitCondition } from "@earendil-works/pi-agent-core";
9
+ import type { AssistantMessageEvent, SimpleStreamOptions, StopReason, TranscriptContext, Usage } from "@earendil-works/pi-ai";
10
+ import type { OpenAiCompactBody } from "../extensions/builtin/compaction/openai-remote-schema.ts";
11
+ import type { UrlSecretField } from "./endpoint.ts";
12
+ import type { SignedPlacementClaim } from "./placement-claim.ts";
13
+ import type { ServiceKeyEntry } from "./roster-entries.ts";
14
+ /**
15
+ * The stream options a brain may send a hand: serializable request shaping only. Never secrets or headers, callbacks,
16
+ * signals or live objects, or the loop-only `commitStep` (4.3, 2.3 options hygiene). `env` stays hand-local because
17
+ * some adapters derive the dial target from it: the Cloudflare providers of `anthropic-messages` and `openai-responses`
18
+ * fill the account and gateway of the model `baseUrl` from it (`resolveCloudflareBaseUrl`), `azure-openai-responses`
19
+ * reads its base URL or resource name from it, and `bedrock-converse-stream` its HTTP proxy. A brain-supplied `env`
20
+ * could therefore redirect the hand's dial, with the hand's own credential, to a host of the brain's choosing. `cwd`
21
+ * stays hand-local because the start message's `cwd` is the one source the hand interprets.
22
+ */
23
+ export declare const WIRE_STREAM_OPTION_KEYS: readonly ["abortServerSideFallback", "affinitySessionId", "cacheRetention", "deferred", "extraBody", "maxRetries", "maxRetryDelayMs", "maxTokens", "metadata", "reasoning", "refusalFallbacks", "samplingParams", "serviceTier", "sessionAffinityKey", "sessionId", "streamKind", "temperature", "thinkingBudgets", "thinkingSelection", "timeoutMs", "toolChoice", "transport", "websocketConnectTimeoutMs"];
24
+ export type WireStreamOptions = Pick<SimpleStreamOptions, (typeof WIRE_STREAM_OPTION_KEYS)[number]>;
25
+ export type HandLocalStreamOptionKey = Exclude<keyof SimpleStreamOptions, keyof WireStreamOptions>;
26
+ export declare const PROVIDER_CALL_ERROR_CODES: readonly ["auth", "rate_limited", "unavailable", "aborted", "unknown_provider", "local_only_unsupported", "stale_epoch", "claim_expired", "claim_invalid", "registry_key_unknown", "registry_key_revoked", "definition_mismatch"];
27
+ export type ProviderCallErrorCode = (typeof PROVIDER_CALL_ERROR_CODES)[number];
28
+ export type SecretField = "apiKey" | `credential:${number}` | `header:${string}` | `modelHeader:${string}:${string}` | UrlSecretField;
29
+ /** A JSON value, for payloads whose precise schema another row owns (see `HookModelRequestInput`, `WrapTable`). */
30
+ export type WireJson = string | number | boolean | null | readonly WireJson[] | {
31
+ readonly [key: string]: WireJson;
32
+ };
33
+ /** The serializable projection of today's request-hook input; CK21 pins its golden shape. */
34
+ export type HookModelRequestInput = WireJson;
35
+ /** keyId -> recipientKeyId -> HPKE wrap, the wraps-document tables (2.2); CK3 owns the wrap encoding. */
36
+ export type WrapTable = Readonly<Record<string, Readonly<Record<string, WireJson>>>>;
37
+ export type ProviderCallKind = {
38
+ readonly kind: "step";
39
+ readonly stepId: string;
40
+ readonly attempt: number;
41
+ } | {
42
+ readonly kind: "aux";
43
+ readonly auxId: string;
44
+ readonly streamKind: string;
45
+ readonly auxKind: "stream" | "warm" | "compact-endpoint";
46
+ };
47
+ /** Step, aux `stream` and aux `warm` carry the admitted request; `compact-endpoint` carries only the body (15.14). */
48
+ export type ProviderCallRequest = {
49
+ readonly context: TranscriptContext;
50
+ readonly options: WireStreamOptions;
51
+ } | {
52
+ readonly compactBody: OpenAiCompactBody;
53
+ };
54
+ export interface ProviderCallStart {
55
+ readonly t: "provider.call.start";
56
+ readonly callId: string;
57
+ readonly sessionId: string;
58
+ readonly call: ProviderCallKind;
59
+ readonly claim: SignedPlacementClaim;
60
+ readonly providerId: ProviderId;
61
+ readonly modelId: string;
62
+ readonly expectedSourceDigest?: string;
63
+ /** Always `"disabled"` for aux calls. */
64
+ readonly hooks: "enabled" | "disabled";
65
+ readonly cwd?: string;
66
+ readonly request: ProviderCallRequest;
67
+ readonly budget: {
68
+ readonly deadlineMs: number;
69
+ readonly maxOutputTokens?: number;
70
+ };
71
+ }
72
+ /** Sent once, before the dial, when the hand ran the session's request hooks; seq 0 is reserved for it. */
73
+ export interface ProviderCallHooked {
74
+ readonly t: "provider.call.hooked";
75
+ readonly callId: string;
76
+ readonly seq: 0;
77
+ readonly request: ProviderCallRequest;
78
+ }
79
+ export interface ProviderCallEvent {
80
+ readonly t: "provider.call.event";
81
+ readonly callId: string;
82
+ readonly seq: number;
83
+ readonly event: AssistantMessageEvent;
84
+ }
85
+ export interface ProviderCallEnd {
86
+ readonly t: "provider.call.end";
87
+ readonly callId: string;
88
+ readonly seq: number;
89
+ readonly usage: Usage;
90
+ readonly stopReason: StopReason;
91
+ }
92
+ export interface ProviderCallError {
93
+ readonly t: "provider.call.error";
94
+ readonly callId: string;
95
+ readonly seq: number;
96
+ readonly code: ProviderCallErrorCode;
97
+ readonly until?: number;
98
+ readonly retryable: boolean;
99
+ readonly usage?: Usage;
100
+ readonly message: string;
101
+ }
102
+ export interface ProviderCallCancel {
103
+ readonly t: "provider.call.cancel";
104
+ readonly callId: string;
105
+ readonly reason: "user" | "superseded" | "budget" | "shutdown";
106
+ }
107
+ export interface ProviderCallAck {
108
+ readonly t: "provider.call.ack";
109
+ readonly callId: string;
110
+ }
111
+ export interface ProviderCallResultQuery {
112
+ readonly t: "provider.call.result";
113
+ readonly callId: string;
114
+ readonly claim: SignedPlacementClaim;
115
+ }
116
+ export type ProviderCallResultReply = {
117
+ readonly t: "provider.call.result.reply";
118
+ readonly callId: string;
119
+ readonly state: "unknown";
120
+ } | {
121
+ readonly t: "provider.call.result.reply";
122
+ readonly callId: string;
123
+ readonly state: "running";
124
+ readonly lastSeq: number;
125
+ readonly hooked?: ProviderCallHooked;
126
+ } | {
127
+ readonly t: "provider.call.result.reply";
128
+ readonly callId: string;
129
+ readonly state: "done";
130
+ readonly hooked?: ProviderCallHooked;
131
+ readonly events: readonly AssistantMessageEvent[];
132
+ readonly end: ProviderCallEnd | ProviderCallError;
133
+ };
134
+ export interface CustodyRefreshRequest {
135
+ readonly t: "custody.refresh.request";
136
+ readonly providerId: ProviderId;
137
+ readonly field: SecretField;
138
+ readonly version: number;
139
+ readonly claim: SignedPlacementClaim;
140
+ }
141
+ export interface CustodyRefreshAck {
142
+ readonly t: "custody.refresh.ack";
143
+ readonly providerId: ProviderId;
144
+ readonly field: SecretField;
145
+ readonly outcome: "changed" | "unchanged" | "unresolved";
146
+ readonly version?: number;
147
+ }
148
+ /** `recipient`: a device enrolled during a custody kill that needs K_cfg/K_proj wraps from a peer (6.6(d), 3.6). */
149
+ export interface CustodyRewrapRequest {
150
+ readonly t: "custody.rewrap.request";
151
+ readonly dekIds: readonly string[];
152
+ readonly custodyEntry: ServiceKeyEntry;
153
+ readonly recipient?: DeviceId;
154
+ }
155
+ /** Device to device over the hand channel; never sent to the service. */
156
+ export interface CustodyRewrapDeliver {
157
+ readonly t: "custody.rewrap.deliver";
158
+ readonly recipient: DeviceId;
159
+ readonly cfgWraps: WrapTable;
160
+ readonly projWraps: WrapTable;
161
+ readonly projCheck: Readonly<Record<string, string>>;
162
+ readonly sig: string;
163
+ }
164
+ export interface HookModelRequest {
165
+ readonly t: "hook.model_request";
166
+ readonly sessionId: string;
167
+ readonly stepId: string;
168
+ readonly claim: SignedPlacementClaim;
169
+ readonly input: HookModelRequestInput;
170
+ }
171
+ export interface HookModelRequestReply {
172
+ readonly t: "hook.model_request.reply";
173
+ readonly sessionId: string;
174
+ readonly stepId: string;
175
+ readonly output: HookModelRequestInput;
176
+ }
177
+ export interface HandSessionAdvertisement {
178
+ readonly t: "hand.session.advertise";
179
+ readonly sessionId: string;
180
+ readonly connectionId: string;
181
+ readonly cwd: string;
182
+ /** H(extension set hash, cwd, project-local hook set), per session (4.9). */
183
+ readonly advertHash: string;
184
+ readonly requestHooks: readonly ProviderId[] | "*";
185
+ readonly streamHooks: readonly ProviderId[] | "*";
186
+ readonly extensionProviders: readonly ProviderId[];
187
+ readonly handLocalProviders: readonly {
188
+ readonly id: ProviderId;
189
+ readonly models: readonly string[];
190
+ }[];
191
+ readonly handDefines: readonly ProviderId[];
192
+ }
193
+ export interface HandPresence {
194
+ readonly deviceId: DeviceId;
195
+ readonly online: boolean;
196
+ readonly capabilities: readonly string[];
197
+ /** sessionId -> advertHash for the sessions bound to this hand. */
198
+ readonly sessionAdverts?: Readonly<Record<string, string>>;
199
+ /** providerId -> this device's current live sourceDigest for the providers it defines; re-sent on file change. */
200
+ readonly liveDigests?: Readonly<Record<ProviderId, string>>;
201
+ }
202
+ export type StepNotice = {
203
+ readonly t: "step.waiting";
204
+ readonly sessionId: string;
205
+ readonly stepId: string;
206
+ readonly steer: string;
207
+ readonly waitFor: readonly WaitCondition[];
208
+ } | {
209
+ readonly t: "step.resumed";
210
+ readonly sessionId: string;
211
+ readonly stepId: string;
212
+ } | {
213
+ readonly t: "step.reset";
214
+ readonly sessionId: string;
215
+ readonly stepId: string;
216
+ readonly cause: "superseded" | "readmitted";
217
+ };
218
+ export interface ProviderCallChannel {
219
+ start(msg: ProviderCallStart): Promise<void>;
220
+ result(msg: ProviderCallResultQuery): Promise<ProviderCallResultReply>;
221
+ ack(msg: ProviderCallAck): Promise<void>;
222
+ cancel(msg: ProviderCallCancel): Promise<void>;
223
+ /** Resumes from any seq; `fromSeq: 0` includes `hooked`. */
224
+ events(callId: string, fromSeq: number): AsyncIterable<ProviderCallHooked | ProviderCallEvent | ProviderCallEnd | ProviderCallError>;
225
+ readonly presence: AsyncIterable<HandPresence>;
226
+ readonly hooks: AsyncIterable<HandSessionAdvertisement>;
227
+ }
228
+ //# sourceMappingURL=wire.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The stream options a brain may send a hand: serializable request shaping only. Never secrets or headers, callbacks,
3
+ * signals or live objects, or the loop-only `commitStep` (4.3, 2.3 options hygiene). `env` stays hand-local because
4
+ * some adapters derive the dial target from it: the Cloudflare providers of `anthropic-messages` and `openai-responses`
5
+ * fill the account and gateway of the model `baseUrl` from it (`resolveCloudflareBaseUrl`), `azure-openai-responses`
6
+ * reads its base URL or resource name from it, and `bedrock-converse-stream` its HTTP proxy. A brain-supplied `env`
7
+ * could therefore redirect the hand's dial, with the hand's own credential, to a host of the brain's choosing. `cwd`
8
+ * stays hand-local because the start message's `cwd` is the one source the hand interprets.
9
+ */
10
+ export const WIRE_STREAM_OPTION_KEYS = [
11
+ "abortServerSideFallback",
12
+ "affinitySessionId",
13
+ "cacheRetention",
14
+ "deferred",
15
+ "extraBody",
16
+ "maxRetries",
17
+ "maxRetryDelayMs",
18
+ "maxTokens",
19
+ "metadata",
20
+ "reasoning",
21
+ "refusalFallbacks",
22
+ "samplingParams",
23
+ "serviceTier",
24
+ "sessionAffinityKey",
25
+ "sessionId",
26
+ "streamKind",
27
+ "temperature",
28
+ "thinkingBudgets",
29
+ "thinkingSelection",
30
+ "timeoutMs",
31
+ "toolChoice",
32
+ "transport",
33
+ "websocketConnectTimeoutMs",
34
+ ];
35
+ export const PROVIDER_CALL_ERROR_CODES = [
36
+ "auth",
37
+ "rate_limited",
38
+ "unavailable",
39
+ "aborted",
40
+ "unknown_provider",
41
+ "local_only_unsupported",
42
+ "stale_epoch",
43
+ "claim_expired",
44
+ "claim_invalid",
45
+ "registry_key_unknown",
46
+ "registry_key_revoked",
47
+ "definition_mismatch",
48
+ ];
49
+ //# sourceMappingURL=wire.js.map
@@ -0,0 +1,50 @@
1
+ import type { Question } from "./schema.ts";
2
+ /**
3
+ * Escape residue in question text (oh-my-openagent#12797). Some models write non-ASCII tool-call text as
4
+ * `\uXXXX` escapes and then drop the backslash (`ub0b4 Tailscale 쓰기` for `내 Tailscale 쓰기`) or double it, so
5
+ * the decoded string still holds `\ub0b4`. The JSON is valid either way; only the text is wrong.
6
+ *
7
+ * Heuristic, applied to every header, question, option label and option description after quoted code is blanked
8
+ * out (fenced ``` or ~~~ blocks, then ``...`` spans, then `...` spans), since quoted code may name an escape on purpose:
9
+ * - A literal `\uXXXX` whose code point is non-ASCII is residue.
10
+ * - A bare run of `uXXXX` tokens is a candidate only when no ASCII letter, digit, `_` or `\` precedes it and
11
+ * none follows it, so `menu1234`, `u1a2b3c4d` and other hex-like ids never match. A candidate is residue
12
+ * when it decodes to non-ASCII text and either
13
+ * - the run has two or more tokens (`ub0b4uc4f0`), each decoding to a letter, mark, number, punctuation or
14
+ * symbol (a paired surrogate counts), or
15
+ * - its single token decodes to a letter in a script the question set already uses outside ASCII, with
16
+ * Hangul, Han, Hiragana, Katakana and Bopomofo counted as one family (a wrong high digit lands a Hangul
17
+ * syllable in Han). `u1234` in English text, or beside Korean text, decodes to Ethiopic and stays.
18
+ * Known gap: a token glued to a preceding ASCII letter (`cafu00e9`) is not detected.
19
+ * Known limit: an escape named in plain prose outside code (\u00e9 with no backticks) is refused once like residue;
20
+ * its re-call opens as written.
21
+ */
22
+ export type EscapeResidue = {
23
+ field: string;
24
+ token: string;
25
+ };
26
+ export declare function findEscapeResidue(questions: readonly Question[]): EscapeResidue[];
27
+ /**
28
+ * The one-refusal bound, scoped to assistant turns. One assistant message's tool calls run in parallel inside one turn,
29
+ * and the model sees a refusal only after that turn ends. So every residue call in a turn with no open retry is refused,
30
+ * and every residue call in a later turn opens as written with a warning, whether or not its text changed. The retry
31
+ * stays open until a turn uses it, so no question set is refused twice in a row and a re-call can never loop.
32
+ */
33
+ export type EscapeResidueRetry = {
34
+ refusedThisTurn: boolean;
35
+ retryOpen: boolean;
36
+ retriedThisTurn: boolean;
37
+ };
38
+ export declare function freshEscapeResidueRetry(): EscapeResidueRetry;
39
+ /**
40
+ * Records any call; true means refuse it. A call made while the retry is open uses it up, clean or not, so a clean
41
+ * re-call closes the retry and a later residue set is refused once again. A residue call with no open retry is refused.
42
+ */
43
+ export declare function refuseEscapeResidue(retry: EscapeResidueRetry, hasResidue: boolean): boolean;
44
+ export declare function endEscapeResidueTurn(retry: EscapeResidueRetry): void;
45
+ export declare class AskUserEscapeResidueError extends Error {
46
+ readonly residue: EscapeResidue[];
47
+ constructor(residue: EscapeResidue[]);
48
+ }
49
+ export declare function escapeResidueNote(residue: readonly EscapeResidue[]): string;
50
+ //# sourceMappingURL=escape-residue.d.ts.map
@@ -0,0 +1,113 @@
1
+ const BARE_RUN = /(?<![\\A-Za-z0-9_])((?:u[0-9a-fA-F]{4})+)(?![A-Za-z0-9_])/g;
2
+ const LITERAL = /\\u([0-9a-fA-F]{4})/g;
3
+ // Order matters: a fence holds backticks and newlines, and a double-backtick span may hold a single backtick.
4
+ const QUOTED_CODE = [/(```|~~~)[\s\S]*?\1/g, /``[^\n]*?``/g, /`[^`\n]*`/g];
5
+ const NON_ASCII_LETTER = /(?!\p{ASCII})\p{L}/gu;
6
+ const PRINTABLE = /^[\p{L}\p{M}\p{N}\p{P}\p{S}]$/u;
7
+ const CJK_FAMILY = "CJK";
8
+ const SCRIPTS = [
9
+ [CJK_FAMILY, /[\p{Script=Hangul}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Bopomofo}]/u],
10
+ ["Latin", /\p{Script=Latin}/u],
11
+ ["Cyrillic", /\p{Script=Cyrillic}/u],
12
+ ["Greek", /\p{Script=Greek}/u],
13
+ ["Arabic", /\p{Script=Arabic}/u],
14
+ ["Hebrew", /\p{Script=Hebrew}/u],
15
+ ["Thai", /\p{Script=Thai}/u],
16
+ ["Devanagari", /\p{Script=Devanagari}/u],
17
+ ];
18
+ function scriptOf(char) {
19
+ return SCRIPTS.find(([, pattern]) => pattern.test(char))?.[0];
20
+ }
21
+ function decode(run) {
22
+ const units = [];
23
+ for (let index = 1; index < run.length; index += 5)
24
+ units.push(Number.parseInt(run.slice(index, index + 4), 16));
25
+ return String.fromCharCode(...units);
26
+ }
27
+ function isResidueRun(run, scripts) {
28
+ const decoded = decode(run);
29
+ const chars = Array.from(decoded);
30
+ if (chars.some((char) => (char.codePointAt(0) ?? 0) < 0x80))
31
+ return false;
32
+ if (run.length > 5)
33
+ return chars.every((char) => PRINTABLE.test(char));
34
+ const char = chars[0] ?? "";
35
+ if (!/\p{L}/u.test(char))
36
+ return false;
37
+ const script = scriptOf(char);
38
+ return script !== undefined && scripts.has(script);
39
+ }
40
+ function textFields(questions) {
41
+ const fields = [];
42
+ questions.forEach((question, index) => {
43
+ const at = `questions[${index}]`;
44
+ fields.push([`${at}.header`, question.header], [`${at}.question`, question.question]);
45
+ question.options.forEach((option, optionIndex) => {
46
+ fields.push([`${at}.options[${optionIndex}].label`, option.label]);
47
+ if (option.description !== undefined)
48
+ fields.push([`${at}.options[${optionIndex}].description`, option.description]);
49
+ });
50
+ });
51
+ return fields;
52
+ }
53
+ export function findEscapeResidue(questions) {
54
+ const fields = textFields(questions).map(([field, text]) => [
55
+ field,
56
+ QUOTED_CODE.reduce((masked, pattern) => masked.replace(pattern, (span) => " ".repeat(span.length)), text),
57
+ ]);
58
+ const scripts = new Set();
59
+ for (const [, text] of fields) {
60
+ for (const [char] of text.matchAll(NON_ASCII_LETTER)) {
61
+ const script = scriptOf(char);
62
+ if (script !== undefined)
63
+ scripts.add(script);
64
+ }
65
+ }
66
+ const residue = [];
67
+ for (const [field, text] of fields) {
68
+ for (const match of text.matchAll(LITERAL)) {
69
+ if (Number.parseInt(match[1] ?? "0", 16) >= 0x80)
70
+ residue.push({ field, token: match[0] });
71
+ }
72
+ for (const match of text.matchAll(BARE_RUN)) {
73
+ const run = match[1] ?? "";
74
+ if (isResidueRun(run, scripts))
75
+ residue.push({ field, token: run });
76
+ }
77
+ }
78
+ return residue;
79
+ }
80
+ export function freshEscapeResidueRetry() {
81
+ return { refusedThisTurn: false, retryOpen: false, retriedThisTurn: false };
82
+ }
83
+ /**
84
+ * Records any call; true means refuse it. A call made while the retry is open uses it up, clean or not, so a clean
85
+ * re-call closes the retry and a later residue set is refused once again. A residue call with no open retry is refused.
86
+ */
87
+ export function refuseEscapeResidue(retry, hasResidue) {
88
+ if (retry.retryOpen) {
89
+ retry.retriedThisTurn = true;
90
+ return false;
91
+ }
92
+ if (hasResidue)
93
+ retry.refusedThisTurn = true;
94
+ return hasResidue;
95
+ }
96
+ export function endEscapeResidueTurn(retry) {
97
+ retry.retryOpen = retry.refusedThisTurn || (retry.retryOpen && !retry.retriedThisTurn);
98
+ retry.refusedThisTurn = false;
99
+ retry.retriedThisTurn = false;
100
+ }
101
+ export class AskUserEscapeResidueError extends Error {
102
+ constructor(residue) {
103
+ const found = residue.map(({ field, token }) => `${field} has "${token}"`).join("; ");
104
+ super(`Escape residue in the question text: ${found}. Non-ASCII text was written as a \\uXXXX escape that lost its backslash or kept a literal one, so the user would see the code instead of the character. Call this tool again with the same questions, writing every non-ASCII character directly as UTF-8 text, never as a \\uXXXX escape.`);
105
+ this.name = "AskUserEscapeResidueError";
106
+ this.residue = residue;
107
+ }
108
+ }
109
+ export function escapeResidueNote(residue) {
110
+ const tokens = [...new Set(residue.map(({ token }) => token))].join(", ");
111
+ return `This question may show undecoded character codes (${tokens}); it is shown as the model wrote it.`;
112
+ }
113
+ //# sourceMappingURL=escape-residue.js.map
@@ -1,4 +1,5 @@
1
1
  import { APP_NAME } from "../../../../config.js";
2
+ import { endEscapeResidueTurn, freshEscapeResidueRetry } from "./escape-residue.js";
2
3
  import { pickVariant, TOOL_NAMES } from "./family.js";
3
4
  import { deliverQueuedQuestionOutcomes, getPendingQuestions } from "./registry.js";
4
5
  import { resumeDanglingQuestion } from "./resume.js";
@@ -19,7 +20,7 @@ export default function askUserExtension(pi) {
19
20
  // In TUI mode interactive-mode's text dispatch handles /answer first.
20
21
  },
21
22
  });
22
- const state = { timedOut: false, unavailable: false };
23
+ const state = { timedOut: false, unavailable: false, escapeResidue: freshEscapeResidueRetry() };
23
24
  let registered = false;
24
25
  const cancelPending = (ctx, origin, message, reportDetachedLoss = false) => {
25
26
  for (const entry of getPendingQuestions(ctx.sessionManager.getSessionId()))
@@ -42,6 +43,7 @@ export default function askUserExtension(pi) {
42
43
  pi.on("session_start", async (event, ctx) => {
43
44
  state.timedOut = false;
44
45
  state.unavailable = false;
46
+ state.escapeResidue = freshEscapeResidueRetry();
45
47
  const sessionId = ctx.sessionManager.getSessionId();
46
48
  for (const entry of getPendingQuestions(sessionId))
47
49
  entry.rebind(pi, ctx, state);
@@ -61,8 +63,12 @@ export default function askUserExtension(pi) {
61
63
  pi.on("model_select", async (event, ctx) => {
62
64
  sync(ctx, event.model);
63
65
  });
66
+ pi.on("turn_end", () => {
67
+ endEscapeResidueTurn(state.escapeResidue);
68
+ });
64
69
  pi.on("agent_end", async () => {
65
70
  state.timedOut = false;
71
+ state.escapeResidue = freshEscapeResidueRetry();
66
72
  });
67
73
  pi.on("session_shutdown", async (event, ctx) => {
68
74
  if (event.reason === "reload") {
@@ -1,3 +1,4 @@
1
+ import { freshEscapeResidueRetry } from "./escape-residue.js";
1
2
  import { TOOL_NAMES } from "./family.js";
2
3
  import { parseAskUserAnswerFrame } from "./format.js";
3
4
  import { ASK_USER_SETTLEMENT_ENTRY, emitAskUserClosed } from "./notify.js";
@@ -143,7 +144,7 @@ export async function resumeDanglingQuestion(pi, event, ctx) {
143
144
  continue;
144
145
  }
145
146
  // The runtime registration owns delivery, including after another reload.
146
- void startQuestion(pi, ctx, request, ctx.signal, { timedOut: false, unavailable: false }, dangling.variant, {
147
+ void startQuestion(pi, ctx, request, ctx.signal, { timedOut: false, unavailable: false, escapeResidue: freshEscapeResidueRetry() }, dangling.variant, {
147
148
  resuming: true,
148
149
  });
149
150
  }
@@ -1,8 +1,11 @@
1
1
  import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "../../types.ts";
2
+ import { type EscapeResidueRetry } from "./escape-residue.ts";
2
3
  import { type AskUserVariant, type QuestionRequest, type QuestionResponse } from "./schema.ts";
3
4
  export interface AskUserState {
4
5
  timedOut: boolean;
5
6
  unavailable: boolean;
7
+ /** One refusal per turn of residue calls; the re-calls in a later turn open as written (oh-my-openagent#12797). */
8
+ escapeResidue: EscapeResidueRetry;
6
9
  }
7
10
  /**
8
11
  * Deliver a settled async answer as one framed user message. The extension owns
@@ -1,5 +1,6 @@
1
1
  import { releaseQuestionInput } from "../../../question-input.js";
2
2
  import { WAKE_SOURCE_STATE_EVENT } from "../monitor-state-event.js";
3
+ import { AskUserEscapeResidueError, escapeResidueNote, findEscapeResidue, refuseEscapeResidue, } from "./escape-residue.js";
3
4
  import { TOOL_NAMES } from "./family.js";
4
5
  import { formatGatedCancellation, formatModelAnswer, formatResultDetails, formatUserMessage, showsPartialAnswers, } from "./format.js";
5
6
  import { ASK_USER_ASKED_EVENT, ASK_USER_QUESTION_ENTRY, ASK_USER_SETTLEMENT_ENTRY, emitAskUserClosed, emitAskUserNotification, } from "./notify.js";
@@ -329,6 +330,13 @@ export function createAskUserTool(variant, pi, state) {
329
330
  emitAskUserClosed(pi, request.requestId, unavailable);
330
331
  return result(variant, unavailable, request);
331
332
  }
333
+ const residue = findEscapeResidue(request.questions);
334
+ if (refuseEscapeResidue(state.escapeResidue, residue.length > 0)) {
335
+ emitAskUserClosed(pi, request.requestId, unavailable);
336
+ throw new AskUserEscapeResidueError(residue);
337
+ }
338
+ if (residue.length > 0)
339
+ ctx.ui.notify(escapeResidueNote(residue), "warning");
332
340
  const completion = startQuestion(pi, ctx, request, signal, state, variant);
333
341
  if (!request.waitForAnswer)
334
342
  return {
@@ -175,6 +175,13 @@ export default function goalExtension(pi) {
175
175
  clearAgentGoalAccounting();
176
176
  }
177
177
  refreshGoalUi(ctx, goal);
178
+ // A session reopened under a live Stop keeps its goal paused until the Stop is released.
179
+ if (await pauseGoalWithHeldSession(ctx))
180
+ return;
181
+ // A goal a live Stop paused (a crashed run included) resumes with the session once the Stop is released, so the
182
+ // resume prompt would only restart its counter under the hold.
183
+ if (goal?.status === "paused" && stopHoldLive(ctx) && pausedByStop(ctx, goal.id))
184
+ return;
178
185
  // Opening the session earns one reminder; renders and extension reloads do not.
179
186
  // Stale stops retain active status, unlike user-paused or blocking stops.
180
187
  if ((event.reason === "startup" || event.reason === "resume") &&
@@ -218,6 +225,13 @@ export default function goalExtension(pi) {
218
225
  }
219
226
  }
220
227
  });
228
+ // A Stop that lands while no turn runs emits no `session_abort`, so the goal pauses and resumes with the session here.
229
+ pi.on("session_hold_changed", async (event, ctx) => {
230
+ if (event.change === "paused")
231
+ await pauseGoalWithHeldSession(ctx);
232
+ else
233
+ await resumeGoalWithReleasedSession(ctx);
234
+ });
221
235
  pi.on("held_wakes_released", async (event, ctx) => {
222
236
  if (!event.sources.goal)
223
237
  return;
@@ -303,6 +317,11 @@ export default function goalExtension(pi) {
303
317
  if (event.aborted === true && event.abortSource === "user" && goal?.status === "active") {
304
318
  goal = await pauseOrBlockOnUserAbort(ctx, goal.id);
305
319
  }
320
+ else if (goal?.status === "active" && goal.continuationStoppedAt === undefined && stopHoldLive(ctx)) {
321
+ // A Stop that joined this turn's agent_end boundary leaves the turn un-aborted; the goal still pauses with it.
322
+ markStopPaused(pi, goal.id);
323
+ goal = await updateGoal(goalStoreRef(ctx), { status: "paused" }, "user");
324
+ }
306
325
  if (goal?.status === "active") {
307
326
  beginAgentGoalAccounting(goal);
308
327
  }
@@ -404,6 +423,33 @@ export default function goalExtension(pi) {
404
423
  return;
405
424
  syncContinuationGoal(ctx, continuedGoal);
406
425
  }
426
+ /** Close the accounting window at the Stop and pause an idle active goal; a running turn pauses at its `agent_end`. */
427
+ async function pauseGoalWithHeldSession(ctx) {
428
+ if (agentTurnInProgress || !stopHoldLive(ctx))
429
+ return false;
430
+ const goal = await readGoal(goalStoreRef(ctx));
431
+ if (goal?.status !== "active" || goal.continuationStoppedAt !== undefined)
432
+ return false;
433
+ const accounted = await accountCurrentAgentTurn(ctx, "active");
434
+ if (accounted?.status !== "active")
435
+ return false;
436
+ markStopPaused(pi, accounted.id);
437
+ const paused = await updateGoal(goalStoreRef(ctx), { status: "paused" }, "user");
438
+ clearAgentGoalAccounting();
439
+ refreshGoalUiBestEffort(ctx, paused);
440
+ return true;
441
+ }
442
+ async function resumeGoalWithReleasedSession(ctx) {
443
+ if (stopHoldLive(ctx))
444
+ return;
445
+ const goal = await readGoal(goalStoreRef(ctx));
446
+ if (goal?.status !== "paused" || !pausedByStop(ctx, goal.id))
447
+ return;
448
+ markStopResumed(pi, goal.id);
449
+ const resumed = await updateGoal(goalStoreRef(ctx), { status: "active" }, "user");
450
+ beginAgentGoalAccounting(resumed);
451
+ refreshGoalUiBestEffort(ctx, resumed);
452
+ }
407
453
  function beginAgentGoalAccounting(goal) {
408
454
  // Goal usage accrues only while its accounting window is open. A stale stop
409
455
  // excludes extension-driven turns, like other inactive goals, until input or resume.
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "../../types.ts";
2
2
  export declare const GOAL_STOP_PAUSED_ENTRY = "goal-stop-paused";
3
3
  export declare const GOAL_STOP_RESUMED_ENTRY = "goal-stop-resumed";
4
+ /** A user Stop, or a parent's Stop inherited by this child, holds the session paused; an interrupted run does not. */
4
5
  export declare function stopHoldLive(ctx: ExtensionContext): boolean;
5
6
  /** The session reopened over a run that never finished: its process died mid-turn (senpi#4683, senpi#5755). */
6
7
  export declare function interruptedRunHeld(ctx: ExtensionContext): boolean;
@@ -1,8 +1,10 @@
1
1
  export const GOAL_STOP_PAUSED_ENTRY = "goal-stop-paused";
2
2
  export const GOAL_STOP_RESUMED_ENTRY = "goal-stop-resumed";
3
+ /** A user Stop, or a parent's Stop inherited by this child, holds the session paused; an interrupted run does not. */
3
4
  export function stopHoldLive(ctx) {
4
- return (ctx.getPauseState?.().holds.some((hold) => hold.kind === "session_hold" && hold.key !== "interrupted-run") ===
5
- true);
5
+ return (ctx
6
+ .getPauseState?.()
7
+ .holds.some((hold) => (hold.kind === "session_hold" && hold.key !== "interrupted-run") || hold.kind === "inherited_hold") === true);
6
8
  }
7
9
  /** The session reopened over a run that never finished: its process died mid-turn (senpi#4683, senpi#5755). */
8
10
  export function interruptedRunHeld(ctx) {
@@ -5,6 +5,32 @@ export declare const CONTINUATION_HOLD_STATE_EVENT = "continuation_hold_state";
5
5
  /** Live widgets of a session's long background jobs; payload `JobWidgetState` (job-widgets/view-types.ts). */
6
6
  export declare const JOB_WIDGET_STATE_EVENT = "job_widget_state";
7
7
  export declare const MONITOR_NOTIFICATION_CUSTOM_TYPE = "senpi-monitor:notification";
8
+ /** A background terminal session's age notice or auto-reap; payload `TerminalBackgroundAgeEvent` (senpi#6201). */
9
+ export declare const TERMINAL_BACKGROUND_AGE_EVENT = "terminal_background_age";
10
+ /**
11
+ * `running`: the session is still running at `ageMinutes` (first at 10 minutes, then every 30). `reaped`: the opt-in
12
+ * auto-reap stopped it at `autoReapMinutes`. `reap_failed`: the auto-reap's one stop at `autoReapMinutes` did not end
13
+ * it, for `reason`; it keeps running and keeps reporting. `stop` is the call that stops it; `autoReapMinutes` is null
14
+ * when nothing will stop it automatically.
15
+ */
16
+ export interface TerminalBackgroundAgeEvent {
17
+ readonly kind: "running" | "reaped" | "reap_failed";
18
+ readonly id: string;
19
+ readonly description: string;
20
+ readonly command?: string;
21
+ readonly startedAtMs: number;
22
+ readonly ageMs: number;
23
+ readonly ageMinutes: number;
24
+ readonly autoReapMinutes: number | null;
25
+ /** Why the auto-reap failed; set on `reap_failed` only. */
26
+ readonly reason?: string;
27
+ readonly stop: {
28
+ readonly tool: "kill_bash";
29
+ readonly args: {
30
+ readonly bash_id: string;
31
+ };
32
+ };
33
+ }
8
34
  /** Per-monitor entry in a monitor notification's `details.monitors`. */
9
35
  export interface MonitorNotificationDetail {
10
36
  readonly id: string;