@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.
Files changed (40) hide show
  1. package/README.md +11 -14
  2. package/dist/agent.js +1 -7
  3. package/dist/agent.js.map +1 -1
  4. package/dist/handoff-checkpoint.d.ts +0 -2
  5. package/dist/handoff-checkpoint.js +38 -53
  6. package/dist/handoff-checkpoint.js.map +1 -1
  7. package/dist/index.d.ts +0 -2
  8. package/dist/index.js +0 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/posthog-api.js +1 -5
  11. package/dist/posthog-api.js.map +1 -1
  12. package/dist/resume.d.ts +5 -6
  13. package/dist/resume.js +2 -41
  14. package/dist/resume.js.map +1 -1
  15. package/dist/server/agent-server.d.ts +1 -2
  16. package/dist/server/agent-server.js +103 -768
  17. package/dist/server/agent-server.js.map +1 -1
  18. package/dist/server/bin.cjs +101 -766
  19. package/dist/server/bin.cjs.map +1 -1
  20. package/dist/types.d.ts +2 -13
  21. package/dist/types.js.map +1 -1
  22. package/package.json +3 -7
  23. package/src/acp-extensions.ts +0 -3
  24. package/src/handoff-checkpoint.test.ts +3 -17
  25. package/src/handoff-checkpoint.ts +15 -45
  26. package/src/resume.ts +5 -11
  27. package/src/sagas/resume-saga.test.ts +27 -77
  28. package/src/sagas/resume-saga.ts +3 -44
  29. package/src/sagas/test-fixtures.ts +17 -76
  30. package/src/server/agent-server.ts +22 -103
  31. package/src/test/fixtures/api.ts +2 -15
  32. package/src/types.ts +0 -16
  33. package/dist/tree-tracker.d.ts +0 -68
  34. package/dist/tree-tracker.js +0 -6431
  35. package/dist/tree-tracker.js.map +0 -1
  36. package/src/sagas/apply-snapshot-saga.test.ts +0 -690
  37. package/src/sagas/apply-snapshot-saga.ts +0 -100
  38. package/src/sagas/capture-tree-saga.test.ts +0 -892
  39. package/src/sagas/capture-tree-saga.ts +0 -150
  40. package/src/tree-tracker.ts +0 -173
@@ -5,11 +5,10 @@ import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import type { SagaLogger } from "@posthog/shared";
8
- import * as tar from "tar";
9
8
  import { vi } from "vitest";
10
9
  import { POSTHOG_NOTIFICATIONS } from "../acp-extensions";
11
10
  import type { PostHogAPIClient } from "../posthog-api";
12
- import type { StoredNotification, TaskRun, TreeSnapshot } from "../types";
11
+ import type { GitCheckpointEvent, StoredNotification, TaskRun } from "../types";
13
12
 
14
13
  const execFileAsync = promisify(execFile);
15
14
 
@@ -128,7 +127,7 @@ export function createMockApiClient(
128
127
  return {
129
128
  uploadTaskArtifacts: vi
130
129
  .fn()
131
- .mockResolvedValue([{ storage_path: "gs://bucket/trees/test.tar.gz" }]),
130
+ .mockResolvedValue([{ storage_path: "gs://bucket/handoff/test.pack" }]),
132
131
  downloadArtifact: vi.fn(),
133
132
  getTaskRun: vi.fn(),
134
133
  fetchTaskRunLogs: vi.fn(),
@@ -136,69 +135,6 @@ export function createMockApiClient(
136
135
  } as unknown as PostHogAPIClient;
137
136
  }
138
137
 
139
- export interface ArchiveFile {
140
- path: string;
141
- content: string;
142
- }
143
-
144
- export interface ArchiveSymlink {
145
- path: string;
146
- target: string;
147
- }
148
-
149
- export async function createArchiveBuffer(
150
- files: Array<ArchiveFile>,
151
- symlinks: Array<ArchiveSymlink> = [],
152
- ): Promise<Buffer> {
153
- const { symlink } = await import("node:fs/promises");
154
- const tmpDir = join(
155
- tmpdir(),
156
- `archive-${Date.now()}-${Math.random().toString(36).slice(2)}`,
157
- );
158
- await mkdir(tmpDir, { recursive: true });
159
-
160
- const filesToArchive =
161
- files.length > 0 ? files : [{ path: ".empty", content: "" }];
162
-
163
- for (const file of filesToArchive) {
164
- const fullPath = join(tmpDir, file.path);
165
- await mkdir(join(fullPath, ".."), { recursive: true });
166
- await writeFile(fullPath, file.content);
167
- }
168
-
169
- const symlinkPaths: string[] = [];
170
- for (const link of symlinks) {
171
- const fullPath = join(tmpDir, link.path);
172
- await mkdir(join(fullPath, ".."), { recursive: true });
173
- await symlink(link.target, fullPath);
174
- symlinkPaths.push(link.path);
175
- }
176
-
177
- const archivePath = join(tmpDir, "archive.tar.gz");
178
- await tar.create({ gzip: true, file: archivePath, cwd: tmpDir }, [
179
- ...filesToArchive.map((f) => f.path),
180
- ...symlinkPaths,
181
- ]);
182
-
183
- const content = await readFile(archivePath);
184
- await rm(tmpDir, { recursive: true, force: true });
185
-
186
- return Buffer.from(content.toString("base64"));
187
- }
188
-
189
- export function createSnapshot(
190
- overrides: Partial<TreeSnapshot> = {},
191
- ): TreeSnapshot {
192
- return {
193
- treeHash: "test-tree-hash",
194
- baseCommit: null,
195
- archiveUrl: "gs://bucket/trees/test.tar.gz",
196
- changes: [],
197
- timestamp: new Date().toISOString(),
198
- ...overrides,
199
- };
200
- }
201
-
202
138
  export function createTaskRun(overrides: Partial<TaskRun> = {}): TaskRun {
203
139
  return {
204
140
  id: "run-1",
@@ -290,17 +226,22 @@ export function createToolResult(
290
226
  });
291
227
  }
292
228
 
293
- export function createTreeSnapshotNotification(
294
- treeHash: string,
295
- archiveUrl?: string,
296
- options: { interrupted?: boolean; device?: { type: "local" | "cloud" } } = {},
229
+ export function createGitCheckpointNotification(
230
+ overrides: Partial<GitCheckpointEvent> = {},
297
231
  ): StoredNotification {
298
- return createNotification(POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT, {
299
- treeHash,
300
- baseCommit: "abc123",
301
- archiveUrl,
302
- changes: [{ path: "file.ts", status: "A" }],
232
+ return createNotification(POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT, {
233
+ checkpointId: "checkpoint-1",
234
+ commit: "commit-1",
235
+ checkpointRef: "refs/posthog-code-checkpoint/checkpoint-1",
236
+ headRef: "refs/posthog-code-handoff/head/checkpoint-1",
237
+ head: "head-1",
238
+ branch: "main",
239
+ indexTree: "index-tree-1",
240
+ worktreeTree: "worktree-tree-1",
303
241
  timestamp: new Date().toISOString(),
304
- ...options,
242
+ upstreamRemote: "origin",
243
+ upstreamMergeRef: "refs/heads/main",
244
+ remoteUrl: "git@github.com:posthog/posthog.git",
245
+ ...overrides,
305
246
  });
306
247
  }
@@ -35,7 +35,6 @@ import {
35
35
  resumeFromLog,
36
36
  } from "../resume";
37
37
  import { SessionLogWriter } from "../session-log-writer";
38
- import { TreeTracker } from "../tree-tracker";
39
38
  import type {
40
39
  AgentMode,
41
40
  DeviceInfo,
@@ -44,7 +43,6 @@ import type {
44
43
  LogLevel,
45
44
  TaskRun,
46
45
  TaskRunArtifact,
47
- TreeSnapshotEvent,
48
46
  } from "../types";
49
47
  import { resourceLink } from "../utils/acp-content";
50
48
  import { AsyncMutex } from "../utils/async-mutex";
@@ -184,7 +182,6 @@ interface ActiveSession {
184
182
  acpSessionId: string;
185
183
  acpConnection: InProcessAcpConnection;
186
184
  clientConnection: ClientSideConnection;
187
- treeTracker: TreeTracker | null;
188
185
  sseController: SseController | null;
189
186
  deviceInfo: DeviceInfo;
190
187
  logWriter: SessionLogWriter;
@@ -494,7 +491,6 @@ export class AgentServer {
494
491
  });
495
492
  this.logger.debug("Resume state loaded", {
496
493
  conversationTurns: this.resumeState.conversation.length,
497
- hasSnapshot: !!this.resumeState.latestSnapshot,
498
494
  hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
499
495
  gitCheckpointBranch:
500
496
  this.resumeState.latestGitCheckpoint?.branch ?? null,
@@ -823,16 +819,6 @@ export class AgentServer {
823
819
  userAgent: `posthog/cloud.hog.dev; version: ${this.config.version ?? packageJson.version}`,
824
820
  });
825
821
 
826
- const treeTracker = this.config.repositoryPath
827
- ? new TreeTracker({
828
- repositoryPath: this.config.repositoryPath,
829
- taskId: payload.task_id,
830
- runId: payload.run_id,
831
- apiClient: posthogAPI,
832
- logger: new Logger({ debug: true, prefix: "[TreeTracker]" }),
833
- })
834
- : null;
835
-
836
822
  const logWriter = new SessionLogWriter({
837
823
  posthogAPI,
838
824
  logger: new Logger({ debug: true, prefix: "[SessionLogWriter]" }),
@@ -948,7 +934,6 @@ export class AgentServer {
948
934
  acpSessionId,
949
935
  acpConnection,
950
936
  clientConnection,
951
- treeTracker,
952
937
  sseController,
953
938
  deviceInfo,
954
939
  logWriter,
@@ -1128,36 +1113,7 @@ export class AgentServer {
1128
1113
  this.resumeState.conversation,
1129
1114
  );
1130
1115
 
1131
- let snapshotApplied = false;
1132
- if (
1133
- this.resumeState.latestSnapshot?.archiveUrl &&
1134
- this.config.repositoryPath &&
1135
- this.posthogAPI
1136
- ) {
1137
- try {
1138
- const treeTracker = new TreeTracker({
1139
- repositoryPath: this.config.repositoryPath,
1140
- taskId: payload.task_id,
1141
- runId: payload.run_id,
1142
- apiClient: this.posthogAPI,
1143
- logger: this.logger.child("TreeTracker"),
1144
- });
1145
- await treeTracker.applyTreeSnapshot(this.resumeState.latestSnapshot);
1146
- treeTracker.setLastTreeHash(this.resumeState.latestSnapshot.treeHash);
1147
- snapshotApplied = true;
1148
- this.logger.info("Tree snapshot applied", {
1149
- treeHash: this.resumeState.latestSnapshot.treeHash,
1150
- changes: this.resumeState.latestSnapshot.changes?.length ?? 0,
1151
- hasArchiveUrl: !!this.resumeState.latestSnapshot.archiveUrl,
1152
- });
1153
- } catch (error) {
1154
- this.logger.warn("Failed to apply tree snapshot", {
1155
- error: error instanceof Error ? error.message : String(error),
1156
- treeHash: this.resumeState.latestSnapshot.treeHash,
1157
- });
1158
- }
1159
- }
1160
-
1116
+ let checkpointApplied = false;
1161
1117
  if (
1162
1118
  this.resumeState.latestGitCheckpoint &&
1163
1119
  this.config.repositoryPath &&
@@ -1174,6 +1130,7 @@ export class AgentServer {
1174
1130
  const metrics = await checkpointTracker.applyFromHandoff(
1175
1131
  this.resumeState.latestGitCheckpoint,
1176
1132
  );
1133
+ checkpointApplied = true;
1177
1134
  this.logger.info("Git checkpoint applied", {
1178
1135
  branch: this.resumeState.latestGitCheckpoint.branch,
1179
1136
  head: this.resumeState.latestGitCheckpoint.head,
@@ -1191,9 +1148,9 @@ export class AgentServer {
1191
1148
 
1192
1149
  const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
1193
1150
 
1194
- const sandboxContext = snapshotApplied
1195
- ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.`
1196
- : `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.`;
1151
+ const sandboxContext = checkpointApplied
1152
+ ? `The workspace environment (all files, packages, and code changes) has been fully restored from the latest checkpoint.`
1153
+ : `The workspace from the previous session was not restored from a checkpoint, so you are starting with a fresh environment. Your conversation history is fully preserved below.`;
1197
1154
 
1198
1155
  let resumePromptBlocks: ContentBlock[];
1199
1156
  if (pendingUserPrompt?.length) {
@@ -1230,7 +1187,7 @@ export class AgentServer {
1230
1187
  conversationTurns: this.resumeState.conversation.length,
1231
1188
  promptLength: promptBlocksToText(resumePromptBlocks).length,
1232
1189
  hasPendingUserMessage: !!pendingUserPrompt?.length,
1233
- snapshotApplied,
1190
+ checkpointApplied,
1234
1191
  hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
1235
1192
  gitCheckpointBranch:
1236
1193
  this.resumeState.latestGitCheckpoint?.branch ?? null,
@@ -1933,7 +1890,8 @@ ${attributionInstructions}
1933
1890
  }
1934
1891
 
1935
1892
  // session/update notifications flow through the tapped stream (like local transport)
1936
- // Only handle tree state capture for file changes here
1893
+ // Capture checkpoints for file-changing tools so cloud resumes restore
1894
+ // from git checkpoints rather than tree snapshots.
1937
1895
  if (params.update?.sessionUpdate === "tool_call_update") {
1938
1896
  const meta = (params.update?._meta as Record<string, unknown>)
1939
1897
  ?.claudeCode as Record<string, unknown> | undefined;
@@ -1943,10 +1901,14 @@ ${attributionInstructions}
1943
1901
  | undefined;
1944
1902
 
1945
1903
  if (
1946
- (toolName === "Write" || toolName === "Edit") &&
1904
+ (toolName === "Write" ||
1905
+ toolName === "Edit" ||
1906
+ toolName === "MultiEdit" ||
1907
+ toolName === "Delete" ||
1908
+ toolName === "Move") &&
1947
1909
  toolResponse?.filePath
1948
1910
  ) {
1949
- await this.captureTreeState();
1911
+ await this.captureCheckpointState();
1950
1912
  }
1951
1913
 
1952
1914
  if (
@@ -2168,15 +2130,9 @@ ${attributionInstructions}
2168
2130
  this.logger.debug("Cleaning up session");
2169
2131
 
2170
2132
  try {
2171
- await this.captureHandoffCheckpoint();
2172
- } catch (error) {
2173
- this.logger.error("Failed to capture handoff checkpoint", error);
2174
- }
2175
-
2176
- try {
2177
- await this.captureTreeState();
2133
+ await this.captureCheckpointState(this.session.pendingHandoffGitState);
2178
2134
  } catch (error) {
2179
- this.logger.error("Failed to capture final tree state", error);
2135
+ this.logger.error("Failed to capture final checkpoint state", error);
2180
2136
  }
2181
2137
 
2182
2138
  try {
@@ -2212,50 +2168,15 @@ ${attributionInstructions}
2212
2168
  this.session = null;
2213
2169
  }
2214
2170
 
2215
- private async captureTreeState(): Promise<void> {
2216
- if (!this.session?.treeTracker) return;
2217
-
2218
- try {
2219
- const snapshot = await this.session.treeTracker.captureTree({});
2220
- if (snapshot) {
2221
- const snapshotWithDevice: TreeSnapshotEvent = {
2222
- ...snapshot,
2223
- device: this.session.deviceInfo,
2224
- };
2225
-
2226
- const notification = {
2227
- jsonrpc: "2.0" as const,
2228
- method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
2229
- params: snapshotWithDevice,
2230
- };
2231
-
2232
- this.broadcastEvent({
2233
- type: "notification",
2234
- timestamp: new Date().toISOString(),
2235
- notification,
2236
- });
2237
-
2238
- // Persist full snapshot (including archiveUrl) so resume can restore files.
2239
- // archiveUrl is a pre-signed S3 URL that expires — if the user resumes
2240
- // after expiry, ApplySnapshotSaga fails gracefully and the agent continues
2241
- // with conversation context but a fresh sandbox (snapshotApplied=false).
2242
- this.session.logWriter.appendRawLine(
2243
- this.session.payload.run_id,
2244
- JSON.stringify(notification),
2245
- );
2246
- }
2247
- } catch (error) {
2248
- this.logger.error("Failed to capture tree state", error);
2249
- }
2250
- }
2251
-
2252
- private async captureHandoffCheckpoint(): Promise<void> {
2253
- if (!this.session?.treeTracker || !this.session.pendingHandoffGitState) {
2171
+ private async captureCheckpointState(
2172
+ localGitState?: HandoffLocalGitState,
2173
+ ): Promise<void> {
2174
+ if (!this.session || !this.config.repositoryPath) {
2254
2175
  return;
2255
2176
  }
2256
2177
  if (!this.posthogAPI) {
2257
2178
  this.logger.warn(
2258
- "Skipping handoff checkpoint capture: PostHog API client is not configured",
2179
+ "Skipping checkpoint capture: PostHog API client is not configured",
2259
2180
  );
2260
2181
  return;
2261
2182
  }
@@ -2268,9 +2189,7 @@ ${attributionInstructions}
2268
2189
  logger: this.logger.child("HandoffCheckpoint"),
2269
2190
  });
2270
2191
 
2271
- const checkpoint = await tracker.captureForHandoff(
2272
- this.session.pendingHandoffGitState,
2273
- );
2192
+ const checkpoint = await tracker.captureForHandoff(localGitState);
2274
2193
  if (!checkpoint) return;
2275
2194
 
2276
2195
  const checkpointWithDevice: GitCheckpointEvent = {
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { vi } from "vitest";
8
8
  import type { PostHogAPIClient } from "../../posthog-api";
9
- import type { TaskRun, TreeSnapshot } from "../../types";
9
+ import type { TaskRun } from "../../types";
10
10
 
11
11
  const execFileAsync = promisify(execFile);
12
12
 
@@ -70,7 +70,7 @@ export function createMockApiClient(
70
70
  return {
71
71
  uploadTaskArtifacts: vi
72
72
  .fn()
73
- .mockResolvedValue([{ storage_path: "gs://bucket/trees/test.tar.gz" }]),
73
+ .mockResolvedValue([{ storage_path: "gs://bucket/handoff/test.pack" }]),
74
74
  downloadArtifact: vi.fn(),
75
75
  getTaskRun: vi.fn(),
76
76
  fetchTaskRunLogs: vi.fn(),
@@ -97,16 +97,3 @@ export function createTaskRun(overrides: Partial<TaskRun> = {}): TaskRun {
97
97
  ...overrides,
98
98
  };
99
99
  }
100
-
101
- export function createSnapshot(
102
- overrides: Partial<TreeSnapshot> = {},
103
- ): TreeSnapshot {
104
- return {
105
- treeHash: "test-tree-hash",
106
- baseCommit: null,
107
- archiveUrl: "gs://bucket/trees/test.tar.gz",
108
- changes: [],
109
- timestamp: new Date().toISOString(),
110
- ...overrides,
111
- };
112
- }
package/src/types.ts CHANGED
@@ -61,7 +61,6 @@ export type ArtifactType =
61
61
  | "reference"
62
62
  | "output"
63
63
  | "artifact"
64
- | "tree_snapshot"
65
64
  | "user_attachment";
66
65
 
67
66
  export interface TaskRunArtifact {
@@ -187,21 +186,6 @@ export interface FileChange {
187
186
  status: FileStatus;
188
187
  }
189
188
 
190
- // Tree snapshot - what TreeTracker captures
191
- export interface TreeSnapshot {
192
- treeHash: string;
193
- baseCommit: string | null;
194
- archiveUrl?: string;
195
- changes: FileChange[];
196
- timestamp: string;
197
- interrupted?: boolean;
198
- }
199
-
200
- // Tree snapshot event - includes device info when sent as notification
201
- export interface TreeSnapshotEvent extends TreeSnapshot {
202
- device?: DeviceInfo;
203
- }
204
-
205
189
  export type HandoffLocalGitState = GitHandoffLocalGitState;
206
190
 
207
191
  export interface GitCheckpoint extends GitHandoffCheckpoint {
@@ -1,68 +0,0 @@
1
- import { PostHogAPIClient } from './posthog-api.js';
2
- import { TreeSnapshot } from './types.js';
3
- import { L as Logger } from './logger-RC7sPv0S.js';
4
- import '@posthog/git/handoff';
5
-
6
- /**
7
- * TreeTracker - Git tree-based state capture for cloud/local sync
8
- *
9
- * Captures the entire working state as a git tree hash + archive:
10
- * - Atomic state snapshots (no partial syncs)
11
- * - Efficient delta detection using git's diffing
12
- * - Simpler resume logic (restore tree, continue)
13
- *
14
- * Uses Saga pattern for atomic operations with automatic rollback on failure.
15
- * Uses a temporary git index to avoid modifying the user's staging area.
16
- */
17
-
18
- interface TreeTrackerConfig {
19
- repositoryPath: string;
20
- taskId: string;
21
- runId: string;
22
- apiClient?: PostHogAPIClient;
23
- logger?: Logger;
24
- }
25
- declare class TreeTracker {
26
- private repositoryPath;
27
- private taskId;
28
- private runId;
29
- private apiClient?;
30
- private logger;
31
- private lastTreeHash;
32
- constructor(config: TreeTrackerConfig);
33
- /**
34
- * Capture current working tree state as a snapshot.
35
- * Uses a temporary index to avoid modifying user's staging area.
36
- * Uses Saga pattern for atomic operation with automatic cleanup on failure.
37
- */
38
- captureTree(options?: {
39
- interrupted?: boolean;
40
- }): Promise<TreeSnapshot | null>;
41
- /**
42
- * Download and apply a tree snapshot.
43
- * Uses Saga pattern for atomic operation with rollback on failure.
44
- */
45
- applyTreeSnapshot(snapshot: TreeSnapshot): Promise<void>;
46
- /**
47
- * Get the last captured tree hash.
48
- */
49
- getLastTreeHash(): string | null;
50
- /**
51
- * Set the last tree hash (used when resuming).
52
- */
53
- setLastTreeHash(hash: string | null): void;
54
- }
55
- /**
56
- * Check if a commit is available on any remote branch.
57
- * Used to validate that cloud can fetch the base commit during handoff.
58
- */
59
- declare function isCommitOnRemote(commit: string, cwd: string): Promise<boolean>;
60
- /**
61
- * Validate that a snapshot can be handed off to cloud execution.
62
- * Cloud needs to be able to fetch the baseCommit from a remote.
63
- *
64
- * @throws Error if the snapshot cannot be restored on cloud
65
- */
66
- declare function validateForCloudHandoff(snapshot: TreeSnapshot, repositoryPath: string): Promise<void>;
67
-
68
- export { TreeSnapshot, TreeTracker, type TreeTrackerConfig, isCommitOnRemote, validateForCloudHandoff };