@posthog/agent 2.1.47 → 2.1.53

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/types.d.ts CHANGED
@@ -103,6 +103,8 @@ interface AgentConfig {
103
103
  otelTransport?: OtelTransportConfig;
104
104
  /** Skip session log persistence (e.g. for preview sessions with no real task) */
105
105
  skipLogPersistence?: boolean;
106
+ /** Local cache path for instant log loading (e.g., ~/.twig) */
107
+ localCachePath?: string;
106
108
  debug?: boolean;
107
109
  onLog?: OnLogCallback;
108
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.1.47",
3
+ "version": "2.1.53",
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": {
@@ -23,8 +23,6 @@ export type AcpConnectionConfig = {
23
23
  /** Deployment environment - "local" for desktop, "cloud" for cloud sandbox */
24
24
  deviceType?: "local" | "cloud";
25
25
  logger?: Logger;
26
- /** Enable dev-only instrumentation (timing, verbose logging) */
27
- debug?: boolean;
28
26
  processCallbacks?: ProcessSpawnedCallback;
29
27
  codexOptions?: CodexProcessOptions;
30
28
  allowedModelIds?: Set<string>;
@@ -196,10 +194,7 @@ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection {
196
194
 
197
195
  let agent: ClaudeAcpAgent | null = null;
198
196
  const agentConnection = new AgentSideConnection((client) => {
199
- agent = new ClaudeAcpAgent(client, logWriter, {
200
- ...config.processCallbacks,
201
- debug: config.debug,
202
- });
197
+ agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks);
203
198
  logger.info(`Created ${agent.adapterName} agent`);
204
199
  return agent;
205
200
  }, agentStream);
@@ -29,7 +29,6 @@ import {
29
29
  type SDKMessage,
30
30
  type SDKUserMessage,
31
31
  } from "@anthropic-ai/claude-agent-sdk";
32
- import { createTimingCollector } from "@posthog/shared";
33
32
  import { v7 as uuidv7 } from "uuid";
34
33
  import packageJson from "../../../package.json" with { type: "json" };
35
34
  import type { SessionContext } from "../../otel-log-writer.js";
@@ -70,8 +69,6 @@ import type {
70
69
  export interface ClaudeAcpAgentOptions {
71
70
  onProcessSpawned?: (info: ProcessSpawnedInfo) => void;
72
71
  onProcessExited?: (pid: number) => void;
73
- /** Enable dev-only instrumentation (timing, verbose logging) */
74
- debug?: boolean;
75
72
  }
76
73
 
77
74
  export class ClaudeAcpAgent extends BaseAcpAgent {
@@ -83,7 +80,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
83
80
  private logWriter?: SessionLogWriter;
84
81
  private options?: ClaudeAcpAgentOptions;
85
82
  private lastSentConfigOptions?: SessionConfigOption[];
86
- private debug: boolean;
87
83
 
88
84
  constructor(
89
85
  client: AgentSideConnection,
@@ -93,7 +89,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
93
89
  super(client);
94
90
  this.logWriter = logWriter;
95
91
  this.options = options;
96
- this.debug = options?.debug ?? false;
97
92
  this.toolUseCache = {};
98
93
  this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
99
94
  }
@@ -141,10 +136,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
141
136
  async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
142
137
  this.checkAuthStatus();
143
138
 
144
- const tc = createTimingCollector(this.debug, (msg, data) =>
145
- this.logger.info(msg, data),
146
- );
147
-
148
139
  const meta = params._meta as NewSessionMeta | undefined;
149
140
  const sessionId = uuidv7();
150
141
  const permissionMode: TwigExecutionMode =
@@ -153,29 +144,25 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
153
144
  ? (meta.permissionMode as TwigExecutionMode)
154
145
  : "default";
155
146
 
156
- const mcpServers = tc.timeSync("parseMcpServers", () =>
157
- parseMcpServers(params),
158
- );
147
+ const mcpServers = parseMcpServers(params);
159
148
 
160
- const options = tc.timeSync("buildSessionOptions", () =>
161
- buildSessionOptions({
162
- cwd: params.cwd,
163
- mcpServers,
164
- permissionMode,
165
- canUseTool: this.createCanUseTool(sessionId),
166
- logger: this.logger,
167
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
168
- userProvidedOptions: meta?.claudeCode?.options,
169
- sessionId,
170
- isResume: false,
171
- onModeChange: this.createOnModeChange(sessionId),
172
- onProcessSpawned: this.options?.onProcessSpawned,
173
- onProcessExited: this.options?.onProcessExited,
174
- }),
175
- );
149
+ const options = buildSessionOptions({
150
+ cwd: params.cwd,
151
+ mcpServers,
152
+ permissionMode,
153
+ canUseTool: this.createCanUseTool(sessionId),
154
+ logger: this.logger,
155
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
156
+ userProvidedOptions: meta?.claudeCode?.options,
157
+ sessionId,
158
+ isResume: false,
159
+ onModeChange: this.createOnModeChange(sessionId),
160
+ onProcessSpawned: this.options?.onProcessSpawned,
161
+ onProcessExited: this.options?.onProcessExited,
162
+ });
176
163
 
177
164
  const input = new Pushable<SDKUserMessage>();
178
- const q = tc.timeSync("sdkQuery", () => query({ prompt: input, options }));
165
+ const q = query({ prompt: input, options });
179
166
 
180
167
  const session = this.createSession(
181
168
  sessionId,
@@ -189,32 +176,24 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
189
176
  this.registerPersistence(sessionId, meta as Record<string, unknown>);
190
177
 
191
178
  if (meta?.taskRunId) {
192
- await tc.time("extNotification", () =>
193
- this.client.extNotification("_posthog/sdk_session", {
194
- taskRunId: meta.taskRunId!,
195
- sessionId,
196
- adapter: "claude",
197
- }),
198
- );
179
+ await this.client.extNotification("_posthog/sdk_session", {
180
+ taskRunId: meta.taskRunId!,
181
+ sessionId,
182
+ adapter: "claude",
183
+ });
199
184
  }
200
185
 
201
186
  // Only await model config — slash commands and MCP metadata are deferred
202
187
  // since they're not needed to return configOptions to the client.
203
- const modelOptions = await tc.time("fetchModels", () =>
204
- this.getModelConfigOptions(),
205
- );
188
+ const modelOptions = await this.getModelConfigOptions();
206
189
 
207
190
  // Deferred: slash commands + MCP metadata (not needed to return configOptions)
208
- this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
191
+ this.deferBackgroundFetches(q, sessionId, mcpServers);
209
192
 
210
193
  session.modelId = modelOptions.currentModelId;
211
194
  await this.trySetModel(q, modelOptions.currentModelId);
212
195
 
213
- const configOptions = await tc.time("buildConfigOptions", () =>
214
- this.buildConfigOptions(modelOptions),
215
- );
216
-
217
- tc.summarize("newSession");
196
+ const configOptions = await this.buildConfigOptions(modelOptions);
218
197
 
219
198
  return {
220
199
  sessionId,
@@ -229,10 +208,6 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
229
208
  async resumeSession(
230
209
  params: LoadSessionRequest,
231
210
  ): Promise<LoadSessionResponse> {
232
- const tc = createTimingCollector(this.debug, (msg, data) =>
233
- this.logger.info(msg, data),
234
- );
235
-
236
211
  const meta = params._meta as NewSessionMeta | undefined;
237
212
  const sessionId = meta?.sessionId;
238
213
  if (!sessionId) {
@@ -242,9 +217,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
242
217
  return {};
243
218
  }
244
219
 
245
- const mcpServers = tc.timeSync("parseMcpServers", () =>
246
- parseMcpServers(params),
247
- );
220
+ const mcpServers = parseMcpServers(params);
248
221
 
249
222
  const permissionMode: TwigExecutionMode =
250
223
  meta?.permissionMode &&
@@ -252,31 +225,25 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
252
225
  ? (meta.permissionMode as TwigExecutionMode)
253
226
  : "default";
254
227
 
255
- const { query: q, session } = await tc.time("initializeQuery", () =>
256
- this.initializeQuery({
257
- cwd: params.cwd,
258
- permissionMode,
259
- mcpServers,
260
- systemPrompt: buildSystemPrompt(meta?.systemPrompt),
261
- userProvidedOptions: meta?.claudeCode?.options,
262
- sessionId,
263
- isResume: true,
264
- additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
265
- }),
266
- );
228
+ const { query: q, session } = await this.initializeQuery({
229
+ cwd: params.cwd,
230
+ permissionMode,
231
+ mcpServers,
232
+ systemPrompt: buildSystemPrompt(meta?.systemPrompt),
233
+ userProvidedOptions: meta?.claudeCode?.options,
234
+ sessionId,
235
+ isResume: true,
236
+ additionalDirectories: meta?.claudeCode?.options?.additionalDirectories,
237
+ });
267
238
 
268
239
  session.taskRunId = meta?.taskRunId;
269
240
 
270
241
  this.registerPersistence(sessionId, meta as Record<string, unknown>);
271
242
 
272
243
  // Deferred: slash commands + MCP metadata (not needed to return configOptions)
273
- this.deferBackgroundFetches(tc, q, sessionId, mcpServers);
244
+ this.deferBackgroundFetches(q, sessionId, mcpServers);
274
245
 
275
- const configOptions = await tc.time("buildConfigOptions", () =>
276
- this.buildConfigOptions(),
277
- );
278
-
279
- tc.summarize("resumeSession");
246
+ const configOptions = await this.buildConfigOptions();
280
247
 
281
248
  return { configOptions };
282
249
  }
@@ -529,16 +496,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
529
496
  * Both populate caches used later — neither is needed to return configOptions.
530
497
  */
531
498
  private deferBackgroundFetches(
532
- tc: ReturnType<typeof createTimingCollector>,
533
499
  q: Query,
534
500
  sessionId: string,
535
501
  mcpServers: ReturnType<typeof parseMcpServers>,
536
502
  ): void {
537
503
  Promise.all([
538
- tc.time("slashCommands", () => getAvailableSlashCommands(q)),
539
- tc.time("mcpMetadata", () =>
540
- fetchMcpToolMetadata(mcpServers, this.logger),
541
- ),
504
+ getAvailableSlashCommands(q),
505
+ fetchMcpToolMetadata(mcpServers, this.logger),
542
506
  ])
543
507
  .then(([slashCommands]) => {
544
508
  this.sendAvailableCommandsUpdate(sessionId, slashCommands);
package/src/agent.ts CHANGED
@@ -36,6 +36,7 @@ export class Agent {
36
36
  this.sessionLogWriter = new SessionLogWriter({
37
37
  posthogAPI: this.posthogAPI,
38
38
  logger: this.logger.child("SessionLogWriter"),
39
+ localCachePath: config.localCachePath,
39
40
  });
40
41
  }
41
42
  }
@@ -111,7 +112,6 @@ export class Agent {
111
112
  taskId,
112
113
  deviceType: "local",
113
114
  logger: this.logger,
114
- debug: this.debug,
115
115
  processCallbacks: options.processCallbacks,
116
116
  allowedModelIds,
117
117
  codexOptions:
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import type { SessionContext } from "./otel-log-writer.js";
2
4
  import type { PostHogAPIClient } from "./posthog-api.js";
3
5
  import type { StoredNotification } from "./types.js";
@@ -8,6 +10,8 @@ export interface SessionLogWriterOptions {
8
10
  posthogAPI?: PostHogAPIClient;
9
11
  /** Logger instance */
10
12
  logger?: Logger;
13
+ /** Local cache path for instant log loading (e.g., ~/.twig) */
14
+ localCachePath?: string;
11
15
  }
12
16
 
13
17
  interface ChunkBuffer {
@@ -34,9 +38,11 @@ export class SessionLogWriter {
34
38
  private sessions: Map<string, SessionState> = new Map();
35
39
  private messageCounts: Map<string, number> = new Map();
36
40
  private logger: Logger;
41
+ private localCachePath?: string;
37
42
 
38
43
  constructor(options: SessionLogWriterOptions = {}) {
39
44
  this.posthogAPI = options.posthogAPI;
45
+ this.localCachePath = options.localCachePath;
40
46
  this.logger =
41
47
  options.logger ??
42
48
  new Logger({ debug: false, prefix: "[SessionLogWriter]" });
@@ -71,7 +77,24 @@ export class SessionLogWriter {
71
77
  taskId: context.taskId,
72
78
  });
73
79
  this.sessions.set(sessionId, { context });
80
+
74
81
  this.lastFlushAttemptTime.set(sessionId, Date.now());
82
+
83
+ if (this.localCachePath) {
84
+ const sessionDir = path.join(
85
+ this.localCachePath,
86
+ "sessions",
87
+ context.runId,
88
+ );
89
+ try {
90
+ fs.mkdirSync(sessionDir, { recursive: true });
91
+ } catch (error) {
92
+ this.logger.warn("Failed to create local cache directory", {
93
+ sessionDir,
94
+ error,
95
+ });
96
+ }
97
+ }
75
98
  }
76
99
 
77
100
  isRegistered(sessionId: string): boolean {
@@ -120,6 +143,8 @@ export class SessionLogWriter {
120
143
  notification: message,
121
144
  };
122
145
 
146
+ this.writeToLocalCache(sessionId, entry);
147
+
123
148
  if (this.posthogAPI) {
124
149
  const pending = this.pendingEntries.get(sessionId) ?? [];
125
150
  pending.push(entry);
@@ -236,6 +261,8 @@ export class SessionLogWriter {
236
261
  },
237
262
  };
238
263
 
264
+ this.writeToLocalCache(sessionId, entry);
265
+
239
266
  if (this.posthogAPI) {
240
267
  const pending = this.pendingEntries.get(sessionId) ?? [];
241
268
  pending.push(entry);
@@ -269,4 +296,27 @@ export class SessionLogWriter {
269
296
  const timeout = setTimeout(() => this.flush(sessionId), delay);
270
297
  this.flushTimeouts.set(sessionId, timeout);
271
298
  }
299
+
300
+ private writeToLocalCache(
301
+ sessionId: string,
302
+ entry: StoredNotification,
303
+ ): void {
304
+ if (!this.localCachePath) return;
305
+
306
+ const session = this.sessions.get(sessionId);
307
+ if (!session) return;
308
+
309
+ const logPath = path.join(
310
+ this.localCachePath,
311
+ "sessions",
312
+ session.context.runId,
313
+ "logs.ndjson",
314
+ );
315
+
316
+ try {
317
+ fs.appendFileSync(logPath, `${JSON.stringify(entry)}\n`);
318
+ } catch (error) {
319
+ this.logger.warn("Failed to write to local cache", { logPath, error });
320
+ }
321
+ }
272
322
  }
package/src/types.ts CHANGED
@@ -143,6 +143,8 @@ export interface AgentConfig {
143
143
  otelTransport?: OtelTransportConfig;
144
144
  /** Skip session log persistence (e.g. for preview sessions with no real task) */
145
145
  skipLogPersistence?: boolean;
146
+ /** Local cache path for instant log loading (e.g., ~/.twig) */
147
+ localCachePath?: string;
146
148
  debug?: boolean;
147
149
  onLog?: OnLogCallback;
148
150
  }