@posthog/agent 2.3.398 → 2.3.401

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.3.398",
3
+ "version": "2.3.401",
4
4
  "repository": "https://github.com/PostHog/code",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -106,9 +106,9 @@
106
106
  "tsx": "^4.20.6",
107
107
  "typescript": "^5.5.0",
108
108
  "vitest": "^2.1.8",
109
- "@posthog/shared": "1.0.0",
110
109
  "@posthog/git": "1.0.0",
111
- "@posthog/enricher": "1.0.0"
110
+ "@posthog/enricher": "1.0.0",
111
+ "@posthog/shared": "1.0.0"
112
112
  },
113
113
  "dependencies": {
114
114
  "@agentclientprotocol/sdk": "0.19.0",
@@ -179,5 +179,6 @@ describe("HandoffCheckpointTracker", () => {
179
179
  expect(status).toContain("M tracked.txt");
180
180
  expect(status).toContain(" M unstaged.txt");
181
181
  expect(status).toContain("?? untracked.txt");
182
+ expect(localRepo.exists(".posthog/tmp")).toBe(false);
182
183
  });
183
184
  });
@@ -1,4 +1,11 @@
1
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import {
2
+ mkdir,
3
+ readdir,
4
+ readFile,
5
+ rm,
6
+ rmdir,
7
+ writeFile,
8
+ } from "node:fs/promises";
2
9
  import { join } from "node:path";
3
10
  import {
4
11
  type GitHandoffBranchDivergence,
@@ -161,6 +168,7 @@ export class HandoffCheckpointTracker {
161
168
  } finally {
162
169
  await this.removeIfPresent(packPath);
163
170
  await this.removeIfPresent(indexPath);
171
+ await this.removeTmpDirIfEmpty(tmpDir);
164
172
  }
165
173
  }
166
174
 
@@ -364,4 +372,12 @@ export class HandoffCheckpointTracker {
364
372
  }
365
373
  await rm(filePath, { force: true }).catch(() => {});
366
374
  }
375
+
376
+ private async removeTmpDirIfEmpty(tmpDir: string): Promise<void> {
377
+ const entries = await readdir(tmpDir).catch(() => null);
378
+ if (!entries || entries.length > 0) {
379
+ return;
380
+ }
381
+ await rmdir(tmpDir).catch(() => {});
382
+ }
367
383
  }
@@ -328,6 +328,7 @@ describe("ApplySnapshotSaga", () => {
328
328
  });
329
329
 
330
330
  expect(repo.exists(".posthog/tmp/test-tree-hash.tar.gz")).toBe(false);
331
+ expect(repo.exists(".posthog/tmp")).toBe(false);
331
332
  });
332
333
 
333
334
  it("cleans up downloaded archive on checkout failure (rollback verification)", async () => {
@@ -1,4 +1,4 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { ApplyTreeSaga as GitApplyTreeSaga } from "@posthog/git/sagas/tree";
4
4
  import { Saga } from "@posthog/shared";
@@ -37,64 +37,78 @@ export class ApplySnapshotSaga extends Saga<
37
37
 
38
38
  const archiveUrl = snapshot.archiveUrl;
39
39
 
40
- await this.step({
41
- name: "create_tmp_dir",
42
- execute: () => mkdir(tmpDir, { recursive: true }),
43
- rollback: async () => {},
44
- });
40
+ try {
41
+ await this.step({
42
+ name: "create_tmp_dir",
43
+ execute: () => mkdir(tmpDir, { recursive: true }),
44
+ rollback: async () => {},
45
+ });
45
46
 
46
- const archivePath = join(tmpDir, `${snapshot.treeHash}.tar.gz`);
47
- this.archivePath = archivePath;
48
- await this.step({
49
- name: "download_archive",
50
- execute: async () => {
51
- const arrayBuffer = await apiClient.downloadArtifact(
52
- taskId,
53
- runId,
54
- archiveUrl,
55
- );
56
- if (!arrayBuffer) {
57
- throw new Error("Failed to download archive");
58
- }
59
- const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
60
- const binaryContent = Buffer.from(base64Content, "base64");
61
- await writeFile(archivePath, binaryContent);
62
- this.log.info("Tree archive downloaded", {
63
- treeHash: snapshot.treeHash,
64
- snapshotBytes: binaryContent.byteLength,
65
- snapshotWireBytes: arrayBuffer.byteLength,
66
- totalBytes: binaryContent.byteLength,
67
- totalWireBytes: arrayBuffer.byteLength,
68
- });
69
- },
70
- rollback: async () => {
71
- if (this.archivePath) {
72
- await rm(this.archivePath, { force: true }).catch(() => {});
73
- }
74
- },
75
- });
47
+ const archivePath = join(tmpDir, `${snapshot.treeHash}.tar.gz`);
48
+ this.archivePath = archivePath;
49
+ await this.step({
50
+ name: "download_archive",
51
+ execute: async () => {
52
+ const arrayBuffer = await apiClient.downloadArtifact(
53
+ taskId,
54
+ runId,
55
+ archiveUrl,
56
+ );
57
+ if (!arrayBuffer) {
58
+ throw new Error("Failed to download archive");
59
+ }
60
+ const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
61
+ const binaryContent = Buffer.from(base64Content, "base64");
62
+ await writeFile(archivePath, binaryContent);
63
+ this.log.info("Tree archive downloaded", {
64
+ treeHash: snapshot.treeHash,
65
+ snapshotBytes: binaryContent.byteLength,
66
+ snapshotWireBytes: arrayBuffer.byteLength,
67
+ totalBytes: binaryContent.byteLength,
68
+ totalWireBytes: arrayBuffer.byteLength,
69
+ });
70
+ },
71
+ rollback: async () => {
72
+ if (this.archivePath) {
73
+ await rm(this.archivePath, { force: true }).catch(() => {});
74
+ }
75
+ },
76
+ });
76
77
 
77
- const gitApplySaga = new GitApplyTreeSaga(this.log);
78
- const applyResult = await gitApplySaga.run({
79
- baseDir: repositoryPath,
80
- treeHash: snapshot.treeHash,
81
- baseCommit: snapshot.baseCommit,
82
- changes: snapshot.changes,
83
- archivePath: this.archivePath,
84
- });
78
+ const gitApplySaga = new GitApplyTreeSaga(this.log);
79
+ const applyResult = await gitApplySaga.run({
80
+ baseDir: repositoryPath,
81
+ treeHash: snapshot.treeHash,
82
+ baseCommit: snapshot.baseCommit,
83
+ changes: snapshot.changes,
84
+ archivePath: this.archivePath,
85
+ });
85
86
 
86
- if (!applyResult.success) {
87
- throw new Error(`Failed to apply tree: ${applyResult.error}`);
88
- }
87
+ if (!applyResult.success) {
88
+ throw new Error(`Failed to apply tree: ${applyResult.error}`);
89
+ }
89
90
 
90
- await rm(this.archivePath, { force: true }).catch(() => {});
91
+ this.log.info("Tree snapshot applied", {
92
+ treeHash: snapshot.treeHash,
93
+ totalChanges: snapshot.changes.length,
94
+ deletedFiles: snapshot.changes.filter((c) => c.status === "D").length,
95
+ });
91
96
 
92
- this.log.info("Tree snapshot applied", {
93
- treeHash: snapshot.treeHash,
94
- totalChanges: snapshot.changes.length,
95
- deletedFiles: snapshot.changes.filter((c) => c.status === "D").length,
96
- });
97
+ return { treeHash: snapshot.treeHash };
98
+ } finally {
99
+ if (this.archivePath) {
100
+ await rm(this.archivePath, { force: true }).catch(() => {});
101
+ }
102
+ await this.removeTmpDirIfEmpty(tmpDir);
103
+ this.archivePath = null;
104
+ }
105
+ }
97
106
 
98
- return { treeHash: snapshot.treeHash };
107
+ private async removeTmpDirIfEmpty(tmpDir: string): Promise<void> {
108
+ const entries = await readdir(tmpDir).catch(() => null);
109
+ if (!entries || entries.length > 0) {
110
+ return;
111
+ }
112
+ await rmdir(tmpDir).catch(() => {});
99
113
  }
100
114
  }
@@ -366,6 +366,24 @@ describe("CaptureTreeSaga", () => {
366
366
  const indexFiles = files.filter((f: string) => f.startsWith("index-"));
367
367
  expect(indexFiles).toHaveLength(0);
368
368
  });
369
+
370
+ it("cleans up uploaded tree archive and tmp dir on success", async () => {
371
+ const mockApiClient = createMockApiClient();
372
+
373
+ await repo.writeFile("new.ts", "content");
374
+
375
+ const saga = new CaptureTreeSaga(mockLogger);
376
+ const result = await saga.run({
377
+ repositoryPath: repo.path,
378
+ taskId: "task-1",
379
+ runId: "run-1",
380
+ lastTreeHash: null,
381
+ apiClient: mockApiClient,
382
+ });
383
+
384
+ expect(result.success).toBe(true);
385
+ expect(repo.exists(".posthog/tmp")).toBe(false);
386
+ });
369
387
  });
370
388
 
371
389
  describe("git state isolation", () => {
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readFile, rm } from "node:fs/promises";
2
+ import { readdir, readFile, rm, rmdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { CaptureTreeSaga as GitCaptureTreeSaga } from "@posthog/git/sagas/tree";
5
5
  import { Saga } from "@posthog/shared";
@@ -45,60 +45,67 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
45
45
  ? join(tmpDir, `tree-${Date.now()}.tar.gz`)
46
46
  : undefined;
47
47
 
48
- const gitCaptureSaga = new GitCaptureTreeSaga(this.log);
49
- const captureResult = await gitCaptureSaga.run({
50
- baseDir: repositoryPath,
51
- lastTreeHash,
52
- archivePath,
53
- });
48
+ try {
49
+ const gitCaptureSaga = new GitCaptureTreeSaga(this.log);
50
+ const captureResult = await gitCaptureSaga.run({
51
+ baseDir: repositoryPath,
52
+ lastTreeHash,
53
+ archivePath,
54
+ });
54
55
 
55
- if (!captureResult.success) {
56
- throw new Error(`Failed to capture tree: ${captureResult.error}`);
57
- }
56
+ if (!captureResult.success) {
57
+ throw new Error(`Failed to capture tree: ${captureResult.error}`);
58
+ }
58
59
 
59
- const {
60
- snapshot: gitSnapshot,
61
- archivePath: createdArchivePath,
62
- changed,
63
- } = captureResult.data;
64
-
65
- if (!changed || !gitSnapshot) {
66
- this.log.debug("No changes since last capture", { lastTreeHash });
67
- return { snapshot: null, newTreeHash: lastTreeHash };
68
- }
60
+ const {
61
+ snapshot: gitSnapshot,
62
+ archivePath: createdArchivePath,
63
+ changed,
64
+ } = captureResult.data;
69
65
 
70
- let archiveUrl: string | undefined;
71
- if (apiClient && createdArchivePath) {
72
- try {
73
- archiveUrl = await this.uploadArchive(
74
- createdArchivePath,
75
- gitSnapshot.treeHash,
76
- apiClient,
77
- taskId,
78
- runId,
79
- );
80
- } finally {
81
- await rm(createdArchivePath, { force: true }).catch(() => {});
66
+ if (!changed || !gitSnapshot) {
67
+ this.log.debug("No changes since last capture", { lastTreeHash });
68
+ return { snapshot: null, newTreeHash: lastTreeHash };
82
69
  }
83
- }
84
70
 
85
- const snapshot: TreeSnapshot = {
86
- treeHash: gitSnapshot.treeHash,
87
- baseCommit: gitSnapshot.baseCommit,
88
- changes: gitSnapshot.changes,
89
- timestamp: gitSnapshot.timestamp,
90
- interrupted,
91
- archiveUrl,
92
- };
93
-
94
- this.log.info("Tree captured", {
95
- treeHash: snapshot.treeHash,
96
- changes: snapshot.changes.length,
97
- interrupted,
98
- archiveUrl,
99
- });
71
+ let archiveUrl: string | undefined;
72
+ if (apiClient && createdArchivePath) {
73
+ try {
74
+ archiveUrl = await this.uploadArchive(
75
+ createdArchivePath,
76
+ gitSnapshot.treeHash,
77
+ apiClient,
78
+ taskId,
79
+ runId,
80
+ );
81
+ } finally {
82
+ await rm(createdArchivePath, { force: true }).catch(() => {});
83
+ }
84
+ }
100
85
 
101
- return { snapshot, newTreeHash: snapshot.treeHash };
86
+ const snapshot: TreeSnapshot = {
87
+ treeHash: gitSnapshot.treeHash,
88
+ baseCommit: gitSnapshot.baseCommit,
89
+ changes: gitSnapshot.changes,
90
+ timestamp: gitSnapshot.timestamp,
91
+ interrupted,
92
+ archiveUrl,
93
+ };
94
+
95
+ this.log.info("Tree captured", {
96
+ treeHash: snapshot.treeHash,
97
+ changes: snapshot.changes.length,
98
+ interrupted,
99
+ archiveUrl,
100
+ });
101
+
102
+ return { snapshot, newTreeHash: snapshot.treeHash };
103
+ } finally {
104
+ if (archivePath) {
105
+ await rm(archivePath, { force: true }).catch(() => {});
106
+ }
107
+ await this.removeTmpDirIfEmpty(tmpDir);
108
+ }
102
109
  }
103
110
 
104
111
  private async uploadArchive(
@@ -147,4 +154,12 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
147
154
 
148
155
  return archiveUrl;
149
156
  }
157
+
158
+ private async removeTmpDirIfEmpty(tmpDir: string): Promise<void> {
159
+ const entries = await readdir(tmpDir).catch(() => null);
160
+ if (!entries || entries.length > 0) {
161
+ return;
162
+ }
163
+ await rmdir(tmpDir).catch(() => {});
164
+ }
150
165
  }