@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/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +1 -0
- package/dist/handoff-checkpoint.js +17 -1
- package/dist/handoff-checkpoint.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +146 -99
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +136 -96
- package/dist/server/bin.cjs.map +1 -1
- package/dist/tree-tracker.js +128 -97
- package/dist/tree-tracker.js.map +1 -1
- package/package.json +3 -3
- package/src/handoff-checkpoint.test.ts +1 -0
- package/src/handoff-checkpoint.ts +17 -1
- package/src/sagas/apply-snapshot-saga.test.ts +1 -0
- package/src/sagas/apply-snapshot-saga.ts +68 -54
- package/src/sagas/capture-tree-saga.test.ts +18 -0
- package/src/sagas/capture-tree-saga.ts +64 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.3.
|
|
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 {
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
if (!applyResult.success) {
|
|
88
|
+
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
89
|
+
}
|
|
89
90
|
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if (!captureResult.success) {
|
|
57
|
+
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
58
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
}
|