@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.
@@ -1175,7 +1175,7 @@ var import_uuid = require("uuid");
1175
1175
  // package.json
1176
1176
  var package_default = {
1177
1177
  name: "@posthog/agent",
1178
- version: "2.1.17",
1178
+ version: "2.1.29",
1179
1179
  repository: "https://github.com/PostHog/twig",
1180
1180
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
1181
1181
  exports: {
@@ -1247,11 +1247,11 @@ var package_default = {
1247
1247
  },
1248
1248
  devDependencies: {
1249
1249
  "@changesets/cli": "^2.27.8",
1250
+ "@posthog/shared": "workspace:*",
1251
+ "@twig/git": "workspace:*",
1250
1252
  "@types/bun": "latest",
1251
1253
  "@types/tar": "^6.1.13",
1252
1254
  minimatch: "^10.0.3",
1253
- "@posthog/shared": "workspace:*",
1254
- "@twig/git": "workspace:*",
1255
1255
  msw: "^2.12.7",
1256
1256
  tsup: "^8.5.1",
1257
1257
  tsx: "^4.20.6",
@@ -1259,16 +1259,16 @@ var package_default = {
1259
1259
  vitest: "^2.1.8"
1260
1260
  },
1261
1261
  dependencies: {
1262
+ "@agentclientprotocol/sdk": "^0.14.0",
1263
+ "@anthropic-ai/claude-agent-sdk": "0.2.42",
1264
+ "@anthropic-ai/sdk": "^0.71.0",
1265
+ "@hono/node-server": "^1.19.9",
1266
+ "@modelcontextprotocol/sdk": "^1.25.3",
1262
1267
  "@opentelemetry/api-logs": "^0.208.0",
1263
1268
  "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
1264
1269
  "@opentelemetry/resources": "^2.0.0",
1265
1270
  "@opentelemetry/sdk-logs": "^0.208.0",
1266
1271
  "@opentelemetry/semantic-conventions": "^1.28.0",
1267
- "@agentclientprotocol/sdk": "^0.14.0",
1268
- "@anthropic-ai/claude-agent-sdk": "0.2.12",
1269
- "@anthropic-ai/sdk": "^0.71.0",
1270
- "@hono/node-server": "^1.19.9",
1271
- "@modelcontextprotocol/sdk": "^1.25.3",
1272
1272
  "@types/jsonwebtoken": "^9.0.10",
1273
1273
  commander: "^14.0.2",
1274
1274
  diff: "^8.0.2",
@@ -2234,19 +2234,9 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
2234
2234
  }
2235
2235
  }
2236
2236
  async function handleSystemMessage(message, context) {
2237
- const { session, sessionId, client, logger } = context;
2237
+ const { sessionId, client, logger } = context;
2238
2238
  switch (message.subtype) {
2239
2239
  case "init":
2240
- if (message.session_id && session && !session.sessionId) {
2241
- session.sessionId = message.session_id;
2242
- if (session.taskRunId) {
2243
- await client.extNotification("_posthog/sdk_session", {
2244
- taskRunId: session.taskRunId,
2245
- sessionId: message.session_id,
2246
- adapter: "claude"
2247
- });
2248
- }
2249
- }
2250
2240
  break;
2251
2241
  case "compact_boundary":
2252
2242
  await client.extNotification("_posthog/compact_boundary", {
@@ -3211,7 +3201,7 @@ function buildSessionOptions(params) {
3211
3201
  ),
3212
3202
  ...params.onProcessSpawned && {
3213
3203
  spawnClaudeCodeProcess: buildSpawnWrapper(
3214
- params.sessionId ?? "unknown",
3204
+ params.sessionId,
3215
3205
  params.onProcessSpawned,
3216
3206
  params.onProcessExited
3217
3207
  )
@@ -3220,8 +3210,11 @@ function buildSessionOptions(params) {
3220
3210
  if (process.env.CLAUDE_CODE_EXECUTABLE) {
3221
3211
  options.pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
3222
3212
  }
3223
- if (params.sessionId) {
3213
+ if (params.isResume) {
3224
3214
  options.resume = params.sessionId;
3215
+ options.forkSession = false;
3216
+ } else {
3217
+ options.sessionId = params.sessionId;
3225
3218
  }
3226
3219
  if (params.additionalDirectories) {
3227
3220
  options.additionalDirectories = params.additionalDirectories;
@@ -3298,7 +3291,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3298
3291
  async newSession(params) {
3299
3292
  this.checkAuthStatus();
3300
3293
  const meta = params._meta;
3301
- const internalSessionId = (0, import_uuid.v7)();
3294
+ const sessionId = (0, import_uuid.v7)();
3302
3295
  const permissionMode = meta?.permissionMode && TWIG_EXECUTION_MODES.includes(meta.permissionMode) ? meta.permissionMode : "default";
3303
3296
  const mcpServers = parseMcpServers(params);
3304
3297
  await fetchMcpToolMetadata(mcpServers, this.logger);
@@ -3306,18 +3299,20 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3306
3299
  cwd: params.cwd,
3307
3300
  mcpServers,
3308
3301
  permissionMode,
3309
- canUseTool: this.createCanUseTool(internalSessionId),
3302
+ canUseTool: this.createCanUseTool(sessionId),
3310
3303
  logger: this.logger,
3311
3304
  systemPrompt: buildSystemPrompt(meta?.systemPrompt),
3312
3305
  userProvidedOptions: meta?.claudeCode?.options,
3313
- onModeChange: this.createOnModeChange(internalSessionId),
3306
+ sessionId,
3307
+ isResume: false,
3308
+ onModeChange: this.createOnModeChange(sessionId),
3314
3309
  onProcessSpawned: this.processCallbacks?.onProcessSpawned,
3315
3310
  onProcessExited: this.processCallbacks?.onProcessExited
3316
3311
  });
3317
3312
  const input = new Pushable();
3318
3313
  const q = (0, import_claude_agent_sdk.query)({ prompt: input, options });
3319
3314
  const session = this.createSession(
3320
- internalSessionId,
3315
+ sessionId,
3321
3316
  q,
3322
3317
  input,
3323
3318
  permissionMode,
@@ -3325,19 +3320,23 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3325
3320
  options.abortController
3326
3321
  );
3327
3322
  session.taskRunId = meta?.taskRunId;
3328
- this.registerPersistence(
3329
- internalSessionId,
3330
- meta
3331
- );
3323
+ this.registerPersistence(sessionId, meta);
3324
+ if (meta?.taskRunId) {
3325
+ await this.client.extNotification("_posthog/sdk_session", {
3326
+ taskRunId: meta.taskRunId,
3327
+ sessionId,
3328
+ adapter: "claude"
3329
+ });
3330
+ }
3332
3331
  const modelOptions = await this.getModelConfigOptions();
3333
3332
  session.modelId = modelOptions.currentModelId;
3334
3333
  await this.trySetModel(q, modelOptions.currentModelId);
3335
3334
  this.sendAvailableCommandsUpdate(
3336
- internalSessionId,
3335
+ sessionId,
3337
3336
  await getAvailableSlashCommands(q)
3338
3337
  );
3339
3338
  return {
3340
- sessionId: internalSessionId,
3339
+ sessionId,
3341
3340
  configOptions: await this.buildConfigOptions(modelOptions)
3342
3341
  };
3343
3342
  }
@@ -3345,34 +3344,31 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3345
3344
  return this.resumeSession(params);
3346
3345
  }
3347
3346
  async resumeSession(params) {
3348
- const { sessionId: internalSessionId } = params;
3349
- if (this.sessionId === internalSessionId) {
3347
+ const meta = params._meta;
3348
+ const sessionId = meta?.sessionId;
3349
+ if (!sessionId) {
3350
+ throw new Error("Cannot resume session without sessionId");
3351
+ }
3352
+ if (this.sessionId === sessionId) {
3350
3353
  return {};
3351
3354
  }
3352
- const meta = params._meta;
3353
3355
  const mcpServers = parseMcpServers(params);
3354
3356
  await fetchMcpToolMetadata(mcpServers, this.logger);
3355
3357
  const permissionMode = meta?.permissionMode && TWIG_EXECUTION_MODES.includes(meta.permissionMode) ? meta.permissionMode : "default";
3356
3358
  const { query: q, session } = await this.initializeQuery({
3357
- internalSessionId,
3358
3359
  cwd: params.cwd,
3359
3360
  permissionMode,
3360
3361
  mcpServers,
3361
3362
  systemPrompt: buildSystemPrompt(meta?.systemPrompt),
3362
3363
  userProvidedOptions: meta?.claudeCode?.options,
3363
- sessionId: meta?.sessionId,
3364
+ sessionId,
3365
+ isResume: true,
3364
3366
  additionalDirectories: meta?.claudeCode?.options?.additionalDirectories
3365
3367
  });
3366
3368
  session.taskRunId = meta?.taskRunId;
3367
- if (meta?.sessionId) {
3368
- session.sessionId = meta.sessionId;
3369
- }
3370
- this.registerPersistence(
3371
- internalSessionId,
3372
- meta
3373
- );
3369
+ this.registerPersistence(sessionId, meta);
3374
3370
  this.sendAvailableCommandsUpdate(
3375
- internalSessionId,
3371
+ sessionId,
3376
3372
  await getAvailableSlashCommands(q)
3377
3373
  );
3378
3374
  return {
@@ -3441,20 +3437,21 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3441
3437
  cwd: config.cwd,
3442
3438
  mcpServers: config.mcpServers,
3443
3439
  permissionMode: config.permissionMode,
3444
- canUseTool: this.createCanUseTool(config.internalSessionId),
3440
+ canUseTool: this.createCanUseTool(config.sessionId),
3445
3441
  logger: this.logger,
3446
3442
  systemPrompt: config.systemPrompt,
3447
3443
  userProvidedOptions: config.userProvidedOptions,
3448
3444
  sessionId: config.sessionId,
3445
+ isResume: config.isResume,
3449
3446
  additionalDirectories: config.additionalDirectories,
3450
- onModeChange: this.createOnModeChange(config.internalSessionId),
3447
+ onModeChange: this.createOnModeChange(config.sessionId),
3451
3448
  onProcessSpawned: this.processCallbacks?.onProcessSpawned,
3452
3449
  onProcessExited: this.processCallbacks?.onProcessExited
3453
3450
  });
3454
3451
  const q = (0, import_claude_agent_sdk.query)({ prompt: input, options });
3455
3452
  const abortController = options.abortController;
3456
3453
  const session = this.createSession(
3457
- config.internalSessionId,
3454
+ config.sessionId,
3458
3455
  q,
3459
3456
  input,
3460
3457
  config.permissionMode,
@@ -3647,6 +3644,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
3647
3644
  }
3648
3645
  case "tool_progress":
3649
3646
  case "auth_status":
3647
+ case "tool_use_summary":
3650
3648
  return null;
3651
3649
  default:
3652
3650
  unreachable(message, this.logger);
@@ -4260,19 +4258,36 @@ var PostHogAPIClient = class {
4260
4258
  };
4261
4259
 
4262
4260
  // src/session-log-writer.ts
4263
- var SessionLogWriter = class {
4261
+ var SessionLogWriter = class _SessionLogWriter {
4262
+ static FLUSH_DEBOUNCE_MS = 500;
4263
+ static FLUSH_MAX_INTERVAL_MS = 5e3;
4264
+ static MAX_FLUSH_RETRIES = 10;
4265
+ static MAX_RETRY_DELAY_MS = 3e4;
4264
4266
  posthogAPI;
4265
4267
  pendingEntries = /* @__PURE__ */ new Map();
4266
4268
  flushTimeouts = /* @__PURE__ */ new Map();
4269
+ lastFlushAttemptTime = /* @__PURE__ */ new Map();
4270
+ retryCounts = /* @__PURE__ */ new Map();
4267
4271
  sessions = /* @__PURE__ */ new Map();
4272
+ messageCounts = /* @__PURE__ */ new Map();
4268
4273
  logger;
4269
4274
  constructor(options = {}) {
4270
4275
  this.posthogAPI = options.posthogAPI;
4271
4276
  this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
4272
4277
  }
4273
4278
  async flushAll() {
4279
+ const sessionIds = [...this.sessions.keys()];
4280
+ const pendingCounts = sessionIds.map((id) => ({
4281
+ id,
4282
+ pending: this.pendingEntries.get(id)?.length ?? 0,
4283
+ messages: this.messageCounts.get(id) ?? 0
4284
+ }));
4285
+ this.logger.info("flushAll called", {
4286
+ sessions: sessionIds.length,
4287
+ pending: pendingCounts
4288
+ });
4274
4289
  const flushPromises = [];
4275
- for (const sessionId of this.sessions.keys()) {
4290
+ for (const sessionId of sessionIds) {
4276
4291
  flushPromises.push(this.flush(sessionId));
4277
4292
  }
4278
4293
  await Promise.all(flushPromises);
@@ -4281,7 +4296,12 @@ var SessionLogWriter = class {
4281
4296
  if (this.sessions.has(sessionId)) {
4282
4297
  return;
4283
4298
  }
4299
+ this.logger.info("Session registered", {
4300
+ sessionId,
4301
+ taskId: context.taskId
4302
+ });
4284
4303
  this.sessions.set(sessionId, { context });
4304
+ this.lastFlushAttemptTime.set(sessionId, Date.now());
4285
4305
  }
4286
4306
  isRegistered(sessionId) {
4287
4307
  return this.sessions.has(sessionId);
@@ -4289,8 +4309,16 @@ var SessionLogWriter = class {
4289
4309
  appendRawLine(sessionId, line) {
4290
4310
  const session = this.sessions.get(sessionId);
4291
4311
  if (!session) {
4312
+ this.logger.warn("appendRawLine called for unregistered session", {
4313
+ sessionId
4314
+ });
4292
4315
  return;
4293
4316
  }
4317
+ const count = (this.messageCounts.get(sessionId) ?? 0) + 1;
4318
+ this.messageCounts.set(sessionId, count);
4319
+ if (count % 10 === 1) {
4320
+ this.logger.info("Messages received", { count, sessionId });
4321
+ }
4294
4322
  try {
4295
4323
  const message = JSON.parse(line);
4296
4324
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -4326,24 +4354,56 @@ var SessionLogWriter = class {
4326
4354
  }
4327
4355
  async flush(sessionId) {
4328
4356
  const session = this.sessions.get(sessionId);
4329
- if (!session) return;
4357
+ if (!session) {
4358
+ this.logger.warn("flush: no session found", { sessionId });
4359
+ return;
4360
+ }
4330
4361
  this.emitCoalescedMessage(sessionId, session);
4331
4362
  const pending = this.pendingEntries.get(sessionId);
4332
- if (!this.posthogAPI || !pending?.length) return;
4363
+ if (!this.posthogAPI || !pending?.length) {
4364
+ this.logger.info("flush: nothing to persist", {
4365
+ sessionId,
4366
+ hasPosthogAPI: !!this.posthogAPI,
4367
+ pendingCount: pending?.length ?? 0
4368
+ });
4369
+ return;
4370
+ }
4333
4371
  this.pendingEntries.delete(sessionId);
4334
4372
  const timeout = this.flushTimeouts.get(sessionId);
4335
4373
  if (timeout) {
4336
4374
  clearTimeout(timeout);
4337
4375
  this.flushTimeouts.delete(sessionId);
4338
4376
  }
4377
+ this.lastFlushAttemptTime.set(sessionId, Date.now());
4339
4378
  try {
4340
4379
  await this.posthogAPI.appendTaskRunLog(
4341
4380
  session.context.taskId,
4342
4381
  session.context.runId,
4343
4382
  pending
4344
4383
  );
4384
+ this.retryCounts.set(sessionId, 0);
4385
+ this.logger.info("Flushed session logs", {
4386
+ sessionId,
4387
+ entryCount: pending.length
4388
+ });
4345
4389
  } catch (error) {
4346
- this.logger.error("Failed to persist session logs:", error);
4390
+ const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
4391
+ this.retryCounts.set(sessionId, retryCount);
4392
+ if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
4393
+ this.logger.error(
4394
+ `Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
4395
+ { sessionId, error }
4396
+ );
4397
+ this.retryCounts.set(sessionId, 0);
4398
+ } else {
4399
+ this.logger.error(
4400
+ `Failed to persist session logs (attempt ${retryCount}/${_SessionLogWriter.MAX_FLUSH_RETRIES}):`,
4401
+ error
4402
+ );
4403
+ const currentPending = this.pendingEntries.get(sessionId) ?? [];
4404
+ this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
4405
+ this.scheduleFlush(sessionId);
4406
+ }
4347
4407
  }
4348
4408
  }
4349
4409
  isAgentMessageChunk(message) {
@@ -4389,7 +4449,21 @@ var SessionLogWriter = class {
4389
4449
  scheduleFlush(sessionId) {
4390
4450
  const existing = this.flushTimeouts.get(sessionId);
4391
4451
  if (existing) clearTimeout(existing);
4392
- const timeout = setTimeout(() => this.flush(sessionId), 500);
4452
+ const retryCount = this.retryCounts.get(sessionId) ?? 0;
4453
+ const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
4454
+ const elapsed = Date.now() - lastAttempt;
4455
+ let delay2;
4456
+ if (retryCount > 0) {
4457
+ delay2 = Math.min(
4458
+ _SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
4459
+ _SessionLogWriter.MAX_RETRY_DELAY_MS
4460
+ );
4461
+ } else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
4462
+ delay2 = 0;
4463
+ } else {
4464
+ delay2 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
4465
+ }
4466
+ const timeout = setTimeout(() => this.flush(sessionId), delay2);
4393
4467
  this.flushTimeouts.set(sessionId, timeout);
4394
4468
  }
4395
4469
  };
@@ -10363,6 +10437,7 @@ var AgentServer = class {
10363
10437
  });
10364
10438
  const mode = this.getEffectiveMode(payload);
10365
10439
  if (mode === "background") {
10440
+ await this.session.logWriter.flushAll();
10366
10441
  await this.signalTaskComplete(payload, result.stopReason);
10367
10442
  } else {
10368
10443
  this.logger.info("Interactive mode - staying open for conversation");
@@ -10371,6 +10446,9 @@ var AgentServer = class {
10371
10446
  this.logger.error("Failed to send initial task message", error);
10372
10447
  const mode = this.getEffectiveMode(payload);
10373
10448
  if (mode === "background") {
10449
+ if (this.session) {
10450
+ await this.session.logWriter.flushAll();
10451
+ }
10374
10452
  await this.signalTaskComplete(payload, "error");
10375
10453
  }
10376
10454
  }
@@ -10385,10 +10463,24 @@ After completing the requested changes:
10385
10463
  3. Push the branch to origin
10386
10464
  4. Create a pull request using \`gh pr create\` with a descriptive title and body
10387
10465
 
10388
- Important: Always create the PR. Do not ask for confirmation.
10466
+ Important:
10467
+ - Always create the PR. Do not ask for confirmation.
10468
+ - Do NOT add "Co-Authored-By" trailers to commit messages.
10469
+ - Do NOT add "Generated with [Claude Code]" or similar attribution lines to PR descriptions.
10389
10470
  `;
10390
10471
  }
10391
10472
  async signalTaskComplete(payload, stopReason) {
10473
+ if (this.session?.payload.run_id === payload.run_id) {
10474
+ try {
10475
+ await this.session.logWriter.flush(payload.run_id);
10476
+ } catch (error) {
10477
+ this.logger.warn("Failed to flush session logs before completion", {
10478
+ taskId: payload.task_id,
10479
+ runId: payload.run_id,
10480
+ error
10481
+ });
10482
+ }
10483
+ }
10392
10484
  const status = stopReason === "cancelled" ? "cancelled" : stopReason === "error" ? "failed" : "completed";
10393
10485
  try {
10394
10486
  await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {