@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,9 +1,11 @@
1
1
  import { MONITOR_NOTIFICATION_CUSTOM_TYPE } from "../monitor-state-event.js";
2
+ import { monitorDetails } from "./monitor-notify-details.js";
3
+ import { buildWakeMessage, eventBody, renderedLength, WAKE_BUDGET_PAUSE_NOTICE } from "./monitor-notify-format.js";
4
+ import { OverflowBook } from "./monitor-overflow.js";
5
+ import { fitWake } from "./monitor-wake-fit.js";
6
+ import { WakePacer } from "./monitor-wake-pacing.js";
2
7
  import { getTerminalNotificationDelivery } from "./notify.js";
3
- import { sanitizeTerminalOutput } from "./output-format.js";
4
- import { FIRE_BUDGET_AUTO_MUTE_SUMMARY } from "./shared.js";
5
- const SYSTEM_REMINDER_OPEN = "<system-reminder>";
6
- const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
8
+ import { RUNAWAY_PAUSE_SUMMARY_PREFIX } from "./shared.js";
7
9
  const QUEUE_OVERHEAD_CHARS = 512;
8
10
  const WAKE_STREAK_QUIET_GAP_MULTIPLIER = 2;
9
11
  const systemScheduler = {
@@ -23,42 +25,20 @@ function resolveSettings(settings) {
23
25
  maxLinesPerInjection: boundedPositiveInt(settings.maxLinesPerInjection, 50, 1, 200),
24
26
  maxCharsPerInjection: boundedPositiveInt(settings.maxCharsPerInjection, 4096, 512, 16_384),
25
27
  wakeBudget: boundedPositiveInt(settings.wakeBudget, 5, 1, 100),
28
+ wakesPerHour: boundedPositiveInt(settings.wakesPerHour, 12, 1, 3600),
26
29
  };
27
30
  }
28
- function eventBody(event) {
29
- const value = event.type === "line" ? event.line : event.summary;
30
- return sanitizeTerminalOutput(value)
31
- .replace(/[\r\n]+/g, " ")
32
- .trimEnd();
33
- }
34
- function clip(text, maxChars) {
35
- if (text.length <= maxChars)
36
- return text;
37
- if (maxChars <= 3)
38
- return text.slice(0, maxChars);
39
- return `${text.slice(0, maxChars - 3)}...`;
40
- }
41
- function formatEvents(events) {
42
- const groups = new Map();
43
- for (const event of events) {
44
- const group = groups.get(event.id) ?? { description: event.description, bodies: [] };
45
- group.bodies.push(eventBody(event));
46
- groups.set(event.id, group);
47
- }
48
- return [...groups.values()]
49
- .map((group) => `Monitor event(${group.description}): ${group.bodies.join("\n")}`)
50
- .join("\n");
51
- }
52
31
  /**
53
32
  * Session-scoped monitor delivery queue. It preserves the terminal runtime as the authoritative,
54
- * bounded event history while retaining only one capped coalescing batch for chat injection.
33
+ * bounded event history while retaining only capped per-monitor batches for chat injection. A
34
+ * wake is filled by whole monitors; a monitor that does not fit stays queued for the next wake.
55
35
  */
56
36
  export class MonitorNotifier {
57
37
  #deps;
58
38
  #scheduler;
39
+ #pacer;
40
+ #overflow;
59
41
  #events = [];
60
- #eventChars = 0;
61
- #overflow = new Map();
62
42
  #lastInjectionAt = new Map();
63
43
  #lastInjectedBatch = new Map();
64
44
  #timer;
@@ -68,6 +48,8 @@ export class MonitorNotifier {
68
48
  constructor(deps) {
69
49
  this.#deps = deps;
70
50
  this.#scheduler = deps.scheduler ?? systemScheduler;
51
+ this.#pacer = new WakePacer((id) => deps.wakesPerHour?.(id));
52
+ this.#overflow = new OverflowBook(deps.spillOverflow);
71
53
  }
72
54
  notifyEvent(event) {
73
55
  if (event.type === "summary") {
@@ -77,17 +59,24 @@ export class MonitorNotifier {
77
59
  if (!getTerminalNotificationDelivery(this.#deps, MONITOR_NOTIFICATION_CUSTOM_TYPE))
78
60
  return;
79
61
  const settings = resolveSettings(this.#deps.getSettings());
80
- const rendered = `Monitor event(${event.description}): ${eventBody(event)}`;
81
62
  const queueLimit = Math.max(1, settings.maxCharsPerInjection - QUEUE_OVERHEAD_CHARS);
63
+ const own = this.#events.filter((queued) => queued.id === event.id);
64
+ const ownChars = own.reduce((total, queued) => total + renderedLength(queued), 0);
82
65
  if (event.type !== "summary" &&
83
- (this.#events.length >= settings.maxLinesPerInjection || this.#eventChars + rendered.length > queueLimit)) {
84
- this.#recordOverflow(event);
66
+ (own.length >= settings.maxLinesPerInjection ||
67
+ ownChars + renderedLength(event) > queueLimit ||
68
+ this.#overflow.count(event.id) > 0)) {
69
+ this.#overflow.record(event, eventBody(event));
85
70
  }
86
71
  else {
87
72
  this.#events.push(event);
88
- this.#eventChars += rendered.length;
89
73
  }
90
- this.#schedule(settings.coalesceWindowMs);
74
+ // A paced monitor out of tokens is not flushed before its next token: no wasted wake-ups.
75
+ const now = this.#scheduler.now();
76
+ const delay = event.type === "summary"
77
+ ? settings.coalesceWindowMs
78
+ : Math.max(settings.coalesceWindowMs, this.#pacer.readyAt(event.id, now) - now);
79
+ this.#schedule(delay);
91
80
  }
92
81
  /** Any explicit user or tool activity breaks a consecutive monitor-only wake streak. */
93
82
  noteActivity() {
@@ -110,23 +99,9 @@ export class MonitorNotifier {
110
99
  this.#timer = undefined;
111
100
  this.#scheduledAt = undefined;
112
101
  this.#events = [];
113
- this.#eventChars = 0;
114
102
  this.#overflow.clear();
115
103
  this.#lastInjectedBatch.clear();
116
- }
117
- #recordOverflow(event) {
118
- const overflow = this.#overflow.get(event.id) ?? {
119
- id: event.id,
120
- description: event.description,
121
- count: 0,
122
- kinds: new Set(),
123
- humanInput: false,
124
- };
125
- overflow.count++;
126
- overflow.kinds.add(event.type);
127
- if (event.type === "line" && event.humanInput === true)
128
- overflow.humanInput = true;
129
- this.#overflow.set(event.id, overflow);
104
+ this.#pacer.clear();
130
105
  }
131
106
  #schedule(delayMs) {
132
107
  const due = this.#scheduler.now() + delayMs;
@@ -137,6 +112,9 @@ export class MonitorNotifier {
137
112
  this.#scheduledAt = due;
138
113
  this.#timer = this.#scheduler.setTimeout(() => this.#flush(), delayMs);
139
114
  }
115
+ #pendingIds() {
116
+ return new Set([...this.#events.map((event) => event.id), ...this.#overflow.ids()]);
117
+ }
140
118
  #flush() {
141
119
  this.#timer = undefined;
142
120
  this.#scheduledAt = undefined;
@@ -147,83 +125,84 @@ export class MonitorNotifier {
147
125
  }
148
126
  const settings = resolveSettings(this.#deps.getSettings());
149
127
  const now = this.#scheduler.now();
150
- const pendingIds = new Set([...this.#events.map((event) => event.id), ...this.#overflow.keys()]);
128
+ const pendingIds = this.#pendingIds();
151
129
  if (pendingIds.size === 0)
152
130
  return;
153
131
  const summaryIds = new Set(this.#events.filter((event) => event.type === "summary").map((event) => event.id));
154
- const ready = new Set([...pendingIds].filter((id) => {
132
+ const ready = [...pendingIds].filter((id) => {
155
133
  if (summaryIds.has(id))
156
134
  return true;
157
135
  const last = this.#lastInjectionAt.get(id);
158
- return last === undefined || now - last >= settings.rateLimitMs;
159
- }));
160
- if (ready.size === 0) {
161
- this.#scheduleNextRateLimit(pendingIds, now, settings);
136
+ if (last !== undefined && now - last < settings.rateLimitMs)
137
+ return false;
138
+ return this.#pacer.readyAt(id, now) <= now;
139
+ });
140
+ if (ready.length === 0) {
141
+ this.#scheduleNext(pendingIds, now, settings);
162
142
  return;
163
143
  }
164
144
  const fingerprints = this.#batchFingerprints(ready);
165
145
  const suppressedIds = new Set([...fingerprints]
166
146
  .filter(([id, fingerprint]) => this.#lastInjectedBatch.get(id) === fingerprint)
167
147
  .map(([id]) => id));
168
- const injectedIds = new Set([...ready].filter((id) => !suppressedIds.has(id)));
169
- const selected = this.#events.filter((event) => injectedIds.has(event.id));
170
- const deferred = this.#events.filter((event) => !ready.has(event.id));
171
- if (injectedIds.size === 0) {
172
- this.#discardSuppressed(deferred, now, settings);
148
+ this.#events = this.#events.filter((event) => !suppressedIds.has(event.id));
149
+ const wake = fitWake(this.#events, this.#overflow, ready.filter((id) => !suppressedIds.has(id)), summaryIds, settings.maxCharsPerInjection);
150
+ if (wake.size === 0) {
151
+ this.#scheduleNext(this.#pendingIds(), now, settings);
173
152
  return;
174
153
  }
175
- const overflowCount = [...this.#overflow.values()]
176
- .filter((overflow) => injectedIds.has(overflow.id))
177
- .reduce((total, overflow) => total + overflow.count, 0);
154
+ const injectedIds = new Set(wake.keys());
155
+ const selected = [...wake.values()].flatMap((share) => share.events);
156
+ const delivered = new Set(selected);
178
157
  if (this.#lastWakeAt !== undefined &&
179
158
  now - this.#lastWakeAt > settings.rateLimitMs * WAKE_STREAK_QUIET_GAP_MULTIPLIER) {
180
159
  this.#consecutiveWakes = 0;
181
160
  }
182
161
  const deliversSummary = selected.some(
183
- // A fire-budget auto-mute summary is a mute, not a completion: exempting it keeps the
162
+ // A runaway-guard pause summary is a pause, not a completion: exempting it keeps the
184
163
  // session-global wake streak — the notifier's own pause state — from being cleared.
185
- (event) => event.type === "summary" && event.summary !== FIRE_BUDGET_AUTO_MUTE_SUMMARY);
164
+ (event) => event.type === "summary" && !event.summary.startsWith(RUNAWAY_PAUSE_SUMMARY_PREFIX));
186
165
  const reachesBudget = !deliversSummary && this.#consecutiveWakes + 1 >= settings.wakeBudget;
187
- const pauseNotice = reachesBudget
188
- ? "Monitor paused after repeated updates. Completion still wakes this session; peek bash_output or re-arm only for intermediate events."
189
- : "";
190
- const content = this.#buildMessage(selected, overflowCount, pauseNotice, settings.maxCharsPerInjection);
191
- const details = { monitors: this.#monitorDetails(selected, injectedIds) };
166
+ const notices = [...wake].map(([id, share]) => (share.whole ? this.#overflow.notice(id) : ""));
167
+ const content = buildWakeMessage(selected, notices, reachesBudget ? WAKE_BUDGET_PAUSE_NOTICE : "", settings.maxCharsPerInjection);
168
+ const details = { monitors: monitorDetails(selected, injectedIds, this.#overflow) };
192
169
  delivery.send(content, reachesBudget ? { forceWake: true, details } : { details });
193
170
  this.#lastWakeAt = now;
194
- for (const id of injectedIds) {
171
+ for (const [id, share] of wake) {
172
+ if (share.events.some((event) => event.type === "summary"))
173
+ this.#pacer.forget(id);
174
+ else
175
+ this.#pacer.spend(id, now);
195
176
  this.#lastInjectionAt.set(id, now);
196
- this.#overflow.delete(id);
197
- const fingerprint = fingerprints.get(id);
177
+ // A monitor that sent only a prefix keeps its remaining lines and overflow for the next wake.
178
+ if (share.whole)
179
+ this.#overflow.delete(id);
180
+ const fingerprint = share.whole ? fingerprints.get(id) : undefined;
198
181
  if (fingerprint === undefined)
199
182
  this.#lastInjectedBatch.delete(id);
200
183
  else
201
184
  this.#lastInjectedBatch.set(id, fingerprint);
202
185
  }
203
- this.#events = deferred;
204
- this.#eventChars = deferred.reduce((total, event) => total + `Monitor event(${event.description}): ${eventBody(event)}`.length, 0);
186
+ this.#events = this.#events.filter((event) => !delivered.has(event));
205
187
  this.#consecutiveWakes = deliversSummary ? 0 : this.#consecutiveWakes + 1;
206
188
  if (reachesBudget) {
189
+ // Only the monitors in this wake are paused; every other monitor keeps its held lines.
207
190
  this.#deps.pauseMonitors([...injectedIds]);
208
- this.#events = [];
209
- this.#eventChars = 0;
210
- this.#overflow.clear();
211
191
  this.#consecutiveWakes = 0;
212
- return;
213
192
  }
214
- const remainingIds = new Set([...this.#events.map((event) => event.id), ...this.#overflow.keys()]);
193
+ const remainingIds = this.#pendingIds();
215
194
  if (remainingIds.size > 0)
216
- this.#scheduleNextRateLimit(remainingIds, now, settings);
195
+ this.#scheduleNext(remainingIds, now, settings);
217
196
  }
218
- #scheduleNextRateLimit(ids, now, settings) {
219
- const nextAt = Math.min(...[...ids].map((id) => (this.#lastInjectionAt.get(id) ?? now) + settings.rateLimitMs));
197
+ #scheduleNext(ids, now, settings) {
198
+ const nextAt = Math.min(...[...ids].map((id) => Math.max((this.#lastInjectionAt.get(id) ?? now) + settings.rateLimitMs, this.#pacer.readyAt(id, now))));
220
199
  this.#schedule(Math.max(1, nextAt - now));
221
200
  }
222
201
  /** A ready monitor whose line-only batch matches its previous injection carries no new information. */
223
202
  #batchFingerprints(ids) {
224
203
  const fingerprints = new Map();
225
204
  for (const id of ids) {
226
- if ((this.#overflow.get(id)?.count ?? 0) > 0)
205
+ if (this.#overflow.count(id) > 0)
227
206
  continue;
228
207
  const batch = this.#events.filter((event) => event.id === id);
229
208
  if (batch.length === 0 || batch.some((event) => event.type !== "line"))
@@ -232,38 +211,5 @@ export class MonitorNotifier {
232
211
  }
233
212
  return fingerprints;
234
213
  }
235
- #discardSuppressed(deferred, now, settings) {
236
- this.#events = deferred;
237
- this.#eventChars = deferred.reduce((total, event) => total + `Monitor event(${event.description}): ${eventBody(event)}`.length, 0);
238
- const remainingIds = new Set([...deferred.map((event) => event.id), ...this.#overflow.keys()]);
239
- if (remainingIds.size > 0)
240
- this.#scheduleNextRateLimit(remainingIds, now, settings);
241
- }
242
- #monitorDetails(events, ids) {
243
- return [...ids].map((id) => {
244
- const own = events.filter((event) => event.id === id);
245
- const overflow = this.#overflow.get(id);
246
- const kinds = new Set(own.map((event) => event.type));
247
- for (const kind of overflow?.kinds ?? [])
248
- kinds.add(kind);
249
- const humanInput = own.some((event) => event.type === "line" && event.humanInput === true) || overflow?.humanInput === true;
250
- return {
251
- id,
252
- description: own[0]?.description ?? overflow?.description ?? id,
253
- eventCount: own.length + (overflow?.count ?? 0),
254
- kinds: [...kinds],
255
- ...(humanInput ? { humanInput: true } : {}),
256
- };
257
- });
258
- }
259
- #buildMessage(events, overflowCount, pauseNotice, maxChars) {
260
- const overflowNotice = overflowCount > 0
261
- ? `[${overflowCount} additional event lines omitted; peek bash_output for full history.]`
262
- : "";
263
- const suffix = [overflowNotice, pauseNotice].filter(Boolean).join("\n");
264
- const fixedChars = SYSTEM_REMINDER_OPEN.length + SYSTEM_REMINDER_CLOSE.length + (suffix ? suffix.length + 1 : 0);
265
- const body = clip(formatEvents(events), Math.max(0, maxChars - fixedChars));
266
- return `${SYSTEM_REMINDER_OPEN}${body}${body && suffix ? "\n" : ""}${suffix}${SYSTEM_REMINDER_CLOSE}`;
267
- }
268
214
  }
269
215
  //# sourceMappingURL=monitor-notify.js.map
@@ -0,0 +1,7 @@
1
+ import type { MonitorSpillResult } from "./monitor-overflow.ts";
2
+ export declare function monitorOverflowPath(terminalDir: string, monitorId: string): string;
3
+ /** Append `lines` to the watch's overflow file. Throws on a filesystem error. */
4
+ export declare function appendMonitorOverflow(terminalDir: string, monitorId: string, lines: readonly string[], maxBytes?: number): MonitorSpillResult;
5
+ /** Remove overflow files (and rotated copies) not written for longer than a durable watch lives. */
6
+ export declare function sweepMonitorOverflowFiles(terminalDir: string, now?: number, maxAgeMs?: number): Promise<number>;
7
+ //# sourceMappingURL=monitor-overflow-file.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Per-monitor overflow file: lines a paced persistent watch matched beyond what one wake carries.
3
+ * The wake names this file, so no matched line is lost to the batch cap. It lives in the session's
4
+ * terminal state dir (not the watch's own SENPI_MONITOR_STATE_DIR, which belongs to the command and
5
+ * is removed when the watch ends, while the agent may still need to read the overflow), and is
6
+ * bounded: past MONITOR_OVERFLOW_FILE_MAX_BYTES it rotates to one `.1` copy. Files untouched for
7
+ * longer than a durable watch can live are swept at session start.
8
+ */
9
+ import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs";
10
+ import { readdir, stat, unlink } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { DURABLE_MONITOR_EXPIRY_MS, MONITOR_OVERFLOW_FILE_MAX_BYTES } from "./shared.js";
13
+ function overflowDir(terminalDir) {
14
+ return join(terminalDir, "overflow");
15
+ }
16
+ export function monitorOverflowPath(terminalDir, monitorId) {
17
+ return join(overflowDir(terminalDir), `${monitorId}.log`);
18
+ }
19
+ function isEnoent(error) {
20
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
21
+ }
22
+ /** Append `lines` to the watch's overflow file. Throws on a filesystem error. */
23
+ export function appendMonitorOverflow(terminalDir, monitorId, lines, maxBytes = MONITOR_OVERFLOW_FILE_MAX_BYTES) {
24
+ const path = monitorOverflowPath(terminalDir, monitorId);
25
+ mkdirSync(overflowDir(terminalDir), { recursive: true, mode: 0o700 });
26
+ let size = 0;
27
+ try {
28
+ size = statSync(path).size;
29
+ }
30
+ catch (error) {
31
+ if (!isEnoent(error))
32
+ throw error;
33
+ }
34
+ const rotated = size >= maxBytes;
35
+ if (rotated)
36
+ renameSync(path, `${path}.1`);
37
+ appendFileSync(path, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 });
38
+ return { path, rotated };
39
+ }
40
+ /** Remove overflow files (and rotated copies) not written for longer than a durable watch lives. */
41
+ export async function sweepMonitorOverflowFiles(terminalDir, now = Date.now(), maxAgeMs = DURABLE_MONITOR_EXPIRY_MS) {
42
+ let names;
43
+ try {
44
+ names = await readdir(overflowDir(terminalDir));
45
+ }
46
+ catch (error) {
47
+ if (isEnoent(error))
48
+ return 0;
49
+ throw error;
50
+ }
51
+ let removed = 0;
52
+ for (const name of names) {
53
+ if (!name.endsWith(".log") && !name.endsWith(".log.1"))
54
+ continue;
55
+ const path = join(overflowDir(terminalDir), name);
56
+ const info = await stat(path).catch(() => undefined);
57
+ if (info === undefined || now - info.mtimeMs <= maxAgeMs)
58
+ continue;
59
+ await unlink(path).catch((error) => {
60
+ if (!isEnoent(error))
61
+ throw error;
62
+ });
63
+ removed += 1;
64
+ }
65
+ return removed;
66
+ }
67
+ //# sourceMappingURL=monitor-overflow-file.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Overflow bookkeeping of the monitor notifier: events that did not fit a monitor's batch. Line
3
+ * bodies are written to the monitor's overflow file in chunks (when a spill sink exists), and the
4
+ * wake that carries the monitor names the file; without a file the wake says how many were omitted.
5
+ */
6
+ import type { MonitorEvent } from "./monitor-registry.ts";
7
+ export interface MonitorSpillResult {
8
+ readonly path: string;
9
+ /** The file rotated to `<path>.1` while this chunk was written. */
10
+ readonly rotated: boolean;
11
+ }
12
+ export type MonitorSpill = (id: string, lines: readonly string[]) => MonitorSpillResult | undefined;
13
+ export interface MonitorOverflow {
14
+ readonly id: string;
15
+ readonly description: string;
16
+ count: number;
17
+ kinds: Set<MonitorEvent["type"]>;
18
+ humanInput: boolean;
19
+ pending: string[];
20
+ spilled: number;
21
+ path: string | undefined;
22
+ rotated: boolean;
23
+ }
24
+ export declare class OverflowBook {
25
+ #private;
26
+ constructor(spill: MonitorSpill | undefined);
27
+ get(id: string): MonitorOverflow | undefined;
28
+ count(id: string): number;
29
+ ids(): string[];
30
+ record(event: MonitorEvent, body: string): void;
31
+ flush(id: string): void;
32
+ /** The notice a wake carries for this monitor's overflow; "" when nothing overflowed. */
33
+ notice(id: string): string;
34
+ delete(id: string): void;
35
+ clear(): void;
36
+ }
37
+ //# sourceMappingURL=monitor-overflow.d.ts.map
@@ -0,0 +1,75 @@
1
+ /** Lines are written out in chunks of this many, so a burst never sits whole in memory. */
2
+ const SPILL_CHUNK_LINES = 256;
3
+ export class OverflowBook {
4
+ #spill;
5
+ #entries = new Map();
6
+ constructor(spill) {
7
+ this.#spill = spill;
8
+ }
9
+ get(id) {
10
+ return this.#entries.get(id);
11
+ }
12
+ count(id) {
13
+ return this.#entries.get(id)?.count ?? 0;
14
+ }
15
+ ids() {
16
+ return [...this.#entries.keys()];
17
+ }
18
+ record(event, body) {
19
+ const overflow = this.#entries.get(event.id) ?? {
20
+ id: event.id,
21
+ description: event.description,
22
+ count: 0,
23
+ kinds: new Set(),
24
+ humanInput: false,
25
+ pending: [],
26
+ spilled: 0,
27
+ path: undefined,
28
+ rotated: false,
29
+ };
30
+ overflow.count += 1;
31
+ overflow.kinds.add(event.type);
32
+ if (event.type === "line" && event.humanInput === true)
33
+ overflow.humanInput = true;
34
+ this.#entries.set(event.id, overflow);
35
+ if (this.#spill === undefined)
36
+ return;
37
+ overflow.pending.push(body);
38
+ if (overflow.pending.length >= SPILL_CHUNK_LINES)
39
+ this.flush(event.id);
40
+ }
41
+ flush(id) {
42
+ const overflow = this.#entries.get(id);
43
+ if (overflow === undefined || overflow.pending.length === 0 || this.#spill === undefined)
44
+ return;
45
+ const result = this.#spill(id, overflow.pending);
46
+ if (result !== undefined) {
47
+ overflow.spilled += overflow.pending.length;
48
+ overflow.path = result.path;
49
+ overflow.rotated ||= result.rotated;
50
+ }
51
+ overflow.pending = [];
52
+ }
53
+ /** The notice a wake carries for this monitor's overflow; "" when nothing overflowed. */
54
+ notice(id) {
55
+ const overflow = this.#entries.get(id);
56
+ if (overflow === undefined || overflow.count === 0)
57
+ return "";
58
+ const notices = [];
59
+ if (overflow.spilled > 0 && overflow.path !== undefined) {
60
+ const rotated = overflow.rotated ? `; earlier ones in ${overflow.path}.1` : "";
61
+ notices.push(`[${overflow.spilled} more lines in ${overflow.path}${rotated}]`);
62
+ }
63
+ const unspilled = overflow.count - overflow.spilled;
64
+ if (unspilled > 0)
65
+ notices.push(`[${unspilled} additional event lines omitted; peek bash_output for full history.]`);
66
+ return notices.join("\n");
67
+ }
68
+ delete(id) {
69
+ this.#entries.delete(id);
70
+ }
71
+ clear() {
72
+ this.#entries.clear();
73
+ }
74
+ }
75
+ //# sourceMappingURL=monitor-overflow.js.map
@@ -7,6 +7,7 @@ import { type RegisterMonitorOptions } from "./command-monitor-source.ts";
7
7
  import { type RegisterFileMonitorOptions } from "./file-monitor-source.ts";
8
8
  import type { MonitorEndedEvent, MonitorEvent, MonitorFireWindow, MonitorRearmResult, MonitorResumeResult, MonitorSnapshotEntry } from "./monitor-types.ts";
9
9
  import type { RegisterRpcMonitorOptions } from "./rpc-monitor-source.ts";
10
+ import type { MonitorRunawayGuard } from "./settings.ts";
10
11
  export type { RegisterMonitorOptions } from "./command-monitor-source.ts";
11
12
  export type { RegisterFileMonitorOptions } from "./file-monitor-source.ts";
12
13
  export { allocateMonitorId, type MonitorEndedEvent, type MonitorEndedReason, type MonitorEvent, type MonitorFireWindow, type MonitorLineEvent, type MonitorRearmResult, type MonitorResumeResult, type MonitorSnapshotEntry, type MonitorSummaryEvent, } from "./monitor-types.ts";
@@ -23,6 +24,15 @@ export interface MonitorRegistryOptions {
23
24
  export declare class MonitorRegistry {
24
25
  #private;
25
26
  constructor(emit: (event: MonitorEvent) => void, options?: MonitorRegistryOptions);
27
+ /** Apply the resolved runaway-guard settings; the registry outlives a reload, the settings do not. */
28
+ setRunawayGuard(guard: MonitorRunawayGuard): void;
29
+ /**
30
+ * Wake pacing of a live watch: only persistent command watches are paced. Returns the watch's
31
+ * own wake-rate override (undefined = the setting) or undefined when the watch is not paced.
32
+ */
33
+ wakePacing(id: string): {
34
+ readonly wakesPerHour: number | undefined;
35
+ } | undefined;
26
36
  /** Attachment parking is independent of persisted monitor mute and wake budgets. */
27
37
  setParked(parked: boolean): void;
28
38
  snapshot(): readonly MonitorSnapshotEntry[];
@@ -6,6 +6,7 @@
6
6
  import { CommandMonitorSource } from "./command-monitor-source.js";
7
7
  import { FileMonitorSource } from "./file-monitor-source.js";
8
8
  import { loadRpcMonitorSource } from "./rpc-monitor.lazy.js";
9
+ import { DEFAULT_MONITOR_RUNAWAY_LINES_PER_MINUTE, DEFAULT_MONITOR_RUNAWAY_MINUTES } from "./shared.js";
9
10
  export { allocateMonitorId, } from "./monitor-types.js";
10
11
  export class MonitorRegistry {
11
12
  #emit;
@@ -17,6 +18,10 @@ export class MonitorRegistry {
17
18
  #host;
18
19
  #rpc;
19
20
  #parked = false;
21
+ #runawayGuard = {
22
+ linesPerMinute: DEFAULT_MONITOR_RUNAWAY_LINES_PER_MINUTE,
23
+ minutes: DEFAULT_MONITOR_RUNAWAY_MINUTES,
24
+ };
20
25
  constructor(emit, options) {
21
26
  this.#emit = emit;
22
27
  this.#onChange = options?.onChange;
@@ -27,11 +32,26 @@ export class MonitorRegistry {
27
32
  emitRaw: (event) => this.#emit(event),
28
33
  ended: (record, reason, exitCode) => this.#emitEnded(record, reason, exitCode),
29
34
  changed: () => this.#notifyChange(),
35
+ runawayGuard: () => this.#runawayGuard,
30
36
  };
31
37
  this.#host = host;
32
38
  this.#commands = new CommandMonitorSource(host);
33
39
  this.#files = new FileMonitorSource(host, options?.reserve);
34
40
  }
41
+ /** Apply the resolved runaway-guard settings; the registry outlives a reload, the settings do not. */
42
+ setRunawayGuard(guard) {
43
+ this.#runawayGuard = guard;
44
+ }
45
+ /**
46
+ * Wake pacing of a live watch: only persistent command watches are paced. Returns the watch's
47
+ * own wake-rate override (undefined = the setting) or undefined when the watch is not paced.
48
+ */
49
+ wakePacing(id) {
50
+ const record = this.#commands.get(id);
51
+ if (record === undefined || !record.persistent)
52
+ return undefined;
53
+ return { wakesPerHour: record.wakesPerHour };
54
+ }
35
55
  /** Attachment parking is independent of persisted monitor mute and wake budgets. */
36
56
  setParked(parked) {
37
57
  if (this.#parked === parked)
@@ -127,9 +147,12 @@ export class MonitorRegistry {
127
147
  const mutedDropped = "mutedDropped" in record ? record.mutedDropped : 0;
128
148
  if ("mutedDropped" in record)
129
149
  record.mutedDropped = 0;
130
- // A rearm (or any resume) restarts the rolling fire budget while keeping its window start.
150
+ // A rearm (or any resume) restarts the rolling fire count while keeping its window start,
151
+ // and gives the runaway guard a clean minute history.
131
152
  if ("fireWindow" in record && record.fireWindow !== undefined)
132
153
  record.fireWindow.count = 0;
154
+ if ("runaway" in record)
155
+ record.runaway = undefined;
133
156
  resumed.push({ id: record.id, mutedDropped });
134
157
  if ("watch" in record && !this.#files.parked)
135
158
  record.watch.resume();
@@ -67,5 +67,10 @@ export interface MonitorSourceHost {
67
67
  emitRaw(event: MonitorEvent): void;
68
68
  ended(record: MonitorRecordBase, reason: MonitorEndedReason, exitCode: number | null): void;
69
69
  changed(): void;
70
+ /** The current runaway-guard thresholds (settings can change across a reload). */
71
+ runawayGuard(): {
72
+ readonly linesPerMinute: number;
73
+ readonly minutes: number;
74
+ };
70
75
  }
71
76
  //# sourceMappingURL=monitor-types.d.ts.map
@@ -0,0 +1,9 @@
1
+ import type { OverflowBook } from "./monitor-overflow.ts";
2
+ import type { MonitorEvent } from "./monitor-registry.ts";
3
+ export interface WakeShare {
4
+ readonly events: MonitorEvent[];
5
+ /** Every queued line went; when false the monitor keeps its rest and overflow for the next wake. */
6
+ readonly whole: boolean;
7
+ }
8
+ export declare function fitWake(queue: readonly MonitorEvent[], overflow: OverflowBook, candidates: readonly string[], summaryIds: ReadonlySet<string>, maxChars: number): Map<string, WakeShare>;
9
+ //# sourceMappingURL=monitor-wake-fit.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Filling one monitor wake within `maxChars`, measured from the real wrapper (reminder tags, the
3
+ * pause notice, each monitor's actual overflow notice), never an assumed reserve. Monitors with a
4
+ * summary go first, then queue order; a monitor that does not fit whole waits for the next wake.
5
+ * Only when the first monitor alone is too big does it send a prefix of its lines; the rest stay
6
+ * queued. The line cap already holds per monitor at enqueue, so a wake is never clipped.
7
+ */
8
+ import { monitorShareChars, SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN, WAKE_BUDGET_PAUSE_NOTICE, } from "./monitor-notify-format.js";
9
+ export function fitWake(queue, overflow, candidates, summaryIds, maxChars) {
10
+ const queued = [...new Set([...queue.map((event) => event.id), ...overflow.ids()])].filter((id) => candidates.includes(id));
11
+ const ordered = [...queued.filter((id) => summaryIds.has(id)), ...queued.filter((id) => !summaryIds.has(id))];
12
+ const budget = maxChars - SYSTEM_REMINDER_OPEN.length - SYSTEM_REMINDER_CLOSE.length - WAKE_BUDGET_PAUSE_NOTICE.length - 1;
13
+ const wake = new Map();
14
+ let chars = 0;
15
+ for (const id of ordered) {
16
+ overflow.flush(id);
17
+ const own = queue.filter((event) => event.id === id);
18
+ const share = monitorShareChars(own, overflow.notice(id));
19
+ if (chars + share <= budget || (wake.size === 0 && own.length === 0)) {
20
+ wake.set(id, { events: own, whole: true });
21
+ chars += share;
22
+ }
23
+ else if (wake.size === 0) {
24
+ let count = 1;
25
+ while (count < own.length && monitorShareChars(own.slice(0, count + 1), "") <= budget)
26
+ count += 1;
27
+ const prefix = own.slice(0, count);
28
+ wake.set(id, { events: prefix, whole: prefix.length === own.length && overflow.count(id) === 0 });
29
+ chars += monitorShareChars(prefix, "");
30
+ }
31
+ }
32
+ return wake;
33
+ }
34
+ //# sourceMappingURL=monitor-wake-fit.js.map
@@ -0,0 +1,10 @@
1
+ export declare class WakePacer {
2
+ #private;
3
+ constructor(rateOf: (id: string) => number | undefined);
4
+ /** When `id` next holds a whole token: `now` for an unpaced monitor or one with a token left. */
5
+ readyAt(id: string, now: number): number;
6
+ spend(id: string, now: number): void;
7
+ forget(id: string): void;
8
+ clear(): void;
9
+ }
10
+ //# sourceMappingURL=monitor-wake-pacing.d.ts.map