@posthog/agent 2.3.401 → 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.
- package/README.md +11 -14
- package/dist/agent.js +1 -7
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +0 -3
- package/dist/handoff-checkpoint.js +38 -69
- package/dist/handoff-checkpoint.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/posthog-api.js +1 -5
- package/dist/posthog-api.js.map +1 -1
- package/dist/resume.d.ts +5 -6
- package/dist/resume.js +2 -41
- package/dist/resume.js.map +1 -1
- package/dist/server/agent-server.d.ts +1 -2
- package/dist/server/agent-server.js +103 -815
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +101 -806
- package/dist/server/bin.cjs.map +1 -1
- package/dist/types.d.ts +2 -13
- package/dist/types.js.map +1 -1
- package/package.json +3 -7
- package/src/acp-extensions.ts +0 -3
- package/src/handoff-checkpoint.test.ts +2 -17
- package/src/handoff-checkpoint.ts +15 -61
- package/src/resume.ts +5 -11
- package/src/sagas/resume-saga.test.ts +27 -77
- package/src/sagas/resume-saga.ts +3 -44
- package/src/sagas/test-fixtures.ts +17 -76
- package/src/server/agent-server.ts +22 -103
- package/src/test/fixtures/api.ts +2 -15
- package/src/types.ts +0 -16
- package/dist/tree-tracker.d.ts +0 -68
- package/dist/tree-tracker.js +0 -6462
- package/dist/tree-tracker.js.map +0 -1
- package/src/sagas/apply-snapshot-saga.test.ts +0 -691
- package/src/sagas/apply-snapshot-saga.ts +0 -114
- package/src/sagas/capture-tree-saga.test.ts +0 -910
- package/src/sagas/capture-tree-saga.ts +0 -165
- 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" | "
|
|
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
|
|
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 | \"
|
|
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.
|
|
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",
|
|
109
|
-
"@posthog/git": "1.0.0",
|
|
110
105
|
"@posthog/enricher": "1.0.0",
|
|
111
|
-
"@posthog/shared": "1.0.0"
|
|
106
|
+
"@posthog/shared": "1.0.0",
|
|
107
|
+
"@posthog/git": "1.0.0"
|
|
112
108
|
},
|
|
113
109
|
"dependencies": {
|
|
114
110
|
"@agentclientprotocol/sdk": "0.19.0",
|
package/src/acp-extensions.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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,
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
readFile,
|
|
5
|
-
rm,
|
|
6
|
-
rmdir,
|
|
7
|
-
writeFile,
|
|
8
|
-
} from "node:fs/promises";
|
|
9
|
-
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";
|
|
10
4
|
import {
|
|
11
5
|
type GitHandoffBranchDivergence,
|
|
12
6
|
type GitHandoffCheckpoint,
|
|
@@ -107,8 +101,12 @@ export class HandoffCheckpointTracker {
|
|
|
107
101
|
indexArtifactPath: uploads.index?.storagePath,
|
|
108
102
|
};
|
|
109
103
|
} finally {
|
|
104
|
+
const tempDir = capture.headPack?.path
|
|
105
|
+
? dirname(capture.headPack.path)
|
|
106
|
+
: dirname(capture.indexFile.path);
|
|
110
107
|
await this.removeIfPresent(capture.headPack?.path);
|
|
111
108
|
await this.removeIfPresent(capture.indexFile.path);
|
|
109
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
112
110
|
}
|
|
113
111
|
}
|
|
114
112
|
|
|
@@ -128,8 +126,9 @@ export class HandoffCheckpointTracker {
|
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
const gitTracker = this.createGitTracker();
|
|
131
|
-
const tmpDir =
|
|
132
|
-
|
|
129
|
+
const tmpDir = await mkdtemp(
|
|
130
|
+
join(tmpdir(), `posthog-code-handoff-${checkpoint.checkpointId}-`),
|
|
131
|
+
);
|
|
133
132
|
|
|
134
133
|
const packPath = join(tmpDir, `${checkpoint.checkpointId}.pack`);
|
|
135
134
|
const indexPath = join(tmpDir, `${checkpoint.checkpointId}.index`);
|
|
@@ -168,7 +167,7 @@ export class HandoffCheckpointTracker {
|
|
|
168
167
|
} finally {
|
|
169
168
|
await this.removeIfPresent(packPath);
|
|
170
169
|
await this.removeIfPresent(indexPath);
|
|
171
|
-
await
|
|
170
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
173
|
|
|
@@ -302,52 +301,24 @@ export class HandoffCheckpointTracker {
|
|
|
302
301
|
uploads: Uploads,
|
|
303
302
|
): void {
|
|
304
303
|
this.logger.info("Captured handoff checkpoint", {
|
|
305
|
-
checkpointId: checkpoint.checkpointId,
|
|
306
304
|
branch: checkpoint.branch,
|
|
307
|
-
head: checkpoint.head,
|
|
308
|
-
|
|
309
|
-
indexArtifactPath: uploads.index?.storagePath,
|
|
310
|
-
...this.buildMetricPayload(uploads),
|
|
305
|
+
head: checkpoint.head?.slice(0, 7),
|
|
306
|
+
totalBytes: this.sumRawBytes(uploads.pack, uploads.index),
|
|
311
307
|
});
|
|
312
308
|
}
|
|
313
309
|
|
|
314
310
|
private logApplyMetrics(
|
|
315
311
|
checkpoint: GitCheckpoint,
|
|
316
|
-
|
|
312
|
+
_downloads: Downloads,
|
|
317
313
|
totalBytes: number,
|
|
318
314
|
): void {
|
|
319
315
|
this.logger.info("Applied handoff checkpoint", {
|
|
320
|
-
checkpointId: checkpoint.checkpointId,
|
|
321
|
-
commit: checkpoint.commit,
|
|
322
316
|
branch: checkpoint.branch,
|
|
323
|
-
head: checkpoint.head,
|
|
324
|
-
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
325
|
-
packWireBytes: downloads.pack?.wireBytes ?? 0,
|
|
326
|
-
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
327
|
-
indexWireBytes: downloads.index?.wireBytes ?? 0,
|
|
317
|
+
head: checkpoint.head?.slice(0, 7),
|
|
328
318
|
totalBytes,
|
|
329
|
-
totalWireBytes: this.sumWireBytes(downloads.pack, downloads.index),
|
|
330
319
|
});
|
|
331
320
|
}
|
|
332
321
|
|
|
333
|
-
private buildMetricPayload(metrics: ArtifactSlotMap<object>): {
|
|
334
|
-
packBytes: number;
|
|
335
|
-
packWireBytes: number;
|
|
336
|
-
indexBytes: number;
|
|
337
|
-
indexWireBytes: number;
|
|
338
|
-
totalBytes: number;
|
|
339
|
-
totalWireBytes: number;
|
|
340
|
-
} {
|
|
341
|
-
return {
|
|
342
|
-
packBytes: metrics.pack?.rawBytes ?? 0,
|
|
343
|
-
packWireBytes: metrics.pack?.wireBytes ?? 0,
|
|
344
|
-
indexBytes: metrics.index?.rawBytes ?? 0,
|
|
345
|
-
indexWireBytes: metrics.index?.wireBytes ?? 0,
|
|
346
|
-
totalBytes: this.sumRawBytes(metrics.pack, metrics.index),
|
|
347
|
-
totalWireBytes: this.sumWireBytes(metrics.pack, metrics.index),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
322
|
private sumRawBytes(
|
|
352
323
|
...artifacts: Array<{ rawBytes: number } | undefined>
|
|
353
324
|
): number {
|
|
@@ -357,27 +328,10 @@ export class HandoffCheckpointTracker {
|
|
|
357
328
|
);
|
|
358
329
|
}
|
|
359
330
|
|
|
360
|
-
private sumWireBytes(
|
|
361
|
-
...artifacts: Array<{ wireBytes: number } | undefined>
|
|
362
|
-
): number {
|
|
363
|
-
return artifacts.reduce(
|
|
364
|
-
(total, artifact) => total + (artifact?.wireBytes ?? 0),
|
|
365
|
-
0,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
331
|
private async removeIfPresent(filePath: string | undefined): Promise<void> {
|
|
370
332
|
if (!filePath) {
|
|
371
333
|
return;
|
|
372
334
|
}
|
|
373
335
|
await rm(filePath, { force: true }).catch(() => {});
|
|
374
336
|
}
|
|
375
|
-
|
|
376
|
-
private async removeTmpDirIfEmpty(tmpDir: string): Promise<void> {
|
|
377
|
-
const entries = await readdir(tmpDir).catch(() => null);
|
|
378
|
-
if (!entries || entries.length > 0) {
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
await rmdir(tmpDir).catch(() => {});
|
|
382
|
-
}
|
|
383
337
|
}
|
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
|
|
6
|
+
* - Finds latest git_checkpoint event
|
|
7
7
|
* - Rebuilds conversation from log events
|
|
8
|
-
* - Restores working tree from
|
|
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 (
|
|
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
|
-
*
|
|
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.
|
|
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("
|
|
416
|
-
it("finds latest
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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.
|
|
500
|
-
|
|
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("
|
|
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
|
-
|
|
513
|
-
|
|
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.
|
|
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
|
-
|
|
486
|
+
createGitCheckpointNotification({
|
|
487
|
+
checkpointId: "checkpoint-1",
|
|
488
|
+
checkpointRef: "refs/posthog-code-checkpoint/checkpoint-1",
|
|
541
489
|
device: { type: "local" },
|
|
542
490
|
}),
|
|
543
|
-
|
|
491
|
+
createGitCheckpointNotification({
|
|
492
|
+
checkpointId: "checkpoint-2",
|
|
493
|
+
checkpointRef: "refs/posthog-code-checkpoint/checkpoint-2",
|
|
544
494
|
device: { type: "cloud" },
|
|
545
495
|
}),
|
|
546
496
|
]);
|
package/src/sagas/resume-saga.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { ContentBlock } from "@agentclientprotocol/sdk";
|
|
2
2
|
import { Saga } from "@posthog/shared";
|
|
3
|
-
import {
|
|
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:
|
|
93
|
+
interrupted: false,
|
|
111
94
|
});
|
|
112
95
|
|
|
113
96
|
return {
|
|
114
97
|
conversation,
|
|
115
|
-
latestSnapshot,
|
|
116
98
|
latestGitCheckpoint,
|
|
117
|
-
interrupted:
|
|
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 {
|