@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.
- package/dist/adapters/claude/session/jsonl-hydration.d.ts +1 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +20 -2
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +43 -0
- package/dist/handoff-checkpoint.js +6684 -0
- package/dist/handoff-checkpoint.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/posthog-api.d.ts +2 -0
- package/dist/posthog-api.js +18 -2
- package/dist/posthog-api.js.map +1 -1
- package/dist/resume.d.ts +4 -8
- package/dist/resume.js +266 -6491
- package/dist/resume.js.map +1 -1
- package/dist/server/agent-server.d.ts +7 -16
- package/dist/server/agent-server.js +2333 -1383
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +2332 -1382
- package/dist/server/bin.cjs.map +1 -1
- package/dist/server/schemas.d.ts +191 -0
- package/dist/server/schemas.js +108 -0
- package/dist/server/schemas.js.map +1 -0
- package/dist/tree-tracker.d.ts +1 -0
- package/dist/tree-tracker.js +18 -4
- package/dist/tree-tracker.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +10 -2
- package/src/acp-extensions.ts +3 -0
- package/src/handoff-checkpoint.test.ts +183 -0
- package/src/handoff-checkpoint.ts +367 -0
- package/src/posthog-api.test.ts +29 -0
- package/src/posthog-api.ts +13 -1
- package/src/resume.ts +27 -12
- package/src/sagas/apply-snapshot-saga.ts +7 -0
- package/src/sagas/capture-tree-saga.ts +10 -3
- package/src/sagas/resume-saga.test.ts +7 -47
- package/src/sagas/resume-saga.ts +42 -64
- package/src/sagas/test-fixtures.ts +46 -0
- package/src/server/agent-server.ts +193 -70
- package/src/server/schemas.ts +21 -2
- package/src/types.ts +24 -0
package/src/posthog-api.ts
CHANGED
|
@@ -153,6 +153,14 @@ export class PostHogAPIClient {
|
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
async resumeRunInCloud(taskId: string, runId: string): Promise<TaskRun> {
|
|
157
|
+
const teamId = this.getTeamId();
|
|
158
|
+
return this.apiRequest<TaskRun>(
|
|
159
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/resume_in_cloud/`,
|
|
160
|
+
{ method: "POST" },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
async updateTaskRun(
|
|
157
165
|
taskId: string,
|
|
158
166
|
runId: string,
|
|
@@ -230,7 +238,11 @@ export class PostHogAPIClient {
|
|
|
230
238
|
},
|
|
231
239
|
);
|
|
232
240
|
|
|
233
|
-
|
|
241
|
+
const manifest = response.artifacts ?? [];
|
|
242
|
+
|
|
243
|
+
// The backend returns the full run artifact manifest after each upload.
|
|
244
|
+
// Callers want the artifacts corresponding to this upload request only.
|
|
245
|
+
return manifest.slice(-artifacts.length);
|
|
234
246
|
}
|
|
235
247
|
|
|
236
248
|
/**
|
package/src/resume.ts
CHANGED
|
@@ -19,14 +19,17 @@ import type { ContentBlock } from "@agentclientprotocol/sdk";
|
|
|
19
19
|
import { selectRecentTurns } from "./adapters/claude/session/jsonl-hydration";
|
|
20
20
|
import type { PostHogAPIClient } from "./posthog-api";
|
|
21
21
|
import { ResumeSaga } from "./sagas/resume-saga";
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
DeviceInfo,
|
|
24
|
+
GitCheckpointEvent,
|
|
25
|
+
TreeSnapshotEvent,
|
|
26
|
+
} from "./types";
|
|
23
27
|
import { Logger } from "./utils/logger";
|
|
24
28
|
|
|
25
29
|
export interface ResumeState {
|
|
26
30
|
conversation: ConversationTurn[];
|
|
27
31
|
latestSnapshot: TreeSnapshotEvent | null;
|
|
28
|
-
|
|
29
|
-
snapshotApplied: boolean;
|
|
32
|
+
latestGitCheckpoint: GitCheckpointEvent | null;
|
|
30
33
|
interrupted: boolean;
|
|
31
34
|
lastDevice?: DeviceInfo;
|
|
32
35
|
logEntryCount: number;
|
|
@@ -56,11 +59,7 @@ export interface ResumeConfig {
|
|
|
56
59
|
/**
|
|
57
60
|
* Resume a task from its persisted log.
|
|
58
61
|
* Returns the rebuilt state for the agent to continue from.
|
|
59
|
-
*
|
|
60
|
-
* Uses Saga pattern internally for atomic operations.
|
|
61
|
-
* Note: snapshotApplied field indicates if files were actually restored -
|
|
62
|
-
* even if latestSnapshot is non-null, files may not have been restored if
|
|
63
|
-
* the snapshot had no archive URL or download/extraction failed.
|
|
62
|
+
* Snapshot and checkpoint application happens in the agent server after SSE connects.
|
|
64
63
|
*/
|
|
65
64
|
export async function resumeFromLog(
|
|
66
65
|
config: ResumeConfig,
|
|
@@ -96,7 +95,7 @@ export async function resumeFromLog(
|
|
|
96
95
|
return {
|
|
97
96
|
conversation: result.data.conversation as ConversationTurn[],
|
|
98
97
|
latestSnapshot: result.data.latestSnapshot,
|
|
99
|
-
|
|
98
|
+
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
100
99
|
interrupted: result.data.interrupted,
|
|
101
100
|
lastDevice: result.data.lastDevice,
|
|
102
101
|
logEntryCount: result.data.logEntryCount,
|
|
@@ -118,15 +117,31 @@ export function conversationToPromptHistory(
|
|
|
118
117
|
const RESUME_HISTORY_TOKEN_BUDGET = 50_000;
|
|
119
118
|
const TOOL_RESULT_MAX_CHARS = 2000;
|
|
120
119
|
|
|
120
|
+
const RESUME_CONTEXT_MARKERS = [
|
|
121
|
+
"You are resuming a previous conversation",
|
|
122
|
+
"Here is the conversation history from the",
|
|
123
|
+
"Continue from where you left off",
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
function isResumeContextTurn(turn: ConversationTurn): boolean {
|
|
127
|
+
if (turn.role !== "user") return false;
|
|
128
|
+
const text = turn.content
|
|
129
|
+
.filter((b) => b.type === "text")
|
|
130
|
+
.map((b) => (b as { type: "text"; text: string }).text)
|
|
131
|
+
.join("");
|
|
132
|
+
return RESUME_CONTEXT_MARKERS.some((marker) => text.includes(marker));
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
export function formatConversationForResume(
|
|
122
136
|
conversation: ConversationTurn[],
|
|
123
137
|
): string {
|
|
124
|
-
const
|
|
138
|
+
const filtered = conversation.filter((turn) => !isResumeContextTurn(turn));
|
|
139
|
+
const selected = selectRecentTurns(filtered, RESUME_HISTORY_TOKEN_BUDGET);
|
|
125
140
|
const parts: string[] = [];
|
|
126
141
|
|
|
127
|
-
if (selected.length <
|
|
142
|
+
if (selected.length < filtered.length) {
|
|
128
143
|
parts.push(
|
|
129
|
-
`*(${
|
|
144
|
+
`*(${filtered.length - selected.length} earlier turns omitted)*`,
|
|
130
145
|
);
|
|
131
146
|
}
|
|
132
147
|
|
|
@@ -59,6 +59,13 @@ export class ApplySnapshotSaga extends Saga<
|
|
|
59
59
|
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
60
60
|
const binaryContent = Buffer.from(base64Content, "base64");
|
|
61
61
|
await writeFile(archivePath, binaryContent);
|
|
62
|
+
this.log.info("Tree archive downloaded", {
|
|
63
|
+
treeHash: snapshot.treeHash,
|
|
64
|
+
snapshotBytes: binaryContent.byteLength,
|
|
65
|
+
snapshotWireBytes: arrayBuffer.byteLength,
|
|
66
|
+
totalBytes: binaryContent.byteLength,
|
|
67
|
+
totalWireBytes: arrayBuffer.byteLength,
|
|
68
|
+
});
|
|
62
69
|
},
|
|
63
70
|
rollback: async () => {
|
|
64
71
|
if (this.archivePath) {
|
|
@@ -113,6 +113,8 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
|
|
|
113
113
|
execute: async () => {
|
|
114
114
|
const archiveContent = await readFile(archivePath);
|
|
115
115
|
const base64Content = archiveContent.toString("base64");
|
|
116
|
+
const snapshotBytes = archiveContent.byteLength;
|
|
117
|
+
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
116
118
|
|
|
117
119
|
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
118
120
|
{
|
|
@@ -123,12 +125,17 @@ export class CaptureTreeSaga extends Saga<CaptureTreeInput, CaptureTreeOutput> {
|
|
|
123
125
|
},
|
|
124
126
|
]);
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
const uploadedArtifact = artifacts[0];
|
|
129
|
+
if (uploadedArtifact?.storage_path) {
|
|
127
130
|
this.log.info("Tree archive uploaded", {
|
|
128
|
-
storagePath:
|
|
131
|
+
storagePath: uploadedArtifact.storage_path,
|
|
129
132
|
treeHash,
|
|
133
|
+
snapshotBytes,
|
|
134
|
+
snapshotWireBytes,
|
|
135
|
+
totalBytes: snapshotBytes,
|
|
136
|
+
totalWireBytes: snapshotWireBytes,
|
|
130
137
|
});
|
|
131
|
-
return
|
|
138
|
+
return uploadedArtifact.storage_path;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
return undefined;
|
|
@@ -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
|
|
472
|
-
it("
|
|
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.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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("
|
|
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", () => {
|
package/src/sagas/resume-saga.ts
CHANGED
|
@@ -2,13 +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,
|
|
7
|
+
GitCheckpointEvent,
|
|
8
8
|
StoredNotification,
|
|
9
9
|
TreeSnapshotEvent,
|
|
10
10
|
} from "../types";
|
|
11
|
-
import { Logger } from "../utils/logger";
|
|
11
|
+
import type { Logger } from "../utils/logger";
|
|
12
12
|
|
|
13
13
|
export interface ConversationTurn {
|
|
14
14
|
role: "user" | "assistant";
|
|
@@ -34,7 +34,7 @@ export interface ResumeInput {
|
|
|
34
34
|
export interface ResumeOutput {
|
|
35
35
|
conversation: ConversationTurn[];
|
|
36
36
|
latestSnapshot: TreeSnapshotEvent | null;
|
|
37
|
-
|
|
37
|
+
latestGitCheckpoint: GitCheckpointEvent | null;
|
|
38
38
|
interrupted: boolean;
|
|
39
39
|
lastDevice?: DeviceInfo;
|
|
40
40
|
logEntryCount: number;
|
|
@@ -44,9 +44,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
44
44
|
readonly sagaName = "ResumeSaga";
|
|
45
45
|
|
|
46
46
|
protected async execute(input: ResumeInput): Promise<ResumeOutput> {
|
|
47
|
-
const { taskId, runId,
|
|
48
|
-
const logger =
|
|
49
|
-
input.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
47
|
+
const { taskId, runId, apiClient } = input;
|
|
50
48
|
|
|
51
49
|
// Step 1: Fetch task run (read-only)
|
|
52
50
|
const taskRun = await this.readOnlyStep("fetch_task_run", () =>
|
|
@@ -75,70 +73,27 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
75
73
|
Promise.resolve(this.findLatestTreeSnapshot(entries)),
|
|
76
74
|
);
|
|
77
75
|
|
|
76
|
+
const latestGitCheckpoint = await this.readOnlyStep(
|
|
77
|
+
"find_git_checkpoint",
|
|
78
|
+
() => Promise.resolve(this.findLatestGitCheckpoint(entries)),
|
|
79
|
+
);
|
|
80
|
+
|
|
78
81
|
// Step 4: Apply snapshot if present (wrapped in step for consistent logging)
|
|
79
|
-
|
|
80
|
-
let snapshotApplied = false;
|
|
81
|
-
if (latestSnapshot?.archiveUrl && repositoryPath) {
|
|
82
|
+
if (latestSnapshot) {
|
|
82
83
|
this.log.info("Found tree snapshot", {
|
|
83
84
|
treeHash: latestSnapshot.treeHash,
|
|
84
|
-
hasArchiveUrl:
|
|
85
|
+
hasArchiveUrl: !!latestSnapshot.archiveUrl,
|
|
85
86
|
changes: latestSnapshot.changes?.length ?? 0,
|
|
86
|
-
interrupted: latestSnapshot.interrupted,
|
|
87
87
|
});
|
|
88
|
+
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
repositoryPath,
|
|
94
|
-
taskId,
|
|
95
|
-
runId,
|
|
96
|
-
apiClient,
|
|
97
|
-
logger: logger.child("TreeTracker"),
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
await treeTracker.applyTreeSnapshot(latestSnapshot);
|
|
102
|
-
treeTracker.setLastTreeHash(latestSnapshot.treeHash);
|
|
103
|
-
snapshotApplied = true;
|
|
104
|
-
this.log.info("Tree snapshot applied successfully", {
|
|
105
|
-
treeHash: latestSnapshot.treeHash,
|
|
106
|
-
});
|
|
107
|
-
} catch (error) {
|
|
108
|
-
// Log but don't fail - continue with conversation rebuild
|
|
109
|
-
// ApplySnapshotSaga handles its own rollback internally
|
|
110
|
-
this.log.warn(
|
|
111
|
-
"Failed to apply tree snapshot, continuing without it",
|
|
112
|
-
{
|
|
113
|
-
error: error instanceof Error ? error.message : String(error),
|
|
114
|
-
treeHash: latestSnapshot.treeHash,
|
|
115
|
-
},
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
rollback: async () => {
|
|
120
|
-
// Inner ApplySnapshotSaga handles its own rollback
|
|
121
|
-
},
|
|
90
|
+
if (latestGitCheckpoint) {
|
|
91
|
+
this.log.info("Found git checkpoint", {
|
|
92
|
+
checkpointId: latestGitCheckpoint.checkpointId,
|
|
93
|
+
branch: latestGitCheckpoint.branch,
|
|
122
94
|
});
|
|
123
|
-
} else if (latestSnapshot?.archiveUrl && !repositoryPath) {
|
|
124
|
-
this.log.warn(
|
|
125
|
-
"Snapshot found but no repositoryPath configured - files cannot be restored",
|
|
126
|
-
{
|
|
127
|
-
treeHash: latestSnapshot.treeHash,
|
|
128
|
-
changes: latestSnapshot.changes?.length ?? 0,
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
} else if (latestSnapshot) {
|
|
132
|
-
this.log.warn(
|
|
133
|
-
"Snapshot found but has no archive URL - files cannot be restored",
|
|
134
|
-
{
|
|
135
|
-
treeHash: latestSnapshot.treeHash,
|
|
136
|
-
changes: latestSnapshot.changes?.length ?? 0,
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
95
|
}
|
|
140
96
|
|
|
141
|
-
// Step 5: Rebuild conversation (read-only, pure computation)
|
|
142
97
|
const conversation = await this.readOnlyStep("rebuild_conversation", () =>
|
|
143
98
|
Promise.resolve(this.rebuildConversation(entries)),
|
|
144
99
|
);
|
|
@@ -151,14 +106,14 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
151
106
|
this.log.info("Resume state rebuilt", {
|
|
152
107
|
turns: conversation.length,
|
|
153
108
|
hasSnapshot: !!latestSnapshot,
|
|
154
|
-
|
|
109
|
+
hasGitCheckpoint: !!latestGitCheckpoint,
|
|
155
110
|
interrupted: latestSnapshot?.interrupted ?? false,
|
|
156
111
|
});
|
|
157
112
|
|
|
158
113
|
return {
|
|
159
114
|
conversation,
|
|
160
115
|
latestSnapshot,
|
|
161
|
-
|
|
116
|
+
latestGitCheckpoint,
|
|
162
117
|
interrupted: latestSnapshot?.interrupted ?? false,
|
|
163
118
|
lastDevice,
|
|
164
119
|
logEntryCount: entries.length,
|
|
@@ -169,7 +124,7 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
169
124
|
return {
|
|
170
125
|
conversation: [],
|
|
171
126
|
latestSnapshot: null,
|
|
172
|
-
|
|
127
|
+
latestGitCheckpoint: null,
|
|
173
128
|
interrupted: false,
|
|
174
129
|
logEntryCount: 0,
|
|
175
130
|
};
|
|
@@ -197,6 +152,29 @@ export class ResumeSaga extends Saga<ResumeInput, ResumeOutput> {
|
|
|
197
152
|
return null;
|
|
198
153
|
}
|
|
199
154
|
|
|
155
|
+
private findLatestGitCheckpoint(
|
|
156
|
+
entries: StoredNotification[],
|
|
157
|
+
): GitCheckpointEvent | null {
|
|
158
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
|
|
159
|
+
|
|
160
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
161
|
+
const entry = entries[i];
|
|
162
|
+
const method = entry.notification?.method;
|
|
163
|
+
if (
|
|
164
|
+
method === sdkPrefixedMethod ||
|
|
165
|
+
method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT
|
|
166
|
+
) {
|
|
167
|
+
const params = entry.notification?.params as
|
|
168
|
+
| GitCheckpointEvent
|
|
169
|
+
| undefined;
|
|
170
|
+
if (params?.checkpointId && params?.checkpointRef) {
|
|
171
|
+
return params;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
200
178
|
private findLastDeviceInfo(
|
|
201
179
|
entries: StoredNotification[],
|
|
202
180
|
): DeviceInfo | undefined {
|
|
@@ -67,6 +67,52 @@ export async function createTestRepo(prefix = "test-repo"): Promise<TestRepo> {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export async function cloneTestRepo(
|
|
71
|
+
sourcePath: string,
|
|
72
|
+
prefix = "test-repo-clone",
|
|
73
|
+
): Promise<TestRepo> {
|
|
74
|
+
const clonePath = join(
|
|
75
|
+
tmpdir(),
|
|
76
|
+
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
77
|
+
);
|
|
78
|
+
await execFileAsync("git", ["clone", sourcePath, clonePath]);
|
|
79
|
+
await execFileAsync("git", ["config", "user.email", "test@test.com"], {
|
|
80
|
+
cwd: clonePath,
|
|
81
|
+
});
|
|
82
|
+
await execFileAsync("git", ["config", "user.name", "Test"], {
|
|
83
|
+
cwd: clonePath,
|
|
84
|
+
});
|
|
85
|
+
await execFileAsync("git", ["config", "commit.gpgsign", "false"], {
|
|
86
|
+
cwd: clonePath,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const git = async (args: string[]): Promise<string> => {
|
|
90
|
+
const { stdout } = await execFileAsync("git", args, { cwd: clonePath });
|
|
91
|
+
return stdout.trim();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
path: clonePath,
|
|
96
|
+
cleanup: () => rm(clonePath, { recursive: true, force: true }),
|
|
97
|
+
git,
|
|
98
|
+
writeFile: async (relativePath: string, content: string) => {
|
|
99
|
+
const fullPath = join(clonePath, relativePath);
|
|
100
|
+
const dir = join(fullPath, "..");
|
|
101
|
+
await mkdir(dir, { recursive: true });
|
|
102
|
+
await writeFile(fullPath, content);
|
|
103
|
+
},
|
|
104
|
+
readFile: async (relativePath: string) => {
|
|
105
|
+
return readFile(join(clonePath, relativePath), "utf-8");
|
|
106
|
+
},
|
|
107
|
+
deleteFile: async (relativePath: string) => {
|
|
108
|
+
await rm(join(clonePath, relativePath), { force: true });
|
|
109
|
+
},
|
|
110
|
+
exists: (relativePath: string) => {
|
|
111
|
+
return existsSync(join(clonePath, relativePath));
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
export function createMockLogger(): SagaLogger {
|
|
71
117
|
return {
|
|
72
118
|
info: vi.fn(),
|