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