@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.
- 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 -2
- package/dist/handoff-checkpoint.js +38 -53
- 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 -768
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +101 -766
- 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 +3 -17
- package/src/handoff-checkpoint.ts +15 -45
- 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 -6431
- package/dist/tree-tracker.js.map +0 -1
- package/src/sagas/apply-snapshot-saga.test.ts +0 -690
- package/src/sagas/apply-snapshot-saga.ts +0 -100
- package/src/sagas/capture-tree-saga.test.ts +0 -892
- package/src/sagas/capture-tree-saga.ts +0 -150
- 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",
|
|
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",
|
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,
|
|
@@ -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 {
|
|
2
|
-
import {
|
|
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 =
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|