@posthog/agent 2.3.386 → 2.3.387

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 (44) hide show
  1. package/dist/adapters/claude/session/jsonl-hydration.d.ts +1 -0
  2. package/dist/agent.d.ts +1 -0
  3. package/dist/agent.js +13 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/handoff-checkpoint.d.ts +39 -0
  6. package/dist/handoff-checkpoint.js +6679 -0
  7. package/dist/handoff-checkpoint.js.map +1 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/posthog-api.d.ts +1 -0
  12. package/dist/posthog-api.js +11 -2
  13. package/dist/posthog-api.js.map +1 -1
  14. package/dist/resume.d.ts +3 -1
  15. package/dist/resume.js +41 -4
  16. package/dist/resume.js.map +1 -1
  17. package/dist/server/agent-server.d.ts +5 -15
  18. package/dist/server/agent-server.js +1330 -394
  19. package/dist/server/agent-server.js.map +1 -1
  20. package/dist/server/bin.cjs +1332 -396
  21. package/dist/server/bin.cjs.map +1 -1
  22. package/dist/server/schemas.d.ts +191 -0
  23. package/dist/server/schemas.js +108 -0
  24. package/dist/server/schemas.js.map +1 -0
  25. package/dist/tree-tracker.d.ts +1 -0
  26. package/dist/tree-tracker.js +18 -4
  27. package/dist/tree-tracker.js.map +1 -1
  28. package/dist/types.d.ts +18 -1
  29. package/dist/types.js +5 -0
  30. package/dist/types.js.map +1 -1
  31. package/package.json +9 -1
  32. package/src/acp-extensions.ts +3 -0
  33. package/src/handoff-checkpoint.test.ts +183 -0
  34. package/src/handoff-checkpoint.ts +361 -0
  35. package/src/posthog-api.test.ts +29 -0
  36. package/src/posthog-api.ts +5 -1
  37. package/src/resume.ts +7 -1
  38. package/src/sagas/apply-snapshot-saga.ts +7 -0
  39. package/src/sagas/capture-tree-saga.ts +10 -3
  40. package/src/sagas/resume-saga.ts +32 -0
  41. package/src/sagas/test-fixtures.ts +46 -0
  42. package/src/server/agent-server.ts +74 -1
  43. package/src/server/schemas.ts +21 -2
  44. package/src/types.ts +24 -0
package/src/resume.ts CHANGED
@@ -19,12 +19,17 @@ 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 { DeviceInfo, TreeSnapshotEvent } from "./types";
22
+ import type {
23
+ DeviceInfo,
24
+ GitCheckpointEvent,
25
+ TreeSnapshotEvent,
26
+ } from "./types";
23
27
  import { Logger } from "./utils/logger";
24
28
 
25
29
  export interface ResumeState {
26
30
  conversation: ConversationTurn[];
27
31
  latestSnapshot: TreeSnapshotEvent | null;
32
+ latestGitCheckpoint: GitCheckpointEvent | null;
28
33
  /** Whether the tree snapshot was successfully applied (files restored) */
29
34
  snapshotApplied: boolean;
30
35
  interrupted: boolean;
@@ -96,6 +101,7 @@ export async function resumeFromLog(
96
101
  return {
97
102
  conversation: result.data.conversation as ConversationTurn[],
98
103
  latestSnapshot: result.data.latestSnapshot,
104
+ latestGitCheckpoint: result.data.latestGitCheckpoint,
99
105
  snapshotApplied: result.data.snapshotApplied,
100
106
  interrupted: result.data.interrupted,
101
107
  lastDevice: result.data.lastDevice,
@@ -59,6 +59,13 @@ export class ApplySnapshotSaga extends Saga<
59
59
  const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
60
60
  const binaryContent = Buffer.from(base64Content, "base64");
61
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
+ });
62
69
  },
63
70
  rollback: async () => {
64
71
  if (this.archivePath) {
@@ -113,6 +113,8 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
113
113
  execute: async () => {
114
114
  const archiveContent = await readFile(archivePath);
115
115
  const base64Content = archiveContent.toString("base64");
116
+ const snapshotBytes = archiveContent.byteLength;
117
+ const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
116
118
 
117
119
  const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
118
120
  {
@@ -123,12 +125,17 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
123
125
  },
124
126
  ]);
125
127
 
126
- if (artifacts.length > 0 && artifacts[0].storage_path) {
128
+ const uploadedArtifact = artifacts[0];
129
+ if (uploadedArtifact?.storage_path) {
127
130
  this.log.info("Tree archive uploaded", {
128
- storagePath: artifacts[0].storage_path,
131
+ storagePath: uploadedArtifact.storage_path,
129
132
  treeHash,
133
+ snapshotBytes,
134
+ snapshotWireBytes,
135
+ totalBytes: snapshotBytes,
136
+ totalWireBytes: snapshotWireBytes,
130
137
  });
131
- return artifacts[0].storage_path;
138
+ return uploadedArtifact.storage_path;
132
139
  }
133
140
 
134
141
  return undefined;
@@ -5,6 +5,7 @@ import type { PostHogAPIClient } from "../posthog-api";
5
5
  import { TreeTracker } from "../tree-tracker";
6
6
  import type {
7
7
  DeviceInfo,
8
+ GitCheckpointEvent,
8
9
  StoredNotification,
9
10
  TreeSnapshotEvent,
10
11
  } from "../types";
@@ -34,6 +35,7 @@ export interface ResumeInput {
34
35
  export interface ResumeOutput {
35
36
  conversation: ConversationTurn[];
36
37
  latestSnapshot: TreeSnapshotEvent | null;
38
+ latestGitCheckpoint: GitCheckpointEvent | null;
37
39
  snapshotApplied: boolean;
38
40
  interrupted: boolean;
39
41
  lastDevice?: DeviceInfo;
@@ -75,6 +77,11 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
75
77
  Promise.resolve(this.findLatestTreeSnapshot(entries)),
76
78
  );
77
79
 
80
+ const latestGitCheckpoint = await this.readOnlyStep(
81
+ "find_git_checkpoint",
82
+ () => Promise.resolve(this.findLatestGitCheckpoint(entries)),
83
+ );
84
+
78
85
  // Step 4: Apply snapshot if present (wrapped in step for consistent logging)
79
86
  // Note: We use a try/catch inside the step because snapshot failure should NOT fail the saga
80
87
  let snapshotApplied = false;
@@ -158,6 +165,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
158
165
  return {
159
166
  conversation,
160
167
  latestSnapshot,
168
+ latestGitCheckpoint,
161
169
  snapshotApplied,
162
170
  interrupted: latestSnapshot?.interrupted ?? false,
163
171
  lastDevice,
@@ -169,6 +177,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
169
177
  return {
170
178
  conversation: [],
171
179
  latestSnapshot: null,
180
+ latestGitCheckpoint: null,
172
181
  snapshotApplied: false,
173
182
  interrupted: false,
174
183
  logEntryCount: 0,
@@ -197,6 +206,29 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
197
206
  return null;
198
207
  }
199
208
 
209
+ private findLatestGitCheckpoint(
210
+ entries: StoredNotification[],
211
+ ): GitCheckpointEvent | null {
212
+ const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
213
+
214
+ for (let i = entries.length - 1; i >= 0; i--) {
215
+ const entry = entries[i];
216
+ const method = entry.notification?.method;
217
+ if (
218
+ method === sdkPrefixedMethod ||
219
+ method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT
220
+ ) {
221
+ const params = entry.notification?.params as
222
+ | GitCheckpointEvent
223
+ | undefined;
224
+ if (params?.checkpointId && params?.checkpointRef) {
225
+ return params;
226
+ }
227
+ }
228
+ }
229
+ return null;
230
+ }
231
+
200
232
  private findLastDeviceInfo(
201
233
  entries: StoredNotification[],
202
234
  ): DeviceInfo | undefined {
@@ -67,6 +67,52 @@ export async function createTestRepo(prefix = "test-repo"): Promise<TestRepo> {
67
67
  };
68
68
  }
69
69
 
70
+ export async function cloneTestRepo(
71
+ sourcePath: string,
72
+ prefix = "test-repo-clone",
73
+ ): Promise<TestRepo> {
74
+ const clonePath = join(
75
+ tmpdir(),
76
+ `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
77
+ );
78
+ await execFileAsync("git", ["clone", sourcePath, clonePath]);
79
+ await execFileAsync("git", ["config", "user.email", "test@test.com"], {
80
+ cwd: clonePath,
81
+ });
82
+ await execFileAsync("git", ["config", "user.name", "Test"], {
83
+ cwd: clonePath,
84
+ });
85
+ await execFileAsync("git", ["config", "commit.gpgsign", "false"], {
86
+ cwd: clonePath,
87
+ });
88
+
89
+ const git = async (args: string[]): Promise<string> => {
90
+ const { stdout } = await execFileAsync("git", args, { cwd: clonePath });
91
+ return stdout.trim();
92
+ };
93
+
94
+ return {
95
+ path: clonePath,
96
+ cleanup: () => rm(clonePath, { recursive: true, force: true }),
97
+ git,
98
+ writeFile: async (relativePath: string, content: string) => {
99
+ const fullPath = join(clonePath, relativePath);
100
+ const dir = join(fullPath, "..");
101
+ await mkdir(dir, { recursive: true });
102
+ await writeFile(fullPath, content);
103
+ },
104
+ readFile: async (relativePath: string) => {
105
+ return readFile(join(clonePath, relativePath), "utf-8");
106
+ },
107
+ deleteFile: async (relativePath: string) => {
108
+ await rm(join(clonePath, relativePath), { force: true });
109
+ },
110
+ exists: (relativePath: string) => {
111
+ return existsSync(join(clonePath, relativePath));
112
+ },
113
+ };
114
+ }
115
+
70
116
  export function createMockLogger(): SagaLogger {
71
117
  return {
72
118
  info: vi.fn(),
@@ -27,6 +27,7 @@ import {
27
27
  } from "../adapters/claude/conversion/sdk-to-acp";
28
28
  import type { PermissionMode } from "../execution-mode";
29
29
  import { DEFAULT_CODEX_MODEL } from "../gateway-models";
30
+ import { HandoffCheckpointTracker } from "../handoff-checkpoint";
30
31
  import { PostHogAPIClient } from "../posthog-api";
31
32
  import {
32
33
  formatConversationForResume,
@@ -38,6 +39,8 @@ import { TreeTracker } from "../tree-tracker";
38
39
  import type {
39
40
  AgentMode,
40
41
  DeviceInfo,
42
+ GitCheckpointEvent,
43
+ HandoffLocalGitState,
41
44
  LogLevel,
42
45
  TaskRun,
43
46
  TaskRunArtifact,
@@ -52,7 +55,11 @@ import {
52
55
  promptBlocksToText,
53
56
  } from "./cloud-prompt";
54
57
  import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt";
55
- import { jsonRpcRequestSchema, validateCommandParams } from "./schemas";
58
+ import {
59
+ handoffLocalGitStateSchema,
60
+ jsonRpcRequestSchema,
61
+ validateCommandParams,
62
+ } from "./schemas";
56
63
  import type { AgentServerConfig } from "./types";
57
64
 
58
65
  const agentErrorClassificationSchema = z.enum([
@@ -185,6 +192,7 @@ interface ActiveSession {
185
192
  permissionMode: PermissionMode;
186
193
  /** Whether a desktop client has ever connected via SSE during this session */
187
194
  hasDesktopConnected: boolean;
195
+ pendingHandoffGitState?: HandoffLocalGitState;
188
196
  }
189
197
 
190
198
  function getTaskRunStateString(
@@ -661,6 +669,10 @@ export class AgentServer {
661
669
  case POSTHOG_NOTIFICATIONS.CLOSE:
662
670
  case "close": {
663
671
  this.logger.debug("Close requested");
672
+ const localGitState = this.extractHandoffLocalGitState(params);
673
+ if (localGitState && this.session) {
674
+ this.session.pendingHandoffGitState = localGitState;
675
+ }
664
676
  await this.cleanupSession();
665
677
  return { closed: true };
666
678
  }
@@ -958,6 +970,7 @@ export class AgentServer {
958
970
  logWriter,
959
971
  permissionMode: initialPermissionMode,
960
972
  hasDesktopConnected: sseController !== null,
973
+ pendingHandoffGitState: undefined,
961
974
  };
962
975
 
963
976
  this.logger = new Logger({
@@ -2104,6 +2117,12 @@ ${attributionInstructions}
2104
2117
 
2105
2118
  this.logger.debug("Cleaning up session");
2106
2119
 
2120
+ try {
2121
+ await this.captureHandoffCheckpoint();
2122
+ } catch (error) {
2123
+ this.logger.error("Failed to capture handoff checkpoint", error);
2124
+ }
2125
+
2107
2126
  try {
2108
2127
  await this.captureTreeState();
2109
2128
  } catch (error) {
@@ -2180,6 +2199,60 @@ ${attributionInstructions}
2180
2199
  }
2181
2200
  }
2182
2201
 
2202
+ private async captureHandoffCheckpoint(): Promise<void> {
2203
+ if (!this.session?.treeTracker || !this.session.pendingHandoffGitState) {
2204
+ return;
2205
+ }
2206
+ if (!this.posthogAPI) {
2207
+ this.logger.warn(
2208
+ "Skipping handoff checkpoint capture: PostHog API client is not configured",
2209
+ );
2210
+ return;
2211
+ }
2212
+
2213
+ const tracker = new HandoffCheckpointTracker({
2214
+ repositoryPath: this.config.repositoryPath ?? "/tmp/workspace",
2215
+ taskId: this.session.payload.task_id,
2216
+ runId: this.session.payload.run_id,
2217
+ apiClient: this.posthogAPI,
2218
+ logger: this.logger.child("HandoffCheckpoint"),
2219
+ });
2220
+
2221
+ const checkpoint = await tracker.captureForHandoff(
2222
+ this.session.pendingHandoffGitState,
2223
+ );
2224
+ if (!checkpoint) return;
2225
+
2226
+ const checkpointWithDevice: GitCheckpointEvent = {
2227
+ ...checkpoint,
2228
+ device: this.session.deviceInfo,
2229
+ };
2230
+
2231
+ const notification = {
2232
+ jsonrpc: "2.0" as const,
2233
+ method: POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT,
2234
+ params: checkpointWithDevice,
2235
+ };
2236
+
2237
+ this.broadcastEvent({
2238
+ type: "notification",
2239
+ timestamp: new Date().toISOString(),
2240
+ notification,
2241
+ });
2242
+
2243
+ this.session.logWriter.appendRawLine(
2244
+ this.session.payload.run_id,
2245
+ JSON.stringify(notification),
2246
+ );
2247
+ }
2248
+
2249
+ private extractHandoffLocalGitState(
2250
+ params: Record<string, unknown>,
2251
+ ): HandoffLocalGitState | null {
2252
+ const result = handoffLocalGitStateSchema.safeParse(params.localGitState);
2253
+ return result.success ? result.data : null;
2254
+ }
2255
+
2183
2256
  private broadcastTurnComplete(stopReason: string): void {
2184
2257
  if (!this.session) return;
2185
2258
  this.broadcastEvent({
@@ -5,6 +5,19 @@ const httpHeaderSchema = z.object({
5
5
  value: z.string(),
6
6
  });
7
7
 
8
+ const nullishString = z
9
+ .string()
10
+ .nullish()
11
+ .transform((value) => value ?? null);
12
+
13
+ export const handoffLocalGitStateSchema = z.object({
14
+ head: nullishString,
15
+ branch: nullishString,
16
+ upstreamHead: nullishString,
17
+ upstreamRemote: nullishString,
18
+ upstreamMergeRef: nullishString,
19
+ });
20
+
8
21
  const remoteMcpServerSchema = z.object({
9
22
  type: z.enum(["http", "sse"]),
10
23
  name: z.string().min(1, "MCP server name is required"),
@@ -83,13 +96,19 @@ export const refreshSessionParamsSchema = z.object({
83
96
  mcpServers: mcpServersSchema,
84
97
  });
85
98
 
99
+ export const closeParamsSchema = z
100
+ .object({
101
+ localGitState: handoffLocalGitStateSchema.optional(),
102
+ })
103
+ .optional();
104
+
86
105
  export const commandParamsSchemas = {
87
106
  user_message: userMessageParamsSchema,
88
107
  "posthog/user_message": userMessageParamsSchema,
89
108
  cancel: z.object({}).optional(),
90
109
  "posthog/cancel": z.object({}).optional(),
91
- close: z.object({}).optional(),
92
- "posthog/close": z.object({}).optional(),
110
+ close: closeParamsSchema,
111
+ "posthog/close": closeParamsSchema,
93
112
  permission_response: permissionResponseParamsSchema,
94
113
  "posthog/permission_response": permissionResponseParamsSchema,
95
114
  set_config_option: setConfigOptionParamsSchema,
package/src/types.ts CHANGED
@@ -1,3 +1,8 @@
1
+ import type {
2
+ GitHandoffCheckpoint,
3
+ HandoffLocalGitState as GitHandoffLocalGitState,
4
+ } from "@posthog/git/handoff";
5
+
1
6
  /**
2
7
  * Stored custom notification following ACP extensibility model.
3
8
  * Custom notifications use underscore-prefixed methods (e.g., `_posthog/phase_start`).
@@ -196,3 +201,22 @@ export interface TreeSnapshot {
196
201
  export interface TreeSnapshotEvent extends TreeSnapshot {
197
202
  device?: DeviceInfo;
198
203
  }
204
+
205
+ export type HandoffLocalGitState = GitHandoffLocalGitState;
206
+
207
+ export interface GitCheckpoint extends GitHandoffCheckpoint {
208
+ artifactPath?: string;
209
+ indexArtifactPath?: string;
210
+ }
211
+
212
+ export interface GitCheckpointEvent extends GitCheckpoint {
213
+ device?: DeviceInfo;
214
+ }
215
+
216
+ /**
217
+ * Keeps the emitted `@posthog/agent/types` entrypoint as a runtime ESM module.
218
+ *
219
+ * `export {}` is stripped by tsup in this package, which leaves `dist/types.js`
220
+ * empty and breaks downstream type resolution for the exported subpath.
221
+ */
222
+ export const AGENT_TYPES_MODULE = true;