@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.
- package/CHANGELOG.md +33 -0
- package/dist/bundle/chunks/{anthropic-messages-S5BTP7U2.js → anthropic-messages-2IQD75YN.js} +1 -1
- package/dist/bundle/chunks/args-SV2KM4KA.js +2 -0
- package/dist/bundle/chunks/azure-openai-responses-HFI6SDH5.js +2 -0
- package/dist/bundle/chunks/{chunk-WQLMQGOW.js → chunk-2AJB5S4C.js} +1 -1
- package/dist/bundle/chunks/{chunk-XSRM367N.js → chunk-3AK3WUN4.js} +2 -2
- package/dist/bundle/chunks/{chunk-6TL72TUD.js → chunk-4HJTPWZY.js} +1 -1
- package/dist/bundle/chunks/chunk-4NPBDGC4.js +26 -0
- package/dist/bundle/chunks/{chunk-E4WOCULF.js → chunk-4XR5HDWC.js} +72 -69
- package/dist/bundle/chunks/{chunk-6IWSMXBA.js → chunk-5GC4IQ3A.js} +1 -1
- package/dist/bundle/chunks/{chunk-7I7ZMSBF.js → chunk-A4B4CMOR.js} +1 -1
- package/dist/bundle/chunks/chunk-B4N4N7BR.js +8 -0
- package/dist/bundle/chunks/{chunk-X23XXKMH.js → chunk-BVFJAXWO.js} +1 -1
- package/dist/bundle/chunks/{chunk-25B6JQC4.js → chunk-C3BJRXEV.js} +1 -1
- package/dist/bundle/chunks/{chunk-DKBJM22C.js → chunk-CQDU6S6C.js} +2 -2
- package/dist/bundle/chunks/{chunk-MF57DS3V.js → chunk-EV4TWAJI.js} +1 -1
- package/dist/bundle/chunks/{chunk-JJXDWX46.js → chunk-F3JGTOLH.js} +1 -1
- package/dist/bundle/chunks/{chunk-CRY5CP3B.js → chunk-G42JHX5Q.js} +3 -3
- package/dist/bundle/chunks/{chunk-5VRI3CME.js → chunk-G7VYQRE7.js} +2 -2
- package/dist/bundle/chunks/chunk-GVSBCJFI.js +2 -0
- package/dist/bundle/chunks/{chunk-KUZPBZUK.js → chunk-HKDGQE6D.js} +2 -2
- package/dist/bundle/chunks/{chunk-HD67U37N.js → chunk-HRV7NEOJ.js} +1 -1
- package/dist/bundle/chunks/chunk-JDETIKL7.js +7 -0
- package/dist/bundle/chunks/{chunk-MVNVDGYF.js → chunk-JGMG227A.js} +1 -1
- package/dist/bundle/chunks/chunk-MHD7SSHF.js +2 -0
- package/dist/bundle/chunks/{chunk-CEQMTVYS.js → chunk-MXLEWTWC.js} +2 -2
- package/dist/bundle/chunks/{chunk-C5W3D56I.js → chunk-N6UN2SDS.js} +3 -3
- package/dist/bundle/chunks/chunk-NTK2MHJ5.js +2 -0
- package/dist/bundle/chunks/chunk-OMN4QWFA.js +3 -0
- package/dist/bundle/chunks/chunk-PLQ5WRXN.js +2 -0
- package/dist/bundle/chunks/{chunk-2MOGCBPT.js → chunk-PUMCQ5HS.js} +1 -1
- package/dist/bundle/chunks/{chunk-55YTTFNY.js → chunk-PUQFKAB4.js} +1 -1
- package/dist/bundle/chunks/{chunk-IJT3GSB2.js → chunk-SN7PTZTC.js} +1 -1
- package/dist/bundle/chunks/{chunk-FWHSGCWR.js → chunk-T2IIUI3J.js} +3 -3
- package/dist/bundle/chunks/{chunk-5AOKDC6G.js → chunk-TSETMVJP.js} +1 -1
- package/dist/bundle/chunks/{chunk-SR7DBNWN.js → chunk-V6AYNGMX.js} +1 -1
- package/dist/bundle/chunks/chunk-VDPJ7W52.js +11 -0
- package/dist/bundle/chunks/{chunk-EPQK674G.js → chunk-VLKULV6F.js} +1 -1
- package/dist/bundle/chunks/{chunk-SL7TSFR5.js → chunk-VXACHCLB.js} +1 -1
- package/dist/bundle/chunks/{chunk-FM7NDZR5.js → chunk-VYSRUDEU.js} +1 -1
- package/dist/bundle/chunks/chunk-YMFGBSO3.js +2 -0
- package/dist/bundle/chunks/cli-main-RA6EBOOU.js +3 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-4QEQ7MGK.js → cloudflare-workers-ai-system-one-BVENEDT6.js} +1 -1
- package/dist/bundle/chunks/cursor-agent.js +3 -3
- package/dist/bundle/chunks/google-generative-ai-ONS2KSGN.js +2 -0
- package/dist/bundle/chunks/google-vertex-5VS7ZESH.js +2 -0
- package/dist/bundle/chunks/help-fast-path-FZY53UXB.js +2 -0
- package/dist/bundle/chunks/host-command-TNUIHEFP.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-EZQ5VCQV.js → host-lifecycle-LDOHAYLK.js} +1 -1
- package/dist/bundle/chunks/host-session-command-OKASUQPA.js +20 -0
- package/dist/bundle/chunks/interactive-mode-PSGTSURY.js +2 -0
- package/dist/bundle/chunks/{mistral-conversations-5EHLE7PZ.js → mistral-conversations-RLDMF2BI.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-OMJZ5VXM.js → multi-session-host-J5BVVUG3.js} +7 -6
- package/dist/bundle/chunks/openai-codex-responses-VRIJIZJG.js +10 -0
- package/dist/bundle/chunks/{openai-completions-NHDMDAS7.js → openai-completions-EZBOIXHP.js} +2 -2
- package/dist/bundle/chunks/{openai-images-PNXQDBSP.js → openai-images-KKM343F2.js} +1 -1
- package/dist/bundle/chunks/openai-responses-CMBH3DCY.js +3 -0
- package/dist/bundle/chunks/{package-manager-cli-7GQDB5DX.js → package-manager-cli-JHVRG35I.js} +1 -1
- package/dist/bundle/chunks/{role-WQWYL4ZD.js → role-LNF5MFGC.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-JK3Q4IWI.js +2 -0
- package/dist/bundle/chunks/rpc-mode-HVSPPRTT.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-FI7H7LGY.js → rpc-monitor-source-2DQZLFFJ.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-IQAKNRTV.js → runtime-session-CLNODAYE.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-XXJC4QQN.js → schedule-command-OMI4VWUV.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-UPYF4OFP.js +3 -0
- package/dist/bundle/chunks/session-picker-NR7CRVNQ.js +2 -0
- package/dist/bundle/chunks/session-worker.js +143 -139
- package/dist/bundle/chunks/{typesafe-system-one-W2ZTYF7X.js → typesafe-system-one-PJIWA5SP.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli/host-command.d.ts +2 -0
- package/dist/cli/host-command.js +14 -19
- package/dist/cli/host-handover-args.d.ts +4 -0
- package/dist/cli/host-handover-args.js +20 -0
- package/dist/core/agent-session.d.ts +29 -1
- package/dist/core/agent-session.js +74 -19
- package/dist/core/auth-storage.d.ts +44 -1
- package/dist/core/auth-storage.js +120 -3
- package/dist/core/client-message-identity.d.ts +3 -0
- package/dist/core/credential-accounts.d.ts +2 -0
- package/dist/core/credential-accounts.js +8 -1
- package/dist/core/credential-pool/rotation-slots.js +3 -2
- package/dist/core/extensions/builtin/account/index.js +6 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +2 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.js +6 -5
- package/dist/core/extensions/builtin/goal/provider-retry.d.ts +2 -2
- package/dist/core/extensions/builtin/goal/provider-retry.js +18 -6
- package/dist/core/extensions/builtin/terminal/command-monitor-source.d.ts +17 -4
- package/dist/core/extensions/builtin/terminal/command-monitor-source.js +33 -6
- package/dist/core/extensions/builtin/terminal/durable-command.js +1 -0
- package/dist/core/extensions/builtin/terminal/extension.js +28 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-details.d.ts +6 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-details.js +19 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-format.d.ts +18 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify-format.js +52 -0
- package/dist/core/extensions/builtin/terminal/monitor-notify.d.ts +8 -1
- package/dist/core/extensions/builtin/terminal/monitor-notify.js +65 -119
- package/dist/core/extensions/builtin/terminal/monitor-overflow-file.d.ts +7 -0
- package/dist/core/extensions/builtin/terminal/monitor-overflow-file.js +67 -0
- package/dist/core/extensions/builtin/terminal/monitor-overflow.d.ts +37 -0
- package/dist/core/extensions/builtin/terminal/monitor-overflow.js +75 -0
- package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/monitor-registry.js +24 -1
- package/dist/core/extensions/builtin/terminal/monitor-types.d.ts +5 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-fit.d.ts +9 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-fit.js +34 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/monitor-wake-pacing.js +41 -0
- package/dist/core/extensions/builtin/terminal/restore-session.js +2 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +5 -1
- package/dist/core/extensions/builtin/terminal/settings.d.ts +10 -0
- package/dist/core/extensions/builtin/terminal/settings.js +13 -1
- package/dist/core/extensions/builtin/terminal/shared.d.ts +17 -10
- package/dist/core/extensions/builtin/terminal/shared.js +19 -10
- package/dist/core/extensions/builtin/terminal/terminal-manifest-model.d.ts +3 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest-parse.js +7 -0
- package/dist/core/extensions/builtin/terminal/terminal-manifest.js +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor-schema.js +6 -1
- package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/tools/monitor.js +9 -1
- package/dist/core/extensions/builtin/tps.js +1 -8
- package/dist/core/extensions/loader.js +3 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.js +12 -1
- package/dist/core/extensions/types.d.ts +3 -1
- package/dist/core/session-manager.d.ts +10 -1
- package/dist/core/session-manager.js +4 -3
- package/dist/core/terminal-settings.d.ts +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/main.d.ts +14 -0
- package/dist/main.js +25 -8
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.js +1 -0
- package/dist/modes/interactive/components/footer.js +4 -2
- package/dist/modes/interactive/interactive-mode.d.ts +5 -0
- package/dist/modes/interactive/interactive-mode.js +13 -1
- package/dist/modes/rpc/client-admission-record.d.ts +5 -0
- package/dist/modes/rpc/client-admission-record.js +2 -0
- package/dist/modes/rpc/connection-handler.d.ts +3 -0
- package/dist/modes/rpc/connection-handler.js +33 -5
- package/dist/modes/rpc/connection-roles.d.ts +30 -0
- package/dist/modes/rpc/connection-roles.js +53 -0
- package/dist/modes/rpc/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- package/dist/modes/rpc/host-capabilities.js +2 -1
- package/dist/modes/rpc/host-decision.d.ts +4 -1
- package/dist/modes/rpc/host-decision.js +6 -3
- package/dist/modes/rpc/host-ensure-liveness.d.ts +11 -0
- package/dist/modes/rpc/host-ensure-liveness.js +26 -0
- package/dist/modes/rpc/host-ensure.js +2 -3
- package/dist/modes/rpc/host-holder.d.ts +27 -0
- package/dist/modes/rpc/host-holder.js +18 -0
- package/dist/modes/rpc/host-runner.d.ts +2 -0
- package/dist/modes/rpc/host-runner.js +8 -1
- package/dist/modes/rpc/host-status-all.js +1 -0
- package/dist/modes/rpc/host-status.d.ts +1 -0
- package/dist/modes/rpc/host-status.js +5 -0
- package/dist/modes/rpc/host-stop-generation.d.ts +22 -0
- package/dist/modes/rpc/host-stop-generation.js +36 -0
- package/dist/modes/rpc/host-stop-start-time.d.ts +15 -0
- package/dist/modes/rpc/host-stop-start-time.js +30 -0
- package/dist/modes/rpc/host-stop.d.ts +13 -1
- package/dist/modes/rpc/host-stop.js +78 -9
- package/dist/modes/rpc/rpc-client.d.ts +9 -0
- package/dist/modes/rpc/rpc-client.js +15 -0
- package/dist/modes/rpc/rpc-types.d.ts +18 -0
- package/dist/modes/rpc/rpc-types.js +2 -0
- package/dist/modes/rpc/session-binding.d.ts +1 -1
- package/dist/modes/rpc/session-command-router.d.ts +1 -0
- package/dist/modes/rpc/session-command-router.js +13 -0
- package/dist/modes/rpc/session-worker-client.d.ts +1 -1
- package/dist/modes/rpc/session-worker-client.js +12 -1
- package/dist/modes/rpc/session-worker-protocol.d.ts +3 -0
- package/dist/modes/rpc/session-worker.js +9 -0
- package/docs/rpc.md +54 -2
- package/docs/session-format.md +5 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/args-CRXNC7IS.js +0 -2
- package/dist/bundle/chunks/azure-openai-responses-2DCY6QKO.js +0 -2
- package/dist/bundle/chunks/chunk-5ZBDN3LB.js +0 -26
- package/dist/bundle/chunks/chunk-AEJUOQ7U.js +0 -2
- package/dist/bundle/chunks/chunk-LAWS3FYQ.js +0 -11
- package/dist/bundle/chunks/chunk-LM76EMEJ.js +0 -7
- package/dist/bundle/chunks/chunk-M237EDHB.js +0 -2
- package/dist/bundle/chunks/chunk-V6RY5A6T.js +0 -2
- package/dist/bundle/chunks/chunk-WIAXG52U.js +0 -2
- package/dist/bundle/chunks/chunk-WTFD4TII.js +0 -8
- package/dist/bundle/chunks/chunk-Z33PKFXM.js +0 -3
- package/dist/bundle/chunks/cli-main-RKC5Z25H.js +0 -3
- package/dist/bundle/chunks/google-generative-ai-3SKUSCIL.js +0 -2
- package/dist/bundle/chunks/google-vertex-YHSIIE4G.js +0 -2
- package/dist/bundle/chunks/help-fast-path-FWS5ARO5.js +0 -2
- package/dist/bundle/chunks/host-command-L66Y7ZL5.js +0 -2
- package/dist/bundle/chunks/host-session-command-2YNQNTEH.js +0 -20
- package/dist/bundle/chunks/interactive-mode-XOKTNBLA.js +0 -2
- package/dist/bundle/chunks/openai-codex-responses-4BIPACKV.js +0 -10
- package/dist/bundle/chunks/openai-responses-2UOPEJLP.js +0 -3
- package/dist/bundle/chunks/rotation-stream-J2NUPRMG.js +0 -2
- package/dist/bundle/chunks/rpc-mode-WM5WH7TH.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-MTOAYRRY.js +0 -3
- 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 {
|
|
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
|
|
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
|
-
(
|
|
84
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
|
164
|
+
(event) => event.type === "summary" && !event.summary.startsWith(RUNAWAY_PAUSE_SUMMARY_PREFIX));
|
|
186
165
|
const reachesBudget = !deliversSummary && this.#consecutiveWakes + 1 >= settings.wakeBudget;
|
|
187
|
-
const
|
|
188
|
-
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
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 =
|
|
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 =
|
|
193
|
+
const remainingIds = this.#pendingIds();
|
|
215
194
|
if (remainingIds.size > 0)
|
|
216
|
-
this.#
|
|
195
|
+
this.#scheduleNext(remainingIds, now, settings);
|
|
217
196
|
}
|
|
218
|
-
#
|
|
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 (
|
|
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
|
|
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
|