@posthog/agent 2.3.5 → 2.3.10

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.
@@ -513,7 +513,7 @@ var require_has_flag = __commonJS({
513
513
  var require_supports_color = __commonJS({
514
514
  "../../node_modules/supports-color/index.js"(exports, module) {
515
515
  "use strict";
516
- var os5 = __require("os");
516
+ var os6 = __require("os");
517
517
  var tty = __require("tty");
518
518
  var hasFlag = require_has_flag();
519
519
  var { env } = process;
@@ -561,7 +561,7 @@ var require_supports_color = __commonJS({
561
561
  return min;
562
562
  }
563
563
  if (process.platform === "win32") {
564
- const osRelease = os5.release().split(".");
564
+ const osRelease = os6.release().split(".");
565
565
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
566
566
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
567
567
  }
@@ -809,10 +809,10 @@ var require_src2 = __commonJS({
809
809
  var fs_1 = __require("fs");
810
810
  var debug_1 = __importDefault(require_src());
811
811
  var log = debug_1.default("@kwsites/file-exists");
812
- function check(path10, isFile, isDirectory) {
813
- log(`checking %s`, path10);
812
+ function check(path11, isFile, isDirectory) {
813
+ log(`checking %s`, path11);
814
814
  try {
815
- const stat = fs_1.statSync(path10);
815
+ const stat = fs_1.statSync(path11);
816
816
  if (stat.isFile() && isFile) {
817
817
  log(`[OK] path represents a file`);
818
818
  return true;
@@ -832,8 +832,8 @@ var require_src2 = __commonJS({
832
832
  throw e;
833
833
  }
834
834
  }
835
- function exists2(path10, type = exports.READABLE) {
836
- return check(path10, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
835
+ function exists2(path11, type = exports.READABLE) {
836
+ return check(path11, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
837
837
  }
838
838
  exports.exists = exists2;
839
839
  exports.FILE = 1;
@@ -908,7 +908,7 @@ import { Hono } from "hono";
908
908
  // package.json
909
909
  var package_default = {
910
910
  name: "@posthog/agent",
911
- version: "2.3.5",
911
+ version: "2.3.10",
912
912
  repository: "https://github.com/PostHog/code",
913
913
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
914
914
  exports: {
@@ -1033,6 +1033,8 @@ var POSTHOG_NOTIFICATIONS = {
1033
1033
  RUN_STARTED: "_posthog/run_started",
1034
1034
  /** Task has completed (success or failure) */
1035
1035
  TASK_COMPLETE: "_posthog/task_complete",
1036
+ /** Agent finished processing a turn (prompt returned, waiting for next input) */
1037
+ TURN_COMPLETE: "_posthog/turn_complete",
1036
1038
  /** Error occurred during task execution */
1037
1039
  ERROR: "_posthog/error",
1038
1040
  /** Console/log output from the agent */
@@ -1580,8 +1582,8 @@ var ToolContentBuilder = class {
1580
1582
  this.items.push({ type: "content", content: image(data, mimeType, uri) });
1581
1583
  return this;
1582
1584
  }
1583
- diff(path10, oldText, newText) {
1584
- this.items.push({ type: "diff", path: path10, oldText, newText });
1585
+ diff(path11, oldText, newText) {
1586
+ this.items.push({ type: "diff", path: path11, oldText, newText });
1585
1587
  return this;
1586
1588
  }
1587
1589
  build() {
@@ -4993,6 +4995,45 @@ function createCodexConnection(config) {
4993
4995
  };
4994
4996
  }
4995
4997
 
4998
+ // src/adapters/claude/session/jsonl-hydration.ts
4999
+ import { randomUUID as randomUUID2 } from "crypto";
5000
+ import * as fs4 from "fs/promises";
5001
+ import * as os5 from "os";
5002
+ import * as path6 from "path";
5003
+ var CHARS_PER_TOKEN = 4;
5004
+ var DEFAULT_MAX_TOKENS = 15e4;
5005
+ function estimateTurnTokens(turn) {
5006
+ let chars = 0;
5007
+ for (const block of turn.content) {
5008
+ if ("text" in block && typeof block.text === "string") {
5009
+ chars += block.text.length;
5010
+ }
5011
+ }
5012
+ if (turn.toolCalls) {
5013
+ for (const tc of turn.toolCalls) {
5014
+ chars += JSON.stringify(tc.input ?? "").length;
5015
+ if (tc.result !== void 0) {
5016
+ chars += typeof tc.result === "string" ? tc.result.length : JSON.stringify(tc.result).length;
5017
+ }
5018
+ }
5019
+ }
5020
+ return Math.ceil(chars / CHARS_PER_TOKEN);
5021
+ }
5022
+ function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
5023
+ let budget = maxTokens;
5024
+ let startIndex = turns.length;
5025
+ for (let i = turns.length - 1; i >= 0; i--) {
5026
+ const cost = estimateTurnTokens(turns[i]);
5027
+ if (cost > budget) break;
5028
+ budget -= cost;
5029
+ startIndex = i;
5030
+ }
5031
+ while (startIndex < turns.length && turns[startIndex].role !== "user") {
5032
+ startIndex++;
5033
+ }
5034
+ return turns.slice(startIndex);
5035
+ }
5036
+
4996
5037
  // src/utils/gateway.ts
4997
5038
  function getLlmGatewayUrl(posthogHost, product = "posthog_code") {
4998
5039
  const url = new URL(posthogHost);
@@ -5176,330 +5217,165 @@ var PostHogAPIClient = class {
5176
5217
  }
5177
5218
  };
5178
5219
 
5179
- // src/session-log-writer.ts
5180
- import fs4 from "fs";
5181
- import fsp from "fs/promises";
5182
- import path6 from "path";
5183
- var SessionLogWriter = class _SessionLogWriter {
5184
- static FLUSH_DEBOUNCE_MS = 500;
5185
- static FLUSH_MAX_INTERVAL_MS = 5e3;
5186
- static MAX_FLUSH_RETRIES = 10;
5187
- static MAX_RETRY_DELAY_MS = 3e4;
5188
- static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
5189
- posthogAPI;
5190
- pendingEntries = /* @__PURE__ */ new Map();
5191
- flushTimeouts = /* @__PURE__ */ new Map();
5192
- lastFlushAttemptTime = /* @__PURE__ */ new Map();
5193
- retryCounts = /* @__PURE__ */ new Map();
5194
- sessions = /* @__PURE__ */ new Map();
5195
- logger;
5196
- localCachePath;
5197
- constructor(options = {}) {
5198
- this.posthogAPI = options.posthogAPI;
5199
- this.localCachePath = options.localCachePath;
5200
- this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
5220
+ // ../shared/dist/index.js
5221
+ var consoleLogger = {
5222
+ info: (_message, _data) => {
5223
+ },
5224
+ debug: (_message, _data) => {
5225
+ },
5226
+ error: (_message, _data) => {
5227
+ },
5228
+ warn: (_message, _data) => {
5201
5229
  }
5202
- async flushAll() {
5203
- const sessionIds = [...this.sessions.keys()];
5204
- const flushPromises = [];
5205
- for (const sessionId of sessionIds) {
5206
- flushPromises.push(this.flush(sessionId));
5207
- }
5208
- await Promise.all(flushPromises);
5230
+ };
5231
+ var Saga = class {
5232
+ completedSteps = [];
5233
+ currentStepName = "unknown";
5234
+ stepTimings = [];
5235
+ log;
5236
+ constructor(logger) {
5237
+ this.log = logger ?? consoleLogger;
5209
5238
  }
5210
- register(sessionId, context) {
5211
- if (this.sessions.has(sessionId)) {
5212
- return;
5239
+ /**
5240
+ * Run the saga with the given input.
5241
+ * Returns a discriminated union result - either success with data or failure with error details.
5242
+ */
5243
+ async run(input) {
5244
+ this.completedSteps = [];
5245
+ this.currentStepName = "unknown";
5246
+ this.stepTimings = [];
5247
+ const sagaStart = performance.now();
5248
+ this.log.info("Starting saga", { sagaName: this.sagaName });
5249
+ try {
5250
+ const result = await this.execute(input);
5251
+ const totalDuration = performance.now() - sagaStart;
5252
+ this.log.debug("Saga completed successfully", {
5253
+ sagaName: this.sagaName,
5254
+ stepsCompleted: this.completedSteps.length,
5255
+ totalDurationMs: Math.round(totalDuration),
5256
+ stepTimings: this.stepTimings
5257
+ });
5258
+ return { success: true, data: result };
5259
+ } catch (error) {
5260
+ this.log.error("Saga failed, initiating rollback", {
5261
+ sagaName: this.sagaName,
5262
+ failedStep: this.currentStepName,
5263
+ error: error instanceof Error ? error.message : String(error)
5264
+ });
5265
+ await this.rollback();
5266
+ return {
5267
+ success: false,
5268
+ error: error instanceof Error ? error.message : String(error),
5269
+ failedStep: this.currentStepName
5270
+ };
5213
5271
  }
5214
- this.logger.info("Session registered", {
5215
- taskId: context.taskId,
5216
- runId: context.runId
5272
+ }
5273
+ /**
5274
+ * Execute a step with its rollback action.
5275
+ * If the step succeeds, its rollback action is stored for potential rollback.
5276
+ * The step name is automatically tracked for error reporting.
5277
+ *
5278
+ * @param config - Step configuration with name, execute, and rollback functions
5279
+ * @returns The result of the execute function
5280
+ * @throws Re-throws any error from the execute function (triggers rollback)
5281
+ */
5282
+ async step(config) {
5283
+ this.currentStepName = config.name;
5284
+ this.log.debug(`Executing step: ${config.name}`);
5285
+ const stepStart = performance.now();
5286
+ const result = await config.execute();
5287
+ const durationMs = Math.round(performance.now() - stepStart);
5288
+ this.stepTimings.push({ name: config.name, durationMs });
5289
+ this.log.debug(`Step completed: ${config.name}`, { durationMs });
5290
+ this.completedSteps.push({
5291
+ name: config.name,
5292
+ rollback: () => config.rollback(result)
5217
5293
  });
5218
- this.sessions.set(sessionId, { context });
5219
- this.lastFlushAttemptTime.set(sessionId, Date.now());
5220
- if (this.localCachePath) {
5221
- const sessionDir = path6.join(
5222
- this.localCachePath,
5223
- "sessions",
5224
- context.runId
5225
- );
5294
+ return result;
5295
+ }
5296
+ /**
5297
+ * Execute a step that doesn't need rollback.
5298
+ * Useful for read-only operations or operations that are idempotent.
5299
+ * The step name is automatically tracked for error reporting.
5300
+ *
5301
+ * @param name - Step name for logging and error tracking
5302
+ * @param execute - The action to execute
5303
+ * @returns The result of the execute function
5304
+ */
5305
+ async readOnlyStep(name, execute) {
5306
+ this.currentStepName = name;
5307
+ this.log.debug(`Executing read-only step: ${name}`);
5308
+ const stepStart = performance.now();
5309
+ const result = await execute();
5310
+ const durationMs = Math.round(performance.now() - stepStart);
5311
+ this.stepTimings.push({ name, durationMs });
5312
+ this.log.debug(`Read-only step completed: ${name}`, { durationMs });
5313
+ return result;
5314
+ }
5315
+ /**
5316
+ * Roll back all completed steps in reverse order.
5317
+ * Rollback errors are logged but don't stop the rollback of other steps.
5318
+ */
5319
+ async rollback() {
5320
+ this.log.info("Rolling back saga", {
5321
+ stepsToRollback: this.completedSteps.length
5322
+ });
5323
+ const stepsReversed = [...this.completedSteps].reverse();
5324
+ for (const step of stepsReversed) {
5226
5325
  try {
5227
- fs4.mkdirSync(sessionDir, { recursive: true });
5326
+ this.log.debug(`Rolling back step: ${step.name}`);
5327
+ await step.rollback();
5328
+ this.log.debug(`Step rolled back: ${step.name}`);
5228
5329
  } catch (error) {
5229
- this.logger.warn("Failed to create local cache directory", {
5230
- sessionDir,
5231
- error
5330
+ this.log.error(`Failed to rollback step: ${step.name}`, {
5331
+ error: error instanceof Error ? error.message : String(error)
5232
5332
  });
5233
5333
  }
5234
5334
  }
5335
+ this.log.info("Rollback completed", {
5336
+ stepsAttempted: this.completedSteps.length
5337
+ });
5235
5338
  }
5236
- isRegistered(sessionId) {
5237
- return this.sessions.has(sessionId);
5339
+ /**
5340
+ * Get the number of completed steps (useful for testing)
5341
+ */
5342
+ getCompletedStepCount() {
5343
+ return this.completedSteps.length;
5238
5344
  }
5239
- appendRawLine(sessionId, line) {
5240
- const session = this.sessions.get(sessionId);
5241
- if (!session) {
5242
- this.logger.warn("appendRawLine called for unregistered session", {
5243
- sessionId
5244
- });
5245
- return;
5246
- }
5247
- try {
5248
- const message = JSON.parse(line);
5249
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5250
- if (this.isAgentMessageChunk(message)) {
5251
- const text2 = this.extractChunkText(message);
5252
- if (text2) {
5253
- if (!session.chunkBuffer) {
5254
- session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
5255
- } else {
5256
- session.chunkBuffer.text += text2;
5257
- }
5258
- }
5259
- return;
5260
- }
5261
- this.emitCoalescedMessage(sessionId, session);
5262
- const nonChunkAgentText = this.extractAgentMessageText(message);
5263
- if (nonChunkAgentText) {
5264
- session.lastAgentMessage = nonChunkAgentText;
5265
- }
5266
- const entry = {
5267
- type: "notification",
5268
- timestamp,
5269
- notification: message
5270
- };
5271
- this.writeToLocalCache(sessionId, entry);
5272
- if (this.posthogAPI) {
5273
- const pending = this.pendingEntries.get(sessionId) ?? [];
5274
- pending.push(entry);
5275
- this.pendingEntries.set(sessionId, pending);
5276
- this.scheduleFlush(sessionId);
5277
- }
5278
- } catch {
5279
- this.logger.warn("Failed to parse raw line for persistence", {
5280
- taskId: session.context.taskId,
5281
- runId: session.context.runId,
5282
- lineLength: line.length
5283
- });
5284
- }
5285
- }
5286
- async flush(sessionId) {
5287
- const session = this.sessions.get(sessionId);
5288
- if (!session) {
5289
- this.logger.warn("flush: no session found", { sessionId });
5290
- return;
5291
- }
5292
- this.emitCoalescedMessage(sessionId, session);
5293
- const pending = this.pendingEntries.get(sessionId);
5294
- if (!this.posthogAPI || !pending?.length) {
5295
- return;
5296
- }
5297
- this.pendingEntries.delete(sessionId);
5298
- const timeout = this.flushTimeouts.get(sessionId);
5299
- if (timeout) {
5300
- clearTimeout(timeout);
5301
- this.flushTimeouts.delete(sessionId);
5302
- }
5303
- this.lastFlushAttemptTime.set(sessionId, Date.now());
5304
- try {
5305
- await this.posthogAPI.appendTaskRunLog(
5306
- session.context.taskId,
5307
- session.context.runId,
5308
- pending
5309
- );
5310
- this.retryCounts.set(sessionId, 0);
5311
- } catch (error) {
5312
- const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
5313
- this.retryCounts.set(sessionId, retryCount);
5314
- if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
5315
- this.logger.error(
5316
- `Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
5317
- {
5318
- taskId: session.context.taskId,
5319
- runId: session.context.runId,
5320
- error
5321
- }
5322
- );
5323
- this.retryCounts.set(sessionId, 0);
5324
- } else {
5325
- if (retryCount === 1) {
5326
- this.logger.warn(
5327
- `Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
5328
- {
5329
- taskId: session.context.taskId,
5330
- runId: session.context.runId,
5331
- error: error instanceof Error ? error.message : String(error)
5332
- }
5333
- );
5334
- }
5335
- const currentPending = this.pendingEntries.get(sessionId) ?? [];
5336
- this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
5337
- this.scheduleFlush(sessionId);
5338
- }
5339
- }
5340
- }
5341
- isAgentMessageChunk(message) {
5342
- if (message.method !== "session/update") return false;
5343
- const params = message.params;
5344
- const update = params?.update;
5345
- return update?.sessionUpdate === "agent_message_chunk";
5346
- }
5347
- extractChunkText(message) {
5348
- const params = message.params;
5349
- const update = params?.update;
5350
- const content = update?.content;
5351
- if (content?.type === "text" && content.text) {
5352
- return content.text;
5353
- }
5354
- return "";
5355
- }
5356
- emitCoalescedMessage(sessionId, session) {
5357
- if (!session.chunkBuffer) return;
5358
- const { text: text2, firstTimestamp } = session.chunkBuffer;
5359
- session.chunkBuffer = void 0;
5360
- session.lastAgentMessage = text2;
5361
- const entry = {
5362
- type: "notification",
5363
- timestamp: firstTimestamp,
5364
- notification: {
5365
- jsonrpc: "2.0",
5366
- method: "session/update",
5367
- params: {
5368
- update: {
5369
- sessionUpdate: "agent_message",
5370
- content: { type: "text", text: text2 }
5371
- }
5372
- }
5373
- }
5374
- };
5375
- this.writeToLocalCache(sessionId, entry);
5376
- if (this.posthogAPI) {
5377
- const pending = this.pendingEntries.get(sessionId) ?? [];
5378
- pending.push(entry);
5379
- this.pendingEntries.set(sessionId, pending);
5380
- this.scheduleFlush(sessionId);
5381
- }
5382
- }
5383
- getLastAgentMessage(sessionId) {
5384
- return this.sessions.get(sessionId)?.lastAgentMessage;
5385
- }
5386
- extractAgentMessageText(message) {
5387
- if (message.method !== "session/update") {
5388
- return null;
5389
- }
5390
- const params = message.params;
5391
- const update = params?.update;
5392
- if (update?.sessionUpdate !== "agent_message") {
5393
- return null;
5394
- }
5395
- const content = update.content;
5396
- if (content?.type === "text" && typeof content.text === "string") {
5397
- const trimmed2 = content.text.trim();
5398
- return trimmed2.length > 0 ? trimmed2 : null;
5399
- }
5400
- if (typeof update.message === "string") {
5401
- const trimmed2 = update.message.trim();
5402
- return trimmed2.length > 0 ? trimmed2 : null;
5403
- }
5404
- return null;
5405
- }
5406
- scheduleFlush(sessionId) {
5407
- const existing = this.flushTimeouts.get(sessionId);
5408
- if (existing) clearTimeout(existing);
5409
- const retryCount = this.retryCounts.get(sessionId) ?? 0;
5410
- const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
5411
- const elapsed = Date.now() - lastAttempt;
5412
- let delay3;
5413
- if (retryCount > 0) {
5414
- delay3 = Math.min(
5415
- _SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
5416
- _SessionLogWriter.MAX_RETRY_DELAY_MS
5417
- );
5418
- } else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
5419
- delay3 = 0;
5420
- } else {
5421
- delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
5422
- }
5423
- const timeout = setTimeout(() => this.flush(sessionId), delay3);
5424
- this.flushTimeouts.set(sessionId, timeout);
5425
- }
5426
- writeToLocalCache(sessionId, entry) {
5427
- if (!this.localCachePath) return;
5428
- const session = this.sessions.get(sessionId);
5429
- if (!session) return;
5430
- const logPath = path6.join(
5431
- this.localCachePath,
5432
- "sessions",
5433
- session.context.runId,
5434
- "logs.ndjson"
5435
- );
5436
- try {
5437
- fs4.appendFileSync(logPath, `${JSON.stringify(entry)}
5438
- `);
5439
- } catch (error) {
5440
- this.logger.warn("Failed to write to local cache", {
5441
- taskId: session.context.taskId,
5442
- runId: session.context.runId,
5443
- logPath,
5444
- error
5445
- });
5446
- }
5447
- }
5448
- static async cleanupOldSessions(localCachePath) {
5449
- const sessionsDir = path6.join(localCachePath, "sessions");
5450
- let deleted = 0;
5451
- try {
5452
- const entries = await fsp.readdir(sessionsDir);
5453
- const now = Date.now();
5454
- for (const entry of entries) {
5455
- const entryPath = path6.join(sessionsDir, entry);
5456
- try {
5457
- const stats = await fsp.stat(entryPath);
5458
- if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
5459
- await fsp.rm(entryPath, { recursive: true, force: true });
5460
- deleted++;
5461
- }
5462
- } catch {
5463
- }
5464
- }
5465
- } catch {
5466
- }
5467
- return deleted;
5468
- }
5469
- };
5470
-
5471
- // ../git/dist/queries.js
5472
- import * as fs6 from "fs/promises";
5473
- import * as path8 from "path";
5474
-
5475
- // ../../node_modules/simple-git/dist/esm/index.js
5476
- var import_file_exists = __toESM(require_dist(), 1);
5477
- var import_debug = __toESM(require_src(), 1);
5478
- var import_promise_deferred = __toESM(require_dist2(), 1);
5479
- var import_promise_deferred2 = __toESM(require_dist2(), 1);
5480
- import { Buffer as Buffer2 } from "buffer";
5481
- import { spawn as spawn3 } from "child_process";
5482
- import { normalize as normalize2 } from "path";
5483
- import { EventEmitter } from "events";
5484
- var __defProp2 = Object.defineProperty;
5485
- var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
5486
- var __getOwnPropNames2 = Object.getOwnPropertyNames;
5487
- var __hasOwnProp2 = Object.prototype.hasOwnProperty;
5488
- var __esm = (fn, res) => function __init() {
5489
- return fn && (res = (0, fn[__getOwnPropNames2(fn)[0]])(fn = 0)), res;
5490
- };
5491
- var __commonJS2 = (cb, mod) => function __require2() {
5492
- return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5493
- };
5494
- var __export = (target, all) => {
5495
- for (var name in all)
5496
- __defProp2(target, name, { get: all[name], enumerable: true });
5497
- };
5498
- var __copyProps2 = (to, from, except, desc) => {
5499
- if (from && typeof from === "object" || typeof from === "function") {
5500
- for (let key of __getOwnPropNames2(from))
5501
- if (!__hasOwnProp2.call(to, key) && key !== except)
5502
- __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
5345
+ };
5346
+
5347
+ // ../git/dist/queries.js
5348
+ import * as fs6 from "fs/promises";
5349
+ import * as path8 from "path";
5350
+
5351
+ // ../../node_modules/simple-git/dist/esm/index.js
5352
+ var import_file_exists = __toESM(require_dist(), 1);
5353
+ var import_debug = __toESM(require_src(), 1);
5354
+ var import_promise_deferred = __toESM(require_dist2(), 1);
5355
+ var import_promise_deferred2 = __toESM(require_dist2(), 1);
5356
+ import { Buffer as Buffer2 } from "buffer";
5357
+ import { spawn as spawn3 } from "child_process";
5358
+ import { normalize as normalize2 } from "path";
5359
+ import { EventEmitter } from "events";
5360
+ var __defProp2 = Object.defineProperty;
5361
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
5362
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
5363
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
5364
+ var __esm = (fn, res) => function __init() {
5365
+ return fn && (res = (0, fn[__getOwnPropNames2(fn)[0]])(fn = 0)), res;
5366
+ };
5367
+ var __commonJS2 = (cb, mod) => function __require2() {
5368
+ return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5369
+ };
5370
+ var __export = (target, all) => {
5371
+ for (var name in all)
5372
+ __defProp2(target, name, { get: all[name], enumerable: true });
5373
+ };
5374
+ var __copyProps2 = (to, from, except, desc) => {
5375
+ if (from && typeof from === "object" || typeof from === "function") {
5376
+ for (let key of __getOwnPropNames2(from))
5377
+ if (!__hasOwnProp2.call(to, key) && key !== except)
5378
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
5503
5379
  }
5504
5380
  return to;
5505
5381
  };
@@ -5509,8 +5385,8 @@ function pathspec(...paths) {
5509
5385
  cache.set(key, paths);
5510
5386
  return key;
5511
5387
  }
5512
- function isPathSpec(path10) {
5513
- return path10 instanceof String && cache.has(path10);
5388
+ function isPathSpec(path11) {
5389
+ return path11 instanceof String && cache.has(path11);
5514
5390
  }
5515
5391
  function toPaths(pathSpec) {
5516
5392
  return cache.get(pathSpec) || [];
@@ -5599,8 +5475,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
5599
5475
  function forEachLineWithContent(input, callback) {
5600
5476
  return toLinesWithContent(input, true).map((line) => callback(line));
5601
5477
  }
5602
- function folderExists(path10) {
5603
- return (0, import_file_exists.exists)(path10, import_file_exists.FOLDER);
5478
+ function folderExists(path11) {
5479
+ return (0, import_file_exists.exists)(path11, import_file_exists.FOLDER);
5604
5480
  }
5605
5481
  function append(target, item) {
5606
5482
  if (Array.isArray(target)) {
@@ -6004,8 +5880,8 @@ function checkIsRepoRootTask() {
6004
5880
  commands,
6005
5881
  format: "utf-8",
6006
5882
  onError,
6007
- parser(path10) {
6008
- return /^\.(git)?$/.test(path10.trim());
5883
+ parser(path11) {
5884
+ return /^\.(git)?$/.test(path11.trim());
6009
5885
  }
6010
5886
  };
6011
5887
  }
@@ -6439,11 +6315,11 @@ function parseGrep(grep) {
6439
6315
  const paths = /* @__PURE__ */ new Set();
6440
6316
  const results = {};
6441
6317
  forEachLineWithContent(grep, (input) => {
6442
- const [path10, line, preview] = input.split(NULL);
6443
- paths.add(path10);
6444
- (results[path10] = results[path10] || []).push({
6318
+ const [path11, line, preview] = input.split(NULL);
6319
+ paths.add(path11);
6320
+ (results[path11] = results[path11] || []).push({
6445
6321
  line: asNumber(line),
6446
- path: path10,
6322
+ path: path11,
6447
6323
  preview
6448
6324
  });
6449
6325
  });
@@ -7208,14 +7084,14 @@ var init_hash_object = __esm({
7208
7084
  init_task();
7209
7085
  }
7210
7086
  });
7211
- function parseInit(bare, path10, text2) {
7087
+ function parseInit(bare, path11, text2) {
7212
7088
  const response = String(text2).trim();
7213
7089
  let result;
7214
7090
  if (result = initResponseRegex.exec(response)) {
7215
- return new InitSummary(bare, path10, false, result[1]);
7091
+ return new InitSummary(bare, path11, false, result[1]);
7216
7092
  }
7217
7093
  if (result = reInitResponseRegex.exec(response)) {
7218
- return new InitSummary(bare, path10, true, result[1]);
7094
+ return new InitSummary(bare, path11, true, result[1]);
7219
7095
  }
7220
7096
  let gitDir = "";
7221
7097
  const tokens = response.split(" ");
@@ -7226,7 +7102,7 @@ function parseInit(bare, path10, text2) {
7226
7102
  break;
7227
7103
  }
7228
7104
  }
7229
- return new InitSummary(bare, path10, /^re/i.test(response), gitDir);
7105
+ return new InitSummary(bare, path11, /^re/i.test(response), gitDir);
7230
7106
  }
7231
7107
  var InitSummary;
7232
7108
  var initResponseRegex;
@@ -7235,9 +7111,9 @@ var init_InitSummary = __esm({
7235
7111
  "src/lib/responses/InitSummary.ts"() {
7236
7112
  "use strict";
7237
7113
  InitSummary = class {
7238
- constructor(bare, path10, existing, gitDir) {
7114
+ constructor(bare, path11, existing, gitDir) {
7239
7115
  this.bare = bare;
7240
- this.path = path10;
7116
+ this.path = path11;
7241
7117
  this.existing = existing;
7242
7118
  this.gitDir = gitDir;
7243
7119
  }
@@ -7249,7 +7125,7 @@ var init_InitSummary = __esm({
7249
7125
  function hasBareCommand(command) {
7250
7126
  return command.includes(bareCommand);
7251
7127
  }
7252
- function initTask(bare = false, path10, customArgs) {
7128
+ function initTask(bare = false, path11, customArgs) {
7253
7129
  const commands = ["init", ...customArgs];
7254
7130
  if (bare && !hasBareCommand(commands)) {
7255
7131
  commands.splice(1, 0, bareCommand);
@@ -7258,7 +7134,7 @@ function initTask(bare = false, path10, customArgs) {
7258
7134
  commands,
7259
7135
  format: "utf-8",
7260
7136
  parser(text2) {
7261
- return parseInit(commands.includes("--bare"), path10, text2);
7137
+ return parseInit(commands.includes("--bare"), path11, text2);
7262
7138
  }
7263
7139
  };
7264
7140
  }
@@ -8074,12 +7950,12 @@ var init_FileStatusSummary = __esm({
8074
7950
  "use strict";
8075
7951
  fromPathRegex = /^(.+)\0(.+)$/;
8076
7952
  FileStatusSummary = class {
8077
- constructor(path10, index, working_dir) {
8078
- this.path = path10;
7953
+ constructor(path11, index, working_dir) {
7954
+ this.path = path11;
8079
7955
  this.index = index;
8080
7956
  this.working_dir = working_dir;
8081
7957
  if (index === "R" || working_dir === "R") {
8082
- const detail = fromPathRegex.exec(path10) || [null, path10, path10];
7958
+ const detail = fromPathRegex.exec(path11) || [null, path11, path11];
8083
7959
  this.from = detail[2] || "";
8084
7960
  this.path = detail[1] || "";
8085
7961
  }
@@ -8110,14 +7986,14 @@ function splitLine(result, lineStr) {
8110
7986
  default:
8111
7987
  return;
8112
7988
  }
8113
- function data(index, workingDir, path10) {
7989
+ function data(index, workingDir, path11) {
8114
7990
  const raw = `${index}${workingDir}`;
8115
7991
  const handler = parsers6.get(raw);
8116
7992
  if (handler) {
8117
- handler(result, path10);
7993
+ handler(result, path11);
8118
7994
  }
8119
7995
  if (raw !== "##" && raw !== "!!") {
8120
- result.files.push(new FileStatusSummary(path10, index, workingDir));
7996
+ result.files.push(new FileStatusSummary(path11, index, workingDir));
8121
7997
  }
8122
7998
  }
8123
7999
  }
@@ -8430,9 +8306,9 @@ var init_simple_git_api = __esm({
8430
8306
  next
8431
8307
  );
8432
8308
  }
8433
- hashObject(path10, write) {
8309
+ hashObject(path11, write) {
8434
8310
  return this._runTask(
8435
- hashObjectTask(path10, write === true),
8311
+ hashObjectTask(path11, write === true),
8436
8312
  trailingFunctionArgument(arguments)
8437
8313
  );
8438
8314
  }
@@ -8785,8 +8661,8 @@ var init_branch = __esm({
8785
8661
  }
8786
8662
  });
8787
8663
  function toPath(input) {
8788
- const path10 = input.trim().replace(/^["']|["']$/g, "");
8789
- return path10 && normalize2(path10);
8664
+ const path11 = input.trim().replace(/^["']|["']$/g, "");
8665
+ return path11 && normalize2(path11);
8790
8666
  }
8791
8667
  var parseCheckIgnore;
8792
8668
  var init_CheckIgnore = __esm({
@@ -9100,8 +8976,8 @@ __export(sub_module_exports, {
9100
8976
  subModuleTask: () => subModuleTask,
9101
8977
  updateSubModuleTask: () => updateSubModuleTask
9102
8978
  });
9103
- function addSubModuleTask(repo, path10) {
9104
- return subModuleTask(["add", repo, path10]);
8979
+ function addSubModuleTask(repo, path11) {
8980
+ return subModuleTask(["add", repo, path11]);
9105
8981
  }
9106
8982
  function initSubModuleTask(customArgs) {
9107
8983
  return subModuleTask(["init", ...customArgs]);
@@ -9431,8 +9307,8 @@ var require_git = __commonJS2({
9431
9307
  }
9432
9308
  return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
9433
9309
  };
9434
- Git2.prototype.submoduleAdd = function(repo, path10, then) {
9435
- return this._runTask(addSubModuleTask2(repo, path10), trailingFunctionArgument2(arguments));
9310
+ Git2.prototype.submoduleAdd = function(repo, path11, then) {
9311
+ return this._runTask(addSubModuleTask2(repo, path11), trailingFunctionArgument2(arguments));
9436
9312
  };
9437
9313
  Git2.prototype.submoduleUpdate = function(args, then) {
9438
9314
  return this._runTask(
@@ -10225,8 +10101,8 @@ async function getHeadSha(baseDir, options) {
10225
10101
  }
10226
10102
 
10227
10103
  // src/sagas/apply-snapshot-saga.ts
10228
- import { mkdir as mkdir3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
10229
- import { join as join6 } from "path";
10104
+ import { mkdir as mkdir4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
10105
+ import { join as join7 } from "path";
10230
10106
 
10231
10107
  // ../git/dist/sagas/tree.js
10232
10108
  import { existsSync as existsSync4 } from "fs";
@@ -10234,148 +10110,21 @@ import * as fs7 from "fs/promises";
10234
10110
  import * as path9 from "path";
10235
10111
  import * as tar from "tar";
10236
10112
 
10237
- // ../shared/dist/index.js
10238
- var consoleLogger = {
10239
- info: (_message, _data) => {
10240
- },
10241
- debug: (_message, _data) => {
10242
- },
10243
- error: (_message, _data) => {
10244
- },
10245
- warn: (_message, _data) => {
10113
+ // ../git/dist/git-saga.js
10114
+ var GitSaga = class extends Saga {
10115
+ _git = null;
10116
+ get git() {
10117
+ if (!this._git) {
10118
+ throw new Error("git client accessed before execute() was called");
10119
+ }
10120
+ return this._git;
10246
10121
  }
10247
- };
10248
- var Saga = class {
10249
- completedSteps = [];
10250
- currentStepName = "unknown";
10251
- stepTimings = [];
10252
- log;
10253
- constructor(logger) {
10254
- this.log = logger ?? consoleLogger;
10255
- }
10256
- /**
10257
- * Run the saga with the given input.
10258
- * Returns a discriminated union result - either success with data or failure with error details.
10259
- */
10260
- async run(input) {
10261
- this.completedSteps = [];
10262
- this.currentStepName = "unknown";
10263
- this.stepTimings = [];
10264
- const sagaStart = performance.now();
10265
- this.log.info("Starting saga", { sagaName: this.sagaName });
10266
- try {
10267
- const result = await this.execute(input);
10268
- const totalDuration = performance.now() - sagaStart;
10269
- this.log.debug("Saga completed successfully", {
10270
- sagaName: this.sagaName,
10271
- stepsCompleted: this.completedSteps.length,
10272
- totalDurationMs: Math.round(totalDuration),
10273
- stepTimings: this.stepTimings
10274
- });
10275
- return { success: true, data: result };
10276
- } catch (error) {
10277
- this.log.error("Saga failed, initiating rollback", {
10278
- sagaName: this.sagaName,
10279
- failedStep: this.currentStepName,
10280
- error: error instanceof Error ? error.message : String(error)
10281
- });
10282
- await this.rollback();
10283
- return {
10284
- success: false,
10285
- error: error instanceof Error ? error.message : String(error),
10286
- failedStep: this.currentStepName
10287
- };
10288
- }
10289
- }
10290
- /**
10291
- * Execute a step with its rollback action.
10292
- * If the step succeeds, its rollback action is stored for potential rollback.
10293
- * The step name is automatically tracked for error reporting.
10294
- *
10295
- * @param config - Step configuration with name, execute, and rollback functions
10296
- * @returns The result of the execute function
10297
- * @throws Re-throws any error from the execute function (triggers rollback)
10298
- */
10299
- async step(config) {
10300
- this.currentStepName = config.name;
10301
- this.log.debug(`Executing step: ${config.name}`);
10302
- const stepStart = performance.now();
10303
- const result = await config.execute();
10304
- const durationMs = Math.round(performance.now() - stepStart);
10305
- this.stepTimings.push({ name: config.name, durationMs });
10306
- this.log.debug(`Step completed: ${config.name}`, { durationMs });
10307
- this.completedSteps.push({
10308
- name: config.name,
10309
- rollback: () => config.rollback(result)
10310
- });
10311
- return result;
10312
- }
10313
- /**
10314
- * Execute a step that doesn't need rollback.
10315
- * Useful for read-only operations or operations that are idempotent.
10316
- * The step name is automatically tracked for error reporting.
10317
- *
10318
- * @param name - Step name for logging and error tracking
10319
- * @param execute - The action to execute
10320
- * @returns The result of the execute function
10321
- */
10322
- async readOnlyStep(name, execute) {
10323
- this.currentStepName = name;
10324
- this.log.debug(`Executing read-only step: ${name}`);
10325
- const stepStart = performance.now();
10326
- const result = await execute();
10327
- const durationMs = Math.round(performance.now() - stepStart);
10328
- this.stepTimings.push({ name, durationMs });
10329
- this.log.debug(`Read-only step completed: ${name}`, { durationMs });
10330
- return result;
10331
- }
10332
- /**
10333
- * Roll back all completed steps in reverse order.
10334
- * Rollback errors are logged but don't stop the rollback of other steps.
10335
- */
10336
- async rollback() {
10337
- this.log.info("Rolling back saga", {
10338
- stepsToRollback: this.completedSteps.length
10339
- });
10340
- const stepsReversed = [...this.completedSteps].reverse();
10341
- for (const step of stepsReversed) {
10342
- try {
10343
- this.log.debug(`Rolling back step: ${step.name}`);
10344
- await step.rollback();
10345
- this.log.debug(`Step rolled back: ${step.name}`);
10346
- } catch (error) {
10347
- this.log.error(`Failed to rollback step: ${step.name}`, {
10348
- error: error instanceof Error ? error.message : String(error)
10349
- });
10350
- }
10351
- }
10352
- this.log.info("Rollback completed", {
10353
- stepsAttempted: this.completedSteps.length
10354
- });
10355
- }
10356
- /**
10357
- * Get the number of completed steps (useful for testing)
10358
- */
10359
- getCompletedStepCount() {
10360
- return this.completedSteps.length;
10361
- }
10362
- };
10363
-
10364
- // ../git/dist/git-saga.js
10365
- var GitSaga = class extends Saga {
10366
- _git = null;
10367
- get git() {
10368
- if (!this._git) {
10369
- throw new Error("git client accessed before execute() was called");
10370
- }
10371
- return this._git;
10372
- }
10373
- async execute(input) {
10374
- const manager = getGitOperationManager();
10375
- return manager.executeWrite(input.baseDir, async (git) => {
10376
- this._git = git;
10377
- return this.executeGitOperations(input);
10378
- }, { signal: input.signal });
10122
+ async execute(input) {
10123
+ const manager = getGitOperationManager();
10124
+ return manager.executeWrite(input.baseDir, async (git) => {
10125
+ this._git = git;
10126
+ return this.executeGitOperations(input);
10127
+ }, { signal: input.signal });
10379
10128
  }
10380
10129
  };
10381
10130
 
@@ -10641,18 +10390,18 @@ var ApplySnapshotSaga = class extends Saga {
10641
10390
  archivePath = null;
10642
10391
  async execute(input) {
10643
10392
  const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
10644
- const tmpDir = join6(repositoryPath, ".posthog", "tmp");
10393
+ const tmpDir = join7(repositoryPath, ".posthog", "tmp");
10645
10394
  if (!snapshot.archiveUrl) {
10646
10395
  throw new Error("Cannot apply snapshot: no archive URL");
10647
10396
  }
10648
10397
  const archiveUrl = snapshot.archiveUrl;
10649
10398
  await this.step({
10650
10399
  name: "create_tmp_dir",
10651
- execute: () => mkdir3(tmpDir, { recursive: true }),
10400
+ execute: () => mkdir4(tmpDir, { recursive: true }),
10652
10401
  rollback: async () => {
10653
10402
  }
10654
10403
  });
10655
- const archivePath = join6(tmpDir, `${snapshot.treeHash}.tar.gz`);
10404
+ const archivePath = join7(tmpDir, `${snapshot.treeHash}.tar.gz`);
10656
10405
  this.archivePath = archivePath;
10657
10406
  await this.step({
10658
10407
  name: "download_archive",
@@ -10667,7 +10416,7 @@ var ApplySnapshotSaga = class extends Saga {
10667
10416
  }
10668
10417
  const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
10669
10418
  const binaryContent = Buffer.from(base64Content, "base64");
10670
- await writeFile3(archivePath, binaryContent);
10419
+ await writeFile4(archivePath, binaryContent);
10671
10420
  },
10672
10421
  rollback: async () => {
10673
10422
  if (this.archivePath) {
@@ -10701,7 +10450,7 @@ var ApplySnapshotSaga = class extends Saga {
10701
10450
  // src/sagas/capture-tree-saga.ts
10702
10451
  import { existsSync as existsSync5 } from "fs";
10703
10452
  import { readFile as readFile3, rm as rm4 } from "fs/promises";
10704
- import { join as join7 } from "path";
10453
+ import { join as join8 } from "path";
10705
10454
  var CaptureTreeSaga2 = class extends Saga {
10706
10455
  sagaName = "CaptureTreeSaga";
10707
10456
  async execute(input) {
@@ -10713,14 +10462,14 @@ var CaptureTreeSaga2 = class extends Saga {
10713
10462
  taskId,
10714
10463
  runId
10715
10464
  } = input;
10716
- const tmpDir = join7(repositoryPath, ".posthog", "tmp");
10717
- if (existsSync5(join7(repositoryPath, ".gitmodules"))) {
10465
+ const tmpDir = join8(repositoryPath, ".posthog", "tmp");
10466
+ if (existsSync5(join8(repositoryPath, ".gitmodules"))) {
10718
10467
  this.log.warn(
10719
10468
  "Repository has submodules - snapshot may not capture submodule state"
10720
10469
  );
10721
10470
  }
10722
10471
  const shouldArchive = !!apiClient;
10723
- const archivePath = shouldArchive ? join7(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
10472
+ const archivePath = shouldArchive ? join8(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
10724
10473
  const gitCaptureSaga = new CaptureTreeSaga(this.log);
10725
10474
  const captureResult = await gitCaptureSaga.run({
10726
10475
  baseDir: repositoryPath,
@@ -10841,57 +10590,632 @@ var TreeTracker = class {
10841
10590
  `Failed to capture tree at step '${result.failedStep}': ${result.error}`
10842
10591
  );
10843
10592
  }
10844
- if (result.data.newTreeHash !== null) {
10845
- this.lastTreeHash = result.data.newTreeHash;
10593
+ if (result.data.newTreeHash !== null) {
10594
+ this.lastTreeHash = result.data.newTreeHash;
10595
+ }
10596
+ return result.data.snapshot;
10597
+ }
10598
+ /**
10599
+ * Download and apply a tree snapshot.
10600
+ * Uses Saga pattern for atomic operation with rollback on failure.
10601
+ */
10602
+ async applyTreeSnapshot(snapshot) {
10603
+ if (!this.apiClient) {
10604
+ throw new Error("Cannot apply snapshot: API client not configured");
10605
+ }
10606
+ if (!snapshot.archiveUrl) {
10607
+ this.logger.warn("Cannot apply snapshot: no archive URL", {
10608
+ treeHash: snapshot.treeHash,
10609
+ changes: snapshot.changes.length
10610
+ });
10611
+ throw new Error("Cannot apply snapshot: no archive URL");
10612
+ }
10613
+ const saga = new ApplySnapshotSaga(this.logger);
10614
+ const result = await saga.run({
10615
+ snapshot,
10616
+ repositoryPath: this.repositoryPath,
10617
+ apiClient: this.apiClient,
10618
+ taskId: this.taskId,
10619
+ runId: this.runId
10620
+ });
10621
+ if (!result.success) {
10622
+ this.logger.error("Failed to apply tree snapshot", {
10623
+ error: result.error,
10624
+ failedStep: result.failedStep,
10625
+ treeHash: snapshot.treeHash
10626
+ });
10627
+ throw new Error(
10628
+ `Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
10629
+ );
10630
+ }
10631
+ this.lastTreeHash = result.data.treeHash;
10632
+ }
10633
+ /**
10634
+ * Get the last captured tree hash.
10635
+ */
10636
+ getLastTreeHash() {
10637
+ return this.lastTreeHash;
10638
+ }
10639
+ /**
10640
+ * Set the last tree hash (used when resuming).
10641
+ */
10642
+ setLastTreeHash(hash) {
10643
+ this.lastTreeHash = hash;
10644
+ }
10645
+ };
10646
+
10647
+ // src/sagas/resume-saga.ts
10648
+ var ResumeSaga = class extends Saga {
10649
+ sagaName = "ResumeSaga";
10650
+ async execute(input) {
10651
+ const { taskId, runId, repositoryPath, apiClient } = input;
10652
+ const logger = input.logger || new Logger({ debug: false, prefix: "[Resume]" });
10653
+ const taskRun = await this.readOnlyStep(
10654
+ "fetch_task_run",
10655
+ () => apiClient.getTaskRun(taskId, runId)
10656
+ );
10657
+ if (!taskRun.log_url) {
10658
+ this.log.info("No log URL found, starting fresh");
10659
+ return this.emptyResult();
10660
+ }
10661
+ const entries = await this.readOnlyStep(
10662
+ "fetch_logs",
10663
+ () => apiClient.fetchTaskRunLogs(taskRun)
10664
+ );
10665
+ if (entries.length === 0) {
10666
+ this.log.info("No log entries found, starting fresh");
10667
+ return this.emptyResult();
10668
+ }
10669
+ this.log.info("Fetched log entries", { count: entries.length });
10670
+ const latestSnapshot = await this.readOnlyStep(
10671
+ "find_snapshot",
10672
+ () => Promise.resolve(this.findLatestTreeSnapshot(entries))
10673
+ );
10674
+ let snapshotApplied = false;
10675
+ if (latestSnapshot?.archiveUrl && repositoryPath) {
10676
+ this.log.info("Found tree snapshot", {
10677
+ treeHash: latestSnapshot.treeHash,
10678
+ hasArchiveUrl: true,
10679
+ changes: latestSnapshot.changes?.length ?? 0,
10680
+ interrupted: latestSnapshot.interrupted
10681
+ });
10682
+ await this.step({
10683
+ name: "apply_snapshot",
10684
+ execute: async () => {
10685
+ const treeTracker = new TreeTracker({
10686
+ repositoryPath,
10687
+ taskId,
10688
+ runId,
10689
+ apiClient,
10690
+ logger: logger.child("TreeTracker")
10691
+ });
10692
+ try {
10693
+ await treeTracker.applyTreeSnapshot(latestSnapshot);
10694
+ treeTracker.setLastTreeHash(latestSnapshot.treeHash);
10695
+ snapshotApplied = true;
10696
+ this.log.info("Tree snapshot applied successfully", {
10697
+ treeHash: latestSnapshot.treeHash
10698
+ });
10699
+ } catch (error) {
10700
+ this.log.warn(
10701
+ "Failed to apply tree snapshot, continuing without it",
10702
+ {
10703
+ error: error instanceof Error ? error.message : String(error),
10704
+ treeHash: latestSnapshot.treeHash
10705
+ }
10706
+ );
10707
+ }
10708
+ },
10709
+ rollback: async () => {
10710
+ }
10711
+ });
10712
+ } else if (latestSnapshot?.archiveUrl && !repositoryPath) {
10713
+ this.log.warn(
10714
+ "Snapshot found but no repositoryPath configured - files cannot be restored",
10715
+ {
10716
+ treeHash: latestSnapshot.treeHash,
10717
+ changes: latestSnapshot.changes?.length ?? 0
10718
+ }
10719
+ );
10720
+ } else if (latestSnapshot) {
10721
+ this.log.warn(
10722
+ "Snapshot found but has no archive URL - files cannot be restored",
10723
+ {
10724
+ treeHash: latestSnapshot.treeHash,
10725
+ changes: latestSnapshot.changes?.length ?? 0
10726
+ }
10727
+ );
10728
+ }
10729
+ const conversation = await this.readOnlyStep(
10730
+ "rebuild_conversation",
10731
+ () => Promise.resolve(this.rebuildConversation(entries))
10732
+ );
10733
+ const lastDevice = await this.readOnlyStep(
10734
+ "find_device",
10735
+ () => Promise.resolve(this.findLastDeviceInfo(entries))
10736
+ );
10737
+ this.log.info("Resume state rebuilt", {
10738
+ turns: conversation.length,
10739
+ hasSnapshot: !!latestSnapshot,
10740
+ snapshotApplied,
10741
+ interrupted: latestSnapshot?.interrupted ?? false
10742
+ });
10743
+ return {
10744
+ conversation,
10745
+ latestSnapshot,
10746
+ snapshotApplied,
10747
+ interrupted: latestSnapshot?.interrupted ?? false,
10748
+ lastDevice,
10749
+ logEntryCount: entries.length
10750
+ };
10751
+ }
10752
+ emptyResult() {
10753
+ return {
10754
+ conversation: [],
10755
+ latestSnapshot: null,
10756
+ snapshotApplied: false,
10757
+ interrupted: false,
10758
+ logEntryCount: 0
10759
+ };
10760
+ }
10761
+ findLatestTreeSnapshot(entries) {
10762
+ const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT}`;
10763
+ for (let i = entries.length - 1; i >= 0; i--) {
10764
+ const entry = entries[i];
10765
+ const method = entry.notification?.method;
10766
+ if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT) {
10767
+ const params = entry.notification.params;
10768
+ if (params?.treeHash) {
10769
+ return params;
10770
+ }
10771
+ }
10772
+ }
10773
+ return null;
10774
+ }
10775
+ findLastDeviceInfo(entries) {
10776
+ for (let i = entries.length - 1; i >= 0; i--) {
10777
+ const entry = entries[i];
10778
+ const params = entry.notification?.params;
10779
+ if (params?.device) {
10780
+ return params.device;
10781
+ }
10782
+ }
10783
+ return void 0;
10784
+ }
10785
+ rebuildConversation(entries) {
10786
+ const turns = [];
10787
+ let currentAssistantContent = [];
10788
+ let currentToolCalls = [];
10789
+ for (const entry of entries) {
10790
+ const method = entry.notification?.method;
10791
+ const params = entry.notification?.params;
10792
+ if (method === "session/update" && params?.update) {
10793
+ const update = params.update;
10794
+ const sessionUpdate = update.sessionUpdate;
10795
+ switch (sessionUpdate) {
10796
+ case "user_message":
10797
+ case "user_message_chunk": {
10798
+ if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
10799
+ turns.push({
10800
+ role: "assistant",
10801
+ content: currentAssistantContent,
10802
+ toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
10803
+ });
10804
+ currentAssistantContent = [];
10805
+ currentToolCalls = [];
10806
+ }
10807
+ const content = update.content;
10808
+ const contentArray = Array.isArray(content) ? content : [content];
10809
+ turns.push({
10810
+ role: "user",
10811
+ content: contentArray
10812
+ });
10813
+ break;
10814
+ }
10815
+ case "agent_message": {
10816
+ const content = update.content;
10817
+ if (content) {
10818
+ if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
10819
+ const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
10820
+ lastBlock.text += content.text;
10821
+ } else {
10822
+ currentAssistantContent.push(content);
10823
+ }
10824
+ }
10825
+ break;
10826
+ }
10827
+ case "agent_message_chunk": {
10828
+ const content = update.content;
10829
+ if (content) {
10830
+ if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
10831
+ const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
10832
+ lastBlock.text += content.text;
10833
+ } else {
10834
+ currentAssistantContent.push(content);
10835
+ }
10836
+ }
10837
+ break;
10838
+ }
10839
+ case "tool_call":
10840
+ case "tool_call_update": {
10841
+ const meta = update._meta?.claudeCode;
10842
+ if (meta) {
10843
+ const toolCallId = meta.toolCallId;
10844
+ const toolName = meta.toolName;
10845
+ const toolInput = meta.toolInput;
10846
+ const toolResponse = meta.toolResponse;
10847
+ if (toolCallId && toolName) {
10848
+ let toolCall = currentToolCalls.find(
10849
+ (tc) => tc.toolCallId === toolCallId
10850
+ );
10851
+ if (!toolCall) {
10852
+ toolCall = {
10853
+ toolCallId,
10854
+ toolName,
10855
+ input: toolInput
10856
+ };
10857
+ currentToolCalls.push(toolCall);
10858
+ }
10859
+ if (toolResponse !== void 0) {
10860
+ toolCall.result = toolResponse;
10861
+ }
10862
+ }
10863
+ }
10864
+ break;
10865
+ }
10866
+ case "tool_result": {
10867
+ const meta = update._meta?.claudeCode;
10868
+ if (meta) {
10869
+ const toolCallId = meta.toolCallId;
10870
+ const toolResponse = meta.toolResponse;
10871
+ if (toolCallId) {
10872
+ const toolCall = currentToolCalls.find(
10873
+ (tc) => tc.toolCallId === toolCallId
10874
+ );
10875
+ if (toolCall && toolResponse !== void 0) {
10876
+ toolCall.result = toolResponse;
10877
+ }
10878
+ }
10879
+ }
10880
+ break;
10881
+ }
10882
+ }
10883
+ }
10884
+ }
10885
+ if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
10886
+ turns.push({
10887
+ role: "assistant",
10888
+ content: currentAssistantContent,
10889
+ toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
10890
+ });
10891
+ }
10892
+ return turns;
10893
+ }
10894
+ };
10895
+
10896
+ // src/resume.ts
10897
+ async function resumeFromLog(config) {
10898
+ const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
10899
+ logger.info("Resuming from log", {
10900
+ taskId: config.taskId,
10901
+ runId: config.runId
10902
+ });
10903
+ const saga = new ResumeSaga(logger);
10904
+ const result = await saga.run({
10905
+ taskId: config.taskId,
10906
+ runId: config.runId,
10907
+ repositoryPath: config.repositoryPath,
10908
+ apiClient: config.apiClient,
10909
+ logger
10910
+ });
10911
+ if (!result.success) {
10912
+ logger.error("Failed to resume from log", {
10913
+ error: result.error,
10914
+ failedStep: result.failedStep
10915
+ });
10916
+ throw new Error(
10917
+ `Failed to resume at step '${result.failedStep}': ${result.error}`
10918
+ );
10919
+ }
10920
+ return {
10921
+ conversation: result.data.conversation,
10922
+ latestSnapshot: result.data.latestSnapshot,
10923
+ snapshotApplied: result.data.snapshotApplied,
10924
+ interrupted: result.data.interrupted,
10925
+ lastDevice: result.data.lastDevice,
10926
+ logEntryCount: result.data.logEntryCount
10927
+ };
10928
+ }
10929
+
10930
+ // src/session-log-writer.ts
10931
+ import fs8 from "fs";
10932
+ import fsp from "fs/promises";
10933
+ import path10 from "path";
10934
+ var SessionLogWriter = class _SessionLogWriter {
10935
+ static FLUSH_DEBOUNCE_MS = 500;
10936
+ static FLUSH_MAX_INTERVAL_MS = 5e3;
10937
+ static MAX_FLUSH_RETRIES = 10;
10938
+ static MAX_RETRY_DELAY_MS = 3e4;
10939
+ static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
10940
+ posthogAPI;
10941
+ pendingEntries = /* @__PURE__ */ new Map();
10942
+ flushTimeouts = /* @__PURE__ */ new Map();
10943
+ lastFlushAttemptTime = /* @__PURE__ */ new Map();
10944
+ retryCounts = /* @__PURE__ */ new Map();
10945
+ sessions = /* @__PURE__ */ new Map();
10946
+ logger;
10947
+ localCachePath;
10948
+ constructor(options = {}) {
10949
+ this.posthogAPI = options.posthogAPI;
10950
+ this.localCachePath = options.localCachePath;
10951
+ this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
10952
+ }
10953
+ async flushAll() {
10954
+ const sessionIds = [...this.sessions.keys()];
10955
+ const flushPromises = [];
10956
+ for (const sessionId of sessionIds) {
10957
+ flushPromises.push(this.flush(sessionId));
10958
+ }
10959
+ await Promise.all(flushPromises);
10960
+ }
10961
+ register(sessionId, context) {
10962
+ if (this.sessions.has(sessionId)) {
10963
+ return;
10964
+ }
10965
+ this.logger.info("Session registered", {
10966
+ taskId: context.taskId,
10967
+ runId: context.runId
10968
+ });
10969
+ this.sessions.set(sessionId, { context });
10970
+ this.lastFlushAttemptTime.set(sessionId, Date.now());
10971
+ if (this.localCachePath) {
10972
+ const sessionDir = path10.join(
10973
+ this.localCachePath,
10974
+ "sessions",
10975
+ context.runId
10976
+ );
10977
+ try {
10978
+ fs8.mkdirSync(sessionDir, { recursive: true });
10979
+ } catch (error) {
10980
+ this.logger.warn("Failed to create local cache directory", {
10981
+ sessionDir,
10982
+ error
10983
+ });
10984
+ }
10985
+ }
10986
+ }
10987
+ isRegistered(sessionId) {
10988
+ return this.sessions.has(sessionId);
10989
+ }
10990
+ appendRawLine(sessionId, line) {
10991
+ const session = this.sessions.get(sessionId);
10992
+ if (!session) {
10993
+ this.logger.warn("appendRawLine called for unregistered session", {
10994
+ sessionId
10995
+ });
10996
+ return;
10997
+ }
10998
+ try {
10999
+ const message = JSON.parse(line);
11000
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11001
+ if (this.isAgentMessageChunk(message)) {
11002
+ const text2 = this.extractChunkText(message);
11003
+ if (text2) {
11004
+ if (!session.chunkBuffer) {
11005
+ session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
11006
+ } else {
11007
+ session.chunkBuffer.text += text2;
11008
+ }
11009
+ }
11010
+ return;
11011
+ }
11012
+ this.emitCoalescedMessage(sessionId, session);
11013
+ const nonChunkAgentText = this.extractAgentMessageText(message);
11014
+ if (nonChunkAgentText) {
11015
+ session.lastAgentMessage = nonChunkAgentText;
11016
+ }
11017
+ const entry = {
11018
+ type: "notification",
11019
+ timestamp,
11020
+ notification: message
11021
+ };
11022
+ this.writeToLocalCache(sessionId, entry);
11023
+ if (this.posthogAPI) {
11024
+ const pending = this.pendingEntries.get(sessionId) ?? [];
11025
+ pending.push(entry);
11026
+ this.pendingEntries.set(sessionId, pending);
11027
+ this.scheduleFlush(sessionId);
11028
+ }
11029
+ } catch {
11030
+ this.logger.warn("Failed to parse raw line for persistence", {
11031
+ taskId: session.context.taskId,
11032
+ runId: session.context.runId,
11033
+ lineLength: line.length
11034
+ });
11035
+ }
11036
+ }
11037
+ async flush(sessionId) {
11038
+ const session = this.sessions.get(sessionId);
11039
+ if (!session) {
11040
+ this.logger.warn("flush: no session found", { sessionId });
11041
+ return;
11042
+ }
11043
+ this.emitCoalescedMessage(sessionId, session);
11044
+ const pending = this.pendingEntries.get(sessionId);
11045
+ if (!this.posthogAPI || !pending?.length) {
11046
+ return;
11047
+ }
11048
+ this.pendingEntries.delete(sessionId);
11049
+ const timeout = this.flushTimeouts.get(sessionId);
11050
+ if (timeout) {
11051
+ clearTimeout(timeout);
11052
+ this.flushTimeouts.delete(sessionId);
11053
+ }
11054
+ this.lastFlushAttemptTime.set(sessionId, Date.now());
11055
+ try {
11056
+ await this.posthogAPI.appendTaskRunLog(
11057
+ session.context.taskId,
11058
+ session.context.runId,
11059
+ pending
11060
+ );
11061
+ this.retryCounts.set(sessionId, 0);
11062
+ } catch (error) {
11063
+ const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
11064
+ this.retryCounts.set(sessionId, retryCount);
11065
+ if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
11066
+ this.logger.error(
11067
+ `Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
11068
+ {
11069
+ taskId: session.context.taskId,
11070
+ runId: session.context.runId,
11071
+ error
11072
+ }
11073
+ );
11074
+ this.retryCounts.set(sessionId, 0);
11075
+ } else {
11076
+ if (retryCount === 1) {
11077
+ this.logger.warn(
11078
+ `Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
11079
+ {
11080
+ taskId: session.context.taskId,
11081
+ runId: session.context.runId,
11082
+ error: error instanceof Error ? error.message : String(error)
11083
+ }
11084
+ );
11085
+ }
11086
+ const currentPending = this.pendingEntries.get(sessionId) ?? [];
11087
+ this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
11088
+ this.scheduleFlush(sessionId);
11089
+ }
11090
+ }
11091
+ }
11092
+ isAgentMessageChunk(message) {
11093
+ if (message.method !== "session/update") return false;
11094
+ const params = message.params;
11095
+ const update = params?.update;
11096
+ return update?.sessionUpdate === "agent_message_chunk";
11097
+ }
11098
+ extractChunkText(message) {
11099
+ const params = message.params;
11100
+ const update = params?.update;
11101
+ const content = update?.content;
11102
+ if (content?.type === "text" && content.text) {
11103
+ return content.text;
11104
+ }
11105
+ return "";
11106
+ }
11107
+ emitCoalescedMessage(sessionId, session) {
11108
+ if (!session.chunkBuffer) return;
11109
+ const { text: text2, firstTimestamp } = session.chunkBuffer;
11110
+ session.chunkBuffer = void 0;
11111
+ session.lastAgentMessage = text2;
11112
+ const entry = {
11113
+ type: "notification",
11114
+ timestamp: firstTimestamp,
11115
+ notification: {
11116
+ jsonrpc: "2.0",
11117
+ method: "session/update",
11118
+ params: {
11119
+ update: {
11120
+ sessionUpdate: "agent_message",
11121
+ content: { type: "text", text: text2 }
11122
+ }
11123
+ }
11124
+ }
11125
+ };
11126
+ this.writeToLocalCache(sessionId, entry);
11127
+ if (this.posthogAPI) {
11128
+ const pending = this.pendingEntries.get(sessionId) ?? [];
11129
+ pending.push(entry);
11130
+ this.pendingEntries.set(sessionId, pending);
11131
+ this.scheduleFlush(sessionId);
10846
11132
  }
10847
- return result.data.snapshot;
10848
11133
  }
10849
- /**
10850
- * Download and apply a tree snapshot.
10851
- * Uses Saga pattern for atomic operation with rollback on failure.
10852
- */
10853
- async applyTreeSnapshot(snapshot) {
10854
- if (!this.apiClient) {
10855
- throw new Error("Cannot apply snapshot: API client not configured");
11134
+ getLastAgentMessage(sessionId) {
11135
+ return this.sessions.get(sessionId)?.lastAgentMessage;
11136
+ }
11137
+ extractAgentMessageText(message) {
11138
+ if (message.method !== "session/update") {
11139
+ return null;
10856
11140
  }
10857
- if (!snapshot.archiveUrl) {
10858
- this.logger.warn("Cannot apply snapshot: no archive URL", {
10859
- treeHash: snapshot.treeHash,
10860
- changes: snapshot.changes.length
10861
- });
10862
- throw new Error("Cannot apply snapshot: no archive URL");
11141
+ const params = message.params;
11142
+ const update = params?.update;
11143
+ if (update?.sessionUpdate !== "agent_message") {
11144
+ return null;
10863
11145
  }
10864
- const saga = new ApplySnapshotSaga(this.logger);
10865
- const result = await saga.run({
10866
- snapshot,
10867
- repositoryPath: this.repositoryPath,
10868
- apiClient: this.apiClient,
10869
- taskId: this.taskId,
10870
- runId: this.runId
10871
- });
10872
- if (!result.success) {
10873
- this.logger.error("Failed to apply tree snapshot", {
10874
- error: result.error,
10875
- failedStep: result.failedStep,
10876
- treeHash: snapshot.treeHash
10877
- });
10878
- throw new Error(
10879
- `Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
11146
+ const content = update.content;
11147
+ if (content?.type === "text" && typeof content.text === "string") {
11148
+ const trimmed2 = content.text.trim();
11149
+ return trimmed2.length > 0 ? trimmed2 : null;
11150
+ }
11151
+ if (typeof update.message === "string") {
11152
+ const trimmed2 = update.message.trim();
11153
+ return trimmed2.length > 0 ? trimmed2 : null;
11154
+ }
11155
+ return null;
11156
+ }
11157
+ scheduleFlush(sessionId) {
11158
+ const existing = this.flushTimeouts.get(sessionId);
11159
+ if (existing) clearTimeout(existing);
11160
+ const retryCount = this.retryCounts.get(sessionId) ?? 0;
11161
+ const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
11162
+ const elapsed = Date.now() - lastAttempt;
11163
+ let delay3;
11164
+ if (retryCount > 0) {
11165
+ delay3 = Math.min(
11166
+ _SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
11167
+ _SessionLogWriter.MAX_RETRY_DELAY_MS
10880
11168
  );
11169
+ } else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
11170
+ delay3 = 0;
11171
+ } else {
11172
+ delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
10881
11173
  }
10882
- this.lastTreeHash = result.data.treeHash;
11174
+ const timeout = setTimeout(() => this.flush(sessionId), delay3);
11175
+ this.flushTimeouts.set(sessionId, timeout);
10883
11176
  }
10884
- /**
10885
- * Get the last captured tree hash.
10886
- */
10887
- getLastTreeHash() {
10888
- return this.lastTreeHash;
11177
+ writeToLocalCache(sessionId, entry) {
11178
+ if (!this.localCachePath) return;
11179
+ const session = this.sessions.get(sessionId);
11180
+ if (!session) return;
11181
+ const logPath = path10.join(
11182
+ this.localCachePath,
11183
+ "sessions",
11184
+ session.context.runId,
11185
+ "logs.ndjson"
11186
+ );
11187
+ try {
11188
+ fs8.appendFileSync(logPath, `${JSON.stringify(entry)}
11189
+ `);
11190
+ } catch (error) {
11191
+ this.logger.warn("Failed to write to local cache", {
11192
+ taskId: session.context.taskId,
11193
+ runId: session.context.runId,
11194
+ logPath,
11195
+ error
11196
+ });
11197
+ }
10889
11198
  }
10890
- /**
10891
- * Set the last tree hash (used when resuming).
10892
- */
10893
- setLastTreeHash(hash) {
10894
- this.lastTreeHash = hash;
11199
+ static async cleanupOldSessions(localCachePath) {
11200
+ const sessionsDir = path10.join(localCachePath, "sessions");
11201
+ let deleted = 0;
11202
+ try {
11203
+ const entries = await fsp.readdir(sessionsDir);
11204
+ const now = Date.now();
11205
+ for (const entry of entries) {
11206
+ const entryPath = path10.join(sessionsDir, entry);
11207
+ try {
11208
+ const stats = await fsp.stat(entryPath);
11209
+ if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
11210
+ await fsp.rm(entryPath, { recursive: true, force: true });
11211
+ deleted++;
11212
+ }
11213
+ } catch {
11214
+ }
11215
+ }
11216
+ } catch {
11217
+ }
11218
+ return deleted;
10895
11219
  }
10896
11220
  };
10897
11221
 
@@ -11104,7 +11428,7 @@ function createTappedWritableStream2(underlying, onMessage, logger) {
11104
11428
  }
11105
11429
  });
11106
11430
  }
11107
- var AgentServer = class {
11431
+ var AgentServer = class _AgentServer {
11108
11432
  config;
11109
11433
  logger;
11110
11434
  server = null;
@@ -11113,6 +11437,7 @@ var AgentServer = class {
11113
11437
  posthogAPI;
11114
11438
  questionRelayedToSlack = false;
11115
11439
  detectedPrUrl = null;
11440
+ resumeState = null;
11116
11441
  emitConsoleLog = (level, _scope, message, data) => {
11117
11442
  if (!this.session) return;
11118
11443
  const formatted = data !== void 0 ? `${message} ${JSON.stringify(data)}` : message;
@@ -11295,6 +11620,32 @@ var AgentServer = class {
11295
11620
  async autoInitializeSession() {
11296
11621
  const { taskId, runId, mode, projectId } = this.config;
11297
11622
  this.logger.info("Auto-initializing session", { taskId, runId, mode });
11623
+ const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
11624
+ if (resumeRunId) {
11625
+ this.logger.info("Resuming from previous run", {
11626
+ resumeRunId,
11627
+ currentRunId: runId
11628
+ });
11629
+ try {
11630
+ this.resumeState = await resumeFromLog({
11631
+ taskId,
11632
+ runId: resumeRunId,
11633
+ repositoryPath: this.config.repositoryPath,
11634
+ apiClient: this.posthogAPI,
11635
+ logger: new Logger({ debug: true, prefix: "[Resume]" })
11636
+ });
11637
+ this.logger.info("Resume state loaded", {
11638
+ conversationTurns: this.resumeState.conversation.length,
11639
+ snapshotApplied: this.resumeState.snapshotApplied,
11640
+ logEntries: this.resumeState.logEntryCount
11641
+ });
11642
+ } catch (error) {
11643
+ this.logger.warn("Failed to load resume state, starting fresh", {
11644
+ error
11645
+ });
11646
+ this.resumeState = null;
11647
+ }
11648
+ }
11298
11649
  const payload = {
11299
11650
  task_id: taskId,
11300
11651
  run_id: runId,
@@ -11359,6 +11710,7 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
11359
11710
  }
11360
11711
  }
11361
11712
  });
11713
+ this.broadcastTurnComplete(result.stopReason);
11362
11714
  return { stopReason: result.stopReason };
11363
11715
  }
11364
11716
  case POSTHOG_NOTIFICATIONS.CANCEL:
@@ -11394,18 +11746,19 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
11394
11746
  name: process.env.HOSTNAME || "cloud-sandbox"
11395
11747
  };
11396
11748
  this.configureEnvironment();
11397
- const treeTracker = this.config.repositoryPath ? new TreeTracker({
11398
- repositoryPath: this.config.repositoryPath,
11399
- taskId: payload.task_id,
11400
- runId: payload.run_id,
11401
- logger: new Logger({ debug: true, prefix: "[TreeTracker]" })
11402
- }) : null;
11403
11749
  const posthogAPI = new PostHogAPIClient({
11404
11750
  apiUrl: this.config.apiUrl,
11405
11751
  projectId: this.config.projectId,
11406
11752
  getApiKey: () => this.config.apiKey,
11407
11753
  userAgent: `posthog/cloud.hog.dev; version: ${this.config.version ?? package_default.version}`
11408
11754
  });
11755
+ const treeTracker = this.config.repositoryPath ? new TreeTracker({
11756
+ repositoryPath: this.config.repositoryPath,
11757
+ taskId: payload.task_id,
11758
+ runId: payload.run_id,
11759
+ apiClient: posthogAPI,
11760
+ logger: new Logger({ debug: true, prefix: "[TreeTracker]" })
11761
+ }) : null;
11409
11762
  const logWriter = new SessionLogWriter({
11410
11763
  posthogAPI,
11411
11764
  logger: new Logger({ debug: true, prefix: "[SessionLogWriter]" })
@@ -11500,26 +11853,55 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
11500
11853
  }
11501
11854
  async sendInitialTaskMessage(payload, prefetchedRun) {
11502
11855
  if (!this.session) return;
11503
- try {
11504
- const task = await this.posthogAPI.getTask(payload.task_id);
11505
- let taskRun = prefetchedRun ?? null;
11506
- if (!taskRun) {
11856
+ let taskRun = prefetchedRun ?? null;
11857
+ if (!taskRun) {
11858
+ try {
11859
+ taskRun = await this.posthogAPI.getTaskRun(
11860
+ payload.task_id,
11861
+ payload.run_id
11862
+ );
11863
+ } catch (error) {
11864
+ this.logger.warn("Failed to fetch task run", {
11865
+ taskId: payload.task_id,
11866
+ runId: payload.run_id,
11867
+ error
11868
+ });
11869
+ }
11870
+ }
11871
+ if (!this.resumeState) {
11872
+ const resumeRunId = this.getResumeRunId(taskRun);
11873
+ if (resumeRunId) {
11874
+ this.logger.info("Resuming from previous run (via TaskRun state)", {
11875
+ resumeRunId,
11876
+ currentRunId: payload.run_id
11877
+ });
11507
11878
  try {
11508
- taskRun = await this.posthogAPI.getTaskRun(
11509
- payload.task_id,
11510
- payload.run_id
11511
- );
11879
+ this.resumeState = await resumeFromLog({
11880
+ taskId: payload.task_id,
11881
+ runId: resumeRunId,
11882
+ repositoryPath: this.config.repositoryPath,
11883
+ apiClient: this.posthogAPI,
11884
+ logger: new Logger({ debug: true, prefix: "[Resume]" })
11885
+ });
11886
+ this.logger.info("Resume state loaded (via TaskRun state)", {
11887
+ conversationTurns: this.resumeState.conversation.length,
11888
+ snapshotApplied: this.resumeState.snapshotApplied,
11889
+ logEntries: this.resumeState.logEntryCount
11890
+ });
11512
11891
  } catch (error) {
11513
- this.logger.warn(
11514
- "Failed to fetch task run for initial prompt override",
11515
- {
11516
- taskId: payload.task_id,
11517
- runId: payload.run_id,
11518
- error
11519
- }
11520
- );
11892
+ this.logger.warn("Failed to load resume state, starting fresh", {
11893
+ error
11894
+ });
11895
+ this.resumeState = null;
11521
11896
  }
11522
11897
  }
11898
+ }
11899
+ if (this.resumeState && this.resumeState.conversation.length > 0) {
11900
+ await this.sendResumeMessage(payload, taskRun);
11901
+ return;
11902
+ }
11903
+ try {
11904
+ const task = await this.posthogAPI.getTask(payload.task_id);
11523
11905
  const initialPromptOverride = taskRun ? this.getInitialPromptOverride(taskRun) : null;
11524
11906
  const initialPrompt = initialPromptOverride ?? task.description;
11525
11907
  if (!initialPrompt) {
@@ -11538,6 +11920,7 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
11538
11920
  this.logger.info("Initial task message completed", {
11539
11921
  stopReason: result.stopReason
11540
11922
  });
11923
+ this.broadcastTurnComplete(result.stopReason);
11541
11924
  if (result.stopReason === "end_turn") {
11542
11925
  await this.relayAgentResponse(payload);
11543
11926
  }
@@ -11549,6 +11932,94 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
11549
11932
  await this.signalTaskComplete(payload, "error");
11550
11933
  }
11551
11934
  }
11935
+ async sendResumeMessage(payload, taskRun) {
11936
+ if (!this.session || !this.resumeState) return;
11937
+ try {
11938
+ const conversationSummary = this.formatConversationForResume(
11939
+ this.resumeState.conversation
11940
+ );
11941
+ const pendingUserMessage = this.getPendingUserMessage(taskRun);
11942
+ const sandboxContext = this.resumeState.snapshotApplied ? `The sandbox environment (all files, packages, and code changes) has been fully restored from a snapshot.` : `The sandbox could not be restored from a snapshot (it may have expired). You are starting with a fresh environment but have the full conversation history below.`;
11943
+ let resumePrompt;
11944
+ if (pendingUserMessage) {
11945
+ resumePrompt = `You are resuming a previous conversation. ${sandboxContext}
11946
+
11947
+ Here is the conversation history from the previous session:
11948
+
11949
+ ${conversationSummary}
11950
+
11951
+ The user has sent a new message:
11952
+
11953
+ ${pendingUserMessage}
11954
+
11955
+ Respond to the user's new message above. You have full context from the previous session.`;
11956
+ } else {
11957
+ resumePrompt = `You are resuming a previous conversation. ${sandboxContext}
11958
+
11959
+ Here is the conversation history from the previous session:
11960
+
11961
+ ${conversationSummary}
11962
+
11963
+ Continue from where you left off. The user is waiting for your response.`;
11964
+ }
11965
+ this.logger.info("Sending resume message", {
11966
+ taskId: payload.task_id,
11967
+ conversationTurns: this.resumeState.conversation.length,
11968
+ promptLength: resumePrompt.length,
11969
+ hasPendingUserMessage: !!pendingUserMessage,
11970
+ snapshotApplied: this.resumeState.snapshotApplied
11971
+ });
11972
+ this.resumeState = null;
11973
+ const result = await this.session.clientConnection.prompt({
11974
+ sessionId: this.session.acpSessionId,
11975
+ prompt: [{ type: "text", text: resumePrompt }]
11976
+ });
11977
+ this.logger.info("Resume message completed", {
11978
+ stopReason: result.stopReason
11979
+ });
11980
+ this.broadcastTurnComplete(result.stopReason);
11981
+ } catch (error) {
11982
+ this.logger.error("Failed to send resume message", error);
11983
+ if (this.session) {
11984
+ await this.session.logWriter.flushAll();
11985
+ }
11986
+ await this.signalTaskComplete(payload, "error");
11987
+ }
11988
+ }
11989
+ static RESUME_HISTORY_TOKEN_BUDGET = 5e4;
11990
+ static TOOL_RESULT_MAX_CHARS = 2e3;
11991
+ formatConversationForResume(conversation) {
11992
+ const selected = selectRecentTurns(
11993
+ conversation,
11994
+ _AgentServer.RESUME_HISTORY_TOKEN_BUDGET
11995
+ );
11996
+ const parts = [];
11997
+ if (selected.length < conversation.length) {
11998
+ parts.push(
11999
+ `*(${conversation.length - selected.length} earlier turns omitted)*`
12000
+ );
12001
+ }
12002
+ for (const turn of selected) {
12003
+ const role = turn.role === "user" ? "User" : "Assistant";
12004
+ const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
12005
+ if (textParts.length > 0) {
12006
+ parts.push(`**${role}**: ${textParts.join("\n")}`);
12007
+ }
12008
+ if (turn.toolCalls?.length) {
12009
+ const toolSummary = turn.toolCalls.map((tc) => {
12010
+ let resultStr = "";
12011
+ if (tc.result !== void 0) {
12012
+ const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
12013
+ resultStr = raw.length > _AgentServer.TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, _AgentServer.TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
12014
+ }
12015
+ return ` - ${tc.toolName}${resultStr}`;
12016
+ }).join("\n");
12017
+ parts.push(`**${role} (tools)**:
12018
+ ${toolSummary}`);
12019
+ }
12020
+ }
12021
+ return parts.join("\n\n");
12022
+ }
11552
12023
  getInitialPromptOverride(taskRun) {
11553
12024
  const state = taskRun.state;
11554
12025
  const override = state?.initial_prompt_override;
@@ -11558,6 +12029,24 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
11558
12029
  const trimmed2 = override.trim();
11559
12030
  return trimmed2.length > 0 ? trimmed2 : null;
11560
12031
  }
12032
+ getPendingUserMessage(taskRun) {
12033
+ if (!taskRun) return null;
12034
+ const state = taskRun.state;
12035
+ const message = state?.pending_user_message;
12036
+ if (typeof message !== "string") {
12037
+ return null;
12038
+ }
12039
+ const trimmed2 = message.trim();
12040
+ return trimmed2.length > 0 ? trimmed2 : null;
12041
+ }
12042
+ getResumeRunId(taskRun) {
12043
+ const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
12044
+ if (envRunId) return envRunId;
12045
+ if (!taskRun) return null;
12046
+ const state = taskRun.state;
12047
+ const stateRunId = state?.resume_from_run_id;
12048
+ return typeof stateRunId === "string" && stateRunId.trim().length > 0 ? stateRunId.trim() : null;
12049
+ }
11561
12050
  buildCloudSystemPrompt(prUrl) {
11562
12051
  if (prUrl) {
11563
12052
  return `
@@ -11882,20 +12371,30 @@ Important:
11882
12371
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11883
12372
  notification
11884
12373
  });
11885
- const { archiveUrl: _, ...paramsWithoutArchive } = snapshotWithDevice;
11886
- const logNotification = {
11887
- ...notification,
11888
- params: paramsWithoutArchive
11889
- };
11890
12374
  this.session.logWriter.appendRawLine(
11891
12375
  this.session.payload.run_id,
11892
- JSON.stringify(logNotification)
12376
+ JSON.stringify(notification)
11893
12377
  );
11894
12378
  }
11895
12379
  } catch (error) {
11896
12380
  this.logger.error("Failed to capture tree state", error);
11897
12381
  }
11898
12382
  }
12383
+ broadcastTurnComplete(stopReason) {
12384
+ if (!this.session) return;
12385
+ this.broadcastEvent({
12386
+ type: "notification",
12387
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12388
+ notification: {
12389
+ jsonrpc: "2.0",
12390
+ method: POSTHOG_NOTIFICATIONS.TURN_COMPLETE,
12391
+ params: {
12392
+ sessionId: this.session.acpSessionId,
12393
+ stopReason
12394
+ }
12395
+ }
12396
+ });
12397
+ }
11899
12398
  broadcastEvent(event) {
11900
12399
  if (this.session?.sseController) {
11901
12400
  this.sendSseEvent(this.session.sseController, event);