@posthog/agent 2.1.22 → 2.1.29
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/{agent-LrKyX9KN.d.ts → agent-DcBmoTR4.d.ts} +7 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +83 -7
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +83 -7
- package/dist/index.js.map +1 -1
- package/dist/server/agent-server.js +102 -8
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +102 -8
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +1 -1
- package/src/server/agent-server.ts +21 -1
- package/src/session-log-writer.test.ts +41 -0
- package/src/session-log-writer.ts +91 -5
package/package.json
CHANGED
|
@@ -578,6 +578,8 @@ export class AgentServer {
|
|
|
578
578
|
// Only auto-complete for background mode
|
|
579
579
|
const mode = this.getEffectiveMode(payload);
|
|
580
580
|
if (mode === "background") {
|
|
581
|
+
// Flush all pending session logs before signaling completion,
|
|
582
|
+
await this.session.logWriter.flushAll();
|
|
581
583
|
await this.signalTaskComplete(payload, result.stopReason);
|
|
582
584
|
} else {
|
|
583
585
|
this.logger.info("Interactive mode - staying open for conversation");
|
|
@@ -587,6 +589,9 @@ export class AgentServer {
|
|
|
587
589
|
// Signal failure for background mode
|
|
588
590
|
const mode = this.getEffectiveMode(payload);
|
|
589
591
|
if (mode === "background") {
|
|
592
|
+
if (this.session) {
|
|
593
|
+
await this.session.logWriter.flushAll();
|
|
594
|
+
}
|
|
590
595
|
await this.signalTaskComplete(payload, "error");
|
|
591
596
|
}
|
|
592
597
|
}
|
|
@@ -602,7 +607,10 @@ After completing the requested changes:
|
|
|
602
607
|
3. Push the branch to origin
|
|
603
608
|
4. Create a pull request using \`gh pr create\` with a descriptive title and body
|
|
604
609
|
|
|
605
|
-
Important:
|
|
610
|
+
Important:
|
|
611
|
+
- Always create the PR. Do not ask for confirmation.
|
|
612
|
+
- Do NOT add "Co-Authored-By" trailers to commit messages.
|
|
613
|
+
- Do NOT add "Generated with [Claude Code]" or similar attribution lines to PR descriptions.
|
|
606
614
|
`;
|
|
607
615
|
}
|
|
608
616
|
|
|
@@ -610,6 +618,18 @@ Important: Always create the PR. Do not ask for confirmation.
|
|
|
610
618
|
payload: JwtPayload,
|
|
611
619
|
stopReason: string,
|
|
612
620
|
): Promise<void> {
|
|
621
|
+
if (this.session?.payload.run_id === payload.run_id) {
|
|
622
|
+
try {
|
|
623
|
+
await this.session.logWriter.flush(payload.run_id);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
this.logger.warn("Failed to flush session logs before completion", {
|
|
626
|
+
taskId: payload.task_id,
|
|
627
|
+
runId: payload.run_id,
|
|
628
|
+
error,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
613
633
|
const status =
|
|
614
634
|
stopReason === "cancelled"
|
|
615
635
|
? "cancelled"
|
|
@@ -61,6 +61,47 @@ describe("SessionLogWriter", () => {
|
|
|
61
61
|
await logWriter.flush(sessionId);
|
|
62
62
|
expect(mockAppendLog).not.toHaveBeenCalled();
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
it("re-queues entries when persistence fails and retries", async () => {
|
|
66
|
+
const sessionId = "s1";
|
|
67
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
68
|
+
|
|
69
|
+
mockAppendLog
|
|
70
|
+
.mockRejectedValueOnce(new Error("network error"))
|
|
71
|
+
.mockResolvedValueOnce(undefined);
|
|
72
|
+
|
|
73
|
+
logWriter.appendRawLine(sessionId, JSON.stringify({ method: "test" }));
|
|
74
|
+
|
|
75
|
+
await logWriter.flush(sessionId);
|
|
76
|
+
await logWriter.flush(sessionId);
|
|
77
|
+
|
|
78
|
+
expect(mockAppendLog).toHaveBeenCalledTimes(2);
|
|
79
|
+
const retriedEntries: StoredNotification[] =
|
|
80
|
+
mockAppendLog.mock.calls[1][2];
|
|
81
|
+
expect(retriedEntries).toHaveLength(1);
|
|
82
|
+
expect(retriedEntries[0].notification.method).toBe("test");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("drops entries after max retries", async () => {
|
|
86
|
+
const sessionId = "s1";
|
|
87
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
88
|
+
|
|
89
|
+
mockAppendLog.mockRejectedValue(new Error("persistent failure"));
|
|
90
|
+
|
|
91
|
+
logWriter.appendRawLine(sessionId, JSON.stringify({ method: "test" }));
|
|
92
|
+
|
|
93
|
+
// Flush 10 times (MAX_FLUSH_RETRIES) — entries should be dropped on the 10th
|
|
94
|
+
for (let i = 0; i < 10; i++) {
|
|
95
|
+
await logWriter.flush(sessionId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
expect(mockAppendLog).toHaveBeenCalledTimes(10);
|
|
99
|
+
|
|
100
|
+
// After max retries the entries are dropped, so an 11th flush has nothing
|
|
101
|
+
mockAppendLog.mockClear();
|
|
102
|
+
await logWriter.flush(sessionId);
|
|
103
|
+
expect(mockAppendLog).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
64
105
|
});
|
|
65
106
|
|
|
66
107
|
describe("agent_message_chunk coalescing", () => {
|
|
@@ -21,10 +21,18 @@ interface SessionState {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export class SessionLogWriter {
|
|
24
|
+
private static readonly FLUSH_DEBOUNCE_MS = 500;
|
|
25
|
+
private static readonly FLUSH_MAX_INTERVAL_MS = 5000;
|
|
26
|
+
private static readonly MAX_FLUSH_RETRIES = 10;
|
|
27
|
+
private static readonly MAX_RETRY_DELAY_MS = 30_000;
|
|
28
|
+
|
|
24
29
|
private posthogAPI?: PostHogAPIClient;
|
|
25
30
|
private pendingEntries: Map<string, StoredNotification[]> = new Map();
|
|
26
31
|
private flushTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
|
32
|
+
private lastFlushAttemptTime: Map<string, number> = new Map();
|
|
33
|
+
private retryCounts: Map<string, number> = new Map();
|
|
27
34
|
private sessions: Map<string, SessionState> = new Map();
|
|
35
|
+
private messageCounts: Map<string, number> = new Map();
|
|
28
36
|
private logger: Logger;
|
|
29
37
|
|
|
30
38
|
constructor(options: SessionLogWriterOptions = {}) {
|
|
@@ -35,8 +43,19 @@ export class SessionLogWriter {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
async flushAll(): Promise<void> {
|
|
46
|
+
const sessionIds = [...this.sessions.keys()];
|
|
47
|
+
const pendingCounts = sessionIds.map((id) => ({
|
|
48
|
+
id,
|
|
49
|
+
pending: this.pendingEntries.get(id)?.length ?? 0,
|
|
50
|
+
messages: this.messageCounts.get(id) ?? 0,
|
|
51
|
+
}));
|
|
52
|
+
this.logger.info("flushAll called", {
|
|
53
|
+
sessions: sessionIds.length,
|
|
54
|
+
pending: pendingCounts,
|
|
55
|
+
});
|
|
56
|
+
|
|
38
57
|
const flushPromises: Promise<void>[] = [];
|
|
39
|
-
for (const sessionId of
|
|
58
|
+
for (const sessionId of sessionIds) {
|
|
40
59
|
flushPromises.push(this.flush(sessionId));
|
|
41
60
|
}
|
|
42
61
|
await Promise.all(flushPromises);
|
|
@@ -47,7 +66,12 @@ export class SessionLogWriter {
|
|
|
47
66
|
return;
|
|
48
67
|
}
|
|
49
68
|
|
|
69
|
+
this.logger.info("Session registered", {
|
|
70
|
+
sessionId,
|
|
71
|
+
taskId: context.taskId,
|
|
72
|
+
});
|
|
50
73
|
this.sessions.set(sessionId, { context });
|
|
74
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
isRegistered(sessionId: string): boolean {
|
|
@@ -57,9 +81,18 @@ export class SessionLogWriter {
|
|
|
57
81
|
appendRawLine(sessionId: string, line: string): void {
|
|
58
82
|
const session = this.sessions.get(sessionId);
|
|
59
83
|
if (!session) {
|
|
84
|
+
this.logger.warn("appendRawLine called for unregistered session", {
|
|
85
|
+
sessionId,
|
|
86
|
+
});
|
|
60
87
|
return;
|
|
61
88
|
}
|
|
62
89
|
|
|
90
|
+
const count = (this.messageCounts.get(sessionId) ?? 0) + 1;
|
|
91
|
+
this.messageCounts.set(sessionId, count);
|
|
92
|
+
if (count % 10 === 1) {
|
|
93
|
+
this.logger.info("Messages received", { count, sessionId });
|
|
94
|
+
}
|
|
95
|
+
|
|
63
96
|
try {
|
|
64
97
|
const message = JSON.parse(line);
|
|
65
98
|
const timestamp = new Date().toISOString();
|
|
@@ -103,13 +136,23 @@ export class SessionLogWriter {
|
|
|
103
136
|
|
|
104
137
|
async flush(sessionId: string): Promise<void> {
|
|
105
138
|
const session = this.sessions.get(sessionId);
|
|
106
|
-
if (!session)
|
|
139
|
+
if (!session) {
|
|
140
|
+
this.logger.warn("flush: no session found", { sessionId });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
107
143
|
|
|
108
144
|
// Emit any buffered chunks before flushing
|
|
109
145
|
this.emitCoalescedMessage(sessionId, session);
|
|
110
146
|
|
|
111
147
|
const pending = this.pendingEntries.get(sessionId);
|
|
112
|
-
if (!this.posthogAPI || !pending?.length)
|
|
148
|
+
if (!this.posthogAPI || !pending?.length) {
|
|
149
|
+
this.logger.info("flush: nothing to persist", {
|
|
150
|
+
sessionId,
|
|
151
|
+
hasPosthogAPI: !!this.posthogAPI,
|
|
152
|
+
pendingCount: pending?.length ?? 0,
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
113
156
|
|
|
114
157
|
this.pendingEntries.delete(sessionId);
|
|
115
158
|
const timeout = this.flushTimeouts.get(sessionId);
|
|
@@ -118,14 +161,38 @@ export class SessionLogWriter {
|
|
|
118
161
|
this.flushTimeouts.delete(sessionId);
|
|
119
162
|
}
|
|
120
163
|
|
|
164
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
165
|
+
|
|
121
166
|
try {
|
|
122
167
|
await this.posthogAPI.appendTaskRunLog(
|
|
123
168
|
session.context.taskId,
|
|
124
169
|
session.context.runId,
|
|
125
170
|
pending,
|
|
126
171
|
);
|
|
172
|
+
this.retryCounts.set(sessionId, 0);
|
|
173
|
+
this.logger.info("Flushed session logs", {
|
|
174
|
+
sessionId,
|
|
175
|
+
entryCount: pending.length,
|
|
176
|
+
});
|
|
127
177
|
} catch (error) {
|
|
128
|
-
this.
|
|
178
|
+
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
179
|
+
this.retryCounts.set(sessionId, retryCount);
|
|
180
|
+
|
|
181
|
+
if (retryCount >= SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
182
|
+
this.logger.error(
|
|
183
|
+
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
184
|
+
{ sessionId, error },
|
|
185
|
+
);
|
|
186
|
+
this.retryCounts.set(sessionId, 0);
|
|
187
|
+
} else {
|
|
188
|
+
this.logger.error(
|
|
189
|
+
`Failed to persist session logs (attempt ${retryCount}/${SessionLogWriter.MAX_FLUSH_RETRIES}):`,
|
|
190
|
+
error,
|
|
191
|
+
);
|
|
192
|
+
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
193
|
+
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
194
|
+
this.scheduleFlush(sessionId);
|
|
195
|
+
}
|
|
129
196
|
}
|
|
130
197
|
}
|
|
131
198
|
|
|
@@ -180,7 +247,26 @@ export class SessionLogWriter {
|
|
|
180
247
|
private scheduleFlush(sessionId: string): void {
|
|
181
248
|
const existing = this.flushTimeouts.get(sessionId);
|
|
182
249
|
if (existing) clearTimeout(existing);
|
|
183
|
-
|
|
250
|
+
|
|
251
|
+
const retryCount = this.retryCounts.get(sessionId) ?? 0;
|
|
252
|
+
const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
|
|
253
|
+
const elapsed = Date.now() - lastAttempt;
|
|
254
|
+
|
|
255
|
+
let delay: number;
|
|
256
|
+
if (retryCount > 0) {
|
|
257
|
+
// Exponential backoff on retries: FLUSH_DEBOUNCE_MS * 2^retryCount, capped
|
|
258
|
+
delay = Math.min(
|
|
259
|
+
SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
|
|
260
|
+
SessionLogWriter.MAX_RETRY_DELAY_MS,
|
|
261
|
+
);
|
|
262
|
+
} else if (elapsed >= SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
263
|
+
// If we've been accumulating for longer than the max interval, flush immediately
|
|
264
|
+
delay = 0;
|
|
265
|
+
} else {
|
|
266
|
+
delay = SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const timeout = setTimeout(() => this.flush(sessionId), delay);
|
|
184
270
|
this.flushTimeouts.set(sessionId, timeout);
|
|
185
271
|
}
|
|
186
272
|
}
|