@posthog/agent 2.3.387 → 2.3.388

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.
@@ -8605,7 +8605,7 @@ import { z as z4 } from "zod";
8605
8605
  // package.json
8606
8606
  var package_default = {
8607
8607
  name: "@posthog/agent",
8608
- version: "2.3.387",
8608
+ version: "2.3.388",
8609
8609
  repository: "https://github.com/PostHog/code",
8610
8610
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
8611
8611
  exports: {
@@ -19157,6 +19157,11 @@ var HandoffCheckpointTracker = class {
19157
19157
  onDivergedBranch: options?.onDivergedBranch
19158
19158
  });
19159
19159
  this.logApplyMetrics(checkpoint, downloads, applyResult.totalBytes);
19160
+ return {
19161
+ packBytes: downloads.pack?.rawBytes ?? 0,
19162
+ indexBytes: downloads.index?.rawBytes ?? 0,
19163
+ totalBytes: applyResult.totalBytes
19164
+ };
19160
19165
  } finally {
19161
19166
  await this.removeIfPresent(packPath);
19162
19167
  await this.removeIfPresent(indexPath);
@@ -19203,22 +19208,22 @@ var HandoffCheckpointTracker = class {
19203
19208
  };
19204
19209
  }
19205
19210
  async uploadArtifacts(specs) {
19206
- const uploads = await Promise.all(
19207
- specs.map(async (spec) => {
19208
- if (!spec.filePath) {
19209
- return [spec.key, void 0];
19210
- }
19211
- return [
19212
- spec.key,
19213
- await this.uploadArtifactFile(
19214
- spec.filePath,
19215
- spec.name,
19216
- spec.contentType
19217
- )
19218
- ];
19219
- })
19220
- );
19221
- return Object.fromEntries(uploads);
19211
+ const results = [];
19212
+ for (const spec of specs) {
19213
+ if (!spec.filePath) {
19214
+ results.push([spec.key, void 0]);
19215
+ continue;
19216
+ }
19217
+ results.push([
19218
+ spec.key,
19219
+ await this.uploadArtifactFile(
19220
+ spec.filePath,
19221
+ spec.name,
19222
+ spec.contentType
19223
+ )
19224
+ ]);
19225
+ }
19226
+ return Object.fromEntries(results);
19222
19227
  }
19223
19228
  async downloadArtifactToFile(artifactPath, filePath, label) {
19224
19229
  if (!this.apiClient) {
@@ -19230,7 +19235,7 @@ var HandoffCheckpointTracker = class {
19230
19235
  artifactPath
19231
19236
  );
19232
19237
  if (!arrayBuffer) {
19233
- throw new Error(`Failed to download ${label}`);
19238
+ throw new Error(`Failed to download ${label} from ${artifactPath}`);
19234
19239
  }
19235
19240
  const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
19236
19241
  const binaryContent = Buffer.from(base64Content, "base64");
@@ -19414,6 +19419,13 @@ var PostHogAPIClient = class {
19414
19419
  `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
19415
19420
  );
19416
19421
  }
19422
+ async resumeRunInCloud(taskId, runId) {
19423
+ const teamId = this.getTeamId();
19424
+ return this.apiRequest(
19425
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/resume_in_cloud/`,
19426
+ { method: "POST" }
19427
+ );
19428
+ }
19417
19429
  async updateTaskRun(taskId, runId, payload) {
19418
19430
  const teamId = this.getTeamId();
19419
19431
  return this.apiRequest(
@@ -19561,1215 +19573,1180 @@ function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
19561
19573
  return turns.slice(startIndex);
19562
19574
  }
19563
19575
 
19564
- // src/sagas/apply-snapshot-saga.ts
19565
- import { mkdir as mkdir7, rm as rm6, writeFile as writeFile5 } from "fs/promises";
19566
- import { join as join12 } from "path";
19567
-
19568
- // ../git/dist/sagas/tree.js
19569
- import { existsSync as existsSync5 } from "fs";
19570
- import * as fs12 from "fs/promises";
19571
- import * as path15 from "path";
19572
- import * as tar from "tar";
19573
- var CaptureTreeSaga = class extends GitSaga {
19574
- sagaName = "CaptureTreeSaga";
19575
- tempIndexPath = null;
19576
- async executeGitOperations(input) {
19577
- const { baseDir, lastTreeHash, archivePath, signal } = input;
19578
- const tmpDir = path15.join(baseDir, ".git", "posthog-code-tmp");
19579
- await this.step({
19580
- name: "create_tmp_dir",
19581
- execute: () => fs12.mkdir(tmpDir, { recursive: true }),
19582
- rollback: async () => {
19583
- }
19584
- });
19585
- this.tempIndexPath = path15.join(tmpDir, `index-${Date.now()}`);
19586
- const tempIndexGit = this.git.env({
19587
- ...process.env,
19588
- GIT_INDEX_FILE: this.tempIndexPath
19589
- });
19590
- await this.step({
19591
- name: "init_temp_index",
19592
- execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
19593
- rollback: async () => {
19594
- if (this.tempIndexPath) {
19595
- await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
19596
- });
19597
- }
19598
- }
19599
- });
19600
- await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
19601
- const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
19602
- if (lastTreeHash && treeHash === lastTreeHash) {
19603
- this.log.debug("No changes since last capture", { treeHash });
19604
- await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
19605
- });
19606
- return { snapshot: null, changed: false };
19576
+ // src/sagas/resume-saga.ts
19577
+ var ResumeSaga = class extends Saga {
19578
+ sagaName = "ResumeSaga";
19579
+ async execute(input) {
19580
+ const { taskId, runId, apiClient } = input;
19581
+ const taskRun = await this.readOnlyStep(
19582
+ "fetch_task_run",
19583
+ () => apiClient.getTaskRun(taskId, runId)
19584
+ );
19585
+ if (!taskRun.log_url) {
19586
+ this.log.info("No log URL found, starting fresh");
19587
+ return this.emptyResult();
19607
19588
  }
19608
- const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
19609
- try {
19610
- return await getHeadSha(baseDir, { abortSignal: signal });
19611
- } catch {
19612
- return null;
19613
- }
19614
- });
19615
- const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
19616
- await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
19617
- });
19618
- const snapshot = {
19619
- treeHash,
19620
- baseCommit,
19621
- changes,
19622
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
19623
- };
19624
- let createdArchivePath;
19625
- if (archivePath) {
19626
- createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
19589
+ const entries = await this.readOnlyStep(
19590
+ "fetch_logs",
19591
+ () => apiClient.fetchTaskRunLogs(taskRun)
19592
+ );
19593
+ if (entries.length === 0) {
19594
+ this.log.info("No log entries found, starting fresh");
19595
+ return this.emptyResult();
19627
19596
  }
19628
- this.log.info("Tree captured", {
19629
- treeHash,
19630
- changes: changes.length,
19631
- archived: !!createdArchivePath
19632
- });
19633
- return { snapshot, archivePath: createdArchivePath, changed: true };
19634
- }
19635
- async createArchive(baseDir, archivePath, changes) {
19636
- const filesToArchive = changes.filter((c) => c.status !== "D").map((c) => c.path);
19637
- if (filesToArchive.length === 0) {
19638
- return void 0;
19597
+ this.log.info("Fetched log entries", { count: entries.length });
19598
+ const latestSnapshot = await this.readOnlyStep(
19599
+ "find_snapshot",
19600
+ () => Promise.resolve(this.findLatestTreeSnapshot(entries))
19601
+ );
19602
+ const latestGitCheckpoint = await this.readOnlyStep(
19603
+ "find_git_checkpoint",
19604
+ () => Promise.resolve(this.findLatestGitCheckpoint(entries))
19605
+ );
19606
+ if (latestSnapshot) {
19607
+ this.log.info("Found tree snapshot", {
19608
+ treeHash: latestSnapshot.treeHash,
19609
+ hasArchiveUrl: !!latestSnapshot.archiveUrl,
19610
+ changes: latestSnapshot.changes?.length ?? 0
19611
+ });
19639
19612
  }
19640
- const existingFiles = filesToArchive.filter((f) => existsSync5(path15.join(baseDir, f)));
19641
- if (existingFiles.length === 0) {
19642
- return void 0;
19613
+ if (latestGitCheckpoint) {
19614
+ this.log.info("Found git checkpoint", {
19615
+ checkpointId: latestGitCheckpoint.checkpointId,
19616
+ branch: latestGitCheckpoint.branch
19617
+ });
19643
19618
  }
19644
- await this.step({
19645
- name: "create_archive",
19646
- execute: async () => {
19647
- const archiveDir = path15.dirname(archivePath);
19648
- await fs12.mkdir(archiveDir, { recursive: true });
19649
- await tar.create({
19650
- gzip: true,
19651
- file: archivePath,
19652
- cwd: baseDir
19653
- }, existingFiles);
19654
- },
19655
- rollback: async () => {
19656
- await fs12.rm(archivePath, { force: true }).catch(() => {
19657
- });
19658
- }
19619
+ const conversation = await this.readOnlyStep(
19620
+ "rebuild_conversation",
19621
+ () => Promise.resolve(this.rebuildConversation(entries))
19622
+ );
19623
+ const lastDevice = await this.readOnlyStep(
19624
+ "find_device",
19625
+ () => Promise.resolve(this.findLastDeviceInfo(entries))
19626
+ );
19627
+ this.log.info("Resume state rebuilt", {
19628
+ turns: conversation.length,
19629
+ hasSnapshot: !!latestSnapshot,
19630
+ hasGitCheckpoint: !!latestGitCheckpoint,
19631
+ interrupted: latestSnapshot?.interrupted ?? false
19659
19632
  });
19660
- return archivePath;
19633
+ return {
19634
+ conversation,
19635
+ latestSnapshot,
19636
+ latestGitCheckpoint,
19637
+ interrupted: latestSnapshot?.interrupted ?? false,
19638
+ lastDevice,
19639
+ logEntryCount: entries.length
19640
+ };
19661
19641
  }
19662
- async getChanges(git, fromRef, toRef) {
19663
- if (!fromRef) {
19664
- const stdout2 = await git.raw(["ls-tree", "-r", "--name-only", toRef]);
19665
- return stdout2.split("\n").filter((p) => p.trim()).map((p) => ({ path: p, status: "A" }));
19666
- }
19667
- const stdout = await git.raw([
19668
- "diff-tree",
19669
- "-r",
19670
- "--name-status",
19671
- fromRef,
19672
- toRef
19673
- ]);
19674
- const changes = [];
19675
- for (const line of stdout.split("\n")) {
19676
- if (!line.trim())
19677
- continue;
19678
- const [status, filePath] = line.split(" ");
19679
- if (!filePath)
19680
- continue;
19681
- let normalizedStatus;
19682
- if (status === "D") {
19683
- normalizedStatus = "D";
19684
- } else if (status === "A") {
19685
- normalizedStatus = "A";
19686
- } else {
19687
- normalizedStatus = "M";
19642
+ emptyResult() {
19643
+ return {
19644
+ conversation: [],
19645
+ latestSnapshot: null,
19646
+ latestGitCheckpoint: null,
19647
+ interrupted: false,
19648
+ logEntryCount: 0
19649
+ };
19650
+ }
19651
+ findLatestTreeSnapshot(entries) {
19652
+ for (let i2 = entries.length - 1; i2 >= 0; i2--) {
19653
+ const entry = entries[i2];
19654
+ if (isNotification(
19655
+ entry.notification?.method,
19656
+ POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
19657
+ )) {
19658
+ const params = entry.notification.params;
19659
+ if (params?.treeHash) {
19660
+ return params;
19661
+ }
19688
19662
  }
19689
- changes.push({ path: filePath, status: normalizedStatus });
19690
19663
  }
19691
- return changes;
19664
+ return null;
19692
19665
  }
19693
- };
19694
- var ApplyTreeSaga = class extends GitSaga {
19695
- sagaName = "ApplyTreeSaga";
19696
- originalHead = null;
19697
- originalBranch = null;
19698
- extractedFiles = [];
19699
- fileBackups = /* @__PURE__ */ new Map();
19700
- async executeGitOperations(input) {
19701
- const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
19702
- const headInfo = await this.readOnlyStep("get_current_head", async () => {
19703
- let head = null;
19704
- let branch = null;
19705
- try {
19706
- head = await this.git.revparse(["HEAD"]);
19707
- } catch {
19708
- head = null;
19666
+ findLatestGitCheckpoint(entries) {
19667
+ const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
19668
+ for (let i2 = entries.length - 1; i2 >= 0; i2--) {
19669
+ const entry = entries[i2];
19670
+ const method = entry.notification?.method;
19671
+ if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
19672
+ const params = entry.notification?.params;
19673
+ if (params?.checkpointId && params?.checkpointRef) {
19674
+ return params;
19675
+ }
19709
19676
  }
19710
- try {
19711
- branch = await this.git.raw(["symbolic-ref", "--short", "HEAD"]);
19712
- } catch {
19713
- branch = null;
19677
+ }
19678
+ return null;
19679
+ }
19680
+ findLastDeviceInfo(entries) {
19681
+ for (let i2 = entries.length - 1; i2 >= 0; i2--) {
19682
+ const entry = entries[i2];
19683
+ const params = entry.notification?.params;
19684
+ if (params?.device) {
19685
+ return params.device;
19714
19686
  }
19715
- return { head, branch };
19716
- });
19717
- this.originalHead = headInfo.head;
19718
- this.originalBranch = headInfo.branch;
19719
- let checkoutPerformed = false;
19720
- if (baseCommit && baseCommit !== this.originalHead) {
19721
- await this.readOnlyStep("check_working_tree", async () => {
19722
- const status = await this.git.status();
19723
- if (!status.isClean()) {
19724
- const changedFiles = status.modified.length + status.staged.length + status.deleted.length;
19725
- throw new Error(`Cannot apply tree: ${changedFiles} uncommitted change(s) exist. Commit or stash your changes first.`);
19726
- }
19727
- });
19728
- await this.step({
19729
- name: "checkout_base",
19730
- execute: async () => {
19731
- await this.git.checkout(baseCommit);
19732
- checkoutPerformed = true;
19733
- this.log.warn("Applied tree from different commit - now in detached HEAD state", {
19734
- originalHead: this.originalHead,
19735
- originalBranch: this.originalBranch,
19736
- baseCommit
19737
- });
19738
- },
19739
- rollback: async () => {
19740
- try {
19741
- if (this.originalBranch) {
19742
- await this.git.checkout(this.originalBranch);
19743
- } else if (this.originalHead) {
19744
- await this.git.checkout(this.originalHead);
19687
+ }
19688
+ return void 0;
19689
+ }
19690
+ rebuildConversation(entries) {
19691
+ const turns = [];
19692
+ let currentAssistantContent = [];
19693
+ let currentToolCalls = [];
19694
+ for (const entry of entries) {
19695
+ const method = entry.notification?.method;
19696
+ const params = entry.notification?.params;
19697
+ if (method === "session/update" && params?.update) {
19698
+ const update = params.update;
19699
+ const sessionUpdate = update.sessionUpdate;
19700
+ switch (sessionUpdate) {
19701
+ case "user_message":
19702
+ case "user_message_chunk": {
19703
+ if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
19704
+ turns.push({
19705
+ role: "assistant",
19706
+ content: currentAssistantContent,
19707
+ toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
19708
+ });
19709
+ currentAssistantContent = [];
19710
+ currentToolCalls = [];
19745
19711
  }
19746
- } catch (error) {
19747
- this.log.warn("Failed to rollback checkout", { error });
19712
+ const content = update.content;
19713
+ const contentArray = Array.isArray(content) ? content : [content];
19714
+ turns.push({
19715
+ role: "user",
19716
+ content: contentArray
19717
+ });
19718
+ break;
19748
19719
  }
19749
- }
19750
- });
19751
- }
19752
- if (archivePath) {
19753
- const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
19754
- await this.readOnlyStep("backup_existing_files", async () => {
19755
- for (const filePath of filesToExtract) {
19756
- const fullPath = path15.join(baseDir, filePath);
19757
- try {
19758
- const content = await fs12.readFile(fullPath);
19759
- this.fileBackups.set(filePath, content);
19760
- } catch {
19720
+ case "agent_message": {
19721
+ const content = update.content;
19722
+ if (content) {
19723
+ if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
19724
+ const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
19725
+ lastBlock.text += content.text;
19726
+ } else {
19727
+ currentAssistantContent.push(content);
19728
+ }
19729
+ }
19730
+ break;
19761
19731
  }
19762
- }
19763
- });
19764
- await this.step({
19765
- name: "extract_archive",
19766
- execute: async () => {
19767
- await tar.extract({
19768
- file: archivePath,
19769
- cwd: baseDir
19770
- });
19771
- this.extractedFiles = filesToExtract;
19772
- },
19773
- rollback: async () => {
19774
- for (const filePath of this.extractedFiles) {
19775
- const fullPath = path15.join(baseDir, filePath);
19776
- const backup = this.fileBackups.get(filePath);
19777
- if (backup) {
19778
- const dir = path15.dirname(fullPath);
19779
- await fs12.mkdir(dir, { recursive: true }).catch(() => {
19780
- });
19781
- await fs12.writeFile(fullPath, backup).catch(() => {
19782
- });
19783
- } else {
19784
- await fs12.rm(fullPath, { force: true }).catch(() => {
19785
- });
19732
+ case "agent_message_chunk": {
19733
+ const content = update.content;
19734
+ if (content) {
19735
+ if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
19736
+ const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
19737
+ lastBlock.text += content.text;
19738
+ } else {
19739
+ currentAssistantContent.push(content);
19740
+ }
19786
19741
  }
19742
+ break;
19787
19743
  }
19788
- }
19789
- });
19790
- }
19791
- for (const change of changes.filter((c) => c.status === "D")) {
19792
- const fullPath = path15.join(baseDir, change.path);
19793
- const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
19794
- try {
19795
- return await fs12.readFile(fullPath);
19796
- } catch {
19797
- return null;
19798
- }
19799
- });
19800
- await this.step({
19801
- name: `delete_${change.path}`,
19802
- execute: async () => {
19803
- await fs12.rm(fullPath, { force: true });
19804
- this.log.debug(`Deleted file: ${change.path}`);
19805
- },
19806
- rollback: async () => {
19807
- if (backupContent) {
19808
- const dir = path15.dirname(fullPath);
19809
- await fs12.mkdir(dir, { recursive: true }).catch(() => {
19810
- });
19811
- await fs12.writeFile(fullPath, backupContent).catch(() => {
19812
- });
19744
+ case "tool_call":
19745
+ case "tool_call_update": {
19746
+ const meta = update._meta?.claudeCode;
19747
+ if (meta) {
19748
+ const toolCallId = meta.toolCallId;
19749
+ const toolName = meta.toolName;
19750
+ const toolInput = meta.toolInput;
19751
+ const toolResponse = meta.toolResponse;
19752
+ if (toolCallId && toolName) {
19753
+ let toolCall = currentToolCalls.find(
19754
+ (tc) => tc.toolCallId === toolCallId
19755
+ );
19756
+ if (!toolCall) {
19757
+ toolCall = {
19758
+ toolCallId,
19759
+ toolName,
19760
+ input: toolInput
19761
+ };
19762
+ currentToolCalls.push(toolCall);
19763
+ }
19764
+ if (toolResponse !== void 0) {
19765
+ toolCall.result = toolResponse;
19766
+ }
19767
+ }
19768
+ }
19769
+ break;
19770
+ }
19771
+ case "tool_result": {
19772
+ const meta = update._meta?.claudeCode;
19773
+ if (meta) {
19774
+ const toolCallId = meta.toolCallId;
19775
+ const toolResponse = meta.toolResponse;
19776
+ if (toolCallId) {
19777
+ const toolCall = currentToolCalls.find(
19778
+ (tc) => tc.toolCallId === toolCallId
19779
+ );
19780
+ if (toolCall && toolResponse !== void 0) {
19781
+ toolCall.result = toolResponse;
19782
+ }
19783
+ }
19784
+ }
19785
+ break;
19813
19786
  }
19814
19787
  }
19788
+ }
19789
+ }
19790
+ if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
19791
+ turns.push({
19792
+ role: "assistant",
19793
+ content: currentAssistantContent,
19794
+ toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
19815
19795
  });
19816
19796
  }
19817
- const deletedCount = changes.filter((c) => c.status === "D").length;
19818
- this.log.info("Tree applied", {
19819
- treeHash,
19820
- totalChanges: changes.length,
19821
- deletedFiles: deletedCount,
19822
- checkoutPerformed
19823
- });
19824
- return { treeHash, checkoutPerformed };
19797
+ return turns;
19825
19798
  }
19826
19799
  };
19827
19800
 
19828
- // src/sagas/apply-snapshot-saga.ts
19829
- var ApplySnapshotSaga = class extends Saga {
19830
- sagaName = "ApplySnapshotSaga";
19831
- archivePath = null;
19832
- async execute(input) {
19833
- const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
19834
- const tmpDir = join12(repositoryPath, ".posthog", "tmp");
19835
- if (!snapshot.archiveUrl) {
19836
- throw new Error("Cannot apply snapshot: no archive URL");
19837
- }
19838
- const archiveUrl = snapshot.archiveUrl;
19839
- await this.step({
19840
- name: "create_tmp_dir",
19841
- execute: () => mkdir7(tmpDir, { recursive: true }),
19842
- rollback: async () => {
19843
- }
19801
+ // src/resume.ts
19802
+ async function resumeFromLog(config) {
19803
+ const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
19804
+ logger.info("Resuming from log", {
19805
+ taskId: config.taskId,
19806
+ runId: config.runId
19807
+ });
19808
+ const saga = new ResumeSaga(logger);
19809
+ const result = await saga.run({
19810
+ taskId: config.taskId,
19811
+ runId: config.runId,
19812
+ repositoryPath: config.repositoryPath,
19813
+ apiClient: config.apiClient,
19814
+ logger
19815
+ });
19816
+ if (!result.success) {
19817
+ logger.error("Failed to resume from log", {
19818
+ error: result.error,
19819
+ failedStep: result.failedStep
19844
19820
  });
19845
- const archivePath = join12(tmpDir, `${snapshot.treeHash}.tar.gz`);
19846
- this.archivePath = archivePath;
19847
- await this.step({
19848
- name: "download_archive",
19849
- execute: async () => {
19850
- const arrayBuffer = await apiClient.downloadArtifact(
19851
- taskId,
19852
- runId,
19853
- archiveUrl
19854
- );
19855
- if (!arrayBuffer) {
19856
- throw new Error("Failed to download archive");
19821
+ throw new Error(
19822
+ `Failed to resume at step '${result.failedStep}': ${result.error}`
19823
+ );
19824
+ }
19825
+ return {
19826
+ conversation: result.data.conversation,
19827
+ latestSnapshot: result.data.latestSnapshot,
19828
+ latestGitCheckpoint: result.data.latestGitCheckpoint,
19829
+ interrupted: result.data.interrupted,
19830
+ lastDevice: result.data.lastDevice,
19831
+ logEntryCount: result.data.logEntryCount
19832
+ };
19833
+ }
19834
+ var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
19835
+ var TOOL_RESULT_MAX_CHARS = 2e3;
19836
+ var RESUME_CONTEXT_MARKERS = [
19837
+ "You are resuming a previous conversation",
19838
+ "Here is the conversation history from the",
19839
+ "Continue from where you left off"
19840
+ ];
19841
+ function isResumeContextTurn(turn) {
19842
+ if (turn.role !== "user") return false;
19843
+ const text2 = turn.content.filter((b) => b.type === "text").map((b) => b.text).join("");
19844
+ return RESUME_CONTEXT_MARKERS.some((marker) => text2.includes(marker));
19845
+ }
19846
+ function formatConversationForResume(conversation) {
19847
+ const filtered = conversation.filter((turn) => !isResumeContextTurn(turn));
19848
+ const selected = selectRecentTurns(filtered, RESUME_HISTORY_TOKEN_BUDGET);
19849
+ const parts2 = [];
19850
+ if (selected.length < filtered.length) {
19851
+ parts2.push(
19852
+ `*(${filtered.length - selected.length} earlier turns omitted)*`
19853
+ );
19854
+ }
19855
+ for (const turn of selected) {
19856
+ const role = turn.role === "user" ? "User" : "Assistant";
19857
+ const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
19858
+ if (textParts.length > 0) {
19859
+ parts2.push(`**${role}**: ${textParts.join("\n")}`);
19860
+ }
19861
+ if (turn.toolCalls?.length) {
19862
+ const toolSummary = turn.toolCalls.map((tc) => {
19863
+ let resultStr = "";
19864
+ if (tc.result !== void 0) {
19865
+ const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
19866
+ resultStr = raw.length > TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
19857
19867
  }
19858
- const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
19859
- const binaryContent = Buffer.from(base64Content, "base64");
19860
- await writeFile5(archivePath, binaryContent);
19861
- this.log.info("Tree archive downloaded", {
19862
- treeHash: snapshot.treeHash,
19863
- snapshotBytes: binaryContent.byteLength,
19864
- snapshotWireBytes: arrayBuffer.byteLength,
19865
- totalBytes: binaryContent.byteLength,
19866
- totalWireBytes: arrayBuffer.byteLength
19867
- });
19868
- },
19869
- rollback: async () => {
19870
- if (this.archivePath) {
19871
- await rm6(this.archivePath, { force: true }).catch(() => {
19872
- });
19873
- }
19874
- }
19875
- });
19876
- const gitApplySaga = new ApplyTreeSaga(this.log);
19877
- const applyResult = await gitApplySaga.run({
19878
- baseDir: repositoryPath,
19879
- treeHash: snapshot.treeHash,
19880
- baseCommit: snapshot.baseCommit,
19881
- changes: snapshot.changes,
19882
- archivePath: this.archivePath
19883
- });
19884
- if (!applyResult.success) {
19885
- throw new Error(`Failed to apply tree: ${applyResult.error}`);
19868
+ return ` - ${tc.toolName}${resultStr}`;
19869
+ }).join("\n");
19870
+ parts2.push(`**${role} (tools)**:
19871
+ ${toolSummary}`);
19886
19872
  }
19887
- await rm6(this.archivePath, { force: true }).catch(() => {
19888
- });
19889
- this.log.info("Tree snapshot applied", {
19890
- treeHash: snapshot.treeHash,
19891
- totalChanges: snapshot.changes.length,
19892
- deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
19893
- });
19894
- return { treeHash: snapshot.treeHash };
19895
19873
  }
19896
- };
19874
+ return parts2.join("\n\n");
19875
+ }
19897
19876
 
19898
- // src/sagas/capture-tree-saga.ts
19899
- import { existsSync as existsSync6 } from "fs";
19900
- import { readFile as readFile6, rm as rm7 } from "fs/promises";
19901
- import { join as join13 } from "path";
19902
- var CaptureTreeSaga2 = class extends Saga {
19903
- sagaName = "CaptureTreeSaga";
19904
- async execute(input) {
19905
- const {
19906
- repositoryPath,
19907
- lastTreeHash,
19908
- interrupted,
19909
- apiClient,
19910
- taskId,
19911
- runId
19912
- } = input;
19913
- const tmpDir = join13(repositoryPath, ".posthog", "tmp");
19914
- if (existsSync6(join13(repositoryPath, ".gitmodules"))) {
19915
- this.log.warn(
19916
- "Repository has submodules - snapshot may not capture submodule state"
19917
- );
19918
- }
19919
- const shouldArchive = !!apiClient;
19920
- const archivePath = shouldArchive ? join13(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
19921
- const gitCaptureSaga = new CaptureTreeSaga(this.log);
19922
- const captureResult = await gitCaptureSaga.run({
19923
- baseDir: repositoryPath,
19924
- lastTreeHash,
19925
- archivePath
19926
- });
19927
- if (!captureResult.success) {
19928
- throw new Error(`Failed to capture tree: ${captureResult.error}`);
19877
+ // src/session-log-writer.ts
19878
+ import fs12 from "fs";
19879
+ import fsp from "fs/promises";
19880
+ import path15 from "path";
19881
+ var SessionLogWriter = class _SessionLogWriter {
19882
+ static FLUSH_DEBOUNCE_MS = 500;
19883
+ static FLUSH_MAX_INTERVAL_MS = 5e3;
19884
+ static MAX_FLUSH_RETRIES = 10;
19885
+ static MAX_RETRY_DELAY_MS = 3e4;
19886
+ static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
19887
+ posthogAPI;
19888
+ pendingEntries = /* @__PURE__ */ new Map();
19889
+ flushTimeouts = /* @__PURE__ */ new Map();
19890
+ lastFlushAttemptTime = /* @__PURE__ */ new Map();
19891
+ retryCounts = /* @__PURE__ */ new Map();
19892
+ sessions = /* @__PURE__ */ new Map();
19893
+ flushQueues = /* @__PURE__ */ new Map();
19894
+ logger;
19895
+ localCachePath;
19896
+ constructor(options = {}) {
19897
+ this.posthogAPI = options.posthogAPI;
19898
+ this.localCachePath = options.localCachePath;
19899
+ this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
19900
+ }
19901
+ async flushAll() {
19902
+ const flushPromises = [];
19903
+ for (const [sessionId, session] of this.sessions) {
19904
+ this.emitCoalescedMessage(sessionId, session);
19905
+ flushPromises.push(this.flush(sessionId));
19929
19906
  }
19930
- const {
19931
- snapshot: gitSnapshot,
19932
- archivePath: createdArchivePath,
19933
- changed
19934
- } = captureResult.data;
19935
- if (!changed || !gitSnapshot) {
19936
- this.log.debug("No changes since last capture", { lastTreeHash });
19937
- return { snapshot: null, newTreeHash: lastTreeHash };
19907
+ await Promise.all(flushPromises);
19908
+ }
19909
+ register(sessionId, context) {
19910
+ if (this.sessions.has(sessionId)) {
19911
+ return;
19938
19912
  }
19939
- let archiveUrl;
19940
- if (apiClient && createdArchivePath) {
19913
+ this.sessions.set(sessionId, { context, currentTurnMessages: [] });
19914
+ this.lastFlushAttemptTime.set(sessionId, Date.now());
19915
+ if (this.localCachePath) {
19916
+ const sessionDir = path15.join(
19917
+ this.localCachePath,
19918
+ "sessions",
19919
+ context.runId
19920
+ );
19941
19921
  try {
19942
- archiveUrl = await this.uploadArchive(
19943
- createdArchivePath,
19944
- gitSnapshot.treeHash,
19945
- apiClient,
19946
- taskId,
19947
- runId
19948
- );
19949
- } finally {
19950
- await rm7(createdArchivePath, { force: true }).catch(() => {
19922
+ fs12.mkdirSync(sessionDir, { recursive: true });
19923
+ } catch (error) {
19924
+ this.logger.warn("Failed to create local cache directory", {
19925
+ sessionDir,
19926
+ error
19951
19927
  });
19952
19928
  }
19953
19929
  }
19954
- const snapshot = {
19955
- treeHash: gitSnapshot.treeHash,
19956
- baseCommit: gitSnapshot.baseCommit,
19957
- changes: gitSnapshot.changes,
19958
- timestamp: gitSnapshot.timestamp,
19959
- interrupted,
19960
- archiveUrl
19961
- };
19962
- this.log.info("Tree captured", {
19963
- treeHash: snapshot.treeHash,
19964
- changes: snapshot.changes.length,
19965
- interrupted,
19966
- archiveUrl
19967
- });
19968
- return { snapshot, newTreeHash: snapshot.treeHash };
19969
19930
  }
19970
- async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
19971
- const archiveUrl = await this.step({
19972
- name: "upload_archive",
19973
- execute: async () => {
19974
- const archiveContent = await readFile6(archivePath);
19975
- const base64Content = archiveContent.toString("base64");
19976
- const snapshotBytes = archiveContent.byteLength;
19977
- const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
19978
- const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
19979
- {
19980
- name: `trees/${treeHash}.tar.gz`,
19981
- type: "tree_snapshot",
19982
- content: base64Content,
19983
- content_type: "application/gzip"
19931
+ isRegistered(sessionId) {
19932
+ return this.sessions.has(sessionId);
19933
+ }
19934
+ appendRawLine(sessionId, line) {
19935
+ const session = this.sessions.get(sessionId);
19936
+ if (!session) {
19937
+ this.logger.warn("appendRawLine called for unregistered session", {
19938
+ sessionId
19939
+ });
19940
+ return;
19941
+ }
19942
+ try {
19943
+ const message = JSON.parse(line);
19944
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
19945
+ if (this.isAgentMessageChunk(message)) {
19946
+ const text2 = this.extractChunkText(message);
19947
+ if (text2) {
19948
+ if (!session.chunkBuffer) {
19949
+ session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
19950
+ } else {
19951
+ session.chunkBuffer.text += text2;
19984
19952
  }
19985
- ]);
19986
- const uploadedArtifact = artifacts[0];
19987
- if (uploadedArtifact?.storage_path) {
19988
- this.log.info("Tree archive uploaded", {
19989
- storagePath: uploadedArtifact.storage_path,
19990
- treeHash,
19991
- snapshotBytes,
19992
- snapshotWireBytes,
19993
- totalBytes: snapshotBytes,
19994
- totalWireBytes: snapshotWireBytes
19995
- });
19996
- return uploadedArtifact.storage_path;
19997
19953
  }
19998
- return void 0;
19999
- },
20000
- rollback: async () => {
20001
- await rm7(archivePath, { force: true }).catch(() => {
20002
- });
19954
+ return;
19955
+ }
19956
+ if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
19957
+ session.chunkBuffer = void 0;
19958
+ } else {
19959
+ this.emitCoalescedMessage(sessionId, session);
19960
+ }
19961
+ const nonChunkAgentText = this.extractAgentMessageText(message);
19962
+ if (nonChunkAgentText) {
19963
+ session.lastAgentMessage = nonChunkAgentText;
19964
+ session.currentTurnMessages.push(nonChunkAgentText);
19965
+ }
19966
+ const entry = {
19967
+ type: "notification",
19968
+ timestamp,
19969
+ notification: message
19970
+ };
19971
+ this.writeToLocalCache(sessionId, entry);
19972
+ if (this.posthogAPI) {
19973
+ const pending = this.pendingEntries.get(sessionId) ?? [];
19974
+ pending.push(entry);
19975
+ this.pendingEntries.set(sessionId, pending);
19976
+ this.scheduleFlush(sessionId);
19977
+ }
19978
+ } catch {
19979
+ this.logger.warn("Failed to parse raw line for persistence", {
19980
+ taskId: session.context.taskId,
19981
+ runId: session.context.runId,
19982
+ lineLength: line.length
19983
+ });
19984
+ }
19985
+ }
19986
+ async flush(sessionId, { coalesce = false } = {}) {
19987
+ if (coalesce) {
19988
+ const session = this.sessions.get(sessionId);
19989
+ if (session) {
19990
+ this.emitCoalescedMessage(sessionId, session);
19991
+ }
19992
+ }
19993
+ const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
19994
+ const next = prev.catch(() => {
19995
+ }).then(() => this._doFlush(sessionId));
19996
+ this.flushQueues.set(sessionId, next);
19997
+ next.finally(() => {
19998
+ if (this.flushQueues.get(sessionId) === next) {
19999
+ this.flushQueues.delete(sessionId);
20003
20000
  }
20004
20001
  });
20005
- return archiveUrl;
20002
+ return next;
20006
20003
  }
20007
- };
20008
-
20009
- // src/tree-tracker.ts
20010
- var TreeTracker = class {
20011
- repositoryPath;
20012
- taskId;
20013
- runId;
20014
- apiClient;
20015
- logger;
20016
- lastTreeHash = null;
20017
- constructor(config) {
20018
- this.repositoryPath = config.repositoryPath;
20019
- this.taskId = config.taskId;
20020
- this.runId = config.runId;
20021
- this.apiClient = config.apiClient;
20022
- this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
20023
- }
20024
- /**
20025
- * Capture current working tree state as a snapshot.
20026
- * Uses a temporary index to avoid modifying user's staging area.
20027
- * Uses Saga pattern for atomic operation with automatic cleanup on failure.
20028
- */
20029
- async captureTree(options) {
20030
- const saga = new CaptureTreeSaga2(this.logger);
20031
- const result = await saga.run({
20032
- repositoryPath: this.repositoryPath,
20033
- taskId: this.taskId,
20034
- runId: this.runId,
20035
- apiClient: this.apiClient,
20036
- lastTreeHash: this.lastTreeHash,
20037
- interrupted: options?.interrupted
20038
- });
20039
- if (!result.success) {
20040
- this.logger.error("Failed to capture tree", {
20041
- error: result.error,
20042
- failedStep: result.failedStep
20043
- });
20044
- throw new Error(
20045
- `Failed to capture tree at step '${result.failedStep}': ${result.error}`
20046
- );
20047
- }
20048
- if (result.data.newTreeHash !== null) {
20049
- this.lastTreeHash = result.data.newTreeHash;
20004
+ async _doFlush(sessionId) {
20005
+ const session = this.sessions.get(sessionId);
20006
+ if (!session) {
20007
+ this.logger.warn("flush: no session found", { sessionId });
20008
+ return;
20050
20009
  }
20051
- return result.data.snapshot;
20052
- }
20053
- /**
20054
- * Download and apply a tree snapshot.
20055
- * Uses Saga pattern for atomic operation with rollback on failure.
20056
- */
20057
- async applyTreeSnapshot(snapshot) {
20058
- if (!this.apiClient) {
20059
- throw new Error("Cannot apply snapshot: API client not configured");
20010
+ const pending = this.pendingEntries.get(sessionId);
20011
+ if (!this.posthogAPI || !pending?.length) {
20012
+ return;
20060
20013
  }
20061
- if (!snapshot.archiveUrl) {
20062
- this.logger.warn("Cannot apply snapshot: no archive URL", {
20063
- treeHash: snapshot.treeHash,
20064
- changes: snapshot.changes.length
20065
- });
20066
- throw new Error("Cannot apply snapshot: no archive URL");
20014
+ this.pendingEntries.delete(sessionId);
20015
+ const timeout = this.flushTimeouts.get(sessionId);
20016
+ if (timeout) {
20017
+ clearTimeout(timeout);
20018
+ this.flushTimeouts.delete(sessionId);
20067
20019
  }
20068
- const saga = new ApplySnapshotSaga(this.logger);
20069
- const result = await saga.run({
20070
- snapshot,
20071
- repositoryPath: this.repositoryPath,
20072
- apiClient: this.apiClient,
20073
- taskId: this.taskId,
20074
- runId: this.runId
20075
- });
20076
- if (!result.success) {
20077
- this.logger.error("Failed to apply tree snapshot", {
20078
- error: result.error,
20079
- failedStep: result.failedStep,
20080
- treeHash: snapshot.treeHash
20081
- });
20082
- throw new Error(
20083
- `Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
20020
+ this.lastFlushAttemptTime.set(sessionId, Date.now());
20021
+ try {
20022
+ await this.posthogAPI.appendTaskRunLog(
20023
+ session.context.taskId,
20024
+ session.context.runId,
20025
+ pending
20084
20026
  );
20027
+ this.retryCounts.set(sessionId, 0);
20028
+ } catch (error) {
20029
+ const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
20030
+ this.retryCounts.set(sessionId, retryCount);
20031
+ if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
20032
+ this.logger.error(
20033
+ `Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
20034
+ {
20035
+ taskId: session.context.taskId,
20036
+ runId: session.context.runId,
20037
+ error
20038
+ }
20039
+ );
20040
+ this.retryCounts.set(sessionId, 0);
20041
+ } else {
20042
+ if (retryCount === 1) {
20043
+ this.logger.warn(
20044
+ `Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
20045
+ {
20046
+ taskId: session.context.taskId,
20047
+ runId: session.context.runId,
20048
+ error: error instanceof Error ? error.message : String(error)
20049
+ }
20050
+ );
20051
+ }
20052
+ const currentPending = this.pendingEntries.get(sessionId) ?? [];
20053
+ this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
20054
+ this.scheduleFlush(sessionId);
20055
+ }
20085
20056
  }
20086
- this.lastTreeHash = result.data.treeHash;
20087
20057
  }
20088
- /**
20089
- * Get the last captured tree hash.
20090
- */
20091
- getLastTreeHash() {
20092
- return this.lastTreeHash;
20058
+ getSessionUpdateType(message) {
20059
+ if (message.method !== "session/update") return void 0;
20060
+ const params = message.params;
20061
+ const update = params?.update;
20062
+ return update?.sessionUpdate;
20093
20063
  }
20094
- /**
20095
- * Set the last tree hash (used when resuming).
20096
- */
20097
- setLastTreeHash(hash) {
20098
- this.lastTreeHash = hash;
20064
+ isDirectAgentMessage(message) {
20065
+ return this.getSessionUpdateType(message) === "agent_message";
20099
20066
  }
20100
- };
20101
-
20102
- // src/sagas/resume-saga.ts
20103
- var ResumeSaga = class extends Saga {
20104
- sagaName = "ResumeSaga";
20105
- async execute(input) {
20106
- const { taskId, runId, repositoryPath, apiClient } = input;
20107
- const logger = input.logger || new Logger({ debug: false, prefix: "[Resume]" });
20108
- const taskRun = await this.readOnlyStep(
20109
- "fetch_task_run",
20110
- () => apiClient.getTaskRun(taskId, runId)
20111
- );
20112
- if (!taskRun.log_url) {
20113
- this.log.info("No log URL found, starting fresh");
20114
- return this.emptyResult();
20115
- }
20116
- const entries = await this.readOnlyStep(
20117
- "fetch_logs",
20118
- () => apiClient.fetchTaskRunLogs(taskRun)
20119
- );
20120
- if (entries.length === 0) {
20121
- this.log.info("No log entries found, starting fresh");
20122
- return this.emptyResult();
20067
+ isAgentMessageChunk(message) {
20068
+ return this.getSessionUpdateType(message) === "agent_message_chunk";
20069
+ }
20070
+ extractChunkText(message) {
20071
+ const params = message.params;
20072
+ const update = params?.update;
20073
+ const content = update?.content;
20074
+ if (content?.type === "text" && content.text) {
20075
+ return content.text;
20123
20076
  }
20124
- this.log.info("Fetched log entries", { count: entries.length });
20125
- const latestSnapshot = await this.readOnlyStep(
20126
- "find_snapshot",
20127
- () => Promise.resolve(this.findLatestTreeSnapshot(entries))
20128
- );
20129
- const latestGitCheckpoint = await this.readOnlyStep(
20130
- "find_git_checkpoint",
20131
- () => Promise.resolve(this.findLatestGitCheckpoint(entries))
20132
- );
20133
- let snapshotApplied = false;
20134
- if (latestSnapshot?.archiveUrl && repositoryPath) {
20135
- this.log.info("Found tree snapshot", {
20136
- treeHash: latestSnapshot.treeHash,
20137
- hasArchiveUrl: true,
20138
- changes: latestSnapshot.changes?.length ?? 0,
20139
- interrupted: latestSnapshot.interrupted
20140
- });
20141
- await this.step({
20142
- name: "apply_snapshot",
20143
- execute: async () => {
20144
- const treeTracker = new TreeTracker({
20145
- repositoryPath,
20146
- taskId,
20147
- runId,
20148
- apiClient,
20149
- logger: logger.child("TreeTracker")
20150
- });
20151
- try {
20152
- await treeTracker.applyTreeSnapshot(latestSnapshot);
20153
- treeTracker.setLastTreeHash(latestSnapshot.treeHash);
20154
- snapshotApplied = true;
20155
- this.log.info("Tree snapshot applied successfully", {
20156
- treeHash: latestSnapshot.treeHash
20157
- });
20158
- } catch (error) {
20159
- this.log.warn(
20160
- "Failed to apply tree snapshot, continuing without it",
20161
- {
20162
- error: error instanceof Error ? error.message : String(error),
20163
- treeHash: latestSnapshot.treeHash
20164
- }
20165
- );
20077
+ return "";
20078
+ }
20079
+ emitCoalescedMessage(sessionId, session) {
20080
+ if (!session.chunkBuffer) return;
20081
+ const { text: text2, firstTimestamp } = session.chunkBuffer;
20082
+ session.chunkBuffer = void 0;
20083
+ session.lastAgentMessage = text2;
20084
+ session.currentTurnMessages.push(text2);
20085
+ const entry = {
20086
+ type: "notification",
20087
+ timestamp: firstTimestamp,
20088
+ notification: {
20089
+ jsonrpc: "2.0",
20090
+ method: "session/update",
20091
+ params: {
20092
+ update: {
20093
+ sessionUpdate: "agent_message",
20094
+ content: { type: "text", text: text2 }
20166
20095
  }
20167
- },
20168
- rollback: async () => {
20169
- }
20170
- });
20171
- } else if (latestSnapshot?.archiveUrl && !repositoryPath) {
20172
- this.log.warn(
20173
- "Snapshot found but no repositoryPath configured - files cannot be restored",
20174
- {
20175
- treeHash: latestSnapshot.treeHash,
20176
- changes: latestSnapshot.changes?.length ?? 0
20177
20096
  }
20178
- );
20179
- } else if (latestSnapshot) {
20180
- this.log.warn(
20181
- "Snapshot found but has no archive URL - files cannot be restored",
20097
+ }
20098
+ };
20099
+ this.writeToLocalCache(sessionId, entry);
20100
+ if (this.posthogAPI) {
20101
+ const pending = this.pendingEntries.get(sessionId) ?? [];
20102
+ pending.push(entry);
20103
+ this.pendingEntries.set(sessionId, pending);
20104
+ this.scheduleFlush(sessionId);
20105
+ }
20106
+ }
20107
+ getLastAgentMessage(sessionId) {
20108
+ return this.sessions.get(sessionId)?.lastAgentMessage;
20109
+ }
20110
+ getFullAgentResponse(sessionId) {
20111
+ const session = this.sessions.get(sessionId);
20112
+ if (!session || session.currentTurnMessages.length === 0) return void 0;
20113
+ if (session.chunkBuffer) {
20114
+ this.logger.warn(
20115
+ "getFullAgentResponse called with non-empty chunk buffer",
20182
20116
  {
20183
- treeHash: latestSnapshot.treeHash,
20184
- changes: latestSnapshot.changes?.length ?? 0
20117
+ sessionId,
20118
+ bufferedLength: session.chunkBuffer.text.length
20185
20119
  }
20186
20120
  );
20187
20121
  }
20188
- const conversation = await this.readOnlyStep(
20189
- "rebuild_conversation",
20190
- () => Promise.resolve(this.rebuildConversation(entries))
20191
- );
20192
- const lastDevice = await this.readOnlyStep(
20193
- "find_device",
20194
- () => Promise.resolve(this.findLastDeviceInfo(entries))
20195
- );
20196
- this.log.info("Resume state rebuilt", {
20197
- turns: conversation.length,
20198
- hasSnapshot: !!latestSnapshot,
20199
- snapshotApplied,
20200
- interrupted: latestSnapshot?.interrupted ?? false
20201
- });
20202
- return {
20203
- conversation,
20204
- latestSnapshot,
20205
- latestGitCheckpoint,
20206
- snapshotApplied,
20207
- interrupted: latestSnapshot?.interrupted ?? false,
20208
- lastDevice,
20209
- logEntryCount: entries.length
20210
- };
20122
+ return session.currentTurnMessages.join("\n\n");
20211
20123
  }
20212
- emptyResult() {
20213
- return {
20214
- conversation: [],
20215
- latestSnapshot: null,
20216
- latestGitCheckpoint: null,
20217
- snapshotApplied: false,
20218
- interrupted: false,
20219
- logEntryCount: 0
20220
- };
20124
+ resetTurnMessages(sessionId) {
20125
+ const session = this.sessions.get(sessionId);
20126
+ if (session) {
20127
+ session.currentTurnMessages = [];
20128
+ }
20221
20129
  }
20222
- findLatestTreeSnapshot(entries) {
20223
- for (let i2 = entries.length - 1; i2 >= 0; i2--) {
20224
- const entry = entries[i2];
20225
- if (isNotification(
20226
- entry.notification?.method,
20227
- POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
20228
- )) {
20229
- const params = entry.notification.params;
20230
- if (params?.treeHash) {
20231
- return params;
20130
+ extractAgentMessageText(message) {
20131
+ if (message.method !== "session/update") {
20132
+ return null;
20133
+ }
20134
+ const params = message.params;
20135
+ const update = params?.update;
20136
+ if (update?.sessionUpdate !== "agent_message") {
20137
+ return null;
20138
+ }
20139
+ const content = update.content;
20140
+ if (content?.type === "text" && typeof content.text === "string") {
20141
+ const trimmed2 = content.text.trim();
20142
+ return trimmed2.length > 0 ? trimmed2 : null;
20143
+ }
20144
+ if (typeof update.message === "string") {
20145
+ const trimmed2 = update.message.trim();
20146
+ return trimmed2.length > 0 ? trimmed2 : null;
20147
+ }
20148
+ return null;
20149
+ }
20150
+ scheduleFlush(sessionId) {
20151
+ const existing = this.flushTimeouts.get(sessionId);
20152
+ if (existing) clearTimeout(existing);
20153
+ const retryCount = this.retryCounts.get(sessionId) ?? 0;
20154
+ const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
20155
+ const elapsed = Date.now() - lastAttempt;
20156
+ let delay3;
20157
+ if (retryCount > 0) {
20158
+ delay3 = Math.min(
20159
+ _SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
20160
+ _SessionLogWriter.MAX_RETRY_DELAY_MS
20161
+ );
20162
+ } else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
20163
+ delay3 = 0;
20164
+ } else {
20165
+ delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
20166
+ }
20167
+ const timeout = setTimeout(() => this.flush(sessionId), delay3);
20168
+ this.flushTimeouts.set(sessionId, timeout);
20169
+ }
20170
+ writeToLocalCache(sessionId, entry) {
20171
+ if (!this.localCachePath) return;
20172
+ const session = this.sessions.get(sessionId);
20173
+ if (!session) return;
20174
+ const logPath = path15.join(
20175
+ this.localCachePath,
20176
+ "sessions",
20177
+ session.context.runId,
20178
+ "logs.ndjson"
20179
+ );
20180
+ try {
20181
+ fs12.appendFileSync(logPath, `${JSON.stringify(entry)}
20182
+ `);
20183
+ } catch (error) {
20184
+ this.logger.warn("Failed to write to local cache", {
20185
+ taskId: session.context.taskId,
20186
+ runId: session.context.runId,
20187
+ logPath,
20188
+ error
20189
+ });
20190
+ }
20191
+ }
20192
+ static async cleanupOldSessions(localCachePath) {
20193
+ const sessionsDir = path15.join(localCachePath, "sessions");
20194
+ let deleted = 0;
20195
+ try {
20196
+ const entries = await fsp.readdir(sessionsDir);
20197
+ const now = Date.now();
20198
+ for (const entry of entries) {
20199
+ const entryPath = path15.join(sessionsDir, entry);
20200
+ try {
20201
+ const stats = await fsp.stat(entryPath);
20202
+ if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
20203
+ await fsp.rm(entryPath, { recursive: true, force: true });
20204
+ deleted++;
20205
+ }
20206
+ } catch {
20232
20207
  }
20233
20208
  }
20209
+ } catch {
20234
20210
  }
20235
- return null;
20211
+ return deleted;
20236
20212
  }
20237
- findLatestGitCheckpoint(entries) {
20238
- const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
20239
- for (let i2 = entries.length - 1; i2 >= 0; i2--) {
20240
- const entry = entries[i2];
20241
- const method = entry.notification?.method;
20242
- if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
20243
- const params = entry.notification?.params;
20244
- if (params?.checkpointId && params?.checkpointRef) {
20245
- return params;
20213
+ };
20214
+
20215
+ // src/sagas/apply-snapshot-saga.ts
20216
+ import { mkdir as mkdir7, rm as rm6, writeFile as writeFile5 } from "fs/promises";
20217
+ import { join as join12 } from "path";
20218
+
20219
+ // ../git/dist/sagas/tree.js
20220
+ import { existsSync as existsSync5 } from "fs";
20221
+ import * as fs13 from "fs/promises";
20222
+ import * as path16 from "path";
20223
+ import * as tar from "tar";
20224
+ var CaptureTreeSaga = class extends GitSaga {
20225
+ sagaName = "CaptureTreeSaga";
20226
+ tempIndexPath = null;
20227
+ async executeGitOperations(input) {
20228
+ const { baseDir, lastTreeHash, archivePath, signal } = input;
20229
+ const tmpDir = path16.join(baseDir, ".git", "posthog-code-tmp");
20230
+ await this.step({
20231
+ name: "create_tmp_dir",
20232
+ execute: () => fs13.mkdir(tmpDir, { recursive: true }),
20233
+ rollback: async () => {
20234
+ }
20235
+ });
20236
+ this.tempIndexPath = path16.join(tmpDir, `index-${Date.now()}`);
20237
+ const tempIndexGit = this.git.env({
20238
+ ...process.env,
20239
+ GIT_INDEX_FILE: this.tempIndexPath
20240
+ });
20241
+ await this.step({
20242
+ name: "init_temp_index",
20243
+ execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
20244
+ rollback: async () => {
20245
+ if (this.tempIndexPath) {
20246
+ await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
20247
+ });
20246
20248
  }
20247
20249
  }
20250
+ });
20251
+ await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
20252
+ const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
20253
+ if (lastTreeHash && treeHash === lastTreeHash) {
20254
+ this.log.debug("No changes since last capture", { treeHash });
20255
+ await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
20256
+ });
20257
+ return { snapshot: null, changed: false };
20248
20258
  }
20249
- return null;
20259
+ const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
20260
+ try {
20261
+ return await getHeadSha(baseDir, { abortSignal: signal });
20262
+ } catch {
20263
+ return null;
20264
+ }
20265
+ });
20266
+ const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
20267
+ await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
20268
+ });
20269
+ const snapshot = {
20270
+ treeHash,
20271
+ baseCommit,
20272
+ changes,
20273
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20274
+ };
20275
+ let createdArchivePath;
20276
+ if (archivePath) {
20277
+ createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
20278
+ }
20279
+ this.log.info("Tree captured", {
20280
+ treeHash,
20281
+ changes: changes.length,
20282
+ archived: !!createdArchivePath
20283
+ });
20284
+ return { snapshot, archivePath: createdArchivePath, changed: true };
20250
20285
  }
20251
- findLastDeviceInfo(entries) {
20252
- for (let i2 = entries.length - 1; i2 >= 0; i2--) {
20253
- const entry = entries[i2];
20254
- const params = entry.notification?.params;
20255
- if (params?.device) {
20256
- return params.device;
20286
+ async createArchive(baseDir, archivePath, changes) {
20287
+ const filesToArchive = changes.filter((c) => c.status !== "D").map((c) => c.path);
20288
+ if (filesToArchive.length === 0) {
20289
+ return void 0;
20290
+ }
20291
+ const existingFiles = filesToArchive.filter((f) => existsSync5(path16.join(baseDir, f)));
20292
+ if (existingFiles.length === 0) {
20293
+ return void 0;
20294
+ }
20295
+ await this.step({
20296
+ name: "create_archive",
20297
+ execute: async () => {
20298
+ const archiveDir = path16.dirname(archivePath);
20299
+ await fs13.mkdir(archiveDir, { recursive: true });
20300
+ await tar.create({
20301
+ gzip: true,
20302
+ file: archivePath,
20303
+ cwd: baseDir
20304
+ }, existingFiles);
20305
+ },
20306
+ rollback: async () => {
20307
+ await fs13.rm(archivePath, { force: true }).catch(() => {
20308
+ });
20257
20309
  }
20310
+ });
20311
+ return archivePath;
20312
+ }
20313
+ async getChanges(git, fromRef, toRef) {
20314
+ if (!fromRef) {
20315
+ const stdout2 = await git.raw(["ls-tree", "-r", "--name-only", toRef]);
20316
+ return stdout2.split("\n").filter((p) => p.trim()).map((p) => ({ path: p, status: "A" }));
20258
20317
  }
20259
- return void 0;
20318
+ const stdout = await git.raw([
20319
+ "diff-tree",
20320
+ "-r",
20321
+ "--name-status",
20322
+ fromRef,
20323
+ toRef
20324
+ ]);
20325
+ const changes = [];
20326
+ for (const line of stdout.split("\n")) {
20327
+ if (!line.trim())
20328
+ continue;
20329
+ const [status, filePath] = line.split(" ");
20330
+ if (!filePath)
20331
+ continue;
20332
+ let normalizedStatus;
20333
+ if (status === "D") {
20334
+ normalizedStatus = "D";
20335
+ } else if (status === "A") {
20336
+ normalizedStatus = "A";
20337
+ } else {
20338
+ normalizedStatus = "M";
20339
+ }
20340
+ changes.push({ path: filePath, status: normalizedStatus });
20341
+ }
20342
+ return changes;
20260
20343
  }
20261
- rebuildConversation(entries) {
20262
- const turns = [];
20263
- let currentAssistantContent = [];
20264
- let currentToolCalls = [];
20265
- for (const entry of entries) {
20266
- const method = entry.notification?.method;
20267
- const params = entry.notification?.params;
20268
- if (method === "session/update" && params?.update) {
20269
- const update = params.update;
20270
- const sessionUpdate = update.sessionUpdate;
20271
- switch (sessionUpdate) {
20272
- case "user_message":
20273
- case "user_message_chunk": {
20274
- if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
20275
- turns.push({
20276
- role: "assistant",
20277
- content: currentAssistantContent,
20278
- toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
20279
- });
20280
- currentAssistantContent = [];
20281
- currentToolCalls = [];
20282
- }
20283
- const content = update.content;
20284
- const contentArray = Array.isArray(content) ? content : [content];
20285
- turns.push({
20286
- role: "user",
20287
- content: contentArray
20288
- });
20289
- break;
20290
- }
20291
- case "agent_message": {
20292
- const content = update.content;
20293
- if (content) {
20294
- if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
20295
- const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
20296
- lastBlock.text += content.text;
20297
- } else {
20298
- currentAssistantContent.push(content);
20299
- }
20300
- }
20301
- break;
20302
- }
20303
- case "agent_message_chunk": {
20304
- const content = update.content;
20305
- if (content) {
20306
- if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
20307
- const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
20308
- lastBlock.text += content.text;
20309
- } else {
20310
- currentAssistantContent.push(content);
20311
- }
20312
- }
20313
- break;
20314
- }
20315
- case "tool_call":
20316
- case "tool_call_update": {
20317
- const meta = update._meta?.claudeCode;
20318
- if (meta) {
20319
- const toolCallId = meta.toolCallId;
20320
- const toolName = meta.toolName;
20321
- const toolInput = meta.toolInput;
20322
- const toolResponse = meta.toolResponse;
20323
- if (toolCallId && toolName) {
20324
- let toolCall = currentToolCalls.find(
20325
- (tc) => tc.toolCallId === toolCallId
20326
- );
20327
- if (!toolCall) {
20328
- toolCall = {
20329
- toolCallId,
20330
- toolName,
20331
- input: toolInput
20332
- };
20333
- currentToolCalls.push(toolCall);
20334
- }
20335
- if (toolResponse !== void 0) {
20336
- toolCall.result = toolResponse;
20337
- }
20338
- }
20344
+ };
20345
+ var ApplyTreeSaga = class extends GitSaga {
20346
+ sagaName = "ApplyTreeSaga";
20347
+ originalHead = null;
20348
+ originalBranch = null;
20349
+ extractedFiles = [];
20350
+ fileBackups = /* @__PURE__ */ new Map();
20351
+ async executeGitOperations(input) {
20352
+ const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
20353
+ const headInfo = await this.readOnlyStep("get_current_head", async () => {
20354
+ let head = null;
20355
+ let branch = null;
20356
+ try {
20357
+ head = await this.git.revparse(["HEAD"]);
20358
+ } catch {
20359
+ head = null;
20360
+ }
20361
+ try {
20362
+ branch = await this.git.raw(["symbolic-ref", "--short", "HEAD"]);
20363
+ } catch {
20364
+ branch = null;
20365
+ }
20366
+ return { head, branch };
20367
+ });
20368
+ this.originalHead = headInfo.head;
20369
+ this.originalBranch = headInfo.branch;
20370
+ let checkoutPerformed = false;
20371
+ if (baseCommit && baseCommit !== this.originalHead) {
20372
+ await this.readOnlyStep("check_working_tree", async () => {
20373
+ const status = await this.git.status();
20374
+ if (!status.isClean()) {
20375
+ const changedFiles = status.modified.length + status.staged.length + status.deleted.length;
20376
+ throw new Error(`Cannot apply tree: ${changedFiles} uncommitted change(s) exist. Commit or stash your changes first.`);
20377
+ }
20378
+ });
20379
+ await this.step({
20380
+ name: "checkout_base",
20381
+ execute: async () => {
20382
+ await this.git.checkout(baseCommit);
20383
+ checkoutPerformed = true;
20384
+ this.log.warn("Applied tree from different commit - now in detached HEAD state", {
20385
+ originalHead: this.originalHead,
20386
+ originalBranch: this.originalBranch,
20387
+ baseCommit
20388
+ });
20389
+ },
20390
+ rollback: async () => {
20391
+ try {
20392
+ if (this.originalBranch) {
20393
+ await this.git.checkout(this.originalBranch);
20394
+ } else if (this.originalHead) {
20395
+ await this.git.checkout(this.originalHead);
20339
20396
  }
20340
- break;
20397
+ } catch (error) {
20398
+ this.log.warn("Failed to rollback checkout", { error });
20341
20399
  }
20342
- case "tool_result": {
20343
- const meta = update._meta?.claudeCode;
20344
- if (meta) {
20345
- const toolCallId = meta.toolCallId;
20346
- const toolResponse = meta.toolResponse;
20347
- if (toolCallId) {
20348
- const toolCall = currentToolCalls.find(
20349
- (tc) => tc.toolCallId === toolCallId
20350
- );
20351
- if (toolCall && toolResponse !== void 0) {
20352
- toolCall.result = toolResponse;
20353
- }
20354
- }
20400
+ }
20401
+ });
20402
+ }
20403
+ if (archivePath) {
20404
+ const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
20405
+ await this.readOnlyStep("backup_existing_files", async () => {
20406
+ for (const filePath of filesToExtract) {
20407
+ const fullPath = path16.join(baseDir, filePath);
20408
+ try {
20409
+ const content = await fs13.readFile(fullPath);
20410
+ this.fileBackups.set(filePath, content);
20411
+ } catch {
20412
+ }
20413
+ }
20414
+ });
20415
+ await this.step({
20416
+ name: "extract_archive",
20417
+ execute: async () => {
20418
+ await tar.extract({
20419
+ file: archivePath,
20420
+ cwd: baseDir
20421
+ });
20422
+ this.extractedFiles = filesToExtract;
20423
+ },
20424
+ rollback: async () => {
20425
+ for (const filePath of this.extractedFiles) {
20426
+ const fullPath = path16.join(baseDir, filePath);
20427
+ const backup = this.fileBackups.get(filePath);
20428
+ if (backup) {
20429
+ const dir = path16.dirname(fullPath);
20430
+ await fs13.mkdir(dir, { recursive: true }).catch(() => {
20431
+ });
20432
+ await fs13.writeFile(fullPath, backup).catch(() => {
20433
+ });
20434
+ } else {
20435
+ await fs13.rm(fullPath, { force: true }).catch(() => {
20436
+ });
20355
20437
  }
20356
- break;
20357
20438
  }
20358
20439
  }
20359
- }
20440
+ });
20360
20441
  }
20361
- if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
20362
- turns.push({
20363
- role: "assistant",
20364
- content: currentAssistantContent,
20365
- toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
20442
+ for (const change of changes.filter((c) => c.status === "D")) {
20443
+ const fullPath = path16.join(baseDir, change.path);
20444
+ const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
20445
+ try {
20446
+ return await fs13.readFile(fullPath);
20447
+ } catch {
20448
+ return null;
20449
+ }
20450
+ });
20451
+ await this.step({
20452
+ name: `delete_${change.path}`,
20453
+ execute: async () => {
20454
+ await fs13.rm(fullPath, { force: true });
20455
+ this.log.debug(`Deleted file: ${change.path}`);
20456
+ },
20457
+ rollback: async () => {
20458
+ if (backupContent) {
20459
+ const dir = path16.dirname(fullPath);
20460
+ await fs13.mkdir(dir, { recursive: true }).catch(() => {
20461
+ });
20462
+ await fs13.writeFile(fullPath, backupContent).catch(() => {
20463
+ });
20464
+ }
20465
+ }
20366
20466
  });
20367
20467
  }
20368
- return turns;
20468
+ const deletedCount = changes.filter((c) => c.status === "D").length;
20469
+ this.log.info("Tree applied", {
20470
+ treeHash,
20471
+ totalChanges: changes.length,
20472
+ deletedFiles: deletedCount,
20473
+ checkoutPerformed
20474
+ });
20475
+ return { treeHash, checkoutPerformed };
20369
20476
  }
20370
20477
  };
20371
20478
 
20372
- // src/resume.ts
20373
- async function resumeFromLog(config) {
20374
- const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
20375
- logger.info("Resuming from log", {
20376
- taskId: config.taskId,
20377
- runId: config.runId
20378
- });
20379
- const saga = new ResumeSaga(logger);
20380
- const result = await saga.run({
20381
- taskId: config.taskId,
20382
- runId: config.runId,
20383
- repositoryPath: config.repositoryPath,
20384
- apiClient: config.apiClient,
20385
- logger
20386
- });
20387
- if (!result.success) {
20388
- logger.error("Failed to resume from log", {
20389
- error: result.error,
20390
- failedStep: result.failedStep
20391
- });
20392
- throw new Error(
20393
- `Failed to resume at step '${result.failedStep}': ${result.error}`
20394
- );
20395
- }
20396
- return {
20397
- conversation: result.data.conversation,
20398
- latestSnapshot: result.data.latestSnapshot,
20399
- latestGitCheckpoint: result.data.latestGitCheckpoint,
20400
- snapshotApplied: result.data.snapshotApplied,
20401
- interrupted: result.data.interrupted,
20402
- lastDevice: result.data.lastDevice,
20403
- logEntryCount: result.data.logEntryCount
20404
- };
20405
- }
20406
- var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
20407
- var TOOL_RESULT_MAX_CHARS = 2e3;
20408
- function formatConversationForResume(conversation) {
20409
- const selected = selectRecentTurns(conversation, RESUME_HISTORY_TOKEN_BUDGET);
20410
- const parts2 = [];
20411
- if (selected.length < conversation.length) {
20412
- parts2.push(
20413
- `*(${conversation.length - selected.length} earlier turns omitted)*`
20414
- );
20415
- }
20416
- for (const turn of selected) {
20417
- const role = turn.role === "user" ? "User" : "Assistant";
20418
- const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
20419
- if (textParts.length > 0) {
20420
- parts2.push(`**${role}**: ${textParts.join("\n")}`);
20479
+ // src/sagas/apply-snapshot-saga.ts
20480
+ var ApplySnapshotSaga = class extends Saga {
20481
+ sagaName = "ApplySnapshotSaga";
20482
+ archivePath = null;
20483
+ async execute(input) {
20484
+ const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
20485
+ const tmpDir = join12(repositoryPath, ".posthog", "tmp");
20486
+ if (!snapshot.archiveUrl) {
20487
+ throw new Error("Cannot apply snapshot: no archive URL");
20421
20488
  }
20422
- if (turn.toolCalls?.length) {
20423
- const toolSummary = turn.toolCalls.map((tc) => {
20424
- let resultStr = "";
20425
- if (tc.result !== void 0) {
20426
- const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
20427
- resultStr = raw.length > TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
20489
+ const archiveUrl = snapshot.archiveUrl;
20490
+ await this.step({
20491
+ name: "create_tmp_dir",
20492
+ execute: () => mkdir7(tmpDir, { recursive: true }),
20493
+ rollback: async () => {
20494
+ }
20495
+ });
20496
+ const archivePath = join12(tmpDir, `${snapshot.treeHash}.tar.gz`);
20497
+ this.archivePath = archivePath;
20498
+ await this.step({
20499
+ name: "download_archive",
20500
+ execute: async () => {
20501
+ const arrayBuffer = await apiClient.downloadArtifact(
20502
+ taskId,
20503
+ runId,
20504
+ archiveUrl
20505
+ );
20506
+ if (!arrayBuffer) {
20507
+ throw new Error("Failed to download archive");
20428
20508
  }
20429
- return ` - ${tc.toolName}${resultStr}`;
20430
- }).join("\n");
20431
- parts2.push(`**${role} (tools)**:
20432
- ${toolSummary}`);
20433
- }
20434
- }
20435
- return parts2.join("\n\n");
20436
- }
20437
-
20438
- // src/session-log-writer.ts
20439
- import fs13 from "fs";
20440
- import fsp from "fs/promises";
20441
- import path16 from "path";
20442
- var SessionLogWriter = class _SessionLogWriter {
20443
- static FLUSH_DEBOUNCE_MS = 500;
20444
- static FLUSH_MAX_INTERVAL_MS = 5e3;
20445
- static MAX_FLUSH_RETRIES = 10;
20446
- static MAX_RETRY_DELAY_MS = 3e4;
20447
- static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
20448
- posthogAPI;
20449
- pendingEntries = /* @__PURE__ */ new Map();
20450
- flushTimeouts = /* @__PURE__ */ new Map();
20451
- lastFlushAttemptTime = /* @__PURE__ */ new Map();
20452
- retryCounts = /* @__PURE__ */ new Map();
20453
- sessions = /* @__PURE__ */ new Map();
20454
- flushQueues = /* @__PURE__ */ new Map();
20455
- logger;
20456
- localCachePath;
20457
- constructor(options = {}) {
20458
- this.posthogAPI = options.posthogAPI;
20459
- this.localCachePath = options.localCachePath;
20460
- this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
20461
- }
20462
- async flushAll() {
20463
- const flushPromises = [];
20464
- for (const [sessionId, session] of this.sessions) {
20465
- this.emitCoalescedMessage(sessionId, session);
20466
- flushPromises.push(this.flush(sessionId));
20467
- }
20468
- await Promise.all(flushPromises);
20469
- }
20470
- register(sessionId, context) {
20471
- if (this.sessions.has(sessionId)) {
20472
- return;
20473
- }
20474
- this.sessions.set(sessionId, { context, currentTurnMessages: [] });
20475
- this.lastFlushAttemptTime.set(sessionId, Date.now());
20476
- if (this.localCachePath) {
20477
- const sessionDir = path16.join(
20478
- this.localCachePath,
20479
- "sessions",
20480
- context.runId
20481
- );
20482
- try {
20483
- fs13.mkdirSync(sessionDir, { recursive: true });
20484
- } catch (error) {
20485
- this.logger.warn("Failed to create local cache directory", {
20486
- sessionDir,
20487
- error
20509
+ const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
20510
+ const binaryContent = Buffer.from(base64Content, "base64");
20511
+ await writeFile5(archivePath, binaryContent);
20512
+ this.log.info("Tree archive downloaded", {
20513
+ treeHash: snapshot.treeHash,
20514
+ snapshotBytes: binaryContent.byteLength,
20515
+ snapshotWireBytes: arrayBuffer.byteLength,
20516
+ totalBytes: binaryContent.byteLength,
20517
+ totalWireBytes: arrayBuffer.byteLength
20488
20518
  });
20489
- }
20490
- }
20491
- }
20492
- isRegistered(sessionId) {
20493
- return this.sessions.has(sessionId);
20494
- }
20495
- appendRawLine(sessionId, line) {
20496
- const session = this.sessions.get(sessionId);
20497
- if (!session) {
20498
- this.logger.warn("appendRawLine called for unregistered session", {
20499
- sessionId
20500
- });
20501
- return;
20502
- }
20503
- try {
20504
- const message = JSON.parse(line);
20505
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
20506
- if (this.isAgentMessageChunk(message)) {
20507
- const text2 = this.extractChunkText(message);
20508
- if (text2) {
20509
- if (!session.chunkBuffer) {
20510
- session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
20511
- } else {
20512
- session.chunkBuffer.text += text2;
20513
- }
20519
+ },
20520
+ rollback: async () => {
20521
+ if (this.archivePath) {
20522
+ await rm6(this.archivePath, { force: true }).catch(() => {
20523
+ });
20514
20524
  }
20515
- return;
20516
- }
20517
- if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
20518
- session.chunkBuffer = void 0;
20519
- } else {
20520
- this.emitCoalescedMessage(sessionId, session);
20521
- }
20522
- const nonChunkAgentText = this.extractAgentMessageText(message);
20523
- if (nonChunkAgentText) {
20524
- session.lastAgentMessage = nonChunkAgentText;
20525
- session.currentTurnMessages.push(nonChunkAgentText);
20526
- }
20527
- const entry = {
20528
- type: "notification",
20529
- timestamp,
20530
- notification: message
20531
- };
20532
- this.writeToLocalCache(sessionId, entry);
20533
- if (this.posthogAPI) {
20534
- const pending = this.pendingEntries.get(sessionId) ?? [];
20535
- pending.push(entry);
20536
- this.pendingEntries.set(sessionId, pending);
20537
- this.scheduleFlush(sessionId);
20538
- }
20539
- } catch {
20540
- this.logger.warn("Failed to parse raw line for persistence", {
20541
- taskId: session.context.taskId,
20542
- runId: session.context.runId,
20543
- lineLength: line.length
20544
- });
20545
- }
20546
- }
20547
- async flush(sessionId, { coalesce = false } = {}) {
20548
- if (coalesce) {
20549
- const session = this.sessions.get(sessionId);
20550
- if (session) {
20551
- this.emitCoalescedMessage(sessionId, session);
20552
20525
  }
20526
+ });
20527
+ const gitApplySaga = new ApplyTreeSaga(this.log);
20528
+ const applyResult = await gitApplySaga.run({
20529
+ baseDir: repositoryPath,
20530
+ treeHash: snapshot.treeHash,
20531
+ baseCommit: snapshot.baseCommit,
20532
+ changes: snapshot.changes,
20533
+ archivePath: this.archivePath
20534
+ });
20535
+ if (!applyResult.success) {
20536
+ throw new Error(`Failed to apply tree: ${applyResult.error}`);
20553
20537
  }
20554
- const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
20555
- const next = prev.catch(() => {
20556
- }).then(() => this._doFlush(sessionId));
20557
- this.flushQueues.set(sessionId, next);
20558
- next.finally(() => {
20559
- if (this.flushQueues.get(sessionId) === next) {
20560
- this.flushQueues.delete(sessionId);
20561
- }
20538
+ await rm6(this.archivePath, { force: true }).catch(() => {
20562
20539
  });
20563
- return next;
20540
+ this.log.info("Tree snapshot applied", {
20541
+ treeHash: snapshot.treeHash,
20542
+ totalChanges: snapshot.changes.length,
20543
+ deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
20544
+ });
20545
+ return { treeHash: snapshot.treeHash };
20564
20546
  }
20565
- async _doFlush(sessionId) {
20566
- const session = this.sessions.get(sessionId);
20567
- if (!session) {
20568
- this.logger.warn("flush: no session found", { sessionId });
20569
- return;
20547
+ };
20548
+
20549
+ // src/sagas/capture-tree-saga.ts
20550
+ import { existsSync as existsSync6 } from "fs";
20551
+ import { readFile as readFile6, rm as rm7 } from "fs/promises";
20552
+ import { join as join13 } from "path";
20553
+ var CaptureTreeSaga2 = class extends Saga {
20554
+ sagaName = "CaptureTreeSaga";
20555
+ async execute(input) {
20556
+ const {
20557
+ repositoryPath,
20558
+ lastTreeHash,
20559
+ interrupted,
20560
+ apiClient,
20561
+ taskId,
20562
+ runId
20563
+ } = input;
20564
+ const tmpDir = join13(repositoryPath, ".posthog", "tmp");
20565
+ if (existsSync6(join13(repositoryPath, ".gitmodules"))) {
20566
+ this.log.warn(
20567
+ "Repository has submodules - snapshot may not capture submodule state"
20568
+ );
20570
20569
  }
20571
- const pending = this.pendingEntries.get(sessionId);
20572
- if (!this.posthogAPI || !pending?.length) {
20573
- return;
20570
+ const shouldArchive = !!apiClient;
20571
+ const archivePath = shouldArchive ? join13(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
20572
+ const gitCaptureSaga = new CaptureTreeSaga(this.log);
20573
+ const captureResult = await gitCaptureSaga.run({
20574
+ baseDir: repositoryPath,
20575
+ lastTreeHash,
20576
+ archivePath
20577
+ });
20578
+ if (!captureResult.success) {
20579
+ throw new Error(`Failed to capture tree: ${captureResult.error}`);
20574
20580
  }
20575
- this.pendingEntries.delete(sessionId);
20576
- const timeout = this.flushTimeouts.get(sessionId);
20577
- if (timeout) {
20578
- clearTimeout(timeout);
20579
- this.flushTimeouts.delete(sessionId);
20581
+ const {
20582
+ snapshot: gitSnapshot,
20583
+ archivePath: createdArchivePath,
20584
+ changed
20585
+ } = captureResult.data;
20586
+ if (!changed || !gitSnapshot) {
20587
+ this.log.debug("No changes since last capture", { lastTreeHash });
20588
+ return { snapshot: null, newTreeHash: lastTreeHash };
20580
20589
  }
20581
- this.lastFlushAttemptTime.set(sessionId, Date.now());
20582
- try {
20583
- await this.posthogAPI.appendTaskRunLog(
20584
- session.context.taskId,
20585
- session.context.runId,
20586
- pending
20587
- );
20588
- this.retryCounts.set(sessionId, 0);
20589
- } catch (error) {
20590
- const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
20591
- this.retryCounts.set(sessionId, retryCount);
20592
- if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
20593
- this.logger.error(
20594
- `Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
20595
- {
20596
- taskId: session.context.taskId,
20597
- runId: session.context.runId,
20598
- error
20599
- }
20590
+ let archiveUrl;
20591
+ if (apiClient && createdArchivePath) {
20592
+ try {
20593
+ archiveUrl = await this.uploadArchive(
20594
+ createdArchivePath,
20595
+ gitSnapshot.treeHash,
20596
+ apiClient,
20597
+ taskId,
20598
+ runId
20600
20599
  );
20601
- this.retryCounts.set(sessionId, 0);
20602
- } else {
20603
- if (retryCount === 1) {
20604
- this.logger.warn(
20605
- `Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
20606
- {
20607
- taskId: session.context.taskId,
20608
- runId: session.context.runId,
20609
- error: error instanceof Error ? error.message : String(error)
20610
- }
20611
- );
20612
- }
20613
- const currentPending = this.pendingEntries.get(sessionId) ?? [];
20614
- this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
20615
- this.scheduleFlush(sessionId);
20600
+ } finally {
20601
+ await rm7(createdArchivePath, { force: true }).catch(() => {
20602
+ });
20616
20603
  }
20617
20604
  }
20605
+ const snapshot = {
20606
+ treeHash: gitSnapshot.treeHash,
20607
+ baseCommit: gitSnapshot.baseCommit,
20608
+ changes: gitSnapshot.changes,
20609
+ timestamp: gitSnapshot.timestamp,
20610
+ interrupted,
20611
+ archiveUrl
20612
+ };
20613
+ this.log.info("Tree captured", {
20614
+ treeHash: snapshot.treeHash,
20615
+ changes: snapshot.changes.length,
20616
+ interrupted,
20617
+ archiveUrl
20618
+ });
20619
+ return { snapshot, newTreeHash: snapshot.treeHash };
20618
20620
  }
20619
- getSessionUpdateType(message) {
20620
- if (message.method !== "session/update") return void 0;
20621
- const params = message.params;
20622
- const update = params?.update;
20623
- return update?.sessionUpdate;
20624
- }
20625
- isDirectAgentMessage(message) {
20626
- return this.getSessionUpdateType(message) === "agent_message";
20627
- }
20628
- isAgentMessageChunk(message) {
20629
- return this.getSessionUpdateType(message) === "agent_message_chunk";
20630
- }
20631
- extractChunkText(message) {
20632
- const params = message.params;
20633
- const update = params?.update;
20634
- const content = update?.content;
20635
- if (content?.type === "text" && content.text) {
20636
- return content.text;
20637
- }
20638
- return "";
20639
- }
20640
- emitCoalescedMessage(sessionId, session) {
20641
- if (!session.chunkBuffer) return;
20642
- const { text: text2, firstTimestamp } = session.chunkBuffer;
20643
- session.chunkBuffer = void 0;
20644
- session.lastAgentMessage = text2;
20645
- session.currentTurnMessages.push(text2);
20646
- const entry = {
20647
- type: "notification",
20648
- timestamp: firstTimestamp,
20649
- notification: {
20650
- jsonrpc: "2.0",
20651
- method: "session/update",
20652
- params: {
20653
- update: {
20654
- sessionUpdate: "agent_message",
20655
- content: { type: "text", text: text2 }
20621
+ async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
20622
+ const archiveUrl = await this.step({
20623
+ name: "upload_archive",
20624
+ execute: async () => {
20625
+ const archiveContent = await readFile6(archivePath);
20626
+ const base64Content = archiveContent.toString("base64");
20627
+ const snapshotBytes = archiveContent.byteLength;
20628
+ const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
20629
+ const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
20630
+ {
20631
+ name: `trees/${treeHash}.tar.gz`,
20632
+ type: "tree_snapshot",
20633
+ content: base64Content,
20634
+ content_type: "application/gzip"
20656
20635
  }
20636
+ ]);
20637
+ const uploadedArtifact = artifacts[0];
20638
+ if (uploadedArtifact?.storage_path) {
20639
+ this.log.info("Tree archive uploaded", {
20640
+ storagePath: uploadedArtifact.storage_path,
20641
+ treeHash,
20642
+ snapshotBytes,
20643
+ snapshotWireBytes,
20644
+ totalBytes: snapshotBytes,
20645
+ totalWireBytes: snapshotWireBytes
20646
+ });
20647
+ return uploadedArtifact.storage_path;
20657
20648
  }
20649
+ return void 0;
20650
+ },
20651
+ rollback: async () => {
20652
+ await rm7(archivePath, { force: true }).catch(() => {
20653
+ });
20658
20654
  }
20659
- };
20660
- this.writeToLocalCache(sessionId, entry);
20661
- if (this.posthogAPI) {
20662
- const pending = this.pendingEntries.get(sessionId) ?? [];
20663
- pending.push(entry);
20664
- this.pendingEntries.set(sessionId, pending);
20665
- this.scheduleFlush(sessionId);
20666
- }
20655
+ });
20656
+ return archiveUrl;
20667
20657
  }
20668
- getLastAgentMessage(sessionId) {
20669
- return this.sessions.get(sessionId)?.lastAgentMessage;
20658
+ };
20659
+
20660
+ // src/tree-tracker.ts
20661
+ var TreeTracker = class {
20662
+ repositoryPath;
20663
+ taskId;
20664
+ runId;
20665
+ apiClient;
20666
+ logger;
20667
+ lastTreeHash = null;
20668
+ constructor(config) {
20669
+ this.repositoryPath = config.repositoryPath;
20670
+ this.taskId = config.taskId;
20671
+ this.runId = config.runId;
20672
+ this.apiClient = config.apiClient;
20673
+ this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
20670
20674
  }
20671
- getFullAgentResponse(sessionId) {
20672
- const session = this.sessions.get(sessionId);
20673
- if (!session || session.currentTurnMessages.length === 0) return void 0;
20674
- if (session.chunkBuffer) {
20675
- this.logger.warn(
20676
- "getFullAgentResponse called with non-empty chunk buffer",
20677
- {
20678
- sessionId,
20679
- bufferedLength: session.chunkBuffer.text.length
20680
- }
20675
+ /**
20676
+ * Capture current working tree state as a snapshot.
20677
+ * Uses a temporary index to avoid modifying user's staging area.
20678
+ * Uses Saga pattern for atomic operation with automatic cleanup on failure.
20679
+ */
20680
+ async captureTree(options) {
20681
+ const saga = new CaptureTreeSaga2(this.logger);
20682
+ const result = await saga.run({
20683
+ repositoryPath: this.repositoryPath,
20684
+ taskId: this.taskId,
20685
+ runId: this.runId,
20686
+ apiClient: this.apiClient,
20687
+ lastTreeHash: this.lastTreeHash,
20688
+ interrupted: options?.interrupted
20689
+ });
20690
+ if (!result.success) {
20691
+ this.logger.error("Failed to capture tree", {
20692
+ error: result.error,
20693
+ failedStep: result.failedStep
20694
+ });
20695
+ throw new Error(
20696
+ `Failed to capture tree at step '${result.failedStep}': ${result.error}`
20681
20697
  );
20682
20698
  }
20683
- return session.currentTurnMessages.join("\n\n");
20684
- }
20685
- resetTurnMessages(sessionId) {
20686
- const session = this.sessions.get(sessionId);
20687
- if (session) {
20688
- session.currentTurnMessages = [];
20699
+ if (result.data.newTreeHash !== null) {
20700
+ this.lastTreeHash = result.data.newTreeHash;
20689
20701
  }
20702
+ return result.data.snapshot;
20690
20703
  }
20691
- extractAgentMessageText(message) {
20692
- if (message.method !== "session/update") {
20693
- return null;
20694
- }
20695
- const params = message.params;
20696
- const update = params?.update;
20697
- if (update?.sessionUpdate !== "agent_message") {
20698
- return null;
20699
- }
20700
- const content = update.content;
20701
- if (content?.type === "text" && typeof content.text === "string") {
20702
- const trimmed2 = content.text.trim();
20703
- return trimmed2.length > 0 ? trimmed2 : null;
20704
+ /**
20705
+ * Download and apply a tree snapshot.
20706
+ * Uses Saga pattern for atomic operation with rollback on failure.
20707
+ */
20708
+ async applyTreeSnapshot(snapshot) {
20709
+ if (!this.apiClient) {
20710
+ throw new Error("Cannot apply snapshot: API client not configured");
20704
20711
  }
20705
- if (typeof update.message === "string") {
20706
- const trimmed2 = update.message.trim();
20707
- return trimmed2.length > 0 ? trimmed2 : null;
20712
+ if (!snapshot.archiveUrl) {
20713
+ this.logger.warn("Cannot apply snapshot: no archive URL", {
20714
+ treeHash: snapshot.treeHash,
20715
+ changes: snapshot.changes.length
20716
+ });
20717
+ throw new Error("Cannot apply snapshot: no archive URL");
20708
20718
  }
20709
- return null;
20710
- }
20711
- scheduleFlush(sessionId) {
20712
- const existing = this.flushTimeouts.get(sessionId);
20713
- if (existing) clearTimeout(existing);
20714
- const retryCount = this.retryCounts.get(sessionId) ?? 0;
20715
- const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
20716
- const elapsed = Date.now() - lastAttempt;
20717
- let delay3;
20718
- if (retryCount > 0) {
20719
- delay3 = Math.min(
20720
- _SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
20721
- _SessionLogWriter.MAX_RETRY_DELAY_MS
20719
+ const saga = new ApplySnapshotSaga(this.logger);
20720
+ const result = await saga.run({
20721
+ snapshot,
20722
+ repositoryPath: this.repositoryPath,
20723
+ apiClient: this.apiClient,
20724
+ taskId: this.taskId,
20725
+ runId: this.runId
20726
+ });
20727
+ if (!result.success) {
20728
+ this.logger.error("Failed to apply tree snapshot", {
20729
+ error: result.error,
20730
+ failedStep: result.failedStep,
20731
+ treeHash: snapshot.treeHash
20732
+ });
20733
+ throw new Error(
20734
+ `Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
20722
20735
  );
20723
- } else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
20724
- delay3 = 0;
20725
- } else {
20726
- delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
20727
20736
  }
20728
- const timeout = setTimeout(() => this.flush(sessionId), delay3);
20729
- this.flushTimeouts.set(sessionId, timeout);
20737
+ this.lastTreeHash = result.data.treeHash;
20730
20738
  }
20731
- writeToLocalCache(sessionId, entry) {
20732
- if (!this.localCachePath) return;
20733
- const session = this.sessions.get(sessionId);
20734
- if (!session) return;
20735
- const logPath = path16.join(
20736
- this.localCachePath,
20737
- "sessions",
20738
- session.context.runId,
20739
- "logs.ndjson"
20740
- );
20741
- try {
20742
- fs13.appendFileSync(logPath, `${JSON.stringify(entry)}
20743
- `);
20744
- } catch (error) {
20745
- this.logger.warn("Failed to write to local cache", {
20746
- taskId: session.context.taskId,
20747
- runId: session.context.runId,
20748
- logPath,
20749
- error
20750
- });
20751
- }
20739
+ /**
20740
+ * Get the last captured tree hash.
20741
+ */
20742
+ getLastTreeHash() {
20743
+ return this.lastTreeHash;
20752
20744
  }
20753
- static async cleanupOldSessions(localCachePath) {
20754
- const sessionsDir = path16.join(localCachePath, "sessions");
20755
- let deleted = 0;
20756
- try {
20757
- const entries = await fsp.readdir(sessionsDir);
20758
- const now = Date.now();
20759
- for (const entry of entries) {
20760
- const entryPath = path16.join(sessionsDir, entry);
20761
- try {
20762
- const stats = await fsp.stat(entryPath);
20763
- if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
20764
- await fsp.rm(entryPath, { recursive: true, force: true });
20765
- deleted++;
20766
- }
20767
- } catch {
20768
- }
20769
- }
20770
- } catch {
20771
- }
20772
- return deleted;
20745
+ /**
20746
+ * Set the last tree hash (used when resuming).
20747
+ */
20748
+ setLastTreeHash(hash) {
20749
+ this.lastTreeHash = hash;
20773
20750
  }
20774
20751
  };
20775
20752
 
@@ -21245,45 +21222,29 @@ var AgentServer = class {
21245
21222
  });
21246
21223
  await this.autoInitializeSession();
21247
21224
  }
21248
- async autoInitializeSession() {
21249
- const { taskId, runId, mode, projectId } = this.config;
21250
- this.logger.debug("Auto-initializing session", { taskId, runId, mode });
21251
- const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
21252
- if (resumeRunId) {
21253
- this.logger.debug("Resuming from previous run", {
21254
- resumeRunId,
21255
- currentRunId: runId
21225
+ async loadResumeState(taskId, resumeRunId, currentRunId) {
21226
+ this.logger.debug("Loading resume state", { resumeRunId, currentRunId });
21227
+ try {
21228
+ this.resumeState = await resumeFromLog({
21229
+ taskId,
21230
+ runId: resumeRunId,
21231
+ repositoryPath: this.config.repositoryPath,
21232
+ apiClient: this.posthogAPI,
21233
+ logger: new Logger({ debug: true, prefix: "[Resume]" })
21256
21234
  });
21257
- try {
21258
- this.resumeState = await resumeFromLog({
21259
- taskId,
21260
- runId: resumeRunId,
21261
- repositoryPath: this.config.repositoryPath,
21262
- apiClient: this.posthogAPI,
21263
- logger: new Logger({ debug: true, prefix: "[Resume]" })
21264
- });
21265
- this.logger.debug("Resume state loaded", {
21266
- conversationTurns: this.resumeState.conversation.length,
21267
- snapshotApplied: this.resumeState.snapshotApplied,
21268
- logEntries: this.resumeState.logEntryCount
21269
- });
21270
- } catch (error) {
21271
- this.logger.debug("Failed to load resume state, starting fresh", {
21272
- error
21273
- });
21274
- this.resumeState = null;
21275
- }
21235
+ this.logger.debug("Resume state loaded", {
21236
+ conversationTurns: this.resumeState.conversation.length,
21237
+ hasSnapshot: !!this.resumeState.latestSnapshot,
21238
+ hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
21239
+ gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null,
21240
+ logEntries: this.resumeState.logEntryCount
21241
+ });
21242
+ } catch (error) {
21243
+ this.logger.debug("Failed to load resume state, starting fresh", {
21244
+ error
21245
+ });
21246
+ this.resumeState = null;
21276
21247
  }
21277
- const payload = {
21278
- task_id: taskId,
21279
- run_id: runId,
21280
- team_id: projectId,
21281
- user_id: 0,
21282
- // System-initiated
21283
- distinct_id: "agent-server",
21284
- mode
21285
- };
21286
- await this.initializeSession(payload, null);
21287
21248
  }
21288
21249
  async stop() {
21289
21250
  this.logger.debug("Stopping agent server...");
@@ -21682,29 +21643,11 @@ var AgentServer = class {
21682
21643
  if (!this.resumeState) {
21683
21644
  const resumeRunId = this.getResumeRunId(taskRun);
21684
21645
  if (resumeRunId) {
21685
- this.logger.debug("Resuming from previous run (via TaskRun state)", {
21646
+ await this.loadResumeState(
21647
+ payload.task_id,
21686
21648
  resumeRunId,
21687
- currentRunId: payload.run_id
21688
- });
21689
- try {
21690
- this.resumeState = await resumeFromLog({
21691
- taskId: payload.task_id,
21692
- runId: resumeRunId,
21693
- repositoryPath: this.config.repositoryPath,
21694
- apiClient: this.posthogAPI,
21695
- logger: new Logger({ debug: true, prefix: "[Resume]" })
21696
- });
21697
- this.logger.debug("Resume state loaded (via TaskRun state)", {
21698
- conversationTurns: this.resumeState.conversation.length,
21699
- snapshotApplied: this.resumeState.snapshotApplied,
21700
- logEntries: this.resumeState.logEntryCount
21701
- });
21702
- } catch (error) {
21703
- this.logger.debug("Failed to load resume state, starting fresh", {
21704
- error
21705
- });
21706
- this.resumeState = null;
21707
- }
21649
+ payload.run_id
21650
+ );
21708
21651
  }
21709
21652
  }
21710
21653
  if (this.resumeState && this.resumeState.conversation.length > 0) {
@@ -21763,8 +21706,59 @@ var AgentServer = class {
21763
21706
  const conversationSummary = formatConversationForResume(
21764
21707
  this.resumeState.conversation
21765
21708
  );
21709
+ let snapshotApplied = false;
21710
+ if (this.resumeState.latestSnapshot?.archiveUrl && this.config.repositoryPath && this.posthogAPI) {
21711
+ try {
21712
+ const treeTracker = new TreeTracker({
21713
+ repositoryPath: this.config.repositoryPath,
21714
+ taskId: payload.task_id,
21715
+ runId: payload.run_id,
21716
+ apiClient: this.posthogAPI,
21717
+ logger: this.logger.child("TreeTracker")
21718
+ });
21719
+ await treeTracker.applyTreeSnapshot(this.resumeState.latestSnapshot);
21720
+ treeTracker.setLastTreeHash(this.resumeState.latestSnapshot.treeHash);
21721
+ snapshotApplied = true;
21722
+ this.logger.info("Tree snapshot applied", {
21723
+ treeHash: this.resumeState.latestSnapshot.treeHash,
21724
+ changes: this.resumeState.latestSnapshot.changes?.length ?? 0,
21725
+ hasArchiveUrl: !!this.resumeState.latestSnapshot.archiveUrl
21726
+ });
21727
+ } catch (error) {
21728
+ this.logger.warn("Failed to apply tree snapshot", {
21729
+ error: error instanceof Error ? error.message : String(error),
21730
+ treeHash: this.resumeState.latestSnapshot.treeHash
21731
+ });
21732
+ }
21733
+ }
21734
+ if (this.resumeState.latestGitCheckpoint && this.config.repositoryPath && this.posthogAPI) {
21735
+ try {
21736
+ const checkpointTracker = new HandoffCheckpointTracker({
21737
+ repositoryPath: this.config.repositoryPath,
21738
+ taskId: payload.task_id,
21739
+ runId: payload.run_id,
21740
+ apiClient: this.posthogAPI,
21741
+ logger: this.logger.child("HandoffCheckpoint")
21742
+ });
21743
+ const metrics = await checkpointTracker.applyFromHandoff(
21744
+ this.resumeState.latestGitCheckpoint
21745
+ );
21746
+ this.logger.info("Git checkpoint applied", {
21747
+ branch: this.resumeState.latestGitCheckpoint.branch,
21748
+ head: this.resumeState.latestGitCheckpoint.head,
21749
+ packBytes: metrics.packBytes,
21750
+ indexBytes: metrics.indexBytes,
21751
+ totalBytes: metrics.totalBytes
21752
+ });
21753
+ } catch (error) {
21754
+ this.logger.warn("Failed to apply git checkpoint", {
21755
+ error: error instanceof Error ? error.message : String(error),
21756
+ branch: this.resumeState.latestGitCheckpoint.branch
21757
+ });
21758
+ }
21759
+ }
21766
21760
  const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
21767
- const sandboxContext = this.resumeState.snapshotApplied ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.` : `The workspace files from the previous session were not restored (the file snapshot may have expired), so you are starting with a fresh environment. Your conversation history is fully preserved below.`;
21761
+ const sandboxContext = snapshotApplied ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.` : `The workspace files from the previous session were not restored (the file snapshot may have expired), so you are starting with a fresh environment. Your conversation history is fully preserved below.`;
21768
21762
  let resumePromptBlocks;
21769
21763
  if (pendingUserPrompt?.length) {
21770
21764
  resumePromptBlocks = [
@@ -21805,7 +21799,9 @@ Continue from where you left off. The user is waiting for your response.`
21805
21799
  conversationTurns: this.resumeState.conversation.length,
21806
21800
  promptLength: promptBlocksToText(resumePromptBlocks).length,
21807
21801
  hasPendingUserMessage: !!pendingUserPrompt?.length,
21808
- snapshotApplied: this.resumeState.snapshotApplied
21802
+ snapshotApplied,
21803
+ hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
21804
+ gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null
21809
21805
  });
21810
21806
  this.resumeState = null;
21811
21807
  this.session.logWriter.resetTurnMessages(payload.run_id);
@@ -21969,6 +21965,24 @@ Continue from where you left off. The user is waiting for your response.`
21969
21965
  const normalizedName = baseName.replace(/[^\w.-]/g, "_");
21970
21966
  return normalizedName.length > 0 ? normalizedName : "attachment";
21971
21967
  }
21968
+ async autoInitializeSession() {
21969
+ const { taskId, runId, mode, projectId } = this.config;
21970
+ this.logger.debug("Auto-initializing session", { taskId, runId, mode });
21971
+ const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
21972
+ if (resumeRunId) {
21973
+ await this.loadResumeState(taskId, resumeRunId, runId);
21974
+ }
21975
+ const payload = {
21976
+ task_id: taskId,
21977
+ run_id: runId,
21978
+ team_id: projectId,
21979
+ user_id: 0,
21980
+ // System-initiated
21981
+ distinct_id: "agent-server",
21982
+ mode
21983
+ };
21984
+ await this.initializeSession(payload, null);
21985
+ }
21972
21986
  getResumeRunId(taskRun) {
21973
21987
  const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
21974
21988
  if (envRunId) return envRunId;