@posthog/agent 2.3.386 → 2.3.388

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 (45) 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 +20 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/handoff-checkpoint.d.ts +43 -0
  6. package/dist/handoff-checkpoint.js +6684 -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 +2 -0
  12. package/dist/posthog-api.js +18 -2
  13. package/dist/posthog-api.js.map +1 -1
  14. package/dist/resume.d.ts +4 -8
  15. package/dist/resume.js +266 -6491
  16. package/dist/resume.js.map +1 -1
  17. package/dist/server/agent-server.d.ts +7 -16
  18. package/dist/server/agent-server.js +2333 -1383
  19. package/dist/server/agent-server.js.map +1 -1
  20. package/dist/server/bin.cjs +2332 -1382
  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 +10 -2
  32. package/src/acp-extensions.ts +3 -0
  33. package/src/handoff-checkpoint.test.ts +183 -0
  34. package/src/handoff-checkpoint.ts +367 -0
  35. package/src/posthog-api.test.ts +29 -0
  36. package/src/posthog-api.ts +13 -1
  37. package/src/resume.ts +27 -12
  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.test.ts +7 -47
  41. package/src/sagas/resume-saga.ts +42 -64
  42. package/src/sagas/test-fixtures.ts +46 -0
  43. package/src/server/agent-server.ts +193 -70
  44. package/src/server/schemas.ts +21 -2
  45. package/src/types.ts +24 -0
@@ -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(
@@ -470,50 +478,34 @@ export class AgentServer {
470
478
  await this.autoInitializeSession();
471
479
  }
472
480
 
473
- private async autoInitializeSession(): Promise<void> {
474
- const { taskId, runId, mode, projectId } = this.config;
475
-
476
- this.logger.debug("Auto-initializing session", { taskId, runId, mode });
477
-
478
- // Check if this is a resume from a previous run
479
- const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
480
- if (resumeRunId) {
481
- this.logger.debug("Resuming from previous run", {
482
- resumeRunId,
483
- currentRunId: runId,
481
+ private async loadResumeState(
482
+ taskId: string,
483
+ resumeRunId: string,
484
+ currentRunId: string,
485
+ ): Promise<void> {
486
+ this.logger.debug("Loading resume state", { resumeRunId, currentRunId });
487
+ try {
488
+ this.resumeState = await resumeFromLog({
489
+ taskId,
490
+ runId: resumeRunId,
491
+ repositoryPath: this.config.repositoryPath,
492
+ apiClient: this.posthogAPI,
493
+ logger: new Logger({ debug: true, prefix: "[Resume]" }),
484
494
  });
485
- try {
486
- this.resumeState = await resumeFromLog({
487
- taskId,
488
- runId: resumeRunId,
489
- repositoryPath: this.config.repositoryPath,
490
- apiClient: this.posthogAPI,
491
- logger: new Logger({ debug: true, prefix: "[Resume]" }),
492
- });
493
- this.logger.debug("Resume state loaded", {
494
- conversationTurns: this.resumeState.conversation.length,
495
- snapshotApplied: this.resumeState.snapshotApplied,
496
- logEntries: this.resumeState.logEntryCount,
497
- });
498
- } catch (error) {
499
- this.logger.debug("Failed to load resume state, starting fresh", {
500
- error,
501
- });
502
- this.resumeState = null;
503
- }
495
+ this.logger.debug("Resume state loaded", {
496
+ conversationTurns: this.resumeState.conversation.length,
497
+ hasSnapshot: !!this.resumeState.latestSnapshot,
498
+ hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
499
+ gitCheckpointBranch:
500
+ this.resumeState.latestGitCheckpoint?.branch ?? null,
501
+ logEntries: this.resumeState.logEntryCount,
502
+ });
503
+ } catch (error) {
504
+ this.logger.debug("Failed to load resume state, starting fresh", {
505
+ error,
506
+ });
507
+ this.resumeState = null;
504
508
  }
505
-
506
- // Create a synthetic payload from config (no JWT needed for auto-init)
507
- const payload: JwtPayload = {
508
- task_id: taskId,
509
- run_id: runId,
510
- team_id: projectId,
511
- user_id: 0, // System-initiated
512
- distinct_id: "agent-server",
513
- mode,
514
- };
515
-
516
- await this.initializeSession(payload, null);
517
509
  }
518
510
 
519
511
  async stop(): Promise<void> {
@@ -661,6 +653,10 @@ export class AgentServer {
661
653
  case POSTHOG_NOTIFICATIONS.CLOSE:
662
654
  case "close": {
663
655
  this.logger.debug("Close requested");
656
+ const localGitState = this.extractHandoffLocalGitState(params);
657
+ if (localGitState && this.session) {
658
+ this.session.pendingHandoffGitState = localGitState;
659
+ }
664
660
  await this.cleanupSession();
665
661
  return { closed: true };
666
662
  }
@@ -958,6 +954,7 @@ export class AgentServer {
958
954
  logWriter,
959
955
  permissionMode: initialPermissionMode,
960
956
  hasDesktopConnected: sseController !== null,
957
+ pendingHandoffGitState: undefined,
961
958
  };
962
959
 
963
960
  this.logger = new Logger({
@@ -1044,33 +1041,14 @@ export class AgentServer {
1044
1041
  }
1045
1042
  }
1046
1043
 
1047
- // Check for resume if not already loaded from env var in autoInitializeSession
1048
1044
  if (!this.resumeState) {
1049
1045
  const resumeRunId = this.getResumeRunId(taskRun);
1050
1046
  if (resumeRunId) {
1051
- this.logger.debug("Resuming from previous run (via TaskRun state)", {
1047
+ await this.loadResumeState(
1048
+ payload.task_id,
1052
1049
  resumeRunId,
1053
- currentRunId: payload.run_id,
1054
- });
1055
- try {
1056
- this.resumeState = await resumeFromLog({
1057
- taskId: payload.task_id,
1058
- runId: resumeRunId,
1059
- repositoryPath: this.config.repositoryPath,
1060
- apiClient: this.posthogAPI,
1061
- logger: new Logger({ debug: true, prefix: "[Resume]" }),
1062
- });
1063
- this.logger.debug("Resume state loaded (via TaskRun state)", {
1064
- conversationTurns: this.resumeState.conversation.length,
1065
- snapshotApplied: this.resumeState.snapshotApplied,
1066
- logEntries: this.resumeState.logEntryCount,
1067
- });
1068
- } catch (error) {
1069
- this.logger.debug("Failed to load resume state, starting fresh", {
1070
- error,
1071
- });
1072
- this.resumeState = null;
1073
- }
1050
+ payload.run_id,
1051
+ );
1074
1052
  }
1075
1053
  }
1076
1054
 
@@ -1150,11 +1128,70 @@ export class AgentServer {
1150
1128
  this.resumeState.conversation,
1151
1129
  );
1152
1130
 
1153
- // Read the pending user prompt from TaskRun state (set by the workflow
1154
- // when the user sends a follow-up message that triggers a resume).
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
+
1161
+ if (
1162
+ this.resumeState.latestGitCheckpoint &&
1163
+ this.config.repositoryPath &&
1164
+ this.posthogAPI
1165
+ ) {
1166
+ try {
1167
+ const checkpointTracker = new HandoffCheckpointTracker({
1168
+ repositoryPath: this.config.repositoryPath,
1169
+ taskId: payload.task_id,
1170
+ runId: payload.run_id,
1171
+ apiClient: this.posthogAPI,
1172
+ logger: this.logger.child("HandoffCheckpoint"),
1173
+ });
1174
+ const metrics = await checkpointTracker.applyFromHandoff(
1175
+ this.resumeState.latestGitCheckpoint,
1176
+ );
1177
+ this.logger.info("Git checkpoint applied", {
1178
+ branch: this.resumeState.latestGitCheckpoint.branch,
1179
+ head: this.resumeState.latestGitCheckpoint.head,
1180
+ packBytes: metrics.packBytes,
1181
+ indexBytes: metrics.indexBytes,
1182
+ totalBytes: metrics.totalBytes,
1183
+ });
1184
+ } catch (error) {
1185
+ this.logger.warn("Failed to apply git checkpoint", {
1186
+ error: error instanceof Error ? error.message : String(error),
1187
+ branch: this.resumeState.latestGitCheckpoint.branch,
1188
+ });
1189
+ }
1190
+ }
1191
+
1155
1192
  const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
1156
1193
 
1157
- const sandboxContext = this.resumeState.snapshotApplied
1194
+ const sandboxContext = snapshotApplied
1158
1195
  ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.`
1159
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.`;
1160
1197
 
@@ -1193,7 +1230,10 @@ export class AgentServer {
1193
1230
  conversationTurns: this.resumeState.conversation.length,
1194
1231
  promptLength: promptBlocksToText(resumePromptBlocks).length,
1195
1232
  hasPendingUserMessage: !!pendingUserPrompt?.length,
1196
- snapshotApplied: this.resumeState.snapshotApplied,
1233
+ snapshotApplied,
1234
+ hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
1235
+ gitCheckpointBranch:
1236
+ this.resumeState.latestGitCheckpoint?.branch ?? null,
1197
1237
  });
1198
1238
 
1199
1239
  // Clear resume state so it's not reused
@@ -1421,6 +1461,29 @@ export class AgentServer {
1421
1461
  return normalizedName.length > 0 ? normalizedName : "attachment";
1422
1462
  }
1423
1463
 
1464
+ private async autoInitializeSession(): Promise<void> {
1465
+ const { taskId, runId, mode, projectId } = this.config;
1466
+
1467
+ this.logger.debug("Auto-initializing session", { taskId, runId, mode });
1468
+
1469
+ const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
1470
+ if (resumeRunId) {
1471
+ await this.loadResumeState(taskId, resumeRunId, runId);
1472
+ }
1473
+
1474
+ // Create a synthetic payload from config (no JWT needed for auto-init)
1475
+ const payload: JwtPayload = {
1476
+ task_id: taskId,
1477
+ run_id: runId,
1478
+ team_id: projectId,
1479
+ user_id: 0, // System-initiated
1480
+ distinct_id: "agent-server",
1481
+ mode,
1482
+ };
1483
+
1484
+ await this.initializeSession(payload, null);
1485
+ }
1486
+
1424
1487
  private getResumeRunId(taskRun: TaskRun | null): string | null {
1425
1488
  // Env var takes precedence (set by backend infra)
1426
1489
  const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
@@ -2104,6 +2167,12 @@ ${attributionInstructions}
2104
2167
 
2105
2168
  this.logger.debug("Cleaning up session");
2106
2169
 
2170
+ try {
2171
+ await this.captureHandoffCheckpoint();
2172
+ } catch (error) {
2173
+ this.logger.error("Failed to capture handoff checkpoint", error);
2174
+ }
2175
+
2107
2176
  try {
2108
2177
  await this.captureTreeState();
2109
2178
  } catch (error) {
@@ -2180,6 +2249,60 @@ ${attributionInstructions}
2180
2249
  }
2181
2250
  }
2182
2251
 
2252
+ private async captureHandoffCheckpoint(): Promise<void> {
2253
+ if (!this.session?.treeTracker || !this.session.pendingHandoffGitState) {
2254
+ return;
2255
+ }
2256
+ if (!this.posthogAPI) {
2257
+ this.logger.warn(
2258
+ "Skipping handoff checkpoint capture: PostHog API client is not configured",
2259
+ );
2260
+ return;
2261
+ }
2262
+
2263
+ const tracker = new HandoffCheckpointTracker({
2264
+ repositoryPath: this.config.repositoryPath ?? "/tmp/workspace",
2265
+ taskId: this.session.payload.task_id,
2266
+ runId: this.session.payload.run_id,
2267
+ apiClient: this.posthogAPI,
2268
+ logger: this.logger.child("HandoffCheckpoint"),
2269
+ });
2270
+
2271
+ const checkpoint = await tracker.captureForHandoff(
2272
+ this.session.pendingHandoffGitState,
2273
+ );
2274
+ if (!checkpoint) return;
2275
+
2276
+ const checkpointWithDevice: GitCheckpointEvent = {
2277
+ ...checkpoint,
2278
+ device: this.session.deviceInfo,
2279
+ };
2280
+
2281
+ const notification = {
2282
+ jsonrpc: "2.0" as const,
2283
+ method: POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT,
2284
+ params: checkpointWithDevice,
2285
+ };
2286
+
2287
+ this.broadcastEvent({
2288
+ type: "notification",
2289
+ timestamp: new Date().toISOString(),
2290
+ notification,
2291
+ });
2292
+
2293
+ this.session.logWriter.appendRawLine(
2294
+ this.session.payload.run_id,
2295
+ JSON.stringify(notification),
2296
+ );
2297
+ }
2298
+
2299
+ private extractHandoffLocalGitState(
2300
+ params: Record<string, unknown>,
2301
+ ): HandoffLocalGitState | null {
2302
+ const result = handoffLocalGitStateSchema.safeParse(params.localGitState);
2303
+ return result.success ? result.data : null;
2304
+ }
2305
+
2183
2306
  private broadcastTurnComplete(stopReason: string): void {
2184
2307
  if (!this.session) return;
2185
2308
  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;