teapot-coding-agent 0.29.2 → 0.29.4
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/dist/log/events.js +104 -6
- package/package.json +1 -1
- package/public/assets/index-9fqlvpxR.js +30 -0
- package/public/assets/index-9fqlvpxR.js.map +1 -0
- package/public/assets/{index-B7AHe2fu.css → index-CnX2G_Uc.css} +1 -1
- package/public/assets/{shiki-Bewi88uT.js → shiki-CDs87bRU.js} +2 -2
- package/public/assets/{shiki-Bewi88uT.js.map → shiki-CDs87bRU.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-CmDjXS7u.js +0 -30
- package/public/assets/index-CmDjXS7u.js.map +0 -1
package/dist/log/events.js
CHANGED
|
@@ -16,6 +16,25 @@
|
|
|
16
16
|
import { createWriteStream } from "node:fs";
|
|
17
17
|
import { mkdirSync } from "node:fs";
|
|
18
18
|
import path from "node:path";
|
|
19
|
+
/**
|
|
20
|
+
* How long an idle write stream is kept open (#143).
|
|
21
|
+
*
|
|
22
|
+
* The stream used to be opened in `load()` and closed only by `close()`, which
|
|
23
|
+
* runs on dispose and shutdown. A loaded agent therefore held a file descriptor
|
|
24
|
+
* for the whole process lifetime — MEASURED on a real master: 493 agents in
|
|
25
|
+
* config meant 493 open `chat.jsonl` descriptors, ~300 MB RSS, growing with
|
|
26
|
+
* every session the operator creates.
|
|
27
|
+
*
|
|
28
|
+
* The log is append-only, so a stream is never needed to keep state: reopening
|
|
29
|
+
* with `flags: "a"` continues exactly where the last write stopped. Closing it
|
|
30
|
+
* is therefore free, and re-opening on the next append costs one `open()` per
|
|
31
|
+
* burst of activity rather than one per session forever.
|
|
32
|
+
*
|
|
33
|
+
* 15s, because a working agent appends many times per second — holding the
|
|
34
|
+
* stream across that activity means file rotation / inspection outside the app
|
|
35
|
+
* still sees a quiet, closed file, and a burst of tool calls costs ONE open.
|
|
36
|
+
*/
|
|
37
|
+
const IDLE_CLOSE_MS = 15_000;
|
|
19
38
|
export class EventLog {
|
|
20
39
|
stream = null;
|
|
21
40
|
seq = 0;
|
|
@@ -24,6 +43,14 @@ export class EventLog {
|
|
|
24
43
|
lastByBranch = new Map();
|
|
25
44
|
/** optional observer (e.g. console logger wired by the master) */
|
|
26
45
|
onEvent = null;
|
|
46
|
+
/** #143: timer that closes the stream once this log goes quiet */
|
|
47
|
+
idleTimer = null;
|
|
48
|
+
/**
|
|
49
|
+
* #143: set by close() so an append that resumes afterwards knows the log was
|
|
50
|
+
* DISPOSED rather than merely idle — an idle log must reopen, a disposed one
|
|
51
|
+
* must keep dropping (and saying so), which is the pre-existing contract.
|
|
52
|
+
*/
|
|
53
|
+
disposed = false;
|
|
27
54
|
filePath;
|
|
28
55
|
agentId;
|
|
29
56
|
constructor(filePath, agentId) {
|
|
@@ -69,10 +96,66 @@ export class EventLog {
|
|
|
69
96
|
catch {
|
|
70
97
|
/* file may not exist yet */
|
|
71
98
|
}
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
// #143: NO stream here. Opening it in load() is what held a descriptor per
|
|
100
|
+
// loaded agent for the lifetime of the process (measured: 493 of them).
|
|
101
|
+
// `load()` still reads and repairs the file — it just does not keep it open.
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Open the append stream if it is not already open (#143).
|
|
105
|
+
*
|
|
106
|
+
* Called from the write chain so an append cannot race a close: both run
|
|
107
|
+
* inside `this.chain`, which is a promise queue.
|
|
108
|
+
*/
|
|
109
|
+
openStream() {
|
|
110
|
+
if (this.stream)
|
|
111
|
+
return this.stream;
|
|
112
|
+
const s = createWriteStream(this.filePath, { flags: "a" });
|
|
113
|
+
s.on("error", (err) => {
|
|
74
114
|
console.error(`[teapot] log write error (${this.filePath}):`, err.message);
|
|
75
115
|
});
|
|
116
|
+
this.stream = s;
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
/** #143: close the stream once this log has been quiet for IDLE_CLOSE_MS. */
|
|
120
|
+
scheduleIdleClose() {
|
|
121
|
+
if (this.idleTimer)
|
|
122
|
+
return; // already armed; the existing deadline stands
|
|
123
|
+
this.idleTimer = setTimeout(() => this.fireIdleClose(), IDLE_CLOSE_MS);
|
|
124
|
+
// the timer must never be the reason a process cannot exit
|
|
125
|
+
this.idleTimer.unref?.();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* What the idle timer does — separated so it can be driven directly.
|
|
129
|
+
*
|
|
130
|
+
* Clears the deadline FIRST so `releaseIdleStream` can tell "nothing new
|
|
131
|
+
* arrived" (close) from "an append re-armed it while I waited" (defer).
|
|
132
|
+
*/
|
|
133
|
+
fireIdleClose() {
|
|
134
|
+
this.idleTimer = null;
|
|
135
|
+
void this.releaseIdleStream();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Close the stream after a quiet period.
|
|
139
|
+
*
|
|
140
|
+
* Runs behind `chain`, so it cannot interleave with a queued write. An append
|
|
141
|
+
* that arrived while this was pending re-armed the timer, and that newer
|
|
142
|
+
* deadline owns the close — hence the check on `idleTimer` AFTER draining,
|
|
143
|
+
* not before.
|
|
144
|
+
*/
|
|
145
|
+
async releaseIdleStream() {
|
|
146
|
+
await this.chain.catch(() => { });
|
|
147
|
+
if (this.disposed)
|
|
148
|
+
return;
|
|
149
|
+
if (!this.stream)
|
|
150
|
+
return;
|
|
151
|
+
// a later append re-armed the timer — let that deadline own the close
|
|
152
|
+
if (this.idleTimer)
|
|
153
|
+
return;
|
|
154
|
+
const s = this.stream;
|
|
155
|
+
this.stream = null;
|
|
156
|
+
await new Promise((res) => s.end(res)).catch(() => {
|
|
157
|
+
/* the write error handler already reported it */
|
|
158
|
+
});
|
|
76
159
|
}
|
|
77
160
|
/** Append an event; resolves when it is handed to the OS (write flushed). */
|
|
78
161
|
append(type, session, branch, data) {
|
|
@@ -110,13 +193,16 @@ export class EventLog {
|
|
|
110
193
|
let written = false;
|
|
111
194
|
const p = new Promise((resolve, reject) => {
|
|
112
195
|
this.chain = this.chain.then(() => {
|
|
113
|
-
if (
|
|
114
|
-
// log
|
|
115
|
-
// but say so: silent drops made post-shutdown writes look
|
|
196
|
+
if (this.disposed) {
|
|
197
|
+
// log DISPOSED (not merely idle) — resolve so callers don't await
|
|
198
|
+
// forever, but say so: silent drops made post-shutdown writes look
|
|
199
|
+
// logged. #143: this is a distinct state from an idle stream, which
|
|
200
|
+
// must reopen instead of dropping.
|
|
116
201
|
console.error(`[teapot] event dropped after log close (${this.agentId}): ${type}`);
|
|
117
202
|
return resolve(evt);
|
|
118
203
|
}
|
|
119
|
-
|
|
204
|
+
// #143: open on demand — an idle log keeps no descriptor
|
|
205
|
+
this.openStream().write(JSON.stringify(evt) + "\n", "utf8", (err) => {
|
|
120
206
|
if (err)
|
|
121
207
|
return reject(err);
|
|
122
208
|
written = true;
|
|
@@ -124,6 +210,10 @@ export class EventLog {
|
|
|
124
210
|
});
|
|
125
211
|
}, () => resolve(evt));
|
|
126
212
|
});
|
|
213
|
+
// #143: re-arm the idle close for every append. Set here rather than inside
|
|
214
|
+
// the write callback: the timer measures quiet time from the last APPEND, and
|
|
215
|
+
// a long write must not count as activity after the fact.
|
|
216
|
+
this.scheduleIdleClose();
|
|
127
217
|
this.chain = this.chain.then(() => { }, () => { });
|
|
128
218
|
// notify once the write is handed to the OS, so a reload the observer
|
|
129
219
|
// triggers can actually see this event
|
|
@@ -161,6 +251,14 @@ export class EventLog {
|
|
|
161
251
|
this.lastByBranch.set(branch, parentEventId ?? "");
|
|
162
252
|
}
|
|
163
253
|
async close() {
|
|
254
|
+
// #143: mark DISPOSED before draining, so an append queued behind us drops
|
|
255
|
+
// loudly instead of reopening the log we are shutting down. This is the
|
|
256
|
+
// difference between "idle" (reopen on the next write) and "closed".
|
|
257
|
+
this.disposed = true;
|
|
258
|
+
if (this.idleTimer) {
|
|
259
|
+
clearTimeout(this.idleTimer);
|
|
260
|
+
this.idleTimer = null;
|
|
261
|
+
}
|
|
164
262
|
await this.chain.catch(() => { });
|
|
165
263
|
if (!this.stream)
|
|
166
264
|
return;
|