@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
package/dist/types.d.ts CHANGED
@@ -41,7 +41,7 @@ interface Task {
41
41
  };
42
42
  latest_run?: TaskRun;
43
43
  }
44
- type ArtifactType = "plan" | "context" | "reference" | "output" | "artifact" | "tree_snapshot" | "user_attachment";
44
+ type ArtifactType = "plan" | "context" | "reference" | "output" | "artifact" | "user_attachment";
45
45
  interface TaskRunArtifact {
46
46
  id?: string;
47
47
  name: string;
@@ -137,17 +137,6 @@ interface FileChange {
137
137
  path: string;
138
138
  status: FileStatus;
139
139
  }
140
- interface TreeSnapshot {
141
- treeHash: string;
142
- baseCommit: string | null;
143
- archiveUrl?: string;
144
- changes: FileChange[];
145
- timestamp: string;
146
- interrupted?: boolean;
147
- }
148
- interface TreeSnapshotEvent extends TreeSnapshot {
149
- device?: DeviceInfo;
150
- }
151
140
  type HandoffLocalGitState = HandoffLocalGitState$1;
152
141
  interface GitCheckpoint extends GitHandoffCheckpoint {
153
142
  artifactPath?: string;
@@ -164,4 +153,4 @@ interface GitCheckpointEvent extends GitCheckpoint {
164
153
  */
165
154
  declare const AGENT_TYPES_MODULE = true;
166
155
 
167
- export { AGENT_TYPES_MODULE, type AgentConfig, type AgentMode, type ArtifactType, type DeviceInfo, type FileChange, type FileStatus, type GitCheckpoint, type GitCheckpointEvent, type HandoffLocalGitState, type LogLevel, type OnLogCallback, type OtelTransportConfig, type PostHogAPIConfig, type ProcessSpawnedCallback, type StoredEntry, type StoredNotification, type Task, type TaskExecutionOptions, type TaskRun, type TaskRunArtifact, type TaskRunEnvironment, type TaskRunStatus, type TreeSnapshot, type TreeSnapshotEvent };
156
+ export { AGENT_TYPES_MODULE, type AgentConfig, type AgentMode, type ArtifactType, type DeviceInfo, type FileChange, type FileStatus, type GitCheckpoint, type GitCheckpointEvent, type HandoffLocalGitState, type LogLevel, type OnLogCallback, type OtelTransportConfig, type PostHogAPIConfig, type ProcessSpawnedCallback, type StoredEntry, type StoredNotification, type Task, type TaskExecutionOptions, type TaskRun, type TaskRunArtifact, type TaskRunEnvironment, type TaskRunStatus };
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type {\n GitHandoffCheckpoint,\n HandoffLocalGitState as GitHandoffLocalGitState,\n} from \"@posthog/git/handoff\";\n\n/**\n * Stored custom notification following ACP extensibility model.\n * Custom notifications use underscore-prefixed methods (e.g., `_posthog/phase_start`).\n * See: https://agentclientprotocol.com/docs/extensibility\n */\nexport interface StoredNotification {\n type: \"notification\";\n /** When this notification was stored */\n timestamp: string;\n /** JSON-RPC 2.0 notification (no id field = notification, not request) */\n notification: {\n jsonrpc: \"2.0\";\n method: string;\n params?: Record<string, unknown>;\n };\n}\n\n/**\n * Type alias for stored log entries.\n */\nexport type StoredEntry = StoredNotification;\n\n// PostHog Task model (matches PostHog Code's OpenAPI schema)\nexport interface Task {\n id: string;\n task_number?: number;\n slug?: string;\n title: string;\n description: string;\n origin_product:\n | \"error_tracking\"\n | \"eval_clusters\"\n | \"user_created\"\n | \"support_queue\"\n | \"session_summaries\";\n github_integration?: number | null;\n repository: string; // Format: \"organization/repository\" (e.g., \"posthog/posthog-js\")\n json_schema?: Record<string, unknown> | null; // JSON schema for task output validation\n created_at: string;\n updated_at: string;\n created_by?: {\n id: number;\n uuid: string;\n distinct_id: string;\n first_name: string;\n email: string;\n };\n latest_run?: TaskRun;\n}\n\n// Log entry structure for TaskRun.log\n\nexport type ArtifactType =\n | \"plan\"\n | \"context\"\n | \"reference\"\n | \"output\"\n | \"artifact\"\n | \"tree_snapshot\"\n | \"user_attachment\";\n\nexport interface TaskRunArtifact {\n id?: string;\n name: string;\n type: ArtifactType;\n source?: string;\n size?: number;\n content_type?: string;\n storage_path?: string;\n uploaded_at?: string;\n}\n\nexport type TaskRunStatus =\n | \"not_started\"\n | \"queued\"\n | \"in_progress\"\n | \"completed\"\n | \"failed\"\n | \"cancelled\";\n\nexport type TaskRunEnvironment = \"local\" | \"cloud\";\n\n// TaskRun model - represents individual execution runs of tasks\nexport interface TaskRun {\n id: string;\n task: string; // Task ID\n team: number;\n branch: string | null;\n stage: string | null; // Current stage (e.g., 'research', 'plan', 'build')\n environment: TaskRunEnvironment;\n status: TaskRunStatus;\n log_url: string;\n error_message: string | null;\n output: Record<string, unknown> | null; // Structured output (PR URL, commit SHA, etc.)\n state: Record<string, unknown>; // Intermediate run state (defaults to {}, never null)\n artifacts?: TaskRunArtifact[];\n created_at: string;\n updated_at: string;\n completed_at: string | null;\n}\n\nexport interface ProcessSpawnedCallback {\n onProcessSpawned?: (info: {\n pid: number;\n command: string;\n sessionId?: string;\n }) => void;\n onProcessExited?: (pid: number) => void;\n onMcpServersReady?: (serverNames: string[]) => void;\n}\n\nexport interface TaskExecutionOptions {\n repositoryPath?: string;\n adapter?: \"claude\" | \"codex\";\n model?: string;\n gatewayUrl?: string;\n codexBinaryPath?: string;\n instructions?: string;\n processCallbacks?: ProcessSpawnedCallback;\n /** Callback invoked when the agent calls the create_output tool for structured output */\n onStructuredOutput?: (output: Record<string, unknown>) => Promise<void>;\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport type OnLogCallback = (\n level: LogLevel,\n scope: string,\n message: string,\n data?: unknown,\n) => void;\n\nexport interface PostHogAPIConfig {\n apiUrl: string;\n getApiKey: () => string | Promise<string>;\n refreshApiKey?: () => string | Promise<string>;\n projectId: number;\n userAgent?: string;\n}\n\nexport interface OtelTransportConfig {\n /** PostHog ingest host, e.g., \"https://us.i.posthog.com\" */\n host: string;\n /** Project API key */\n apiKey: string;\n /** Override the logs endpoint path (default: /i/v1/logs) */\n logsPath?: string;\n}\n\nexport interface AgentConfig {\n posthog?: PostHogAPIConfig;\n /** OTEL transport config for shipping logs to PostHog Logs */\n otelTransport?: OtelTransportConfig;\n /** Skip session log persistence (e.g. for preview sessions with no real task) */\n skipLogPersistence?: boolean;\n /** Local cache path for instant log loading (e.g., ~/.posthog-code) */\n localCachePath?: string;\n /**\n * Annotate files the agent reads with PostHog enrichment (event volume,\n * flag rollout/staleness, experiment links). Defaults to enabled when\n * `posthog` config is present; set `{ enabled: false }` to opt out.\n */\n enricher?: { enabled?: boolean };\n debug?: boolean;\n onLog?: OnLogCallback;\n}\n\n// Device info for tracking where work happens\nexport interface DeviceInfo {\n type: \"local\" | \"cloud\";\n name?: string;\n}\n\n// Agent execution mode - for tracking interactive vs background runs, when backgrounded an agent will continue working without asking questions\nexport type AgentMode = \"interactive\" | \"background\";\n\n// Git file status codes\nexport type FileStatus = \"A\" | \"M\" | \"D\";\n\nexport interface FileChange {\n path: string;\n status: FileStatus;\n}\n\n// Tree snapshot - what TreeTracker captures\nexport interface TreeSnapshot {\n treeHash: string;\n baseCommit: string | null;\n archiveUrl?: string;\n changes: FileChange[];\n timestamp: string;\n interrupted?: boolean;\n}\n\n// Tree snapshot event - includes device info when sent as notification\nexport interface TreeSnapshotEvent extends TreeSnapshot {\n device?: DeviceInfo;\n}\n\nexport type HandoffLocalGitState = GitHandoffLocalGitState;\n\nexport interface GitCheckpoint extends GitHandoffCheckpoint {\n artifactPath?: string;\n indexArtifactPath?: string;\n}\n\nexport interface GitCheckpointEvent extends GitCheckpoint {\n device?: DeviceInfo;\n}\n\n/**\n * Keeps the emitted `@posthog/agent/types` entrypoint as a runtime ESM module.\n *\n * `export {}` is stripped by tsup in this package, which leaves `dist/types.js`\n * empty and breaks downstream type resolution for the exported subpath.\n */\nexport const AGENT_TYPES_MODULE = true;\n"],"mappings":";AA6NO,IAAM,qBAAqB;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type {\n GitHandoffCheckpoint,\n HandoffLocalGitState as GitHandoffLocalGitState,\n} from \"@posthog/git/handoff\";\n\n/**\n * Stored custom notification following ACP extensibility model.\n * Custom notifications use underscore-prefixed methods (e.g., `_posthog/phase_start`).\n * See: https://agentclientprotocol.com/docs/extensibility\n */\nexport interface StoredNotification {\n type: \"notification\";\n /** When this notification was stored */\n timestamp: string;\n /** JSON-RPC 2.0 notification (no id field = notification, not request) */\n notification: {\n jsonrpc: \"2.0\";\n method: string;\n params?: Record<string, unknown>;\n };\n}\n\n/**\n * Type alias for stored log entries.\n */\nexport type StoredEntry = StoredNotification;\n\n// PostHog Task model (matches PostHog Code's OpenAPI schema)\nexport interface Task {\n id: string;\n task_number?: number;\n slug?: string;\n title: string;\n description: string;\n origin_product:\n | \"error_tracking\"\n | \"eval_clusters\"\n | \"user_created\"\n | \"support_queue\"\n | \"session_summaries\";\n github_integration?: number | null;\n repository: string; // Format: \"organization/repository\" (e.g., \"posthog/posthog-js\")\n json_schema?: Record<string, unknown> | null; // JSON schema for task output validation\n created_at: string;\n updated_at: string;\n created_by?: {\n id: number;\n uuid: string;\n distinct_id: string;\n first_name: string;\n email: string;\n };\n latest_run?: TaskRun;\n}\n\n// Log entry structure for TaskRun.log\n\nexport type ArtifactType =\n | \"plan\"\n | \"context\"\n | \"reference\"\n | \"output\"\n | \"artifact\"\n | \"user_attachment\";\n\nexport interface TaskRunArtifact {\n id?: string;\n name: string;\n type: ArtifactType;\n source?: string;\n size?: number;\n content_type?: string;\n storage_path?: string;\n uploaded_at?: string;\n}\n\nexport type TaskRunStatus =\n | \"not_started\"\n | \"queued\"\n | \"in_progress\"\n | \"completed\"\n | \"failed\"\n | \"cancelled\";\n\nexport type TaskRunEnvironment = \"local\" | \"cloud\";\n\n// TaskRun model - represents individual execution runs of tasks\nexport interface TaskRun {\n id: string;\n task: string; // Task ID\n team: number;\n branch: string | null;\n stage: string | null; // Current stage (e.g., 'research', 'plan', 'build')\n environment: TaskRunEnvironment;\n status: TaskRunStatus;\n log_url: string;\n error_message: string | null;\n output: Record<string, unknown> | null; // Structured output (PR URL, commit SHA, etc.)\n state: Record<string, unknown>; // Intermediate run state (defaults to {}, never null)\n artifacts?: TaskRunArtifact[];\n created_at: string;\n updated_at: string;\n completed_at: string | null;\n}\n\nexport interface ProcessSpawnedCallback {\n onProcessSpawned?: (info: {\n pid: number;\n command: string;\n sessionId?: string;\n }) => void;\n onProcessExited?: (pid: number) => void;\n onMcpServersReady?: (serverNames: string[]) => void;\n}\n\nexport interface TaskExecutionOptions {\n repositoryPath?: string;\n adapter?: \"claude\" | \"codex\";\n model?: string;\n gatewayUrl?: string;\n codexBinaryPath?: string;\n instructions?: string;\n processCallbacks?: ProcessSpawnedCallback;\n /** Callback invoked when the agent calls the create_output tool for structured output */\n onStructuredOutput?: (output: Record<string, unknown>) => Promise<void>;\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport type OnLogCallback = (\n level: LogLevel,\n scope: string,\n message: string,\n data?: unknown,\n) => void;\n\nexport interface PostHogAPIConfig {\n apiUrl: string;\n getApiKey: () => string | Promise<string>;\n refreshApiKey?: () => string | Promise<string>;\n projectId: number;\n userAgent?: string;\n}\n\nexport interface OtelTransportConfig {\n /** PostHog ingest host, e.g., \"https://us.i.posthog.com\" */\n host: string;\n /** Project API key */\n apiKey: string;\n /** Override the logs endpoint path (default: /i/v1/logs) */\n logsPath?: string;\n}\n\nexport interface AgentConfig {\n posthog?: PostHogAPIConfig;\n /** OTEL transport config for shipping logs to PostHog Logs */\n otelTransport?: OtelTransportConfig;\n /** Skip session log persistence (e.g. for preview sessions with no real task) */\n skipLogPersistence?: boolean;\n /** Local cache path for instant log loading (e.g., ~/.posthog-code) */\n localCachePath?: string;\n /**\n * Annotate files the agent reads with PostHog enrichment (event volume,\n * flag rollout/staleness, experiment links). Defaults to enabled when\n * `posthog` config is present; set `{ enabled: false }` to opt out.\n */\n enricher?: { enabled?: boolean };\n debug?: boolean;\n onLog?: OnLogCallback;\n}\n\n// Device info for tracking where work happens\nexport interface DeviceInfo {\n type: \"local\" | \"cloud\";\n name?: string;\n}\n\n// Agent execution mode - for tracking interactive vs background runs, when backgrounded an agent will continue working without asking questions\nexport type AgentMode = \"interactive\" | \"background\";\n\n// Git file status codes\nexport type FileStatus = \"A\" | \"M\" | \"D\";\n\nexport interface FileChange {\n path: string;\n status: FileStatus;\n}\n\nexport type HandoffLocalGitState = GitHandoffLocalGitState;\n\nexport interface GitCheckpoint extends GitHandoffCheckpoint {\n artifactPath?: string;\n indexArtifactPath?: string;\n}\n\nexport interface GitCheckpointEvent extends GitCheckpoint {\n device?: DeviceInfo;\n}\n\n/**\n * Keeps the emitted `@posthog/agent/types` entrypoint as a runtime ESM module.\n *\n * `export {}` is stripped by tsup in this package, which leaves `dist/types.js`\n * empty and breaks downstream type resolution for the exported subpath.\n */\nexport const AGENT_TYPES_MODULE = true;\n"],"mappings":";AA6MO,IAAM,qBAAqB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.3.398",
3
+ "version": "2.3.403",
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": {
@@ -68,10 +68,6 @@
68
68
  "types": "./dist/handoff-checkpoint.d.ts",
69
69
  "import": "./dist/handoff-checkpoint.js"
70
70
  },
71
- "./tree-tracker": {
72
- "types": "./dist/tree-tracker.d.ts",
73
- "import": "./dist/tree-tracker.js"
74
- },
75
71
  "./server": {
76
72
  "types": "./dist/server/agent-server.d.ts",
77
73
  "import": "./dist/server/agent-server.js"
@@ -106,9 +102,9 @@
106
102
  "tsx": "^4.20.6",
107
103
  "typescript": "^5.5.0",
108
104
  "vitest": "^2.1.8",
105
+ "@posthog/enricher": "1.0.0",
109
106
  "@posthog/shared": "1.0.0",
110
- "@posthog/git": "1.0.0",
111
- "@posthog/enricher": "1.0.0"
107
+ "@posthog/git": "1.0.0"
112
108
  },
113
109
  "dependencies": {
114
110
  "@agentclientprotocol/sdk": "0.19.0",
@@ -34,9 +34,6 @@ export const POSTHOG_NOTIFICATIONS = {
34
34
  /** Maps taskRunId to agent's sessionId and adapter type (for resumption) */
35
35
  SDK_SESSION: "_posthog/sdk_session",
36
36
 
37
- /** Tree state snapshot captured (git tree hash + file archive) */
38
- TREE_SNAPSHOT: "_posthog/tree_snapshot",
39
-
40
37
  /** Git checkpoint captured for handoff */
41
38
  GIT_CHECKPOINT: "_posthog/git_checkpoint",
42
39
 
@@ -20,8 +20,6 @@ interface HandoffRepos {
20
20
  localGitState: HandoffLocalGitState;
21
21
  }
22
22
 
23
- const WORKTREE_FILES = ["tracked.txt", "unstaged.txt", "untracked.txt"];
24
-
25
23
  function createMockApi(store: BundleStore) {
26
24
  return {
27
25
  uploadTaskArtifacts: async (
@@ -64,7 +62,7 @@ function createBundleStore(): BundleStore {
64
62
  artifacts: {},
65
63
  manifest: [
66
64
  {
67
- storage_path: "gs://bucket/handoff-0-existing-tree_snapshot.tar.gz",
65
+ storage_path: "gs://bucket/handoff-0-existing-checkpoint.pack",
68
66
  },
69
67
  ],
70
68
  };
@@ -128,15 +126,6 @@ async function makeCloudChanges(repo: TestRepo): Promise<void> {
128
126
  await repo.writeFile("untracked.txt", "untracked\n");
129
127
  }
130
128
 
131
- async function mirrorRestoredWorktree(
132
- cloudRepo: TestRepo,
133
- localRepo: TestRepo,
134
- ): Promise<void> {
135
- for (const file of WORKTREE_FILES) {
136
- await localRepo.writeFile(file, await cloudRepo.readFile(file));
137
- }
138
- }
139
-
140
129
  describe("HandoffCheckpointTracker", () => {
141
130
  const cleanups: Array<() => Promise<void>> = [];
142
131
 
@@ -144,7 +133,7 @@ describe("HandoffCheckpointTracker", () => {
144
133
  await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()));
145
134
  });
146
135
 
147
- it("restores head commit and index state for handoff replay", async () => {
136
+ it("restores head, worktree, and index state for handoff replay", async () => {
148
137
  const { cloudRepo, localRepo, branch, localGitState } =
149
138
  await prepareHandoffRepos(cleanups);
150
139
  await makeCloudChanges(cloudRepo);
@@ -162,10 +151,6 @@ describe("HandoffCheckpointTracker", () => {
162
151
  const applyTracker = createTracker(localRepo.path, apiClient);
163
152
  await applyTracker.applyFromHandoff(checkpoint);
164
153
 
165
- // The handoff service restores files separately via tree_snapshot.
166
- // Mirror that here so the restored git index can be validated.
167
- await mirrorRestoredWorktree(cloudRepo, localRepo);
168
-
169
154
  expect(await localRepo.git(["rev-parse", "HEAD"])).toBe(checkpoint.head);
170
155
  expect(await localRepo.git(["rev-parse", "--abbrev-ref", "HEAD"])).toBe(
171
156
  branch,
@@ -179,5 +164,6 @@ describe("HandoffCheckpointTracker", () => {
179
164
  expect(status).toContain("M tracked.txt");
180
165
  expect(status).toContain(" M unstaged.txt");
181
166
  expect(status).toContain("?? untracked.txt");
167
+ expect(localRepo.exists(".posthog/tmp")).toBe(false);
182
168
  });
183
169
  });
@@ -1,5 +1,6 @@
1
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, join } from "node:path";
3
4
  import {
4
5
  type GitHandoffBranchDivergence,
5
6
  type GitHandoffCheckpoint,
@@ -100,8 +101,12 @@ export class HandoffCheckpointTracker {
100
101
  indexArtifactPath: uploads.index?.storagePath,
101
102
  };
102
103
  } finally {
104
+ const tempDir = capture.headPack?.path
105
+ ? dirname(capture.headPack.path)
106
+ : dirname(capture.indexFile.path);
103
107
  await this.removeIfPresent(capture.headPack?.path);
104
108
  await this.removeIfPresent(capture.indexFile.path);
109
+ await rm(tempDir, { recursive: true, force: true }).catch(() => {});
105
110
  }
106
111
  }
107
112
 
@@ -121,8 +126,9 @@ export class HandoffCheckpointTracker {
121
126
  }
122
127
 
123
128
  const gitTracker = this.createGitTracker();
124
- const tmpDir = join(this.repositoryPath, ".posthog", "tmp");
125
- await mkdir(tmpDir, { recursive: true });
129
+ const tmpDir = await mkdtemp(
130
+ join(tmpdir(), `posthog-code-handoff-${checkpoint.checkpointId}-`),
131
+ );
126
132
 
127
133
  const packPath = join(tmpDir, `${checkpoint.checkpointId}.pack`);
128
134
  const indexPath = join(tmpDir, `${checkpoint.checkpointId}.index`);
@@ -161,6 +167,7 @@ export class HandoffCheckpointTracker {
161
167
  } finally {
162
168
  await this.removeIfPresent(packPath);
163
169
  await this.removeIfPresent(indexPath);
170
+ await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
164
171
  }
165
172
  }
166
173
 
@@ -294,52 +301,24 @@ export class HandoffCheckpointTracker {
294
301
  uploads: Uploads,
295
302
  ): void {
296
303
  this.logger.info("Captured handoff checkpoint", {
297
- checkpointId: checkpoint.checkpointId,
298
304
  branch: checkpoint.branch,
299
- head: checkpoint.head,
300
- artifactPath: uploads.pack?.storagePath,
301
- indexArtifactPath: uploads.index?.storagePath,
302
- ...this.buildMetricPayload(uploads),
305
+ head: checkpoint.head?.slice(0, 7),
306
+ totalBytes: this.sumRawBytes(uploads.pack, uploads.index),
303
307
  });
304
308
  }
305
309
 
306
310
  private logApplyMetrics(
307
311
  checkpoint: GitCheckpoint,
308
- downloads: Downloads,
312
+ _downloads: Downloads,
309
313
  totalBytes: number,
310
314
  ): void {
311
315
  this.logger.info("Applied handoff checkpoint", {
312
- checkpointId: checkpoint.checkpointId,
313
- commit: checkpoint.commit,
314
316
  branch: checkpoint.branch,
315
- head: checkpoint.head,
316
- packBytes: downloads.pack?.rawBytes ?? 0,
317
- packWireBytes: downloads.pack?.wireBytes ?? 0,
318
- indexBytes: downloads.index?.rawBytes ?? 0,
319
- indexWireBytes: downloads.index?.wireBytes ?? 0,
317
+ head: checkpoint.head?.slice(0, 7),
320
318
  totalBytes,
321
- totalWireBytes: this.sumWireBytes(downloads.pack, downloads.index),
322
319
  });
323
320
  }
324
321
 
325
- private buildMetricPayload(metrics: ArtifactSlotMap<object>): {
326
- packBytes: number;
327
- packWireBytes: number;
328
- indexBytes: number;
329
- indexWireBytes: number;
330
- totalBytes: number;
331
- totalWireBytes: number;
332
- } {
333
- return {
334
- packBytes: metrics.pack?.rawBytes ?? 0,
335
- packWireBytes: metrics.pack?.wireBytes ?? 0,
336
- indexBytes: metrics.index?.rawBytes ?? 0,
337
- indexWireBytes: metrics.index?.wireBytes ?? 0,
338
- totalBytes: this.sumRawBytes(metrics.pack, metrics.index),
339
- totalWireBytes: this.sumWireBytes(metrics.pack, metrics.index),
340
- };
341
- }
342
-
343
322
  private sumRawBytes(
344
323
  ...artifacts: Array<{ rawBytes: number } | undefined>
345
324
  ): number {
@@ -349,15 +328,6 @@ export class HandoffCheckpointTracker {
349
328
  );
350
329
  }
351
330
 
352
- private sumWireBytes(
353
- ...artifacts: Array<{ wireBytes: number } | undefined>
354
- ): number {
355
- return artifacts.reduce(
356
- (total, artifact) => total + (artifact?.wireBytes ?? 0),
357
- 0,
358
- );
359
- }
360
-
361
331
  private async removeIfPresent(filePath: string | undefined): Promise<void> {
362
332
  if (!filePath) {
363
333
  return;
package/src/resume.ts CHANGED
@@ -3,15 +3,15 @@
3
3
  *
4
4
  * Handles resuming a task from any point:
5
5
  * - Fetches log via the PostHog API
6
- * - Finds latest tree_snapshot event
6
+ * - Finds latest git_checkpoint event
7
7
  * - Rebuilds conversation from log events
8
- * - Restores working tree from snapshot
8
+ * - Restores working tree from checkpoint
9
9
  *
10
10
  * Uses Saga pattern for atomic operations with clear success/failure tracking.
11
11
  *
12
12
  * The log is the single source of truth for:
13
13
  * - Conversation history (user_message, agent_message_chunk, tool_call, tool_result)
14
- * - Working tree state (tree_snapshot events)
14
+ * - Working tree state (git_checkpoint events)
15
15
  * - Session metadata (device info, mode changes)
16
16
  */
17
17
 
@@ -19,16 +19,11 @@ import type { ContentBlock } from "@agentclientprotocol/sdk";
19
19
  import { selectRecentTurns } from "./adapters/claude/session/jsonl-hydration";
20
20
  import type { PostHogAPIClient } from "./posthog-api";
21
21
  import { ResumeSaga } from "./sagas/resume-saga";
22
- import type {
23
- DeviceInfo,
24
- GitCheckpointEvent,
25
- TreeSnapshotEvent,
26
- } from "./types";
22
+ import type { DeviceInfo, GitCheckpointEvent } from "./types";
27
23
  import { Logger } from "./utils/logger";
28
24
 
29
25
  export interface ResumeState {
30
26
  conversation: ConversationTurn[];
31
- latestSnapshot: TreeSnapshotEvent | null;
32
27
  latestGitCheckpoint: GitCheckpointEvent | null;
33
28
  interrupted: boolean;
34
29
  lastDevice?: DeviceInfo;
@@ -59,7 +54,7 @@ export interface ResumeConfig {
59
54
  /**
60
55
  * Resume a task from its persisted log.
61
56
  * Returns the rebuilt state for the agent to continue from.
62
- * Snapshot and checkpoint application happens in the agent server after SSE connects.
57
+ * Checkpoint application happens in the agent server after SSE connects.
63
58
  */
64
59
  export async function resumeFromLog(
65
60
  config: ResumeConfig,
@@ -94,7 +89,6 @@ export async function resumeFromLog(
94
89
 
95
90
  return {
96
91
  conversation: result.data.conversation as ConversationTurn[],
97
- latestSnapshot: result.data.latestSnapshot,
98
92
  latestGitCheckpoint: result.data.latestGitCheckpoint,
99
93
  interrupted: result.data.interrupted,
100
94
  lastDevice: result.data.lastDevice,
@@ -1,19 +1,17 @@
1
1
  import type { SagaLogger } from "@posthog/shared";
2
2
  import { afterEach, beforeEach, describe, expect, it, type vi } from "vitest";
3
- import { POSTHOG_NOTIFICATIONS } from "../acp-extensions";
4
3
  import type { PostHogAPIClient } from "../posthog-api";
5
4
  import { ResumeSaga } from "./resume-saga";
6
5
  import {
7
6
  createAgentChunk,
8
7
  createAgentMessage,
8
+ createGitCheckpointNotification,
9
9
  createMockApiClient,
10
10
  createMockLogger,
11
- createNotification,
12
11
  createTaskRun,
13
12
  createTestRepo,
14
13
  createToolCall,
15
14
  createToolResult,
16
- createTreeSnapshotNotification,
17
15
  createUserMessage,
18
16
  type TestRepo,
19
17
  } from "./test-fixtures";
@@ -50,7 +48,7 @@ describe("ResumeSaga", () => {
50
48
  expect(result.success).toBe(true);
51
49
  if (result.success) {
52
50
  expect(result.data.conversation).toHaveLength(0);
53
- expect(result.data.latestSnapshot).toBeNull();
51
+ expect(result.data.latestGitCheckpoint).toBeNull();
54
52
  expect(result.data.logEntryCount).toBe(0);
55
53
  }
56
54
  });
@@ -412,76 +410,24 @@ describe("ResumeSaga", () => {
412
410
  });
413
411
  });
414
412
 
415
- describe("snapshot finding", () => {
416
- it("finds latest tree snapshot", async () => {
413
+ describe("checkpoint finding", () => {
414
+ it("finds latest git checkpoint", async () => {
417
415
  (mockApiClient.getTaskRun as ReturnType<typeof vi.fn>).mockResolvedValue(
418
416
  createTaskRun(),
419
417
  );
420
418
  (
421
419
  mockApiClient.fetchTaskRunLogs as ReturnType<typeof vi.fn>
422
420
  ).mockResolvedValue([
423
- createTreeSnapshotNotification("hash-1"),
424
- createUserMessage("continue"),
425
- createTreeSnapshotNotification("hash-2", "gs://bucket/hash-2.tar.gz"),
426
- ]);
427
-
428
- const saga = new ResumeSaga(mockLogger);
429
- const result = await saga.run({
430
- taskId: "task-1",
431
- runId: "run-1",
432
- repositoryPath: repo.path,
433
- apiClient: mockApiClient,
434
- });
435
-
436
- expect(result.success).toBe(true);
437
- if (!result.success) return;
438
-
439
- expect(result.data.latestSnapshot?.treeHash).toBe("hash-2");
440
- });
441
-
442
- it("returns interrupted flag from snapshot", async () => {
443
- (mockApiClient.getTaskRun as ReturnType<typeof vi.fn>).mockResolvedValue(
444
- createTaskRun(),
445
- );
446
- (
447
- mockApiClient.fetchTaskRunLogs as ReturnType<typeof vi.fn>
448
- ).mockResolvedValue([
449
- createTreeSnapshotNotification("hash-1", "gs://bucket/file.tar.gz", {
450
- interrupted: true,
421
+ createGitCheckpointNotification({
422
+ checkpointId: "checkpoint-1",
423
+ checkpointRef: "refs/posthog-code-checkpoint/checkpoint-1",
424
+ head: "head-1",
451
425
  }),
452
- ]);
453
-
454
- const saga = new ResumeSaga(mockLogger);
455
- const result = await saga.run({
456
- taskId: "task-1",
457
- runId: "run-1",
458
- repositoryPath: repo.path,
459
- apiClient: mockApiClient,
460
- });
461
-
462
- expect(result.success).toBe(true);
463
- if (!result.success) return;
464
-
465
- expect(result.data.interrupted).toBe(true);
466
- });
467
- });
468
-
469
- describe("snapshot metadata", () => {
470
- it("returns latest snapshot metadata when archive URL present", async () => {
471
- const baseCommit = await repo.git(["rev-parse", "HEAD"]);
472
-
473
- (mockApiClient.getTaskRun as ReturnType<typeof vi.fn>).mockResolvedValue(
474
- createTaskRun(),
475
- );
476
- (
477
- mockApiClient.fetchTaskRunLogs as ReturnType<typeof vi.fn>
478
- ).mockResolvedValue([
479
- createNotification(POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT, {
480
- treeHash: "hash-1",
481
- baseCommit,
482
- archiveUrl: "gs://bucket/hash-1.tar.gz",
483
- changes: [{ path: "restored.ts", status: "A" }],
484
- timestamp: new Date().toISOString(),
426
+ createUserMessage("continue"),
427
+ createGitCheckpointNotification({
428
+ checkpointId: "checkpoint-2",
429
+ checkpointRef: "refs/posthog-code-checkpoint/checkpoint-2",
430
+ head: "head-2",
485
431
  }),
486
432
  ]);
487
433
 
@@ -496,21 +442,22 @@ describe("ResumeSaga", () => {
496
442
  expect(result.success).toBe(true);
497
443
  if (!result.success) return;
498
444
 
499
- expect(result.data.latestSnapshot?.treeHash).toBe("hash-1");
500
- expect(result.data.latestSnapshot?.archiveUrl).toBe(
501
- "gs://bucket/hash-1.tar.gz",
445
+ expect(result.data.latestGitCheckpoint?.checkpointId).toBe(
446
+ "checkpoint-2",
502
447
  );
503
448
  });
504
449
 
505
- it("returns snapshot metadata even when no archive URL", async () => {
450
+ it("does not mark resume as interrupted from checkpoint state", async () => {
506
451
  (mockApiClient.getTaskRun as ReturnType<typeof vi.fn>).mockResolvedValue(
507
452
  createTaskRun(),
508
453
  );
509
454
  (
510
455
  mockApiClient.fetchTaskRunLogs as ReturnType<typeof vi.fn>
511
456
  ).mockResolvedValue([
512
- createTreeSnapshotNotification("hash-1"),
513
- createUserMessage("hello"),
457
+ createGitCheckpointNotification({
458
+ checkpointId: "checkpoint-1",
459
+ checkpointRef: "refs/posthog-code-checkpoint/checkpoint-1",
460
+ }),
514
461
  ]);
515
462
 
516
463
  const saga = new ResumeSaga(mockLogger);
@@ -524,8 +471,7 @@ describe("ResumeSaga", () => {
524
471
  expect(result.success).toBe(true);
525
472
  if (!result.success) return;
526
473
 
527
- expect(result.data.latestSnapshot).not.toBeNull();
528
- expect(result.data.conversation).toHaveLength(1);
474
+ expect(result.data.interrupted).toBe(false);
529
475
  });
530
476
  });
531
477
 
@@ -537,10 +483,14 @@ describe("ResumeSaga", () => {
537
483
  (
538
484
  mockApiClient.fetchTaskRunLogs as ReturnType<typeof vi.fn>
539
485
  ).mockResolvedValue([
540
- createTreeSnapshotNotification("hash-1", undefined, {
486
+ createGitCheckpointNotification({
487
+ checkpointId: "checkpoint-1",
488
+ checkpointRef: "refs/posthog-code-checkpoint/checkpoint-1",
541
489
  device: { type: "local" },
542
490
  }),
543
- createTreeSnapshotNotification("hash-2", undefined, {
491
+ createGitCheckpointNotification({
492
+ checkpointId: "checkpoint-2",
493
+ checkpointRef: "refs/posthog-code-checkpoint/checkpoint-2",
544
494
  device: { type: "cloud" },
545
495
  }),
546
496
  ]);
@@ -1,12 +1,11 @@
1
1
  import type { ContentBlock } from "@agentclientprotocol/sdk";
2
2
  import { Saga } from "@posthog/shared";
3
- import { isNotification, POSTHOG_NOTIFICATIONS } from "../acp-extensions";
3
+ import { POSTHOG_NOTIFICATIONS } from "../acp-extensions";
4
4
  import type { PostHogAPIClient } from "../posthog-api";
5
5
  import type {
6
6
  DeviceInfo,
7
7
  GitCheckpointEvent,
8
8
  StoredNotification,
9
- TreeSnapshotEvent,
10
9
  } from "../types";
11
10
  import type { Logger } from "../utils/logger";
12
11
 
@@ -33,7 +32,6 @@ export interface ResumeInput {
33
32
 
34
33
  export interface ResumeOutput {
35
34
  conversation: ConversationTurn[];
36
- latestSnapshot: TreeSnapshotEvent | null;
37
35
  latestGitCheckpoint: GitCheckpointEvent | null;
38
36
  interrupted: boolean;
39
37
  lastDevice?: DeviceInfo;
@@ -68,25 +66,11 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
68
66
 
69
67
  this.log.info("Fetched log entries", { count: entries.length });
70
68
 
71
- // Step 3: Find latest snapshot (read-only, pure computation)
72
- const latestSnapshot = await this.readOnlyStep("find_snapshot", () =>
73
- Promise.resolve(this.findLatestTreeSnapshot(entries)),
74
- );
75
-
76
69
  const latestGitCheckpoint = await this.readOnlyStep(
77
70
  "find_git_checkpoint",
78
71
  () => Promise.resolve(this.findLatestGitCheckpoint(entries)),
79
72
  );
80
73
 
81
- // Step 4: Apply snapshot if present (wrapped in step for consistent logging)
82
- if (latestSnapshot) {
83
- this.log.info("Found tree snapshot", {
84
- treeHash: latestSnapshot.treeHash,
85
- hasArchiveUrl: !!latestSnapshot.archiveUrl,
86
- changes: latestSnapshot.changes?.length ?? 0,
87
- });
88
- }
89
-
90
74
  if (latestGitCheckpoint) {
91
75
  this.log.info("Found git checkpoint", {
92
76
  checkpointId: latestGitCheckpoint.checkpointId,
@@ -105,16 +89,14 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
105
89
 
106
90
  this.log.info("Resume state rebuilt", {
107
91
  turns: conversation.length,
108
- hasSnapshot: !!latestSnapshot,
109
92
  hasGitCheckpoint: !!latestGitCheckpoint,
110
- interrupted: latestSnapshot?.interrupted ?? false,
93
+ interrupted: false,
111
94
  });
112
95
 
113
96
  return {
114
97
  conversation,
115
- latestSnapshot,
116
98
  latestGitCheckpoint,
117
- interrupted: latestSnapshot?.interrupted ?? false,
99
+ interrupted: false,
118
100
  lastDevice,
119
101
  logEntryCount: entries.length,
120
102
  };
@@ -123,35 +105,12 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
123
105
  private emptyResult(): ResumeOutput {
124
106
  return {
125
107
  conversation: [],
126
- latestSnapshot: null,
127
108
  latestGitCheckpoint: null,
128
109
  interrupted: false,
129
110
  logEntryCount: 0,
130
111
  };
131
112
  }
132
113
 
133
- private findLatestTreeSnapshot(
134
- entries: StoredNotification[],
135
- ): TreeSnapshotEvent | null {
136
- for (let i = entries.length - 1; i >= 0; i--) {
137
- const entry = entries[i];
138
- if (
139
- isNotification(
140
- entry.notification?.method,
141
- POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
142
- )
143
- ) {
144
- const params = entry.notification.params as
145
- | TreeSnapshotEvent
146
- | undefined;
147
- if (params?.treeHash) {
148
- return params;
149
- }
150
- }
151
- }
152
- return null;
153
- }
154
-
155
114
  private findLatestGitCheckpoint(
156
115
  entries: StoredNotification[],
157
116
  ): GitCheckpointEvent | null {