@posthog/agent 2.3.126 → 2.3.137

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.3.126",
3
+ "version": "2.3.137",
4
4
  "repository": "https://github.com/PostHog/code",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -876,58 +876,92 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
876
876
  { sessionId, taskId, taskRunId: meta?.taskRunId },
877
877
  );
878
878
 
879
- try {
880
- const result = await withTimeout(
881
- q.initializationResult(),
882
- SESSION_VALIDATION_TIMEOUT_MS,
883
- );
884
- if (result.result === "timeout") {
885
- throw new Error(
886
- `Session ${isResume ? (forkSession ? "fork" : "resumption") : "initialization"} timed out for sessionId=${sessionId}`,
879
+ if (isResume) {
880
+ // Resume must block on initialization to validate the session is still alive.
881
+ // For stale sessions this throws (e.g. "No conversation found").
882
+ try {
883
+ const result = await withTimeout(
884
+ q.initializationResult(),
885
+ SESSION_VALIDATION_TIMEOUT_MS,
887
886
  );
887
+ if (result.result === "timeout") {
888
+ throw new Error(
889
+ `Session ${forkSession ? "fork" : "resumption"} timed out for sessionId=${sessionId}`,
890
+ );
891
+ }
892
+ } catch (err) {
893
+ settingsManager.dispose();
894
+ if (
895
+ err instanceof Error &&
896
+ err.message === "Query closed before response received"
897
+ ) {
898
+ throw RequestError.resourceNotFound(sessionId);
899
+ }
900
+ this.logger.error(
901
+ forkSession ? "Session fork failed" : "Session resumption failed",
902
+ {
903
+ sessionId,
904
+ taskId,
905
+ taskRunId: meta?.taskRunId,
906
+ error: err instanceof Error ? err.message : String(err),
907
+ },
908
+ );
909
+ throw err;
888
910
  }
889
- } catch (err) {
890
- settingsManager.dispose();
891
- if (
892
- isResume &&
893
- err instanceof Error &&
894
- err.message === "Query closed before response received"
895
- ) {
896
- throw RequestError.resourceNotFound(sessionId);
897
- }
898
- this.logger.error(
899
- isResume
900
- ? forkSession
901
- ? "Session fork failed"
902
- : "Session resumption failed"
903
- : "Session initialization failed",
904
- {
911
+ }
912
+
913
+ // Kick off SDK initialization for new sessions so it runs concurrently
914
+ // with the model config fetch below (the gateway REST call is independent).
915
+ const initPromise = !isResume
916
+ ? withTimeout(q.initializationResult(), SESSION_VALIDATION_TIMEOUT_MS)
917
+ : undefined;
918
+
919
+ const [modelOptions] = await Promise.all([
920
+ this.getModelConfigOptions(
921
+ settingsManager.getSettings().model || meta?.model || undefined,
922
+ ),
923
+ ...(meta?.taskRunId
924
+ ? [
925
+ this.client.extNotification("_posthog/sdk_session", {
926
+ taskRunId: meta.taskRunId,
927
+ sessionId,
928
+ adapter: "claude",
929
+ }),
930
+ ]
931
+ : []),
932
+ ]);
933
+
934
+ if (initPromise) {
935
+ try {
936
+ const initResult = await initPromise;
937
+ if (initResult.result === "timeout") {
938
+ settingsManager.dispose();
939
+ throw new Error(
940
+ `Session initialization timed out for sessionId=${sessionId}`,
941
+ );
942
+ }
943
+ } catch (err) {
944
+ settingsManager.dispose();
945
+ this.logger.error("Session initialization failed", {
905
946
  sessionId,
906
947
  taskId,
907
948
  taskRunId: meta?.taskRunId,
908
949
  error: err instanceof Error ? err.message : String(err),
909
- },
910
- );
911
- throw err;
912
- }
913
-
914
- if (meta?.taskRunId) {
915
- await this.client.extNotification("_posthog/sdk_session", {
916
- taskRunId: meta.taskRunId,
917
- sessionId,
918
- adapter: "claude",
919
- });
950
+ });
951
+ throw err;
952
+ }
920
953
  }
921
954
 
922
- // Resolve model: settings model takes priority, then gateway
923
955
  const settingsModel = settingsManager.getSettings().model;
924
- const modelOptions = await this.getModelConfigOptions();
925
- const resolvedModelId = settingsModel || modelOptions.currentModelId;
956
+ const metaModel = meta?.model;
957
+ const resolvedModelId =
958
+ settingsModel || metaModel || modelOptions.currentModelId;
926
959
  session.modelId = resolvedModelId;
927
960
  session.lastContextWindowSize =
928
961
  this.getContextWindowForModel(resolvedModelId);
929
962
 
930
963
  const resolvedSdkModel = toSdkModelId(resolvedModelId);
964
+
931
965
  if (!isResume && resolvedSdkModel !== DEFAULT_MODEL) {
932
966
  await this.session.query.setModel(resolvedSdkModel);
933
967
  }
@@ -108,6 +108,8 @@ export type NewSessionMeta = {
108
108
  persistence?: { taskId?: string; runId?: string; logUrl?: string };
109
109
  additionalRoots?: string[];
110
110
  allowedDomains?: string[];
111
+ /** Model ID to use for this session (e.g. "claude-sonnet-4-6") */
112
+ model?: string;
111
113
  claudeCode?: {
112
114
  options?: Options;
113
115
  };
@@ -169,6 +169,12 @@ export class AgentServer {
169
169
  private initializationPromise: Promise<void> | null = null;
170
170
  private pendingEvents: Record<string, unknown>[] = [];
171
171
 
172
+ private detachSseController(controller: SseController): void {
173
+ if (this.session?.sseController === controller) {
174
+ this.session.sseController = null;
175
+ }
176
+ }
177
+
172
178
  private emitConsoleLog = (
173
179
  level: LogLevel,
174
180
  _scope: string,
@@ -250,18 +256,15 @@ export class AgentServer {
250
256
  controller.enqueue(
251
257
  new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`),
252
258
  );
253
- } catch (error) {
254
- this.logger.debug(
255
- "SSE send failed (stream may be closed)",
256
- error,
257
- );
259
+ } catch {
260
+ this.detachSseController(sseController);
258
261
  }
259
262
  },
260
263
  close: () => {
261
264
  try {
262
265
  controller.close();
263
- } catch (error) {
264
- this.logger.debug("SSE close failed (already closed)", error);
266
+ } catch {
267
+ this.detachSseController(sseController);
265
268
  }
266
269
  },
267
270
  };
@@ -1577,6 +1580,10 @@ Important:
1577
1580
  }
1578
1581
 
1579
1582
  private sendSseEvent(controller: SseController, data: unknown): void {
1580
- controller.send(data);
1583
+ try {
1584
+ controller.send(data);
1585
+ } catch {
1586
+ this.detachSseController(controller);
1587
+ }
1581
1588
  }
1582
1589
  }
@@ -235,6 +235,71 @@ describe("Question relay", () => {
235
235
 
236
236
  expect(result.outcome.outcome).toBe("selected");
237
237
  });
238
+
239
+ it("keeps auto-approving permissions after SSE send failures", async () => {
240
+ const appendRawLine = vi.fn();
241
+ const brokenSseController = {
242
+ send: vi.fn(() => {
243
+ throw new Error("stream closed");
244
+ }),
245
+ close: vi.fn(),
246
+ };
247
+
248
+ const cloudPermissionServer = server as TestableAgentServer & {
249
+ emitConsoleLog: (
250
+ level: "debug" | "info" | "warn" | "error",
251
+ scope: string,
252
+ message: string,
253
+ data?: unknown,
254
+ ) => void;
255
+ logger: { debug: (message: string, data?: unknown) => void };
256
+ session: {
257
+ payload: typeof TEST_PAYLOAD;
258
+ sseController: typeof brokenSseController | null;
259
+ logWriter: {
260
+ appendRawLine: (runId: string, line: string) => void;
261
+ };
262
+ };
263
+ };
264
+
265
+ cloudPermissionServer.session = {
266
+ payload: TEST_PAYLOAD,
267
+ sseController: brokenSseController,
268
+ logWriter: {
269
+ appendRawLine,
270
+ },
271
+ };
272
+ cloudPermissionServer.logger = {
273
+ debug: (message: string, data?: unknown) => {
274
+ cloudPermissionServer.emitConsoleLog(
275
+ "debug",
276
+ "agent",
277
+ message,
278
+ data,
279
+ );
280
+ },
281
+ };
282
+
283
+ const client = cloudPermissionServer.createCloudClient(TEST_PAYLOAD);
284
+
285
+ const firstResult = await client.requestPermission({
286
+ options: ALLOW_OPTIONS,
287
+ toolCall: { _meta: { codeToolKind: "bash" } },
288
+ });
289
+
290
+ expect(firstResult.outcome.outcome).toBe("selected");
291
+ expect(brokenSseController.send).toHaveBeenCalledTimes(1);
292
+ expect(cloudPermissionServer.session.sseController).toBeNull();
293
+
294
+ const secondResult = await client.requestPermission({
295
+ options: ALLOW_OPTIONS,
296
+ toolCall: { _meta: { codeToolKind: "bash" } },
297
+ });
298
+
299
+ expect(secondResult.outcome.outcome).toBe("selected");
300
+ expect(brokenSseController.send).toHaveBeenCalledTimes(1);
301
+ expect(appendRawLine).toHaveBeenCalledTimes(2);
302
+ });
238
303
  });
239
304
  });
240
305