@posthog/agent 2.3.387 → 2.3.398

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 (37) hide show
  1. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
  2. package/dist/adapters/claude/mcp/tool-metadata.d.ts +24 -0
  3. package/dist/adapters/claude/mcp/tool-metadata.js +165 -0
  4. package/dist/adapters/claude/mcp/tool-metadata.js.map +1 -0
  5. package/dist/adapters/claude/tools.js.map +1 -1
  6. package/dist/agent.js +120 -3
  7. package/dist/agent.js.map +1 -1
  8. package/dist/handoff-checkpoint.d.ts +5 -1
  9. package/dist/handoff-checkpoint.js +22 -17
  10. package/dist/handoff-checkpoint.js.map +1 -1
  11. package/dist/index.d.ts +7 -9
  12. package/dist/index.js.map +1 -1
  13. package/dist/posthog-api.d.ts +1 -0
  14. package/dist/posthog-api.js +12 -1
  15. package/dist/posthog-api.js.map +1 -1
  16. package/dist/resume.d.ts +1 -7
  17. package/dist/resume.js +251 -6513
  18. package/dist/resume.js.map +1 -1
  19. package/dist/server/agent-server.d.ts +2 -1
  20. package/dist/server/agent-server.js +1305 -1181
  21. package/dist/server/agent-server.js.map +1 -1
  22. package/dist/server/bin.cjs +1303 -1179
  23. package/dist/server/bin.cjs.map +1 -1
  24. package/package.json +5 -1
  25. package/src/adapters/claude/claude-agent.ts +5 -0
  26. package/src/adapters/claude/mcp/tool-metadata.test.ts +93 -0
  27. package/src/adapters/claude/mcp/tool-metadata.ts +33 -0
  28. package/src/adapters/claude/permissions/permission-handlers.test.ts +165 -0
  29. package/src/adapters/claude/permissions/permission-handlers.ts +105 -0
  30. package/src/adapters/claude/session/instructions.ts +9 -1
  31. package/src/adapters/claude/types.ts +2 -0
  32. package/src/handoff-checkpoint.ts +25 -19
  33. package/src/posthog-api.ts +8 -0
  34. package/src/resume.ts +20 -11
  35. package/src/sagas/resume-saga.test.ts +7 -47
  36. package/src/sagas/resume-saga.ts +10 -64
  37. package/src/server/agent-server.ts +119 -69
@@ -6,7 +6,6 @@ import { ResumeSaga } from "./resume-saga";
6
6
  import {
7
7
  createAgentChunk,
8
8
  createAgentMessage,
9
- createArchiveBuffer,
10
9
  createMockApiClient,
11
10
  createMockLogger,
12
11
  createNotification,
@@ -52,7 +51,6 @@ describe("ResumeSaga", () => {
52
51
  if (result.success) {
53
52
  expect(result.data.conversation).toHaveLength(0);
54
53
  expect(result.data.latestSnapshot).toBeNull();
55
- expect(result.data.snapshotApplied).toBe(false);
56
54
  expect(result.data.logEntryCount).toBe(0);
57
55
  }
58
56
  });
@@ -468,14 +466,10 @@ describe("ResumeSaga", () => {
468
466
  });
469
467
  });
470
468
 
471
- describe("snapshot application", () => {
472
- it("applies snapshot when archive URL present", async () => {
469
+ describe("snapshot metadata", () => {
470
+ it("returns latest snapshot metadata when archive URL present", async () => {
473
471
  const baseCommit = await repo.git(["rev-parse", "HEAD"]);
474
472
 
475
- const archive = await createArchiveBuffer([
476
- { path: "restored.ts", content: "restored content" },
477
- ]);
478
-
479
473
  (mockApiClient.getTaskRun as ReturnType<typeof vi.fn>).mockResolvedValue(
480
474
  createTaskRun(),
481
475
  );
@@ -490,9 +484,6 @@ describe("ResumeSaga", () => {
490
484
  timestamp: new Date().toISOString(),
491
485
  }),
492
486
  ]);
493
- (
494
- mockApiClient.downloadArtifact as ReturnType<typeof vi.fn>
495
- ).mockResolvedValue(archive);
496
487
 
497
488
  const saga = new ResumeSaga(mockLogger);
498
489
  const result = await saga.run({
@@ -505,13 +496,13 @@ describe("ResumeSaga", () => {
505
496
  expect(result.success).toBe(true);
506
497
  if (!result.success) return;
507
498
 
508
- expect(result.data.snapshotApplied).toBe(true);
509
-
510
- const content = await repo.readFile("restored.ts");
511
- expect(content).toBe("restored content");
499
+ expect(result.data.latestSnapshot?.treeHash).toBe("hash-1");
500
+ expect(result.data.latestSnapshot?.archiveUrl).toBe(
501
+ "gs://bucket/hash-1.tar.gz",
502
+ );
512
503
  });
513
504
 
514
- it("continues without snapshot when no archive URL", async () => {
505
+ it("returns snapshot metadata even when no archive URL", async () => {
515
506
  (mockApiClient.getTaskRun as ReturnType<typeof vi.fn>).mockResolvedValue(
516
507
  createTaskRun(),
517
508
  );
@@ -533,40 +524,9 @@ describe("ResumeSaga", () => {
533
524
  expect(result.success).toBe(true);
534
525
  if (!result.success) return;
535
526
 
536
- expect(result.data.snapshotApplied).toBe(false);
537
527
  expect(result.data.latestSnapshot).not.toBeNull();
538
528
  expect(result.data.conversation).toHaveLength(1);
539
529
  });
540
-
541
- it("continues without snapshot when apply fails", async () => {
542
- (mockApiClient.getTaskRun as ReturnType<typeof vi.fn>).mockResolvedValue(
543
- createTaskRun(),
544
- );
545
- (
546
- mockApiClient.fetchTaskRunLogs as ReturnType<typeof vi.fn>
547
- ).mockResolvedValue([
548
- createTreeSnapshotNotification("hash-1", "gs://bucket/hash-1.tar.gz"),
549
- createUserMessage("hello"),
550
- ]);
551
- (
552
- mockApiClient.downloadArtifact as ReturnType<typeof vi.fn>
553
- ).mockRejectedValue(new Error("Download failed"));
554
-
555
- const saga = new ResumeSaga(mockLogger);
556
- const result = await saga.run({
557
- taskId: "task-1",
558
- runId: "run-1",
559
- repositoryPath: repo.path,
560
- apiClient: mockApiClient,
561
- });
562
-
563
- expect(result.success).toBe(true);
564
- if (!result.success) return;
565
-
566
- expect(result.data.snapshotApplied).toBe(false);
567
- expect(result.data.conversation).toHaveLength(1);
568
- expect(mockLogger.warn).toHaveBeenCalled();
569
- });
570
530
  });
571
531
 
572
532
  describe("device info", () => {
@@ -2,14 +2,13 @@ import type { ContentBlock } from "@agentclientprotocol/sdk";
2
2
  import { Saga } from "@posthog/shared";
3
3
  import { isNotification, POSTHOG_NOTIFICATIONS } from "../acp-extensions";
4
4
  import type { PostHogAPIClient } from "../posthog-api";
5
- import { TreeTracker } from "../tree-tracker";
6
5
  import type {
7
6
  DeviceInfo,
8
7
  GitCheckpointEvent,
9
8
  StoredNotification,
10
9
  TreeSnapshotEvent,
11
10
  } from "../types";
12
- import { Logger } from "../utils/logger";
11
+ import type { Logger } from "../utils/logger";
13
12
 
14
13
  export interface ConversationTurn {
15
14
  role: "user" | "assistant";
@@ -36,7 +35,6 @@ export interface ResumeOutput {
36
35
  conversation: ConversationTurn[];
37
36
  latestSnapshot: TreeSnapshotEvent | null;
38
37
  latestGitCheckpoint: GitCheckpointEvent | null;
39
- snapshotApplied: boolean;
40
38
  interrupted: boolean;
41
39
  lastDevice?: DeviceInfo;
42
40
  logEntryCount: number;
@@ -46,9 +44,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
46
44
  readonly sagaName = "ResumeSaga";
47
45
 
48
46
  protected async execute(input: ResumeInput): Promise<ResumeOutput> {
49
- const { taskId, runId, repositoryPath, apiClient } = input;
50
- const logger =
51
- input.logger || new Logger({ debug: false, prefix: "[Resume]" });
47
+ const { taskId, runId, apiClient } = input;
52
48
 
53
49
  // Step 1: Fetch task run (read-only)
54
50
  const taskRun = await this.readOnlyStep("fetch_task_run", () =>
@@ -83,69 +79,21 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
83
79
  );
84
80
 
85
81
  // Step 4: Apply snapshot if present (wrapped in step for consistent logging)
86
- // Note: We use a try/catch inside the step because snapshot failure should NOT fail the saga
87
- let snapshotApplied = false;
88
- if (latestSnapshot?.archiveUrl && repositoryPath) {
82
+ if (latestSnapshot) {
89
83
  this.log.info("Found tree snapshot", {
90
84
  treeHash: latestSnapshot.treeHash,
91
- hasArchiveUrl: true,
85
+ hasArchiveUrl: !!latestSnapshot.archiveUrl,
92
86
  changes: latestSnapshot.changes?.length ?? 0,
93
- interrupted: latestSnapshot.interrupted,
94
87
  });
88
+ }
95
89
 
96
- await this.step({
97
- name: "apply_snapshot",
98
- execute: async () => {
99
- const treeTracker = new TreeTracker({
100
- repositoryPath,
101
- taskId,
102
- runId,
103
- apiClient,
104
- logger: logger.child("TreeTracker"),
105
- });
106
-
107
- try {
108
- await treeTracker.applyTreeSnapshot(latestSnapshot);
109
- treeTracker.setLastTreeHash(latestSnapshot.treeHash);
110
- snapshotApplied = true;
111
- this.log.info("Tree snapshot applied successfully", {
112
- treeHash: latestSnapshot.treeHash,
113
- });
114
- } catch (error) {
115
- // Log but don't fail - continue with conversation rebuild
116
- // ApplySnapshotSaga handles its own rollback internally
117
- this.log.warn(
118
- "Failed to apply tree snapshot, continuing without it",
119
- {
120
- error: error instanceof Error ? error.message : String(error),
121
- treeHash: latestSnapshot.treeHash,
122
- },
123
- );
124
- }
125
- },
126
- rollback: async () => {
127
- // Inner ApplySnapshotSaga handles its own rollback
128
- },
90
+ if (latestGitCheckpoint) {
91
+ this.log.info("Found git checkpoint", {
92
+ checkpointId: latestGitCheckpoint.checkpointId,
93
+ branch: latestGitCheckpoint.branch,
129
94
  });
130
- } else if (latestSnapshot?.archiveUrl && !repositoryPath) {
131
- this.log.warn(
132
- "Snapshot found but no repositoryPath configured - files cannot be restored",
133
- {
134
- treeHash: latestSnapshot.treeHash,
135
- changes: latestSnapshot.changes?.length ?? 0,
136
- },
137
- );
138
- } else if (latestSnapshot) {
139
- this.log.warn(
140
- "Snapshot found but has no archive URL - files cannot be restored",
141
- {
142
- treeHash: latestSnapshot.treeHash,
143
- changes: latestSnapshot.changes?.length ?? 0,
144
- },
145
- );
146
95
  }
147
96
 
148
- // Step 5: Rebuild conversation (read-only, pure computation)
149
97
  const conversation = await this.readOnlyStep("rebuild_conversation", () =>
150
98
  Promise.resolve(this.rebuildConversation(entries)),
151
99
  );
@@ -158,7 +106,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
158
106
  this.log.info("Resume state rebuilt", {
159
107
  turns: conversation.length,
160
108
  hasSnapshot: !!latestSnapshot,
161
- snapshotApplied,
109
+ hasGitCheckpoint: !!latestGitCheckpoint,
162
110
  interrupted: latestSnapshot?.interrupted ?? false,
163
111
  });
164
112
 
@@ -166,7 +114,6 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
166
114
  conversation,
167
115
  latestSnapshot,
168
116
  latestGitCheckpoint,
169
- snapshotApplied,
170
117
  interrupted: latestSnapshot?.interrupted ?? false,
171
118
  lastDevice,
172
119
  logEntryCount: entries.length,
@@ -178,7 +125,6 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
178
125
  conversation: [],
179
126
  latestSnapshot: null,
180
127
  latestGitCheckpoint: null,
181
- snapshotApplied: false,
182
128
  interrupted: false,
183
129
  logEntryCount: 0,
184
130
  };
@@ -478,50 +478,34 @@ export class AgentServer {
478
478
  await this.autoInitializeSession();
479
479
  }
480
480
 
481
- private async autoInitializeSession(): Promise<void> {
482
- const { taskId, runId, mode, projectId } = this.config;
483
-
484
- this.logger.debug("Auto-initializing session", { taskId, runId, mode });
485
-
486
- // Check if this is a resume from a previous run
487
- const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
488
- if (resumeRunId) {
489
- this.logger.debug("Resuming from previous run", {
490
- resumeRunId,
491
- 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]" }),
492
494
  });
493
- try {
494
- this.resumeState = await resumeFromLog({
495
- taskId,
496
- runId: resumeRunId,
497
- repositoryPath: this.config.repositoryPath,
498
- apiClient: this.posthogAPI,
499
- logger: new Logger({ debug: true, prefix: "[Resume]" }),
500
- });
501
- this.logger.debug("Resume state loaded", {
502
- conversationTurns: this.resumeState.conversation.length,
503
- snapshotApplied: this.resumeState.snapshotApplied,
504
- logEntries: this.resumeState.logEntryCount,
505
- });
506
- } catch (error) {
507
- this.logger.debug("Failed to load resume state, starting fresh", {
508
- error,
509
- });
510
- this.resumeState = null;
511
- }
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;
512
508
  }
513
-
514
- // Create a synthetic payload from config (no JWT needed for auto-init)
515
- const payload: JwtPayload = {
516
- task_id: taskId,
517
- run_id: runId,
518
- team_id: projectId,
519
- user_id: 0, // System-initiated
520
- distinct_id: "agent-server",
521
- mode,
522
- };
523
-
524
- await this.initializeSession(payload, null);
525
509
  }
526
510
 
527
511
  async stop(): Promise<void> {
@@ -1057,33 +1041,14 @@ export class AgentServer {
1057
1041
  }
1058
1042
  }
1059
1043
 
1060
- // Check for resume if not already loaded from env var in autoInitializeSession
1061
1044
  if (!this.resumeState) {
1062
1045
  const resumeRunId = this.getResumeRunId(taskRun);
1063
1046
  if (resumeRunId) {
1064
- this.logger.debug("Resuming from previous run (via TaskRun state)", {
1047
+ await this.loadResumeState(
1048
+ payload.task_id,
1065
1049
  resumeRunId,
1066
- currentRunId: payload.run_id,
1067
- });
1068
- try {
1069
- this.resumeState = await resumeFromLog({
1070
- taskId: payload.task_id,
1071
- runId: resumeRunId,
1072
- repositoryPath: this.config.repositoryPath,
1073
- apiClient: this.posthogAPI,
1074
- logger: new Logger({ debug: true, prefix: "[Resume]" }),
1075
- });
1076
- this.logger.debug("Resume state loaded (via TaskRun state)", {
1077
- conversationTurns: this.resumeState.conversation.length,
1078
- snapshotApplied: this.resumeState.snapshotApplied,
1079
- logEntries: this.resumeState.logEntryCount,
1080
- });
1081
- } catch (error) {
1082
- this.logger.debug("Failed to load resume state, starting fresh", {
1083
- error,
1084
- });
1085
- this.resumeState = null;
1086
- }
1050
+ payload.run_id,
1051
+ );
1087
1052
  }
1088
1053
  }
1089
1054
 
@@ -1163,11 +1128,70 @@ export class AgentServer {
1163
1128
  this.resumeState.conversation,
1164
1129
  );
1165
1130
 
1166
- // Read the pending user prompt from TaskRun state (set by the workflow
1167
- // 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
+
1168
1192
  const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
1169
1193
 
1170
- const sandboxContext = this.resumeState.snapshotApplied
1194
+ const sandboxContext = snapshotApplied
1171
1195
  ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.`
1172
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.`;
1173
1197
 
@@ -1206,7 +1230,10 @@ export class AgentServer {
1206
1230
  conversationTurns: this.resumeState.conversation.length,
1207
1231
  promptLength: promptBlocksToText(resumePromptBlocks).length,
1208
1232
  hasPendingUserMessage: !!pendingUserPrompt?.length,
1209
- snapshotApplied: this.resumeState.snapshotApplied,
1233
+ snapshotApplied,
1234
+ hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
1235
+ gitCheckpointBranch:
1236
+ this.resumeState.latestGitCheckpoint?.branch ?? null,
1210
1237
  });
1211
1238
 
1212
1239
  // Clear resume state so it's not reused
@@ -1434,6 +1461,29 @@ export class AgentServer {
1434
1461
  return normalizedName.length > 0 ? normalizedName : "attachment";
1435
1462
  }
1436
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
+
1437
1487
  private getResumeRunId(taskRun: TaskRun | null): string | null {
1438
1488
  // Env var takes precedence (set by backend infra)
1439
1489
  const envRunId = process.env.POSTHOG_RESUME_RUN_ID;