@posthog/agent 2.3.398 → 2.3.403
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/README.md +11 -14
- package/dist/agent.js +1 -7
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +0 -2
- package/dist/handoff-checkpoint.js +38 -53
- package/dist/handoff-checkpoint.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/posthog-api.js +1 -5
- package/dist/posthog-api.js.map +1 -1
- package/dist/resume.d.ts +5 -6
- package/dist/resume.js +2 -41
- package/dist/resume.js.map +1 -1
- package/dist/server/agent-server.d.ts +1 -2
- package/dist/server/agent-server.js +103 -768
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +101 -766
- package/dist/server/bin.cjs.map +1 -1
- package/dist/types.d.ts +2 -13
- package/dist/types.js.map +1 -1
- package/package.json +3 -7
- package/src/acp-extensions.ts +0 -3
- package/src/handoff-checkpoint.test.ts +3 -17
- package/src/handoff-checkpoint.ts +15 -45
- package/src/resume.ts +5 -11
- package/src/sagas/resume-saga.test.ts +27 -77
- package/src/sagas/resume-saga.ts +3 -44
- package/src/sagas/test-fixtures.ts +17 -76
- package/src/server/agent-server.ts +22 -103
- package/src/test/fixtures/api.ts +2 -15
- package/src/types.ts +0 -16
- package/dist/tree-tracker.d.ts +0 -68
- package/dist/tree-tracker.js +0 -6431
- package/dist/tree-tracker.js.map +0 -1
- package/src/sagas/apply-snapshot-saga.test.ts +0 -690
- package/src/sagas/apply-snapshot-saga.ts +0 -100
- package/src/sagas/capture-tree-saga.test.ts +0 -892
- package/src/sagas/capture-tree-saga.ts +0 -150
- package/src/tree-tracker.ts +0 -173
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile, rm } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { CaptureTreeSaga as GitCaptureTreeSaga } from "@posthog/git/sagas/tree";
|
|
5
|
-
import { Saga } from "@posthog/shared";
|
|
6
|
-
import type { PostHogAPIClient } from "../posthog-api";
|
|
7
|
-
import type { TreeSnapshot } from "../types";
|
|
8
|
-
|
|
9
|
-
export interface CaptureTreeInput {
|
|
10
|
-
repositoryPath: string;
|
|
11
|
-
taskId: string;
|
|
12
|
-
runId: string;
|
|
13
|
-
apiClient?: PostHogAPIClient;
|
|
14
|
-
lastTreeHash: string | null;
|
|
15
|
-
interrupted?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface CaptureTreeOutput {
|
|
19
|
-
snapshot: TreeSnapshot | null;
|
|
20
|
-
newTreeHash: string | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
|
|
24
|
-
readonly sagaName = "CaptureTreeSaga";
|
|
25
|
-
|
|
26
|
-
protected async execute(input: CaptureTreeInput): Promise<CaptureTreeOutput> {
|
|
27
|
-
const {
|
|
28
|
-
repositoryPath,
|
|
29
|
-
lastTreeHash,
|
|
30
|
-
interrupted,
|
|
31
|
-
apiClient,
|
|
32
|
-
taskId,
|
|
33
|
-
runId,
|
|
34
|
-
} = input;
|
|
35
|
-
const tmpDir = join(repositoryPath, ".posthog", "tmp");
|
|
36
|
-
|
|
37
|
-
if (existsSync(join(repositoryPath, ".gitmodules"))) {
|
|
38
|
-
this.log.warn(
|
|
39
|
-
"Repository has submodules - snapshot may not capture submodule state",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const shouldArchive = !!apiClient;
|
|
44
|
-
const archivePath = shouldArchive
|
|
45
|
-
? join(tmpDir, `tree-${Date.now()}.tar.gz`)
|
|
46
|
-
: undefined;
|
|
47
|
-
|
|
48
|
-
const gitCaptureSaga = new GitCaptureTreeSaga(this.log);
|
|
49
|
-
const captureResult = await gitCaptureSaga.run({
|
|
50
|
-
baseDir: repositoryPath,
|
|
51
|
-
lastTreeHash,
|
|
52
|
-
archivePath,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
if (!captureResult.success) {
|
|
56
|
-
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
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
|
-
}
|
|
69
|
-
|
|
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(() => {});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
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
|
-
});
|
|
100
|
-
|
|
101
|
-
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private async uploadArchive(
|
|
105
|
-
archivePath: string,
|
|
106
|
-
treeHash: string,
|
|
107
|
-
apiClient: PostHogAPIClient,
|
|
108
|
-
taskId: string,
|
|
109
|
-
runId: string,
|
|
110
|
-
): Promise<string | undefined> {
|
|
111
|
-
const archiveUrl = await this.step({
|
|
112
|
-
name: "upload_archive",
|
|
113
|
-
execute: async () => {
|
|
114
|
-
const archiveContent = await readFile(archivePath);
|
|
115
|
-
const base64Content = archiveContent.toString("base64");
|
|
116
|
-
const snapshotBytes = archiveContent.byteLength;
|
|
117
|
-
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
118
|
-
|
|
119
|
-
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
120
|
-
{
|
|
121
|
-
name: `trees/${treeHash}.tar.gz`,
|
|
122
|
-
type: "tree_snapshot",
|
|
123
|
-
content: base64Content,
|
|
124
|
-
content_type: "application/gzip",
|
|
125
|
-
},
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
const uploadedArtifact = artifacts[0];
|
|
129
|
-
if (uploadedArtifact?.storage_path) {
|
|
130
|
-
this.log.info("Tree archive uploaded", {
|
|
131
|
-
storagePath: uploadedArtifact.storage_path,
|
|
132
|
-
treeHash,
|
|
133
|
-
snapshotBytes,
|
|
134
|
-
snapshotWireBytes,
|
|
135
|
-
totalBytes: snapshotBytes,
|
|
136
|
-
totalWireBytes: snapshotWireBytes,
|
|
137
|
-
});
|
|
138
|
-
return uploadedArtifact.storage_path;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return undefined;
|
|
142
|
-
},
|
|
143
|
-
rollback: async () => {
|
|
144
|
-
await rm(archivePath, { force: true }).catch(() => {});
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
return archiveUrl;
|
|
149
|
-
}
|
|
150
|
-
}
|
package/src/tree-tracker.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TreeTracker - Git tree-based state capture for cloud/local sync
|
|
3
|
-
*
|
|
4
|
-
* Captures the entire working state as a git tree hash + archive:
|
|
5
|
-
* - Atomic state snapshots (no partial syncs)
|
|
6
|
-
* - Efficient delta detection using git's diffing
|
|
7
|
-
* - Simpler resume logic (restore tree, continue)
|
|
8
|
-
*
|
|
9
|
-
* Uses Saga pattern for atomic operations with automatic rollback on failure.
|
|
10
|
-
* Uses a temporary git index to avoid modifying the user's staging area.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { isCommitOnRemote as gitIsCommitOnRemote } from "@posthog/git/queries";
|
|
14
|
-
import type { PostHogAPIClient } from "./posthog-api";
|
|
15
|
-
import { ApplySnapshotSaga } from "./sagas/apply-snapshot-saga";
|
|
16
|
-
import { CaptureTreeSaga } from "./sagas/capture-tree-saga";
|
|
17
|
-
import type { TreeSnapshot } from "./types";
|
|
18
|
-
import { Logger } from "./utils/logger";
|
|
19
|
-
|
|
20
|
-
export type { TreeSnapshot };
|
|
21
|
-
|
|
22
|
-
export interface TreeTrackerConfig {
|
|
23
|
-
repositoryPath: string;
|
|
24
|
-
taskId: string;
|
|
25
|
-
runId: string;
|
|
26
|
-
apiClient?: PostHogAPIClient;
|
|
27
|
-
logger?: Logger;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class TreeTracker {
|
|
31
|
-
private repositoryPath: string;
|
|
32
|
-
private taskId: string;
|
|
33
|
-
private runId: string;
|
|
34
|
-
private apiClient?: PostHogAPIClient;
|
|
35
|
-
private logger: Logger;
|
|
36
|
-
private lastTreeHash: string | null = null;
|
|
37
|
-
|
|
38
|
-
constructor(config: TreeTrackerConfig) {
|
|
39
|
-
this.repositoryPath = config.repositoryPath;
|
|
40
|
-
this.taskId = config.taskId;
|
|
41
|
-
this.runId = config.runId;
|
|
42
|
-
this.apiClient = config.apiClient;
|
|
43
|
-
this.logger =
|
|
44
|
-
config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Capture current working tree state as a snapshot.
|
|
49
|
-
* Uses a temporary index to avoid modifying user's staging area.
|
|
50
|
-
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
51
|
-
*/
|
|
52
|
-
async captureTree(options?: {
|
|
53
|
-
interrupted?: boolean;
|
|
54
|
-
}): Promise<TreeSnapshot | null> {
|
|
55
|
-
const saga = new CaptureTreeSaga(this.logger);
|
|
56
|
-
|
|
57
|
-
const result = await saga.run({
|
|
58
|
-
repositoryPath: this.repositoryPath,
|
|
59
|
-
taskId: this.taskId,
|
|
60
|
-
runId: this.runId,
|
|
61
|
-
apiClient: this.apiClient,
|
|
62
|
-
lastTreeHash: this.lastTreeHash,
|
|
63
|
-
interrupted: options?.interrupted,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
if (!result.success) {
|
|
67
|
-
this.logger.error("Failed to capture tree", {
|
|
68
|
-
error: result.error,
|
|
69
|
-
failedStep: result.failedStep,
|
|
70
|
-
});
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Failed to capture tree at step '${result.failedStep}': ${result.error}`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Only update lastTreeHash on success
|
|
77
|
-
if (result.data.newTreeHash !== null) {
|
|
78
|
-
this.lastTreeHash = result.data.newTreeHash;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return result.data.snapshot;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Download and apply a tree snapshot.
|
|
86
|
-
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
87
|
-
*/
|
|
88
|
-
async applyTreeSnapshot(snapshot: TreeSnapshot): Promise<void> {
|
|
89
|
-
if (!this.apiClient) {
|
|
90
|
-
throw new Error("Cannot apply snapshot: API client not configured");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!snapshot.archiveUrl) {
|
|
94
|
-
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
95
|
-
treeHash: snapshot.treeHash,
|
|
96
|
-
changes: snapshot.changes.length,
|
|
97
|
-
});
|
|
98
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const saga = new ApplySnapshotSaga(this.logger);
|
|
102
|
-
|
|
103
|
-
const result = await saga.run({
|
|
104
|
-
snapshot,
|
|
105
|
-
repositoryPath: this.repositoryPath,
|
|
106
|
-
apiClient: this.apiClient,
|
|
107
|
-
taskId: this.taskId,
|
|
108
|
-
runId: this.runId,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (!result.success) {
|
|
112
|
-
this.logger.error("Failed to apply tree snapshot", {
|
|
113
|
-
error: result.error,
|
|
114
|
-
failedStep: result.failedStep,
|
|
115
|
-
treeHash: snapshot.treeHash,
|
|
116
|
-
});
|
|
117
|
-
throw new Error(
|
|
118
|
-
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Only update lastTreeHash on success
|
|
123
|
-
this.lastTreeHash = result.data.treeHash;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get the last captured tree hash.
|
|
128
|
-
*/
|
|
129
|
-
getLastTreeHash(): string | null {
|
|
130
|
-
return this.lastTreeHash;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Set the last tree hash (used when resuming).
|
|
135
|
-
*/
|
|
136
|
-
setLastTreeHash(hash: string | null): void {
|
|
137
|
-
this.lastTreeHash = hash;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Check if a commit is available on any remote branch.
|
|
143
|
-
* Used to validate that cloud can fetch the base commit during handoff.
|
|
144
|
-
*/
|
|
145
|
-
export async function isCommitOnRemote(
|
|
146
|
-
commit: string,
|
|
147
|
-
cwd: string,
|
|
148
|
-
): Promise<boolean> {
|
|
149
|
-
return gitIsCommitOnRemote(cwd, commit);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Validate that a snapshot can be handed off to cloud execution.
|
|
154
|
-
* Cloud needs to be able to fetch the baseCommit from a remote.
|
|
155
|
-
*
|
|
156
|
-
* @throws Error if the snapshot cannot be restored on cloud
|
|
157
|
-
*/
|
|
158
|
-
export async function validateForCloudHandoff(
|
|
159
|
-
snapshot: TreeSnapshot,
|
|
160
|
-
repositoryPath: string,
|
|
161
|
-
): Promise<void> {
|
|
162
|
-
if (!snapshot.baseCommit) {
|
|
163
|
-
throw new Error("Cannot hand off to cloud: no base commit");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const onRemote = await isCommitOnRemote(snapshot.baseCommit, repositoryPath);
|
|
167
|
-
if (!onRemote) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`Cannot hand off to cloud: commit ${snapshot.baseCommit.slice(0, 7)} is not pushed. ` +
|
|
170
|
-
`Run 'git push' to push your branch first.`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
}
|