@code-yeongyu/senpi 2026.10.21-1 → 2026.10.21-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 (204) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/bundle/chunks/{anthropic-messages-S5BTP7U2.js → anthropic-messages-2IQD75YN.js} +1 -1
  3. package/dist/bundle/chunks/args-SV2KM4KA.js +2 -0
  4. package/dist/bundle/chunks/azure-openai-responses-HFI6SDH5.js +2 -0
  5. package/dist/bundle/chunks/{chunk-WQLMQGOW.js → chunk-2AJB5S4C.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-XSRM367N.js → chunk-3AK3WUN4.js} +2 -2
  7. package/dist/bundle/chunks/{chunk-6TL72TUD.js → chunk-4HJTPWZY.js} +1 -1
  8. package/dist/bundle/chunks/chunk-4NPBDGC4.js +26 -0
  9. package/dist/bundle/chunks/{chunk-E4WOCULF.js → chunk-4XR5HDWC.js} +72 -69
  10. package/dist/bundle/chunks/{chunk-6IWSMXBA.js → chunk-5GC4IQ3A.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-7I7ZMSBF.js → chunk-A4B4CMOR.js} +1 -1
  12. package/dist/bundle/chunks/chunk-B4N4N7BR.js +8 -0
  13. package/dist/bundle/chunks/{chunk-X23XXKMH.js → chunk-BVFJAXWO.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-25B6JQC4.js → chunk-C3BJRXEV.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-DKBJM22C.js → chunk-CQDU6S6C.js} +2 -2
  16. package/dist/bundle/chunks/{chunk-MF57DS3V.js → chunk-EV4TWAJI.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-JJXDWX46.js → chunk-F3JGTOLH.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-CRY5CP3B.js → chunk-G42JHX5Q.js} +3 -3
  19. package/dist/bundle/chunks/{chunk-5VRI3CME.js → chunk-G7VYQRE7.js} +2 -2
  20. package/dist/bundle/chunks/chunk-GVSBCJFI.js +2 -0
  21. package/dist/bundle/chunks/{chunk-KUZPBZUK.js → chunk-HKDGQE6D.js} +2 -2
  22. package/dist/bundle/chunks/{chunk-HD67U37N.js → chunk-HRV7NEOJ.js} +1 -1
  23. package/dist/bundle/chunks/chunk-JDETIKL7.js +7 -0
  24. package/dist/bundle/chunks/{chunk-MVNVDGYF.js → chunk-JGMG227A.js} +1 -1
  25. package/dist/bundle/chunks/chunk-MHD7SSHF.js +2 -0
  26. package/dist/bundle/chunks/{chunk-CEQMTVYS.js → chunk-MXLEWTWC.js} +2 -2
  27. package/dist/bundle/chunks/{chunk-C5W3D56I.js → chunk-N6UN2SDS.js} +3 -3
  28. package/dist/bundle/chunks/chunk-NTK2MHJ5.js +2 -0
  29. package/dist/bundle/chunks/chunk-OMN4QWFA.js +3 -0
  30. package/dist/bundle/chunks/chunk-PLQ5WRXN.js +2 -0
  31. package/dist/bundle/chunks/{chunk-2MOGCBPT.js → chunk-PUMCQ5HS.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-55YTTFNY.js → chunk-PUQFKAB4.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-IJT3GSB2.js → chunk-SN7PTZTC.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-FWHSGCWR.js → chunk-T2IIUI3J.js} +3 -3
  35. package/dist/bundle/chunks/{chunk-5AOKDC6G.js → chunk-TSETMVJP.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-SR7DBNWN.js → chunk-V6AYNGMX.js} +1 -1
  37. package/dist/bundle/chunks/chunk-VDPJ7W52.js +11 -0
  38. package/dist/bundle/chunks/{chunk-EPQK674G.js → chunk-VLKULV6F.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-SL7TSFR5.js → chunk-VXACHCLB.js} +1 -1
  40. package/dist/bundle/chunks/{chunk-FM7NDZR5.js → chunk-VYSRUDEU.js} +1 -1
  41. package/dist/bundle/chunks/chunk-YMFGBSO3.js +2 -0
  42. package/dist/bundle/chunks/cli-main-RA6EBOOU.js +3 -0
  43. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-4QEQ7MGK.js → cloudflare-workers-ai-system-one-BVENEDT6.js} +1 -1
  44. package/dist/bundle/chunks/cursor-agent.js +3 -3
  45. package/dist/bundle/chunks/google-generative-ai-ONS2KSGN.js +2 -0
  46. package/dist/bundle/chunks/google-vertex-5VS7ZESH.js +2 -0
  47. package/dist/bundle/chunks/help-fast-path-FZY53UXB.js +2 -0
  48. package/dist/bundle/chunks/host-command-TNUIHEFP.js +2 -0
  49. package/dist/bundle/chunks/{host-lifecycle-EZQ5VCQV.js → host-lifecycle-LDOHAYLK.js} +1 -1
  50. package/dist/bundle/chunks/host-session-command-OKASUQPA.js +20 -0
  51. package/dist/bundle/chunks/interactive-mode-PSGTSURY.js +2 -0
  52. package/dist/bundle/chunks/{mistral-conversations-5EHLE7PZ.js → mistral-conversations-RLDMF2BI.js} +1 -1
  53. package/dist/bundle/chunks/{multi-session-host-OMJZ5VXM.js → multi-session-host-J5BVVUG3.js} +7 -6
  54. package/dist/bundle/chunks/openai-codex-responses-VRIJIZJG.js +10 -0
  55. package/dist/bundle/chunks/{openai-completions-NHDMDAS7.js → openai-completions-EZBOIXHP.js} +2 -2
  56. package/dist/bundle/chunks/{openai-images-PNXQDBSP.js → openai-images-KKM343F2.js} +1 -1
  57. package/dist/bundle/chunks/openai-responses-CMBH3DCY.js +3 -0
  58. package/dist/bundle/chunks/{package-manager-cli-7GQDB5DX.js → package-manager-cli-JHVRG35I.js} +1 -1
  59. package/dist/bundle/chunks/{role-WQWYL4ZD.js → role-LNF5MFGC.js} +1 -1
  60. package/dist/bundle/chunks/rotation-stream-JK3Q4IWI.js +2 -0
  61. package/dist/bundle/chunks/rpc-mode-HVSPPRTT.js +2 -0
  62. package/dist/bundle/chunks/{rpc-monitor-source-FI7H7LGY.js → rpc-monitor-source-2DQZLFFJ.js} +1 -1
  63. package/dist/bundle/chunks/{runtime-session-IQAKNRTV.js → runtime-session-CLNODAYE.js} +1 -1
  64. package/dist/bundle/chunks/{schedule-command-XXJC4QQN.js → schedule-command-OMI4VWUV.js} +1 -1
  65. package/dist/bundle/chunks/session-control-endpoint-UPYF4OFP.js +3 -0
  66. package/dist/bundle/chunks/session-picker-NR7CRVNQ.js +2 -0
  67. package/dist/bundle/chunks/session-worker.js +143 -139
  68. package/dist/bundle/chunks/{typesafe-system-one-W2ZTYF7X.js → typesafe-system-one-PJIWA5SP.js} +1 -1
  69. package/dist/bundle/cli.js +2 -2
  70. package/dist/bundle/index.js +1 -1
  71. package/dist/bundle/rpc-entry.js +1 -1
  72. package/dist/bundle/runtime-manifest.json +1 -1
  73. package/dist/cli/host-command.d.ts +2 -0
  74. package/dist/cli/host-command.js +14 -19
  75. package/dist/cli/host-handover-args.d.ts +4 -0
  76. package/dist/cli/host-handover-args.js +20 -0
  77. package/dist/core/agent-session.d.ts +29 -1
  78. package/dist/core/agent-session.js +74 -19
  79. package/dist/core/auth-storage.d.ts +44 -1
  80. package/dist/core/auth-storage.js +120 -3
  81. package/dist/core/client-message-identity.d.ts +3 -0
  82. package/dist/core/credential-accounts.d.ts +2 -0
  83. package/dist/core/credential-accounts.js +8 -1
  84. package/dist/core/credential-pool/rotation-slots.js +3 -2
  85. package/dist/core/extensions/builtin/account/index.js +6 -1
  86. package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +2 -1
  87. package/dist/core/extensions/builtin/compaction/openai-remote.js +6 -5
  88. package/dist/core/extensions/builtin/goal/provider-retry.d.ts +2 -2
  89. package/dist/core/extensions/builtin/goal/provider-retry.js +18 -6
  90. package/dist/core/extensions/builtin/terminal/command-monitor-source.d.ts +17 -4
  91. package/dist/core/extensions/builtin/terminal/command-monitor-source.js +33 -6
  92. package/dist/core/extensions/builtin/terminal/durable-command.js +1 -0
  93. package/dist/core/extensions/builtin/terminal/extension.js +28 -0
  94. package/dist/core/extensions/builtin/terminal/monitor-notify-details.d.ts +6 -0
  95. package/dist/core/extensions/builtin/terminal/monitor-notify-details.js +19 -0
  96. package/dist/core/extensions/builtin/terminal/monitor-notify-format.d.ts +18 -0
  97. package/dist/core/extensions/builtin/terminal/monitor-notify-format.js +52 -0
  98. package/dist/core/extensions/builtin/terminal/monitor-notify.d.ts +8 -1
  99. package/dist/core/extensions/builtin/terminal/monitor-notify.js +65 -119
  100. package/dist/core/extensions/builtin/terminal/monitor-overflow-file.d.ts +7 -0
  101. package/dist/core/extensions/builtin/terminal/monitor-overflow-file.js +67 -0
  102. package/dist/core/extensions/builtin/terminal/monitor-overflow.d.ts +37 -0
  103. package/dist/core/extensions/builtin/terminal/monitor-overflow.js +75 -0
  104. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +10 -0
  105. package/dist/core/extensions/builtin/terminal/monitor-registry.js +24 -1
  106. package/dist/core/extensions/builtin/terminal/monitor-types.d.ts +5 -0
  107. package/dist/core/extensions/builtin/terminal/monitor-wake-fit.d.ts +9 -0
  108. package/dist/core/extensions/builtin/terminal/monitor-wake-fit.js +34 -0
  109. package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.d.ts +10 -0
  110. package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.js +41 -0
  111. package/dist/core/extensions/builtin/terminal/restore-session.js +2 -0
  112. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +5 -1
  113. package/dist/core/extensions/builtin/terminal/settings.d.ts +10 -0
  114. package/dist/core/extensions/builtin/terminal/settings.js +13 -1
  115. package/dist/core/extensions/builtin/terminal/shared.d.ts +17 -10
  116. package/dist/core/extensions/builtin/terminal/shared.js +19 -10
  117. package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +3 -0
  118. package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +7 -0
  119. package/dist/core/extensions/builtin/terminal/terminal-manifest.js +1 -0
  120. package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts +1 -0
  121. package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js +6 -1
  122. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +1 -0
  123. package/dist/core/extensions/builtin/terminal/tools/monitor.js +9 -1
  124. package/dist/core/extensions/builtin/tps.js +1 -8
  125. package/dist/core/extensions/loader.js +3 -1
  126. package/dist/core/extensions/runner.d.ts +1 -0
  127. package/dist/core/extensions/runner.js +12 -1
  128. package/dist/core/extensions/types.d.ts +3 -1
  129. package/dist/core/session-manager.d.ts +10 -1
  130. package/dist/core/session-manager.js +4 -3
  131. package/dist/core/terminal-settings.d.ts +3 -0
  132. package/dist/index.d.ts +1 -1
  133. package/dist/main.d.ts +14 -0
  134. package/dist/main.js +25 -8
  135. package/dist/modes/index.d.ts +1 -0
  136. package/dist/modes/index.js +1 -0
  137. package/dist/modes/interactive/components/footer.js +4 -2
  138. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  139. package/dist/modes/interactive/interactive-mode.js +13 -1
  140. package/dist/modes/rpc/client-admission-record.d.ts +5 -0
  141. package/dist/modes/rpc/client-admission-record.js +2 -0
  142. package/dist/modes/rpc/connection-handler.d.ts +3 -0
  143. package/dist/modes/rpc/connection-handler.js +33 -5
  144. package/dist/modes/rpc/connection-roles.d.ts +30 -0
  145. package/dist/modes/rpc/connection-roles.js +53 -0
  146. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  147. package/dist/modes/rpc/custom-capability.js +6 -0
  148. package/dist/modes/rpc/host-capabilities.js +2 -1
  149. package/dist/modes/rpc/host-decision.d.ts +4 -1
  150. package/dist/modes/rpc/host-decision.js +6 -3
  151. package/dist/modes/rpc/host-ensure-liveness.d.ts +11 -0
  152. package/dist/modes/rpc/host-ensure-liveness.js +26 -0
  153. package/dist/modes/rpc/host-ensure.js +2 -3
  154. package/dist/modes/rpc/host-holder.d.ts +27 -0
  155. package/dist/modes/rpc/host-holder.js +18 -0
  156. package/dist/modes/rpc/host-runner.d.ts +2 -0
  157. package/dist/modes/rpc/host-runner.js +8 -1
  158. package/dist/modes/rpc/host-status-all.js +1 -0
  159. package/dist/modes/rpc/host-status.d.ts +1 -0
  160. package/dist/modes/rpc/host-status.js +5 -0
  161. package/dist/modes/rpc/host-stop-generation.d.ts +22 -0
  162. package/dist/modes/rpc/host-stop-generation.js +36 -0
  163. package/dist/modes/rpc/host-stop-start-time.d.ts +15 -0
  164. package/dist/modes/rpc/host-stop-start-time.js +30 -0
  165. package/dist/modes/rpc/host-stop.d.ts +13 -1
  166. package/dist/modes/rpc/host-stop.js +78 -9
  167. package/dist/modes/rpc/rpc-client.d.ts +9 -0
  168. package/dist/modes/rpc/rpc-client.js +15 -0
  169. package/dist/modes/rpc/rpc-types.d.ts +18 -0
  170. package/dist/modes/rpc/rpc-types.js +2 -0
  171. package/dist/modes/rpc/session-binding.d.ts +1 -1
  172. package/dist/modes/rpc/session-command-router.d.ts +1 -0
  173. package/dist/modes/rpc/session-command-router.js +13 -0
  174. package/dist/modes/rpc/session-worker-client.d.ts +1 -1
  175. package/dist/modes/rpc/session-worker-client.js +12 -1
  176. package/dist/modes/rpc/session-worker-protocol.d.ts +3 -0
  177. package/dist/modes/rpc/session-worker.js +9 -0
  178. package/docs/rpc.md +54 -2
  179. package/docs/session-format.md +5 -0
  180. package/package.json +6 -6
  181. package/dist/bundle/chunks/args-CRXNC7IS.js +0 -2
  182. package/dist/bundle/chunks/azure-openai-responses-2DCY6QKO.js +0 -2
  183. package/dist/bundle/chunks/chunk-5ZBDN3LB.js +0 -26
  184. package/dist/bundle/chunks/chunk-AEJUOQ7U.js +0 -2
  185. package/dist/bundle/chunks/chunk-LAWS3FYQ.js +0 -11
  186. package/dist/bundle/chunks/chunk-LM76EMEJ.js +0 -7
  187. package/dist/bundle/chunks/chunk-M237EDHB.js +0 -2
  188. package/dist/bundle/chunks/chunk-V6RY5A6T.js +0 -2
  189. package/dist/bundle/chunks/chunk-WIAXG52U.js +0 -2
  190. package/dist/bundle/chunks/chunk-WTFD4TII.js +0 -8
  191. package/dist/bundle/chunks/chunk-Z33PKFXM.js +0 -3
  192. package/dist/bundle/chunks/cli-main-RKC5Z25H.js +0 -3
  193. package/dist/bundle/chunks/google-generative-ai-3SKUSCIL.js +0 -2
  194. package/dist/bundle/chunks/google-vertex-YHSIIE4G.js +0 -2
  195. package/dist/bundle/chunks/help-fast-path-FWS5ARO5.js +0 -2
  196. package/dist/bundle/chunks/host-command-L66Y7ZL5.js +0 -2
  197. package/dist/bundle/chunks/host-session-command-2YNQNTEH.js +0 -20
  198. package/dist/bundle/chunks/interactive-mode-XOKTNBLA.js +0 -2
  199. package/dist/bundle/chunks/openai-codex-responses-4BIPACKV.js +0 -10
  200. package/dist/bundle/chunks/openai-responses-2UOPEJLP.js +0 -3
  201. package/dist/bundle/chunks/rotation-stream-J2NUPRMG.js +0 -2
  202. package/dist/bundle/chunks/rpc-mode-WM5WH7TH.js +0 -2
  203. package/dist/bundle/chunks/session-control-endpoint-MTOAYRRY.js +0 -3
  204. package/dist/bundle/chunks/session-picker-LU7BPLNI.js +0 -2
@@ -1,4 +1,5 @@
1
1
  import { convertResponsesMessages, getContextProvenance, normalizeContext, } from "@earendil-works/pi-ai";
2
+ import { deriveOpenAIPromptCacheKey } from "@earendil-works/pi-ai/api/openai-prompt-cache";
2
3
  import { streamSimple } from "@earendil-works/pi-ai/compat";
3
4
  import { convertToLlm } from "../../../messages.js";
4
5
  import { buildContextEntries, buildSessionContext, getSessionContextEntryId, SESSION_CONTEXT_ENTRY_ID, sessionEntryToContextMessages, } from "../../../session-manager.js";
@@ -25,11 +26,12 @@ export function createOpenAiRemoteCompactionRequest(options) {
25
26
  if (input.length === 0)
26
27
  return undefined;
27
28
  return {
29
+ sessionId: options.sessionId,
28
30
  body: {
29
31
  model: options.model.id,
30
32
  input,
31
33
  ...(options.systemPrompt ? { instructions: options.systemPrompt } : {}),
32
- ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
34
+ prompt_cache_key: deriveOpenAIPromptCacheKey(options.model.id, normalizeContext({ systemPrompt: options.systemPrompt, messages: [] })),
33
35
  ...(options.serviceTier ? { service_tier: options.serviceTier } : {}),
34
36
  },
35
37
  inputItemCount: input.length,
@@ -98,7 +100,6 @@ export function createOpenAiResponsesStreamCompactionPayload(payload, request) {
98
100
  ...payload,
99
101
  model: request.body.model,
100
102
  input: [...leadingPromptMessages(payload.input), ...createOpenAiResponsesStreamCompactionInput(request)],
101
- ...(request.body.prompt_cache_key ? { prompt_cache_key: request.body.prompt_cache_key } : {}),
102
103
  ...(request.body.service_tier ? { service_tier: request.body.service_tier } : {}),
103
104
  };
104
105
  }
@@ -173,7 +174,7 @@ async function runOpenAiResponsesStreamCompaction(options) {
173
174
  }
174
175
  return transformedPayload;
175
176
  },
176
- sessionId: options.request.body.prompt_cache_key,
177
+ sessionId: options.request.sessionId,
177
178
  signal: options.signal,
178
179
  transport: "websocket",
179
180
  });
@@ -361,7 +362,7 @@ export async function runOpenAiRemoteCompaction(ctx, event, emit, dependencies =
361
362
  branchEntries: event.branchEntries,
362
363
  messages: providerRequest?.messages,
363
364
  tokensBefore: event.preparation.tokensBefore,
364
- promptCacheKey: ctx.sessionManager.getSessionId(),
365
+ sessionId: ctx.sessionManager.getSessionId(),
365
366
  serviceTier,
366
367
  });
367
368
  if (!request) {
@@ -403,7 +404,7 @@ export async function runOpenAiRemoteCompaction(ctx, event, emit, dependencies =
403
404
  const transformedHeaders = providerRequest
404
405
  ? await providerRequest.transformHeaders(auth.headers ?? {})
405
406
  : auth.headers;
406
- const requestHeaders = createOpenAiRemoteCompactionHeaders(requestModel, { ...auth, headers: transformedHeaders }, request.body.prompt_cache_key);
407
+ const requestHeaders = createOpenAiRemoteCompactionHeaders(requestModel, { ...auth, headers: transformedHeaders }, request.sessionId);
407
408
  if (!requestHeaders) {
408
409
  emit?.({
409
410
  version: 1,
@@ -3,8 +3,8 @@ export declare const PROVIDER_RETRY_BASE_MS = 15000;
3
3
  export declare const PROVIDER_RETRY_CAP_MS: number;
4
4
  export declare const PROVIDER_RETRY_WAKE_CAP_MS: number;
5
5
  /**
6
- * The earliest future instant the failed turn's error names (never the assistant's own text), e.g. a credential pool's
7
- * "blocked until 2026-10-14T12:15:00.000Z": the moment an account frees up.
6
+ * Prefer a provider's structured usage reset, then its retry-after marker. Legacy
7
+ * pool guidance falls back to the earliest future ISO instant in the error only.
8
8
  */
9
9
  export declare function providerRetryAt(messages: readonly AgentMessage[], now: number): number | undefined;
10
10
  /**
@@ -1,18 +1,30 @@
1
+ import { parseRetryAfterMsMarker } from "@earendil-works/pi-ai/utils/retry-hint";
1
2
  import { lastAssistantMessage } from "./last-assistant-message.js";
2
3
  export const PROVIDER_RETRY_BASE_MS = 15_000;
3
4
  export const PROVIDER_RETRY_CAP_MS = 30 * 60_000;
4
5
  export const PROVIDER_RETRY_WAKE_CAP_MS = 6 * 60 * 60_000;
5
6
  const WAKE_MARGIN_MS = 1_000;
6
7
  const ISO_INSTANT = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g;
7
- function failureText(messages) {
8
- return lastAssistantMessage(messages)?.errorMessage ?? "";
9
- }
10
8
  /**
11
- * The earliest future instant the failed turn's error names (never the assistant's own text), e.g. a credential pool's
12
- * "blocked until 2026-10-14T12:15:00.000Z": the moment an account frees up.
9
+ * Prefer a provider's structured usage reset, then its retry-after marker. Legacy
10
+ * pool guidance falls back to the earliest future ISO instant in the error only.
13
11
  */
14
12
  export function providerRetryAt(messages, now) {
15
- const instants = [...failureText(messages).matchAll(ISO_INSTANT)]
13
+ const failure = lastAssistantMessage(messages);
14
+ const diagnostic = failure?.providerDiagnostic;
15
+ const reset = diagnostic?.resetsAt;
16
+ if ((diagnostic?.category === "quota" || diagnostic?.category === "rate_limit") &&
17
+ reset !== undefined &&
18
+ Number.isFinite(reset) &&
19
+ reset > now) {
20
+ return reset;
21
+ }
22
+ const text = failure?.errorMessage ?? "";
23
+ const retryAfterMs = parseRetryAfterMsMarker(text);
24
+ if (retryAfterMs !== undefined && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
25
+ return now + retryAfterMs;
26
+ }
27
+ const instants = [...text.matchAll(ISO_INSTANT)]
16
28
  .map((match) => Date.parse(match[0]))
17
29
  .filter((at) => Number.isFinite(at) && at > now);
18
30
  return instants.length === 0 ? undefined : Math.min(...instants);
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Command watches: a PTY session's output, line-buffered and filtered, with the durable fire budget
3
- * that auto-mutes a restartable watch that fires too often. Output stays in the runtime's bounded
4
- * history; a record holds at most one unfinished line.
2
+ * Command watches: a PTY session's output, line-buffered and filtered. A durable (restartable) watch
3
+ * counts its matched lines in a persisted rolling fire window and carries the runaway guard, which
4
+ * pauses it with a visible notice only after a sustained flood; a busy watch is never muted, its
5
+ * wakes are paced by the notifier instead. Output stays in the runtime's bounded history; a record
6
+ * holds at most one unfinished line.
5
7
  */
6
8
  import { MonitorLineBuffer } from "./monitor-line-buffer.ts";
7
9
  import { type MonitorFireWindow, type MonitorRecordBase, type MonitorSourceHost } from "./monitor-types.ts";
@@ -17,7 +19,7 @@ export interface RegisterMonitorOptions {
17
19
  readonly description: string;
18
20
  readonly runtime: TerminalRuntimeSession;
19
21
  readonly filter?: RegExp;
20
- /** Durability class from the manifest spec; non-ephemeral records carry the fire budget. */
22
+ /** Durability class from the manifest spec; non-ephemeral records carry the fire window and runaway guard. */
21
23
  readonly durabilityClass?: MonitorDurabilityClass;
22
24
  /** Persisted fire window re-bound by a restore, so a restart cannot reset the budget. */
23
25
  readonly fireWindow?: MonitorFireWindow;
@@ -25,6 +27,14 @@ export interface RegisterMonitorOptions {
25
27
  readonly expiresAt?: number;
26
28
  /** Output lines are a person's words (a chat bridge); goal continuation counts them as new input. */
27
29
  readonly humanInput?: boolean;
30
+ /** Per-monitor wake-rate override of a persistent watch; unset uses terminal.monitorWakesPerHour. */
31
+ readonly wakesPerHour?: number;
32
+ }
33
+ /** One-minute bucket of matched lines plus the run of consecutive flood minutes before it. */
34
+ interface RunawayWindow {
35
+ minuteStartMs: number;
36
+ count: number;
37
+ floodMinutes: number;
28
38
  }
29
39
  export interface MonitorRecord extends MonitorRecordBase {
30
40
  readonly id: string;
@@ -45,6 +55,8 @@ export interface MonitorRecord extends MonitorRecordBase {
45
55
  paused: boolean;
46
56
  settled: boolean;
47
57
  fireWindow: MonitorFireWindow | undefined;
58
+ runaway: RunawayWindow | undefined;
59
+ readonly wakesPerHour: number | undefined;
48
60
  unsubscribeOutput: (() => void) | undefined;
49
61
  unsubscribeExit: (() => void) | undefined;
50
62
  readonly humanInput: boolean;
@@ -60,4 +72,5 @@ export declare class CommandMonitorSource {
60
72
  adoptFireWindow(monitorId: string, fireWindow: MonitorFireWindow): boolean;
61
73
  dispose(): void;
62
74
  }
75
+ export {};
63
76
  //# sourceMappingURL=command-monitor-source.d.ts.map
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Command watches: a PTY session's output, line-buffered and filtered, with the durable fire budget
3
- * that auto-mutes a restartable watch that fires too often. Output stays in the runtime's bounded
4
- * history; a record holds at most one unfinished line.
2
+ * Command watches: a PTY session's output, line-buffered and filtered. A durable (restartable) watch
3
+ * counts its matched lines in a persisted rolling fire window and carries the runaway guard, which
4
+ * pauses it with a visible notice only after a sustained flood; a busy watch is never muted, its
5
+ * wakes are paced by the notifier instead. Output stays in the runtime's bounded history; a record
6
+ * holds at most one unfinished line.
5
7
  */
6
8
  import { MonitorLineBuffer } from "./monitor-line-buffer.js";
7
9
  import { allocateMonitorId, } from "./monitor-types.js";
8
- import { DEFAULT_DURABLE_MONITOR_FIRE_BUDGET, FIRE_BUDGET_AUTO_MUTE_SUMMARY, FIRE_BUDGET_WINDOW_MS } from "./shared.js";
10
+ import { FIRE_BUDGET_WINDOW_MS, runawayPauseSummary } from "./shared.js";
9
11
  import { describeExit } from "./tools/spawn.js";
10
12
  export class CommandMonitorSource {
11
13
  #records = new Map();
@@ -43,6 +45,8 @@ export class CommandMonitorSource {
43
45
  paused: false,
44
46
  settled: false,
45
47
  fireWindow: durable ? { ...(options.fireWindow ?? { startMs: Date.now(), count: 0 }) } : undefined,
48
+ runaway: undefined,
49
+ wakesPerHour: options.wakesPerHour,
46
50
  unsubscribeOutput: undefined,
47
51
  unsubscribeExit: undefined,
48
52
  humanInput: options.humanInput === true,
@@ -96,19 +100,42 @@ export class CommandMonitorSource {
96
100
  if (now - record.fireWindow.startMs >= FIRE_BUDGET_WINDOW_MS)
97
101
  record.fireWindow = { startMs: now, count: 0 };
98
102
  record.fireWindow.count += 1;
99
- if (record.fireWindow.count >= DEFAULT_DURABLE_MONITOR_FIRE_BUDGET) {
103
+ if (this.#floodsRunaway(record, now)) {
104
+ const guard = this.#host.runawayGuard();
100
105
  record.paused = true;
101
106
  this.#host.emit(record, {
102
107
  type: "summary",
103
108
  id: record.id,
104
109
  description: record.description,
105
- summary: FIRE_BUDGET_AUTO_MUTE_SUMMARY,
110
+ summary: runawayPauseSummary(record.id, guard.linesPerMinute, guard.minutes),
106
111
  });
107
112
  this.#host.changed();
108
113
  }
109
114
  }
110
115
  }
111
116
  }
117
+ /**
118
+ * Count one matched line into the record's minute bucket. A minute with more lines than the guard
119
+ * allows is a flood minute; the watch trips once the current minute floods and the minutes right
120
+ * before it flooded too, `minutes` in a row. A quiet or skipped minute resets the run.
121
+ */
122
+ #floodsRunaway(record, now) {
123
+ const guard = this.#host.runawayGuard();
124
+ const previous = record.runaway;
125
+ let current = previous;
126
+ if (current === undefined || now - current.minuteStartMs >= 60_000) {
127
+ const elapsedMinutes = previous === undefined ? 0 : Math.floor((now - previous.minuteStartMs) / 60_000);
128
+ const previousFlooded = previous !== undefined && elapsedMinutes === 1 && previous.count > guard.linesPerMinute;
129
+ current = {
130
+ minuteStartMs: previous === undefined ? now : previous.minuteStartMs + elapsedMinutes * 60_000,
131
+ count: 0,
132
+ floodMinutes: previous !== undefined && previousFlooded ? previous.floodMinutes + 1 : 0,
133
+ };
134
+ record.runaway = current;
135
+ }
136
+ current.count += 1;
137
+ return current.count > guard.linesPerMinute && current.floodMinutes + 1 >= guard.minutes;
138
+ }
112
139
  #settle(record) {
113
140
  if (record.settled)
114
141
  return;
@@ -129,6 +129,7 @@ export function createRestartableCommandHandler(deps) {
129
129
  runtime: spawned.runtime,
130
130
  filter: compileFilter(monitor.filter),
131
131
  ...(monitor.humanInput === true ? { humanInput: true } : {}),
132
+ ...(monitor.wakesPerHour !== undefined ? { wakesPerHour: monitor.wakesPerHour } : {}),
132
133
  // The persisted deadline rides through verbatim; a restore never extends it.
133
134
  ...(monitor.expiresAt !== null ? { expiresAt: monitor.expiresAt } : {}),
134
135
  });
@@ -6,6 +6,7 @@ import { isEvalOnlyRouting } from "../eval-only-routing.js";
6
6
  import { buildToolContext, bundleSinks, createBundle, sessionKeyOf, } from "./extension-state.js";
7
7
  import { currentLeaseToken, releaseTerminalLease, retireLeaseToken } from "./manifest-lease.js";
8
8
  import { MonitorNotifier } from "./monitor-notify.js";
9
+ import { appendMonitorOverflow } from "./monitor-overflow-file.js";
9
10
  import { terminalStateDir } from "./monitor-state-dir.js";
10
11
  import { MONITOR_STATUS_KEY } from "./monitor-status.js";
11
12
  import { MonitorStatusTicker } from "./monitor-status-ticker.js";
@@ -24,6 +25,26 @@ import { createBashResizeTool } from "./tools/bash-resize.js";
24
25
  import { TERMINAL_CHILD_JOB_KINDS, terminalChildJob } from "./tools/child-placement.js";
25
26
  import { createKillBashTool } from "./tools/kill-bash.js";
26
27
  import { bindTerminalManifestWriter, createMonitorTool } from "./tools/monitor.js";
28
+ /**
29
+ * Only paced (persistent command) watches of a session with a durable state dir get an overflow file.
30
+ * A failing file is reported once per monitor, not once per spilled chunk.
31
+ */
32
+ function spillMonitorOverflow(state, warned, id, lines) {
33
+ const terminalDir = terminalStateDir(state.ctx);
34
+ const monitorId = state.bundle?.manager.monitorIdOf(id);
35
+ if (terminalDir === undefined || monitorId === undefined || state.bundle?.monitors.wakePacing(id) === undefined)
36
+ return undefined;
37
+ try {
38
+ return appendMonitorOverflow(terminalDir, monitorId, lines);
39
+ }
40
+ catch (error) {
41
+ if (warned.has(monitorId))
42
+ return undefined;
43
+ warned.add(monitorId);
44
+ state.ctx?.ui?.notify?.(`Could not write the overflow file of ${monitorId}: ${error instanceof Error ? error.message : String(error)}`, "warning");
45
+ return undefined;
46
+ }
47
+ }
27
48
  function shouldStepAside(ctx) {
28
49
  return isAnthropicBashEnabled() && ctx?.model?.api === "anthropic-messages";
29
50
  }
@@ -106,12 +127,18 @@ export function registerTerminalExtension(pi) {
106
127
  getMode: () => state.settings.notify,
107
128
  });
108
129
  state.monitorNotifier?.dispose();
130
+ const overflowWarned = new Set();
109
131
  state.monitorNotifier = new MonitorNotifier({
110
132
  sendMessage: (message, options) => pi.sendMessage(message, options),
111
133
  getContext: () => state.ctx,
112
134
  getMode: () => state.settings.notify,
113
135
  getSettings: () => state.settings.monitor,
114
136
  pauseMonitors: (ids) => state.bundle?.monitors.pause(ids) ?? [],
137
+ wakesPerHour: (id) => {
138
+ const pacing = state.bundle?.monitors.wakePacing(id);
139
+ return pacing === undefined ? undefined : (pacing.wakesPerHour ?? state.settings.monitor.wakesPerHour);
140
+ },
141
+ spillOverflow: (id, lines) => spillMonitorOverflow(state, overflowWarned, id, lines),
115
142
  });
116
143
  const sessionKey = sessionKeyOf(ctx);
117
144
  if (event.reason === "reload") {
@@ -128,6 +155,7 @@ export function registerTerminalExtension(pi) {
128
155
  state.bundle = createBundle(state);
129
156
  }
130
157
  state.bundle ??= createBundle(state);
158
+ state.bundle.monitors.setRunawayGuard(state.settings.monitorRunaway);
131
159
  state.bundle.bind(bundleSinks(pi, state));
132
160
  const holdsLease = sessionKey !== undefined && currentLeaseToken(encodedSessionId(sessionKey)) !== undefined;
133
161
  if (event.reason === "reload" && holdsLease) {
@@ -0,0 +1,6 @@
1
+ import type { MonitorNotificationDetail } from "../monitor-state-event.ts";
2
+ import type { OverflowBook } from "./monitor-overflow.ts";
3
+ import type { MonitorEvent } from "./monitor-registry.ts";
4
+ /** Per-monitor attribution of one wake: what it carried inline plus what overflowed. */
5
+ export declare function monitorDetails(events: readonly MonitorEvent[], ids: ReadonlySet<string>, overflowBook: OverflowBook): MonitorNotificationDetail[];
6
+ //# sourceMappingURL=monitor-notify-details.d.ts.map
@@ -0,0 +1,19 @@
1
+ /** Per-monitor attribution of one wake: what it carried inline plus what overflowed. */
2
+ export function monitorDetails(events, ids, overflowBook) {
3
+ return [...ids].map((id) => {
4
+ const own = events.filter((event) => event.id === id);
5
+ const overflow = overflowBook.get(id);
6
+ const kinds = new Set(own.map((event) => event.type));
7
+ for (const kind of overflow?.kinds ?? [])
8
+ kinds.add(kind);
9
+ const humanInput = own.some((event) => event.type === "line" && event.humanInput === true) || overflow?.humanInput === true;
10
+ return {
11
+ id,
12
+ description: own[0]?.description ?? overflow?.description ?? id,
13
+ eventCount: own.length + (overflow?.count ?? 0),
14
+ kinds: [...kinds],
15
+ ...(humanInput ? { humanInput: true } : {}),
16
+ };
17
+ });
18
+ }
19
+ //# sourceMappingURL=monitor-notify-details.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Rendering of monitor wakes: one event body, a monitor's group header, and the size of a
3
+ * monitor's share of a wake, so the notifier can fill a wake by whole monitors without clipping.
4
+ */
5
+ import type { MonitorEvent } from "./monitor-registry.ts";
6
+ export declare const SYSTEM_REMINDER_OPEN = "<system-reminder>";
7
+ export declare const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
8
+ export declare const WAKE_BUDGET_PAUSE_NOTICE = "Monitor paused after repeated updates. Completion still wakes this session; peek bash_output or re-arm only for intermediate events.";
9
+ export declare function eventBody(event: MonitorEvent): string;
10
+ export declare function renderedLength(event: MonitorEvent): number;
11
+ /** Characters one monitor adds to a wake: its group, its overflow notice, and their separators. */
12
+ export declare function monitorShareChars(events: readonly MonitorEvent[], notice: string): number;
13
+ /**
14
+ * One wake message. The notifier fills a wake by whole monitors within `maxChars`, so the clip is a
15
+ * last-resort guard that a correctly sized wake never reaches.
16
+ */
17
+ export declare function buildWakeMessage(events: readonly MonitorEvent[], notices: readonly string[], pauseNotice: string, maxChars: number): string;
18
+ //# sourceMappingURL=monitor-notify-format.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { sanitizeTerminalOutput } from "./output-format.js";
2
+ export const SYSTEM_REMINDER_OPEN = "<system-reminder>";
3
+ export const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
4
+ export const WAKE_BUDGET_PAUSE_NOTICE = "Monitor paused after repeated updates. Completion still wakes this session; peek bash_output or re-arm only for intermediate events.";
5
+ export function eventBody(event) {
6
+ const value = event.type === "line" ? event.line : event.summary;
7
+ return sanitizeTerminalOutput(value)
8
+ .replace(/[\r\n]+/g, " ")
9
+ .trimEnd();
10
+ }
11
+ export function renderedLength(event) {
12
+ return `Monitor event(${event.description}): ${eventBody(event)}`.length;
13
+ }
14
+ function groupHeader(description) {
15
+ return `Monitor event(${description}): `;
16
+ }
17
+ /** Characters one monitor adds to a wake: its group, its overflow notice, and their separators. */
18
+ export function monitorShareChars(events, notice) {
19
+ const first = events[0];
20
+ const group = first === undefined
21
+ ? 0
22
+ : groupHeader(first.description).length +
23
+ events.reduce((total, event) => total + eventBody(event).length + 1, 0);
24
+ return group + (notice.length > 0 ? notice.length + 1 : 0);
25
+ }
26
+ function clip(text, maxChars) {
27
+ if (text.length <= maxChars)
28
+ return text;
29
+ if (maxChars <= 3)
30
+ return text.slice(0, maxChars);
31
+ return `${text.slice(0, maxChars - 3)}...`;
32
+ }
33
+ function formatEvents(events) {
34
+ const groups = new Map();
35
+ for (const event of events) {
36
+ const group = groups.get(event.id) ?? { description: event.description, bodies: [] };
37
+ group.bodies.push(eventBody(event));
38
+ groups.set(event.id, group);
39
+ }
40
+ return [...groups.values()].map((group) => `${groupHeader(group.description)}${group.bodies.join("\n")}`).join("\n");
41
+ }
42
+ /**
43
+ * One wake message. The notifier fills a wake by whole monitors within `maxChars`, so the clip is a
44
+ * last-resort guard that a correctly sized wake never reaches.
45
+ */
46
+ export function buildWakeMessage(events, notices, pauseNotice, maxChars) {
47
+ const suffix = [...notices, pauseNotice].filter(Boolean).join("\n");
48
+ const fixedChars = SYSTEM_REMINDER_OPEN.length + SYSTEM_REMINDER_CLOSE.length + (suffix ? suffix.length + 1 : 0);
49
+ const body = clip(formatEvents(events), Math.max(0, maxChars - fixedChars));
50
+ return `${SYSTEM_REMINDER_OPEN}${body}${body && suffix ? "\n" : ""}${suffix}${SYSTEM_REMINDER_CLOSE}`;
51
+ }
52
+ //# sourceMappingURL=monitor-notify-format.js.map
@@ -1,6 +1,8 @@
1
+ import { type MonitorSpill } from "./monitor-overflow.ts";
1
2
  import type { MonitorEvent } from "./monitor-registry.ts";
2
3
  import { type TerminalNotifierDeps } from "./notify.ts";
3
4
  import type { MonitorDeliverySettings } from "./settings.ts";
5
+ export type { MonitorSpill, MonitorSpillResult } from "./monitor-overflow.ts";
4
6
  export interface MonitorNotificationScheduler {
5
7
  now(): number;
6
8
  setTimeout(callback: () => void, delayMs: number): unknown;
@@ -10,11 +12,16 @@ export interface MonitorNotifierDeps extends TerminalNotifierDeps {
10
12
  readonly getSettings: () => MonitorDeliverySettings;
11
13
  /** Pause the monitors contributing to a shared wake-budget exhaustion. */
12
14
  readonly pauseMonitors: (ids: readonly string[]) => readonly string[];
15
+ /** Wake rate (and burst) of a paced monitor; undefined leaves the monitor unpaced. */
16
+ readonly wakesPerHour?: (id: string) => number | undefined;
17
+ /** Append overflow lines to the monitor's overflow file; undefined when it has none. */
18
+ readonly spillOverflow?: MonitorSpill;
13
19
  readonly scheduler?: MonitorNotificationScheduler;
14
20
  }
15
21
  /**
16
22
  * Session-scoped monitor delivery queue. It preserves the terminal runtime as the authoritative,
17
- * bounded event history while retaining only one capped coalescing batch for chat injection.
23
+ * bounded event history while retaining only capped per-monitor batches for chat injection. A
24
+ * wake is filled by whole monitors; a monitor that does not fit stays queued for the next wake.
18
25
  */
19
26
  export declare class MonitorNotifier {
20
27
  #private;