@posthog/agent 2.1.17 → 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 +127 -53
- package/dist/agent.js.map +1 -1
- package/dist/claude-cli/cli.js +5349 -3347
- package/dist/claude-cli/vendor/ripgrep/arm64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-win32/ripgrep.node +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +127 -53
- package/dist/index.js.map +1 -1
- package/dist/server/agent-server.js +146 -54
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +146 -54
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +6 -6
- package/src/adapters/claude/claude-agent.ts +35 -28
- package/src/adapters/claude/conversion/sdk-to-acp.ts +1 -11
- package/src/adapters/claude/session/options.ts +7 -3
- package/src/adapters/claude/types.ts +0 -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/src/test/mocks/claude-sdk.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.29",
|
|
4
4
|
"repository": "https://github.com/PostHog/twig",
|
|
5
5
|
"description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
6
6
|
"exports": {
|
|
@@ -76,16 +76,16 @@
|
|
|
76
76
|
"@twig/git": "1.0.0"
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
|
+
"@agentclientprotocol/sdk": "^0.14.0",
|
|
80
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.42",
|
|
81
|
+
"@anthropic-ai/sdk": "^0.71.0",
|
|
82
|
+
"@hono/node-server": "^1.19.9",
|
|
83
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
79
84
|
"@opentelemetry/api-logs": "^0.208.0",
|
|
80
85
|
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
|
81
86
|
"@opentelemetry/resources": "^2.0.0",
|
|
82
87
|
"@opentelemetry/sdk-logs": "^0.208.0",
|
|
83
88
|
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
84
|
-
"@agentclientprotocol/sdk": "^0.14.0",
|
|
85
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.12",
|
|
86
|
-
"@anthropic-ai/sdk": "^0.71.0",
|
|
87
|
-
"@hono/node-server": "^1.19.9",
|
|
88
|
-
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
89
89
|
"@types/jsonwebtoken": "^9.0.10",
|
|
90
90
|
"commander": "^14.0.2",
|
|
91
91
|
"diff": "^8.0.2",
|
|
@@ -137,7 +137,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
137
137
|
this.checkAuthStatus();
|
|
138
138
|
|
|
139
139
|
const meta = params._meta as NewSessionMeta | undefined;
|
|
140
|
-
const
|
|
140
|
+
const sessionId = uuidv7();
|
|
141
141
|
const permissionMode: TwigExecutionMode =
|
|
142
142
|
meta?.permissionMode &&
|
|
143
143
|
TWIG_EXECUTION_MODES.includes(meta.permissionMode as TwigExecutionMode)
|
|
@@ -151,11 +151,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
151
151
|
cwd: params.cwd,
|
|
152
152
|
mcpServers,
|
|
153
153
|
permissionMode,
|
|
154
|
-
canUseTool: this.createCanUseTool(
|
|
154
|
+
canUseTool: this.createCanUseTool(sessionId),
|
|
155
155
|
logger: this.logger,
|
|
156
156
|
systemPrompt: buildSystemPrompt(meta?.systemPrompt),
|
|
157
157
|
userProvidedOptions: meta?.claudeCode?.options,
|
|
158
|
-
|
|
158
|
+
sessionId,
|
|
159
|
+
isResume: false,
|
|
160
|
+
onModeChange: this.createOnModeChange(sessionId),
|
|
159
161
|
onProcessSpawned: this.processCallbacks?.onProcessSpawned,
|
|
160
162
|
onProcessExited: this.processCallbacks?.onProcessExited,
|
|
161
163
|
});
|
|
@@ -164,7 +166,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
164
166
|
const q = query({ prompt: input, options });
|
|
165
167
|
|
|
166
168
|
const session = this.createSession(
|
|
167
|
-
|
|
169
|
+
sessionId,
|
|
168
170
|
q,
|
|
169
171
|
input,
|
|
170
172
|
permissionMode,
|
|
@@ -172,21 +174,27 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
172
174
|
options.abortController as AbortController,
|
|
173
175
|
);
|
|
174
176
|
session.taskRunId = meta?.taskRunId;
|
|
175
|
-
this.registerPersistence(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
this.registerPersistence(sessionId, meta as Record<string, unknown>);
|
|
178
|
+
|
|
179
|
+
if (meta?.taskRunId) {
|
|
180
|
+
await this.client.extNotification("_posthog/sdk_session", {
|
|
181
|
+
taskRunId: meta.taskRunId,
|
|
182
|
+
sessionId,
|
|
183
|
+
adapter: "claude",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
179
187
|
const modelOptions = await this.getModelConfigOptions();
|
|
180
188
|
session.modelId = modelOptions.currentModelId;
|
|
181
189
|
await this.trySetModel(q, modelOptions.currentModelId);
|
|
182
190
|
|
|
183
191
|
this.sendAvailableCommandsUpdate(
|
|
184
|
-
|
|
192
|
+
sessionId,
|
|
185
193
|
await getAvailableSlashCommands(q),
|
|
186
194
|
);
|
|
187
195
|
|
|
188
196
|
return {
|
|
189
|
-
sessionId
|
|
197
|
+
sessionId,
|
|
190
198
|
configOptions: await this.buildConfigOptions(modelOptions),
|
|
191
199
|
};
|
|
192
200
|
}
|
|
@@ -198,12 +206,15 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
198
206
|
async resumeSession(
|
|
199
207
|
params: LoadSessionRequest,
|
|
200
208
|
): Promise<LoadSessionResponse> {
|
|
201
|
-
const
|
|
202
|
-
|
|
209
|
+
const meta = params._meta as NewSessionMeta | undefined;
|
|
210
|
+
const sessionId = meta?.sessionId;
|
|
211
|
+
if (!sessionId) {
|
|
212
|
+
throw new Error("Cannot resume session without sessionId");
|
|
213
|
+
}
|
|
214
|
+
if (this.sessionId === sessionId) {
|
|
203
215
|
return {};
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
const meta = params._meta as NewSessionMeta | undefined;
|
|
207
218
|
const mcpServers = parseMcpServers(params);
|
|
208
219
|
await fetchMcpToolMetadata(mcpServers, this.logger);
|
|
209
220
|
|
|
@@ -214,27 +225,21 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
214
225
|
: "default";
|
|
215
226
|
|
|
216
227
|
const { query: q, session } = await this.initializeQuery({
|
|
217
|
-
internalSessionId,
|
|
218
228
|
cwd: params.cwd,
|
|
219
229
|
permissionMode,
|
|
220
230
|
mcpServers,
|
|
221
231
|
systemPrompt: buildSystemPrompt(meta?.systemPrompt),
|
|
222
232
|
userProvidedOptions: meta?.claudeCode?.options,
|
|
223
|
-
sessionId
|
|
233
|
+
sessionId,
|
|
234
|
+
isResume: true,
|
|
224
235
|
additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
|
|
225
236
|
});
|
|
226
237
|
|
|
227
238
|
session.taskRunId = meta?.taskRunId;
|
|
228
|
-
if (meta?.sessionId) {
|
|
229
|
-
session.sessionId = meta.sessionId;
|
|
230
|
-
}
|
|
231
239
|
|
|
232
|
-
this.registerPersistence(
|
|
233
|
-
internalSessionId,
|
|
234
|
-
meta as Record<string, unknown>,
|
|
235
|
-
);
|
|
240
|
+
this.registerPersistence(sessionId, meta as Record<string, unknown>);
|
|
236
241
|
this.sendAvailableCommandsUpdate(
|
|
237
|
-
|
|
242
|
+
sessionId,
|
|
238
243
|
await getAvailableSlashCommands(q),
|
|
239
244
|
);
|
|
240
245
|
|
|
@@ -322,13 +327,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
322
327
|
}
|
|
323
328
|
|
|
324
329
|
private async initializeQuery(config: {
|
|
325
|
-
internalSessionId: string;
|
|
326
330
|
cwd: string;
|
|
327
331
|
permissionMode: TwigExecutionMode;
|
|
328
332
|
mcpServers: ReturnType<typeof parseMcpServers>;
|
|
329
333
|
userProvidedOptions?: Options;
|
|
330
334
|
systemPrompt?: Options["systemPrompt"];
|
|
331
|
-
sessionId
|
|
335
|
+
sessionId: string;
|
|
336
|
+
isResume: boolean;
|
|
332
337
|
additionalDirectories?: string[];
|
|
333
338
|
}): Promise<{
|
|
334
339
|
query: Query;
|
|
@@ -341,13 +346,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
341
346
|
cwd: config.cwd,
|
|
342
347
|
mcpServers: config.mcpServers,
|
|
343
348
|
permissionMode: config.permissionMode,
|
|
344
|
-
canUseTool: this.createCanUseTool(config.
|
|
349
|
+
canUseTool: this.createCanUseTool(config.sessionId),
|
|
345
350
|
logger: this.logger,
|
|
346
351
|
systemPrompt: config.systemPrompt,
|
|
347
352
|
userProvidedOptions: config.userProvidedOptions,
|
|
348
353
|
sessionId: config.sessionId,
|
|
354
|
+
isResume: config.isResume,
|
|
349
355
|
additionalDirectories: config.additionalDirectories,
|
|
350
|
-
onModeChange: this.createOnModeChange(config.
|
|
356
|
+
onModeChange: this.createOnModeChange(config.sessionId),
|
|
351
357
|
onProcessSpawned: this.processCallbacks?.onProcessSpawned,
|
|
352
358
|
onProcessExited: this.processCallbacks?.onProcessExited,
|
|
353
359
|
});
|
|
@@ -356,7 +362,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
356
362
|
const abortController = options.abortController as AbortController;
|
|
357
363
|
|
|
358
364
|
const session = this.createSession(
|
|
359
|
-
config.
|
|
365
|
+
config.sessionId,
|
|
360
366
|
q,
|
|
361
367
|
input,
|
|
362
368
|
config.permissionMode,
|
|
@@ -596,6 +602,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
596
602
|
|
|
597
603
|
case "tool_progress":
|
|
598
604
|
case "auth_status":
|
|
605
|
+
case "tool_use_summary":
|
|
599
606
|
return null;
|
|
600
607
|
|
|
601
608
|
default:
|
|
@@ -328,20 +328,10 @@ export async function handleSystemMessage(
|
|
|
328
328
|
message: any,
|
|
329
329
|
context: MessageHandlerContext,
|
|
330
330
|
): Promise<void> {
|
|
331
|
-
const {
|
|
331
|
+
const { sessionId, client, logger } = context;
|
|
332
332
|
|
|
333
333
|
switch (message.subtype) {
|
|
334
334
|
case "init":
|
|
335
|
-
if (message.session_id && session && !session.sessionId) {
|
|
336
|
-
session.sessionId = message.session_id;
|
|
337
|
-
if (session.taskRunId) {
|
|
338
|
-
await client.extNotification("_posthog/sdk_session", {
|
|
339
|
-
taskRunId: session.taskRunId,
|
|
340
|
-
sessionId: message.session_id,
|
|
341
|
-
adapter: "claude",
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
335
|
break;
|
|
346
336
|
case "compact_boundary":
|
|
347
337
|
await client.extNotification("_posthog/compact_boundary", {
|
|
@@ -27,7 +27,8 @@ export interface BuildOptionsParams {
|
|
|
27
27
|
logger: Logger;
|
|
28
28
|
systemPrompt?: Options["systemPrompt"];
|
|
29
29
|
userProvidedOptions?: Options;
|
|
30
|
-
sessionId
|
|
30
|
+
sessionId: string;
|
|
31
|
+
isResume: boolean;
|
|
31
32
|
additionalDirectories?: string[];
|
|
32
33
|
onModeChange?: OnModeChange;
|
|
33
34
|
onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
|
|
@@ -213,7 +214,7 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
|
|
|
213
214
|
),
|
|
214
215
|
...(params.onProcessSpawned && {
|
|
215
216
|
spawnClaudeCodeProcess: buildSpawnWrapper(
|
|
216
|
-
params.sessionId
|
|
217
|
+
params.sessionId,
|
|
217
218
|
params.onProcessSpawned,
|
|
218
219
|
params.onProcessExited,
|
|
219
220
|
),
|
|
@@ -224,8 +225,11 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
|
|
|
224
225
|
options.pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
if (params.
|
|
228
|
+
if (params.isResume) {
|
|
228
229
|
options.resume = params.sessionId;
|
|
230
|
+
options.forkSession = false;
|
|
231
|
+
} else {
|
|
232
|
+
options.sessionId = params.sessionId;
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
if (params.additionalDirectories) {
|
|
@@ -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
|
}
|
|
@@ -95,6 +95,11 @@ export function createMockQuery(
|
|
|
95
95
|
.fn()
|
|
96
96
|
.mockResolvedValue({ added: [], removed: [], errors: {} }),
|
|
97
97
|
streamInput: vi.fn().mockResolvedValue(undefined),
|
|
98
|
+
close: vi.fn(),
|
|
99
|
+
initializationResult: vi.fn().mockResolvedValue({}),
|
|
100
|
+
reconnectMcpServer: vi.fn().mockResolvedValue(undefined),
|
|
101
|
+
toggleMcpServer: vi.fn().mockResolvedValue(undefined),
|
|
102
|
+
stopTask: vi.fn().mockResolvedValue(undefined),
|
|
98
103
|
[Symbol.asyncDispose]: vi.fn().mockResolvedValue(undefined),
|
|
99
104
|
_abortController: abortController,
|
|
100
105
|
_mockHelpers: {
|
|
@@ -158,6 +163,7 @@ export function createSuccessResult(
|
|
|
158
163
|
is_error: false,
|
|
159
164
|
num_turns: 1,
|
|
160
165
|
result: "Done",
|
|
166
|
+
stop_reason: null,
|
|
161
167
|
total_cost_usd: 0.01,
|
|
162
168
|
usage: {
|
|
163
169
|
input_tokens: 100,
|
|
@@ -190,6 +196,7 @@ export function createErrorResult(
|
|
|
190
196
|
duration_ms: 100,
|
|
191
197
|
duration_api_ms: 50,
|
|
192
198
|
num_turns: 1,
|
|
199
|
+
stop_reason: null,
|
|
193
200
|
total_cost_usd: 0.01,
|
|
194
201
|
usage: {
|
|
195
202
|
input_tokens: 100,
|