@posthog/agent 2.3.387 → 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/agent.js +8 -1
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +5 -1
- package/dist/handoff-checkpoint.js +22 -17
- package/dist/handoff-checkpoint.js.map +1 -1
- package/dist/posthog-api.d.ts +1 -0
- package/dist/posthog-api.js +8 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/resume.d.ts +1 -7
- package/dist/resume.js +251 -6513
- package/dist/resume.js.map +1 -1
- package/dist/server/agent-server.d.ts +2 -1
- package/dist/server/agent-server.js +1193 -1179
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1199 -1185
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +2 -2
- package/src/handoff-checkpoint.ts +25 -19
- package/src/posthog-api.ts +8 -0
- package/src/resume.ts +20 -11
- package/src/sagas/resume-saga.test.ts +7 -47
- package/src/sagas/resume-saga.ts +10 -64
- package/src/server/agent-server.ts +119 -69
|
@@ -8605,7 +8605,7 @@ import { z as z4 } from "zod";
|
|
|
8605
8605
|
// package.json
|
|
8606
8606
|
var package_default = {
|
|
8607
8607
|
name: "@posthog/agent",
|
|
8608
|
-
version: "2.3.
|
|
8608
|
+
version: "2.3.388",
|
|
8609
8609
|
repository: "https://github.com/PostHog/code",
|
|
8610
8610
|
description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
8611
8611
|
exports: {
|
|
@@ -19157,6 +19157,11 @@ var HandoffCheckpointTracker = class {
|
|
|
19157
19157
|
onDivergedBranch: options?.onDivergedBranch
|
|
19158
19158
|
});
|
|
19159
19159
|
this.logApplyMetrics(checkpoint, downloads, applyResult.totalBytes);
|
|
19160
|
+
return {
|
|
19161
|
+
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
19162
|
+
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
19163
|
+
totalBytes: applyResult.totalBytes
|
|
19164
|
+
};
|
|
19160
19165
|
} finally {
|
|
19161
19166
|
await this.removeIfPresent(packPath);
|
|
19162
19167
|
await this.removeIfPresent(indexPath);
|
|
@@ -19203,22 +19208,22 @@ var HandoffCheckpointTracker = class {
|
|
|
19203
19208
|
};
|
|
19204
19209
|
}
|
|
19205
19210
|
async uploadArtifacts(specs) {
|
|
19206
|
-
const
|
|
19207
|
-
|
|
19208
|
-
|
|
19209
|
-
|
|
19210
|
-
|
|
19211
|
-
|
|
19212
|
-
|
|
19213
|
-
|
|
19214
|
-
|
|
19215
|
-
|
|
19216
|
-
|
|
19217
|
-
|
|
19218
|
-
|
|
19219
|
-
|
|
19220
|
-
|
|
19221
|
-
return Object.fromEntries(
|
|
19211
|
+
const results = [];
|
|
19212
|
+
for (const spec of specs) {
|
|
19213
|
+
if (!spec.filePath) {
|
|
19214
|
+
results.push([spec.key, void 0]);
|
|
19215
|
+
continue;
|
|
19216
|
+
}
|
|
19217
|
+
results.push([
|
|
19218
|
+
spec.key,
|
|
19219
|
+
await this.uploadArtifactFile(
|
|
19220
|
+
spec.filePath,
|
|
19221
|
+
spec.name,
|
|
19222
|
+
spec.contentType
|
|
19223
|
+
)
|
|
19224
|
+
]);
|
|
19225
|
+
}
|
|
19226
|
+
return Object.fromEntries(results);
|
|
19222
19227
|
}
|
|
19223
19228
|
async downloadArtifactToFile(artifactPath, filePath, label) {
|
|
19224
19229
|
if (!this.apiClient) {
|
|
@@ -19230,7 +19235,7 @@ var HandoffCheckpointTracker = class {
|
|
|
19230
19235
|
artifactPath
|
|
19231
19236
|
);
|
|
19232
19237
|
if (!arrayBuffer) {
|
|
19233
|
-
throw new Error(`Failed to download ${label}`);
|
|
19238
|
+
throw new Error(`Failed to download ${label} from ${artifactPath}`);
|
|
19234
19239
|
}
|
|
19235
19240
|
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19236
19241
|
const binaryContent = Buffer.from(base64Content, "base64");
|
|
@@ -19414,6 +19419,13 @@ var PostHogAPIClient = class {
|
|
|
19414
19419
|
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
19415
19420
|
);
|
|
19416
19421
|
}
|
|
19422
|
+
async resumeRunInCloud(taskId, runId) {
|
|
19423
|
+
const teamId = this.getTeamId();
|
|
19424
|
+
return this.apiRequest(
|
|
19425
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/resume_in_cloud/`,
|
|
19426
|
+
{ method: "POST" }
|
|
19427
|
+
);
|
|
19428
|
+
}
|
|
19417
19429
|
async updateTaskRun(taskId, runId, payload) {
|
|
19418
19430
|
const teamId = this.getTeamId();
|
|
19419
19431
|
return this.apiRequest(
|
|
@@ -19561,1215 +19573,1180 @@ function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
|
19561
19573
|
return turns.slice(startIndex);
|
|
19562
19574
|
}
|
|
19563
19575
|
|
|
19564
|
-
// src/sagas/
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
|
|
19571
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
19575
|
-
|
|
19576
|
-
async executeGitOperations(input) {
|
|
19577
|
-
const { baseDir, lastTreeHash, archivePath, signal } = input;
|
|
19578
|
-
const tmpDir = path15.join(baseDir, ".git", "posthog-code-tmp");
|
|
19579
|
-
await this.step({
|
|
19580
|
-
name: "create_tmp_dir",
|
|
19581
|
-
execute: () => fs12.mkdir(tmpDir, { recursive: true }),
|
|
19582
|
-
rollback: async () => {
|
|
19583
|
-
}
|
|
19584
|
-
});
|
|
19585
|
-
this.tempIndexPath = path15.join(tmpDir, `index-${Date.now()}`);
|
|
19586
|
-
const tempIndexGit = this.git.env({
|
|
19587
|
-
...process.env,
|
|
19588
|
-
GIT_INDEX_FILE: this.tempIndexPath
|
|
19589
|
-
});
|
|
19590
|
-
await this.step({
|
|
19591
|
-
name: "init_temp_index",
|
|
19592
|
-
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
19593
|
-
rollback: async () => {
|
|
19594
|
-
if (this.tempIndexPath) {
|
|
19595
|
-
await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
19596
|
-
});
|
|
19597
|
-
}
|
|
19598
|
-
}
|
|
19599
|
-
});
|
|
19600
|
-
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
19601
|
-
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
19602
|
-
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
19603
|
-
this.log.debug("No changes since last capture", { treeHash });
|
|
19604
|
-
await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
19605
|
-
});
|
|
19606
|
-
return { snapshot: null, changed: false };
|
|
19576
|
+
// src/sagas/resume-saga.ts
|
|
19577
|
+
var ResumeSaga = class extends Saga {
|
|
19578
|
+
sagaName = "ResumeSaga";
|
|
19579
|
+
async execute(input) {
|
|
19580
|
+
const { taskId, runId, apiClient } = input;
|
|
19581
|
+
const taskRun = await this.readOnlyStep(
|
|
19582
|
+
"fetch_task_run",
|
|
19583
|
+
() => apiClient.getTaskRun(taskId, runId)
|
|
19584
|
+
);
|
|
19585
|
+
if (!taskRun.log_url) {
|
|
19586
|
+
this.log.info("No log URL found, starting fresh");
|
|
19587
|
+
return this.emptyResult();
|
|
19607
19588
|
}
|
|
19608
|
-
const
|
|
19609
|
-
|
|
19610
|
-
|
|
19611
|
-
|
|
19612
|
-
|
|
19613
|
-
|
|
19614
|
-
|
|
19615
|
-
const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
|
|
19616
|
-
await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
19617
|
-
});
|
|
19618
|
-
const snapshot = {
|
|
19619
|
-
treeHash,
|
|
19620
|
-
baseCommit,
|
|
19621
|
-
changes,
|
|
19622
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
19623
|
-
};
|
|
19624
|
-
let createdArchivePath;
|
|
19625
|
-
if (archivePath) {
|
|
19626
|
-
createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
|
|
19589
|
+
const entries = await this.readOnlyStep(
|
|
19590
|
+
"fetch_logs",
|
|
19591
|
+
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
19592
|
+
);
|
|
19593
|
+
if (entries.length === 0) {
|
|
19594
|
+
this.log.info("No log entries found, starting fresh");
|
|
19595
|
+
return this.emptyResult();
|
|
19627
19596
|
}
|
|
19628
|
-
this.log.info("
|
|
19629
|
-
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
|
|
19633
|
-
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
|
|
19637
|
-
if (
|
|
19638
|
-
|
|
19597
|
+
this.log.info("Fetched log entries", { count: entries.length });
|
|
19598
|
+
const latestSnapshot = await this.readOnlyStep(
|
|
19599
|
+
"find_snapshot",
|
|
19600
|
+
() => Promise.resolve(this.findLatestTreeSnapshot(entries))
|
|
19601
|
+
);
|
|
19602
|
+
const latestGitCheckpoint = await this.readOnlyStep(
|
|
19603
|
+
"find_git_checkpoint",
|
|
19604
|
+
() => Promise.resolve(this.findLatestGitCheckpoint(entries))
|
|
19605
|
+
);
|
|
19606
|
+
if (latestSnapshot) {
|
|
19607
|
+
this.log.info("Found tree snapshot", {
|
|
19608
|
+
treeHash: latestSnapshot.treeHash,
|
|
19609
|
+
hasArchiveUrl: !!latestSnapshot.archiveUrl,
|
|
19610
|
+
changes: latestSnapshot.changes?.length ?? 0
|
|
19611
|
+
});
|
|
19639
19612
|
}
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19613
|
+
if (latestGitCheckpoint) {
|
|
19614
|
+
this.log.info("Found git checkpoint", {
|
|
19615
|
+
checkpointId: latestGitCheckpoint.checkpointId,
|
|
19616
|
+
branch: latestGitCheckpoint.branch
|
|
19617
|
+
});
|
|
19643
19618
|
}
|
|
19644
|
-
await this.
|
|
19645
|
-
|
|
19646
|
-
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
});
|
|
19658
|
-
}
|
|
19619
|
+
const conversation = await this.readOnlyStep(
|
|
19620
|
+
"rebuild_conversation",
|
|
19621
|
+
() => Promise.resolve(this.rebuildConversation(entries))
|
|
19622
|
+
);
|
|
19623
|
+
const lastDevice = await this.readOnlyStep(
|
|
19624
|
+
"find_device",
|
|
19625
|
+
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
19626
|
+
);
|
|
19627
|
+
this.log.info("Resume state rebuilt", {
|
|
19628
|
+
turns: conversation.length,
|
|
19629
|
+
hasSnapshot: !!latestSnapshot,
|
|
19630
|
+
hasGitCheckpoint: !!latestGitCheckpoint,
|
|
19631
|
+
interrupted: latestSnapshot?.interrupted ?? false
|
|
19659
19632
|
});
|
|
19660
|
-
return
|
|
19633
|
+
return {
|
|
19634
|
+
conversation,
|
|
19635
|
+
latestSnapshot,
|
|
19636
|
+
latestGitCheckpoint,
|
|
19637
|
+
interrupted: latestSnapshot?.interrupted ?? false,
|
|
19638
|
+
lastDevice,
|
|
19639
|
+
logEntryCount: entries.length
|
|
19640
|
+
};
|
|
19661
19641
|
}
|
|
19662
|
-
|
|
19663
|
-
|
|
19664
|
-
|
|
19665
|
-
|
|
19666
|
-
|
|
19667
|
-
|
|
19668
|
-
|
|
19669
|
-
|
|
19670
|
-
|
|
19671
|
-
|
|
19672
|
-
|
|
19673
|
-
|
|
19674
|
-
|
|
19675
|
-
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
|
|
19681
|
-
|
|
19682
|
-
if (status === "D") {
|
|
19683
|
-
normalizedStatus = "D";
|
|
19684
|
-
} else if (status === "A") {
|
|
19685
|
-
normalizedStatus = "A";
|
|
19686
|
-
} else {
|
|
19687
|
-
normalizedStatus = "M";
|
|
19642
|
+
emptyResult() {
|
|
19643
|
+
return {
|
|
19644
|
+
conversation: [],
|
|
19645
|
+
latestSnapshot: null,
|
|
19646
|
+
latestGitCheckpoint: null,
|
|
19647
|
+
interrupted: false,
|
|
19648
|
+
logEntryCount: 0
|
|
19649
|
+
};
|
|
19650
|
+
}
|
|
19651
|
+
findLatestTreeSnapshot(entries) {
|
|
19652
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19653
|
+
const entry = entries[i2];
|
|
19654
|
+
if (isNotification(
|
|
19655
|
+
entry.notification?.method,
|
|
19656
|
+
POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
19657
|
+
)) {
|
|
19658
|
+
const params = entry.notification.params;
|
|
19659
|
+
if (params?.treeHash) {
|
|
19660
|
+
return params;
|
|
19661
|
+
}
|
|
19688
19662
|
}
|
|
19689
|
-
changes.push({ path: filePath, status: normalizedStatus });
|
|
19690
19663
|
}
|
|
19691
|
-
return
|
|
19664
|
+
return null;
|
|
19692
19665
|
}
|
|
19693
|
-
|
|
19694
|
-
|
|
19695
|
-
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
|
|
19699
|
-
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
|
|
19703
|
-
let head = null;
|
|
19704
|
-
let branch = null;
|
|
19705
|
-
try {
|
|
19706
|
-
head = await this.git.revparse(["HEAD"]);
|
|
19707
|
-
} catch {
|
|
19708
|
-
head = null;
|
|
19666
|
+
findLatestGitCheckpoint(entries) {
|
|
19667
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
|
|
19668
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19669
|
+
const entry = entries[i2];
|
|
19670
|
+
const method = entry.notification?.method;
|
|
19671
|
+
if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
|
|
19672
|
+
const params = entry.notification?.params;
|
|
19673
|
+
if (params?.checkpointId && params?.checkpointRef) {
|
|
19674
|
+
return params;
|
|
19675
|
+
}
|
|
19709
19676
|
}
|
|
19710
|
-
|
|
19711
|
-
|
|
19712
|
-
|
|
19713
|
-
|
|
19677
|
+
}
|
|
19678
|
+
return null;
|
|
19679
|
+
}
|
|
19680
|
+
findLastDeviceInfo(entries) {
|
|
19681
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19682
|
+
const entry = entries[i2];
|
|
19683
|
+
const params = entry.notification?.params;
|
|
19684
|
+
if (params?.device) {
|
|
19685
|
+
return params.device;
|
|
19714
19686
|
}
|
|
19715
|
-
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
|
|
19727
|
-
|
|
19728
|
-
|
|
19729
|
-
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
|
|
19734
|
-
|
|
19735
|
-
|
|
19736
|
-
|
|
19737
|
-
|
|
19738
|
-
|
|
19739
|
-
rollback: async () => {
|
|
19740
|
-
try {
|
|
19741
|
-
if (this.originalBranch) {
|
|
19742
|
-
await this.git.checkout(this.originalBranch);
|
|
19743
|
-
} else if (this.originalHead) {
|
|
19744
|
-
await this.git.checkout(this.originalHead);
|
|
19687
|
+
}
|
|
19688
|
+
return void 0;
|
|
19689
|
+
}
|
|
19690
|
+
rebuildConversation(entries) {
|
|
19691
|
+
const turns = [];
|
|
19692
|
+
let currentAssistantContent = [];
|
|
19693
|
+
let currentToolCalls = [];
|
|
19694
|
+
for (const entry of entries) {
|
|
19695
|
+
const method = entry.notification?.method;
|
|
19696
|
+
const params = entry.notification?.params;
|
|
19697
|
+
if (method === "session/update" && params?.update) {
|
|
19698
|
+
const update = params.update;
|
|
19699
|
+
const sessionUpdate = update.sessionUpdate;
|
|
19700
|
+
switch (sessionUpdate) {
|
|
19701
|
+
case "user_message":
|
|
19702
|
+
case "user_message_chunk": {
|
|
19703
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19704
|
+
turns.push({
|
|
19705
|
+
role: "assistant",
|
|
19706
|
+
content: currentAssistantContent,
|
|
19707
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19708
|
+
});
|
|
19709
|
+
currentAssistantContent = [];
|
|
19710
|
+
currentToolCalls = [];
|
|
19745
19711
|
}
|
|
19746
|
-
|
|
19747
|
-
|
|
19712
|
+
const content = update.content;
|
|
19713
|
+
const contentArray = Array.isArray(content) ? content : [content];
|
|
19714
|
+
turns.push({
|
|
19715
|
+
role: "user",
|
|
19716
|
+
content: contentArray
|
|
19717
|
+
});
|
|
19718
|
+
break;
|
|
19748
19719
|
}
|
|
19749
|
-
|
|
19750
|
-
|
|
19751
|
-
|
|
19752
|
-
|
|
19753
|
-
|
|
19754
|
-
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
} catch {
|
|
19720
|
+
case "agent_message": {
|
|
19721
|
+
const content = update.content;
|
|
19722
|
+
if (content) {
|
|
19723
|
+
if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
|
|
19724
|
+
const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
|
|
19725
|
+
lastBlock.text += content.text;
|
|
19726
|
+
} else {
|
|
19727
|
+
currentAssistantContent.push(content);
|
|
19728
|
+
}
|
|
19729
|
+
}
|
|
19730
|
+
break;
|
|
19761
19731
|
}
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
|
|
19765
|
-
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
this.extractedFiles = filesToExtract;
|
|
19772
|
-
},
|
|
19773
|
-
rollback: async () => {
|
|
19774
|
-
for (const filePath of this.extractedFiles) {
|
|
19775
|
-
const fullPath = path15.join(baseDir, filePath);
|
|
19776
|
-
const backup = this.fileBackups.get(filePath);
|
|
19777
|
-
if (backup) {
|
|
19778
|
-
const dir = path15.dirname(fullPath);
|
|
19779
|
-
await fs12.mkdir(dir, { recursive: true }).catch(() => {
|
|
19780
|
-
});
|
|
19781
|
-
await fs12.writeFile(fullPath, backup).catch(() => {
|
|
19782
|
-
});
|
|
19783
|
-
} else {
|
|
19784
|
-
await fs12.rm(fullPath, { force: true }).catch(() => {
|
|
19785
|
-
});
|
|
19732
|
+
case "agent_message_chunk": {
|
|
19733
|
+
const content = update.content;
|
|
19734
|
+
if (content) {
|
|
19735
|
+
if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
|
|
19736
|
+
const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
|
|
19737
|
+
lastBlock.text += content.text;
|
|
19738
|
+
} else {
|
|
19739
|
+
currentAssistantContent.push(content);
|
|
19740
|
+
}
|
|
19786
19741
|
}
|
|
19742
|
+
break;
|
|
19787
19743
|
}
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
19812
|
-
}
|
|
19744
|
+
case "tool_call":
|
|
19745
|
+
case "tool_call_update": {
|
|
19746
|
+
const meta = update._meta?.claudeCode;
|
|
19747
|
+
if (meta) {
|
|
19748
|
+
const toolCallId = meta.toolCallId;
|
|
19749
|
+
const toolName = meta.toolName;
|
|
19750
|
+
const toolInput = meta.toolInput;
|
|
19751
|
+
const toolResponse = meta.toolResponse;
|
|
19752
|
+
if (toolCallId && toolName) {
|
|
19753
|
+
let toolCall = currentToolCalls.find(
|
|
19754
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19755
|
+
);
|
|
19756
|
+
if (!toolCall) {
|
|
19757
|
+
toolCall = {
|
|
19758
|
+
toolCallId,
|
|
19759
|
+
toolName,
|
|
19760
|
+
input: toolInput
|
|
19761
|
+
};
|
|
19762
|
+
currentToolCalls.push(toolCall);
|
|
19763
|
+
}
|
|
19764
|
+
if (toolResponse !== void 0) {
|
|
19765
|
+
toolCall.result = toolResponse;
|
|
19766
|
+
}
|
|
19767
|
+
}
|
|
19768
|
+
}
|
|
19769
|
+
break;
|
|
19770
|
+
}
|
|
19771
|
+
case "tool_result": {
|
|
19772
|
+
const meta = update._meta?.claudeCode;
|
|
19773
|
+
if (meta) {
|
|
19774
|
+
const toolCallId = meta.toolCallId;
|
|
19775
|
+
const toolResponse = meta.toolResponse;
|
|
19776
|
+
if (toolCallId) {
|
|
19777
|
+
const toolCall = currentToolCalls.find(
|
|
19778
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19779
|
+
);
|
|
19780
|
+
if (toolCall && toolResponse !== void 0) {
|
|
19781
|
+
toolCall.result = toolResponse;
|
|
19782
|
+
}
|
|
19783
|
+
}
|
|
19784
|
+
}
|
|
19785
|
+
break;
|
|
19813
19786
|
}
|
|
19814
19787
|
}
|
|
19788
|
+
}
|
|
19789
|
+
}
|
|
19790
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19791
|
+
turns.push({
|
|
19792
|
+
role: "assistant",
|
|
19793
|
+
content: currentAssistantContent,
|
|
19794
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19815
19795
|
});
|
|
19816
19796
|
}
|
|
19817
|
-
|
|
19818
|
-
this.log.info("Tree applied", {
|
|
19819
|
-
treeHash,
|
|
19820
|
-
totalChanges: changes.length,
|
|
19821
|
-
deletedFiles: deletedCount,
|
|
19822
|
-
checkoutPerformed
|
|
19823
|
-
});
|
|
19824
|
-
return { treeHash, checkoutPerformed };
|
|
19797
|
+
return turns;
|
|
19825
19798
|
}
|
|
19826
19799
|
};
|
|
19827
19800
|
|
|
19828
|
-
// src/
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19801
|
+
// src/resume.ts
|
|
19802
|
+
async function resumeFromLog(config) {
|
|
19803
|
+
const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
19804
|
+
logger.info("Resuming from log", {
|
|
19805
|
+
taskId: config.taskId,
|
|
19806
|
+
runId: config.runId
|
|
19807
|
+
});
|
|
19808
|
+
const saga = new ResumeSaga(logger);
|
|
19809
|
+
const result = await saga.run({
|
|
19810
|
+
taskId: config.taskId,
|
|
19811
|
+
runId: config.runId,
|
|
19812
|
+
repositoryPath: config.repositoryPath,
|
|
19813
|
+
apiClient: config.apiClient,
|
|
19814
|
+
logger
|
|
19815
|
+
});
|
|
19816
|
+
if (!result.success) {
|
|
19817
|
+
logger.error("Failed to resume from log", {
|
|
19818
|
+
error: result.error,
|
|
19819
|
+
failedStep: result.failedStep
|
|
19844
19820
|
});
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19821
|
+
throw new Error(
|
|
19822
|
+
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
19823
|
+
);
|
|
19824
|
+
}
|
|
19825
|
+
return {
|
|
19826
|
+
conversation: result.data.conversation,
|
|
19827
|
+
latestSnapshot: result.data.latestSnapshot,
|
|
19828
|
+
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
19829
|
+
interrupted: result.data.interrupted,
|
|
19830
|
+
lastDevice: result.data.lastDevice,
|
|
19831
|
+
logEntryCount: result.data.logEntryCount
|
|
19832
|
+
};
|
|
19833
|
+
}
|
|
19834
|
+
var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
|
|
19835
|
+
var TOOL_RESULT_MAX_CHARS = 2e3;
|
|
19836
|
+
var RESUME_CONTEXT_MARKERS = [
|
|
19837
|
+
"You are resuming a previous conversation",
|
|
19838
|
+
"Here is the conversation history from the",
|
|
19839
|
+
"Continue from where you left off"
|
|
19840
|
+
];
|
|
19841
|
+
function isResumeContextTurn(turn) {
|
|
19842
|
+
if (turn.role !== "user") return false;
|
|
19843
|
+
const text2 = turn.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
19844
|
+
return RESUME_CONTEXT_MARKERS.some((marker) => text2.includes(marker));
|
|
19845
|
+
}
|
|
19846
|
+
function formatConversationForResume(conversation) {
|
|
19847
|
+
const filtered = conversation.filter((turn) => !isResumeContextTurn(turn));
|
|
19848
|
+
const selected = selectRecentTurns(filtered, RESUME_HISTORY_TOKEN_BUDGET);
|
|
19849
|
+
const parts2 = [];
|
|
19850
|
+
if (selected.length < filtered.length) {
|
|
19851
|
+
parts2.push(
|
|
19852
|
+
`*(${filtered.length - selected.length} earlier turns omitted)*`
|
|
19853
|
+
);
|
|
19854
|
+
}
|
|
19855
|
+
for (const turn of selected) {
|
|
19856
|
+
const role = turn.role === "user" ? "User" : "Assistant";
|
|
19857
|
+
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
19858
|
+
if (textParts.length > 0) {
|
|
19859
|
+
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
19860
|
+
}
|
|
19861
|
+
if (turn.toolCalls?.length) {
|
|
19862
|
+
const toolSummary = turn.toolCalls.map((tc) => {
|
|
19863
|
+
let resultStr = "";
|
|
19864
|
+
if (tc.result !== void 0) {
|
|
19865
|
+
const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
|
|
19866
|
+
resultStr = raw.length > TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
|
|
19857
19867
|
}
|
|
19858
|
-
|
|
19859
|
-
|
|
19860
|
-
|
|
19861
|
-
|
|
19862
|
-
treeHash: snapshot.treeHash,
|
|
19863
|
-
snapshotBytes: binaryContent.byteLength,
|
|
19864
|
-
snapshotWireBytes: arrayBuffer.byteLength,
|
|
19865
|
-
totalBytes: binaryContent.byteLength,
|
|
19866
|
-
totalWireBytes: arrayBuffer.byteLength
|
|
19867
|
-
});
|
|
19868
|
-
},
|
|
19869
|
-
rollback: async () => {
|
|
19870
|
-
if (this.archivePath) {
|
|
19871
|
-
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
19872
|
-
});
|
|
19873
|
-
}
|
|
19874
|
-
}
|
|
19875
|
-
});
|
|
19876
|
-
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
19877
|
-
const applyResult = await gitApplySaga.run({
|
|
19878
|
-
baseDir: repositoryPath,
|
|
19879
|
-
treeHash: snapshot.treeHash,
|
|
19880
|
-
baseCommit: snapshot.baseCommit,
|
|
19881
|
-
changes: snapshot.changes,
|
|
19882
|
-
archivePath: this.archivePath
|
|
19883
|
-
});
|
|
19884
|
-
if (!applyResult.success) {
|
|
19885
|
-
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
19868
|
+
return ` - ${tc.toolName}${resultStr}`;
|
|
19869
|
+
}).join("\n");
|
|
19870
|
+
parts2.push(`**${role} (tools)**:
|
|
19871
|
+
${toolSummary}`);
|
|
19886
19872
|
}
|
|
19887
|
-
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
19888
|
-
});
|
|
19889
|
-
this.log.info("Tree snapshot applied", {
|
|
19890
|
-
treeHash: snapshot.treeHash,
|
|
19891
|
-
totalChanges: snapshot.changes.length,
|
|
19892
|
-
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
19893
|
-
});
|
|
19894
|
-
return { treeHash: snapshot.treeHash };
|
|
19895
19873
|
}
|
|
19896
|
-
|
|
19874
|
+
return parts2.join("\n\n");
|
|
19875
|
+
}
|
|
19897
19876
|
|
|
19898
|
-
// src/
|
|
19899
|
-
import
|
|
19900
|
-
import
|
|
19901
|
-
import
|
|
19902
|
-
var
|
|
19903
|
-
|
|
19904
|
-
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
if (!captureResult.success) {
|
|
19928
|
-
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
19877
|
+
// src/session-log-writer.ts
|
|
19878
|
+
import fs12 from "fs";
|
|
19879
|
+
import fsp from "fs/promises";
|
|
19880
|
+
import path15 from "path";
|
|
19881
|
+
var SessionLogWriter = class _SessionLogWriter {
|
|
19882
|
+
static FLUSH_DEBOUNCE_MS = 500;
|
|
19883
|
+
static FLUSH_MAX_INTERVAL_MS = 5e3;
|
|
19884
|
+
static MAX_FLUSH_RETRIES = 10;
|
|
19885
|
+
static MAX_RETRY_DELAY_MS = 3e4;
|
|
19886
|
+
static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
19887
|
+
posthogAPI;
|
|
19888
|
+
pendingEntries = /* @__PURE__ */ new Map();
|
|
19889
|
+
flushTimeouts = /* @__PURE__ */ new Map();
|
|
19890
|
+
lastFlushAttemptTime = /* @__PURE__ */ new Map();
|
|
19891
|
+
retryCounts = /* @__PURE__ */ new Map();
|
|
19892
|
+
sessions = /* @__PURE__ */ new Map();
|
|
19893
|
+
flushQueues = /* @__PURE__ */ new Map();
|
|
19894
|
+
logger;
|
|
19895
|
+
localCachePath;
|
|
19896
|
+
constructor(options = {}) {
|
|
19897
|
+
this.posthogAPI = options.posthogAPI;
|
|
19898
|
+
this.localCachePath = options.localCachePath;
|
|
19899
|
+
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
19900
|
+
}
|
|
19901
|
+
async flushAll() {
|
|
19902
|
+
const flushPromises = [];
|
|
19903
|
+
for (const [sessionId, session] of this.sessions) {
|
|
19904
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19905
|
+
flushPromises.push(this.flush(sessionId));
|
|
19929
19906
|
}
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
|
|
19935
|
-
if (!changed || !gitSnapshot) {
|
|
19936
|
-
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
19937
|
-
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
19907
|
+
await Promise.all(flushPromises);
|
|
19908
|
+
}
|
|
19909
|
+
register(sessionId, context) {
|
|
19910
|
+
if (this.sessions.has(sessionId)) {
|
|
19911
|
+
return;
|
|
19938
19912
|
}
|
|
19939
|
-
|
|
19940
|
-
|
|
19913
|
+
this.sessions.set(sessionId, { context, currentTurnMessages: [] });
|
|
19914
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
19915
|
+
if (this.localCachePath) {
|
|
19916
|
+
const sessionDir = path15.join(
|
|
19917
|
+
this.localCachePath,
|
|
19918
|
+
"sessions",
|
|
19919
|
+
context.runId
|
|
19920
|
+
);
|
|
19941
19921
|
try {
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
runId
|
|
19948
|
-
);
|
|
19949
|
-
} finally {
|
|
19950
|
-
await rm7(createdArchivePath, { force: true }).catch(() => {
|
|
19922
|
+
fs12.mkdirSync(sessionDir, { recursive: true });
|
|
19923
|
+
} catch (error) {
|
|
19924
|
+
this.logger.warn("Failed to create local cache directory", {
|
|
19925
|
+
sessionDir,
|
|
19926
|
+
error
|
|
19951
19927
|
});
|
|
19952
19928
|
}
|
|
19953
19929
|
}
|
|
19954
|
-
const snapshot = {
|
|
19955
|
-
treeHash: gitSnapshot.treeHash,
|
|
19956
|
-
baseCommit: gitSnapshot.baseCommit,
|
|
19957
|
-
changes: gitSnapshot.changes,
|
|
19958
|
-
timestamp: gitSnapshot.timestamp,
|
|
19959
|
-
interrupted,
|
|
19960
|
-
archiveUrl
|
|
19961
|
-
};
|
|
19962
|
-
this.log.info("Tree captured", {
|
|
19963
|
-
treeHash: snapshot.treeHash,
|
|
19964
|
-
changes: snapshot.changes.length,
|
|
19965
|
-
interrupted,
|
|
19966
|
-
archiveUrl
|
|
19967
|
-
});
|
|
19968
|
-
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
19969
19930
|
}
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19931
|
+
isRegistered(sessionId) {
|
|
19932
|
+
return this.sessions.has(sessionId);
|
|
19933
|
+
}
|
|
19934
|
+
appendRawLine(sessionId, line) {
|
|
19935
|
+
const session = this.sessions.get(sessionId);
|
|
19936
|
+
if (!session) {
|
|
19937
|
+
this.logger.warn("appendRawLine called for unregistered session", {
|
|
19938
|
+
sessionId
|
|
19939
|
+
});
|
|
19940
|
+
return;
|
|
19941
|
+
}
|
|
19942
|
+
try {
|
|
19943
|
+
const message = JSON.parse(line);
|
|
19944
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19945
|
+
if (this.isAgentMessageChunk(message)) {
|
|
19946
|
+
const text2 = this.extractChunkText(message);
|
|
19947
|
+
if (text2) {
|
|
19948
|
+
if (!session.chunkBuffer) {
|
|
19949
|
+
session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
|
|
19950
|
+
} else {
|
|
19951
|
+
session.chunkBuffer.text += text2;
|
|
19984
19952
|
}
|
|
19985
|
-
]);
|
|
19986
|
-
const uploadedArtifact = artifacts[0];
|
|
19987
|
-
if (uploadedArtifact?.storage_path) {
|
|
19988
|
-
this.log.info("Tree archive uploaded", {
|
|
19989
|
-
storagePath: uploadedArtifact.storage_path,
|
|
19990
|
-
treeHash,
|
|
19991
|
-
snapshotBytes,
|
|
19992
|
-
snapshotWireBytes,
|
|
19993
|
-
totalBytes: snapshotBytes,
|
|
19994
|
-
totalWireBytes: snapshotWireBytes
|
|
19995
|
-
});
|
|
19996
|
-
return uploadedArtifact.storage_path;
|
|
19997
19953
|
}
|
|
19998
|
-
return
|
|
19999
|
-
}
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
19954
|
+
return;
|
|
19955
|
+
}
|
|
19956
|
+
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
19957
|
+
session.chunkBuffer = void 0;
|
|
19958
|
+
} else {
|
|
19959
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19960
|
+
}
|
|
19961
|
+
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
19962
|
+
if (nonChunkAgentText) {
|
|
19963
|
+
session.lastAgentMessage = nonChunkAgentText;
|
|
19964
|
+
session.currentTurnMessages.push(nonChunkAgentText);
|
|
19965
|
+
}
|
|
19966
|
+
const entry = {
|
|
19967
|
+
type: "notification",
|
|
19968
|
+
timestamp,
|
|
19969
|
+
notification: message
|
|
19970
|
+
};
|
|
19971
|
+
this.writeToLocalCache(sessionId, entry);
|
|
19972
|
+
if (this.posthogAPI) {
|
|
19973
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
19974
|
+
pending.push(entry);
|
|
19975
|
+
this.pendingEntries.set(sessionId, pending);
|
|
19976
|
+
this.scheduleFlush(sessionId);
|
|
19977
|
+
}
|
|
19978
|
+
} catch {
|
|
19979
|
+
this.logger.warn("Failed to parse raw line for persistence", {
|
|
19980
|
+
taskId: session.context.taskId,
|
|
19981
|
+
runId: session.context.runId,
|
|
19982
|
+
lineLength: line.length
|
|
19983
|
+
});
|
|
19984
|
+
}
|
|
19985
|
+
}
|
|
19986
|
+
async flush(sessionId, { coalesce = false } = {}) {
|
|
19987
|
+
if (coalesce) {
|
|
19988
|
+
const session = this.sessions.get(sessionId);
|
|
19989
|
+
if (session) {
|
|
19990
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19991
|
+
}
|
|
19992
|
+
}
|
|
19993
|
+
const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
|
|
19994
|
+
const next = prev.catch(() => {
|
|
19995
|
+
}).then(() => this._doFlush(sessionId));
|
|
19996
|
+
this.flushQueues.set(sessionId, next);
|
|
19997
|
+
next.finally(() => {
|
|
19998
|
+
if (this.flushQueues.get(sessionId) === next) {
|
|
19999
|
+
this.flushQueues.delete(sessionId);
|
|
20003
20000
|
}
|
|
20004
20001
|
});
|
|
20005
|
-
return
|
|
20002
|
+
return next;
|
|
20006
20003
|
}
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20012
|
-
taskId;
|
|
20013
|
-
runId;
|
|
20014
|
-
apiClient;
|
|
20015
|
-
logger;
|
|
20016
|
-
lastTreeHash = null;
|
|
20017
|
-
constructor(config) {
|
|
20018
|
-
this.repositoryPath = config.repositoryPath;
|
|
20019
|
-
this.taskId = config.taskId;
|
|
20020
|
-
this.runId = config.runId;
|
|
20021
|
-
this.apiClient = config.apiClient;
|
|
20022
|
-
this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
20023
|
-
}
|
|
20024
|
-
/**
|
|
20025
|
-
* Capture current working tree state as a snapshot.
|
|
20026
|
-
* Uses a temporary index to avoid modifying user's staging area.
|
|
20027
|
-
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
20028
|
-
*/
|
|
20029
|
-
async captureTree(options) {
|
|
20030
|
-
const saga = new CaptureTreeSaga2(this.logger);
|
|
20031
|
-
const result = await saga.run({
|
|
20032
|
-
repositoryPath: this.repositoryPath,
|
|
20033
|
-
taskId: this.taskId,
|
|
20034
|
-
runId: this.runId,
|
|
20035
|
-
apiClient: this.apiClient,
|
|
20036
|
-
lastTreeHash: this.lastTreeHash,
|
|
20037
|
-
interrupted: options?.interrupted
|
|
20038
|
-
});
|
|
20039
|
-
if (!result.success) {
|
|
20040
|
-
this.logger.error("Failed to capture tree", {
|
|
20041
|
-
error: result.error,
|
|
20042
|
-
failedStep: result.failedStep
|
|
20043
|
-
});
|
|
20044
|
-
throw new Error(
|
|
20045
|
-
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
20046
|
-
);
|
|
20047
|
-
}
|
|
20048
|
-
if (result.data.newTreeHash !== null) {
|
|
20049
|
-
this.lastTreeHash = result.data.newTreeHash;
|
|
20004
|
+
async _doFlush(sessionId) {
|
|
20005
|
+
const session = this.sessions.get(sessionId);
|
|
20006
|
+
if (!session) {
|
|
20007
|
+
this.logger.warn("flush: no session found", { sessionId });
|
|
20008
|
+
return;
|
|
20050
20009
|
}
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
* Download and apply a tree snapshot.
|
|
20055
|
-
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
20056
|
-
*/
|
|
20057
|
-
async applyTreeSnapshot(snapshot) {
|
|
20058
|
-
if (!this.apiClient) {
|
|
20059
|
-
throw new Error("Cannot apply snapshot: API client not configured");
|
|
20010
|
+
const pending = this.pendingEntries.get(sessionId);
|
|
20011
|
+
if (!this.posthogAPI || !pending?.length) {
|
|
20012
|
+
return;
|
|
20060
20013
|
}
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20014
|
+
this.pendingEntries.delete(sessionId);
|
|
20015
|
+
const timeout = this.flushTimeouts.get(sessionId);
|
|
20016
|
+
if (timeout) {
|
|
20017
|
+
clearTimeout(timeout);
|
|
20018
|
+
this.flushTimeouts.delete(sessionId);
|
|
20067
20019
|
}
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
20074
|
-
runId: this.runId
|
|
20075
|
-
});
|
|
20076
|
-
if (!result.success) {
|
|
20077
|
-
this.logger.error("Failed to apply tree snapshot", {
|
|
20078
|
-
error: result.error,
|
|
20079
|
-
failedStep: result.failedStep,
|
|
20080
|
-
treeHash: snapshot.treeHash
|
|
20081
|
-
});
|
|
20082
|
-
throw new Error(
|
|
20083
|
-
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
20020
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20021
|
+
try {
|
|
20022
|
+
await this.posthogAPI.appendTaskRunLog(
|
|
20023
|
+
session.context.taskId,
|
|
20024
|
+
session.context.runId,
|
|
20025
|
+
pending
|
|
20084
20026
|
);
|
|
20027
|
+
this.retryCounts.set(sessionId, 0);
|
|
20028
|
+
} catch (error) {
|
|
20029
|
+
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
20030
|
+
this.retryCounts.set(sessionId, retryCount);
|
|
20031
|
+
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
20032
|
+
this.logger.error(
|
|
20033
|
+
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
20034
|
+
{
|
|
20035
|
+
taskId: session.context.taskId,
|
|
20036
|
+
runId: session.context.runId,
|
|
20037
|
+
error
|
|
20038
|
+
}
|
|
20039
|
+
);
|
|
20040
|
+
this.retryCounts.set(sessionId, 0);
|
|
20041
|
+
} else {
|
|
20042
|
+
if (retryCount === 1) {
|
|
20043
|
+
this.logger.warn(
|
|
20044
|
+
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
20045
|
+
{
|
|
20046
|
+
taskId: session.context.taskId,
|
|
20047
|
+
runId: session.context.runId,
|
|
20048
|
+
error: error instanceof Error ? error.message : String(error)
|
|
20049
|
+
}
|
|
20050
|
+
);
|
|
20051
|
+
}
|
|
20052
|
+
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
20053
|
+
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
20054
|
+
this.scheduleFlush(sessionId);
|
|
20055
|
+
}
|
|
20085
20056
|
}
|
|
20086
|
-
this.lastTreeHash = result.data.treeHash;
|
|
20087
20057
|
}
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
return
|
|
20058
|
+
getSessionUpdateType(message) {
|
|
20059
|
+
if (message.method !== "session/update") return void 0;
|
|
20060
|
+
const params = message.params;
|
|
20061
|
+
const update = params?.update;
|
|
20062
|
+
return update?.sessionUpdate;
|
|
20093
20063
|
}
|
|
20094
|
-
|
|
20095
|
-
|
|
20096
|
-
*/
|
|
20097
|
-
setLastTreeHash(hash) {
|
|
20098
|
-
this.lastTreeHash = hash;
|
|
20064
|
+
isDirectAgentMessage(message) {
|
|
20065
|
+
return this.getSessionUpdateType(message) === "agent_message";
|
|
20099
20066
|
}
|
|
20100
|
-
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
const
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
"fetch_task_run",
|
|
20110
|
-
() => apiClient.getTaskRun(taskId, runId)
|
|
20111
|
-
);
|
|
20112
|
-
if (!taskRun.log_url) {
|
|
20113
|
-
this.log.info("No log URL found, starting fresh");
|
|
20114
|
-
return this.emptyResult();
|
|
20115
|
-
}
|
|
20116
|
-
const entries = await this.readOnlyStep(
|
|
20117
|
-
"fetch_logs",
|
|
20118
|
-
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
20119
|
-
);
|
|
20120
|
-
if (entries.length === 0) {
|
|
20121
|
-
this.log.info("No log entries found, starting fresh");
|
|
20122
|
-
return this.emptyResult();
|
|
20067
|
+
isAgentMessageChunk(message) {
|
|
20068
|
+
return this.getSessionUpdateType(message) === "agent_message_chunk";
|
|
20069
|
+
}
|
|
20070
|
+
extractChunkText(message) {
|
|
20071
|
+
const params = message.params;
|
|
20072
|
+
const update = params?.update;
|
|
20073
|
+
const content = update?.content;
|
|
20074
|
+
if (content?.type === "text" && content.text) {
|
|
20075
|
+
return content.text;
|
|
20123
20076
|
}
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
20129
|
-
|
|
20130
|
-
|
|
20131
|
-
|
|
20132
|
-
|
|
20133
|
-
|
|
20134
|
-
|
|
20135
|
-
|
|
20136
|
-
|
|
20137
|
-
|
|
20138
|
-
|
|
20139
|
-
|
|
20140
|
-
|
|
20141
|
-
|
|
20142
|
-
name: "apply_snapshot",
|
|
20143
|
-
execute: async () => {
|
|
20144
|
-
const treeTracker = new TreeTracker({
|
|
20145
|
-
repositoryPath,
|
|
20146
|
-
taskId,
|
|
20147
|
-
runId,
|
|
20148
|
-
apiClient,
|
|
20149
|
-
logger: logger.child("TreeTracker")
|
|
20150
|
-
});
|
|
20151
|
-
try {
|
|
20152
|
-
await treeTracker.applyTreeSnapshot(latestSnapshot);
|
|
20153
|
-
treeTracker.setLastTreeHash(latestSnapshot.treeHash);
|
|
20154
|
-
snapshotApplied = true;
|
|
20155
|
-
this.log.info("Tree snapshot applied successfully", {
|
|
20156
|
-
treeHash: latestSnapshot.treeHash
|
|
20157
|
-
});
|
|
20158
|
-
} catch (error) {
|
|
20159
|
-
this.log.warn(
|
|
20160
|
-
"Failed to apply tree snapshot, continuing without it",
|
|
20161
|
-
{
|
|
20162
|
-
error: error instanceof Error ? error.message : String(error),
|
|
20163
|
-
treeHash: latestSnapshot.treeHash
|
|
20164
|
-
}
|
|
20165
|
-
);
|
|
20077
|
+
return "";
|
|
20078
|
+
}
|
|
20079
|
+
emitCoalescedMessage(sessionId, session) {
|
|
20080
|
+
if (!session.chunkBuffer) return;
|
|
20081
|
+
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
20082
|
+
session.chunkBuffer = void 0;
|
|
20083
|
+
session.lastAgentMessage = text2;
|
|
20084
|
+
session.currentTurnMessages.push(text2);
|
|
20085
|
+
const entry = {
|
|
20086
|
+
type: "notification",
|
|
20087
|
+
timestamp: firstTimestamp,
|
|
20088
|
+
notification: {
|
|
20089
|
+
jsonrpc: "2.0",
|
|
20090
|
+
method: "session/update",
|
|
20091
|
+
params: {
|
|
20092
|
+
update: {
|
|
20093
|
+
sessionUpdate: "agent_message",
|
|
20094
|
+
content: { type: "text", text: text2 }
|
|
20166
20095
|
}
|
|
20167
|
-
},
|
|
20168
|
-
rollback: async () => {
|
|
20169
|
-
}
|
|
20170
|
-
});
|
|
20171
|
-
} else if (latestSnapshot?.archiveUrl && !repositoryPath) {
|
|
20172
|
-
this.log.warn(
|
|
20173
|
-
"Snapshot found but no repositoryPath configured - files cannot be restored",
|
|
20174
|
-
{
|
|
20175
|
-
treeHash: latestSnapshot.treeHash,
|
|
20176
|
-
changes: latestSnapshot.changes?.length ?? 0
|
|
20177
20096
|
}
|
|
20178
|
-
|
|
20179
|
-
}
|
|
20180
|
-
|
|
20181
|
-
|
|
20097
|
+
}
|
|
20098
|
+
};
|
|
20099
|
+
this.writeToLocalCache(sessionId, entry);
|
|
20100
|
+
if (this.posthogAPI) {
|
|
20101
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20102
|
+
pending.push(entry);
|
|
20103
|
+
this.pendingEntries.set(sessionId, pending);
|
|
20104
|
+
this.scheduleFlush(sessionId);
|
|
20105
|
+
}
|
|
20106
|
+
}
|
|
20107
|
+
getLastAgentMessage(sessionId) {
|
|
20108
|
+
return this.sessions.get(sessionId)?.lastAgentMessage;
|
|
20109
|
+
}
|
|
20110
|
+
getFullAgentResponse(sessionId) {
|
|
20111
|
+
const session = this.sessions.get(sessionId);
|
|
20112
|
+
if (!session || session.currentTurnMessages.length === 0) return void 0;
|
|
20113
|
+
if (session.chunkBuffer) {
|
|
20114
|
+
this.logger.warn(
|
|
20115
|
+
"getFullAgentResponse called with non-empty chunk buffer",
|
|
20182
20116
|
{
|
|
20183
|
-
|
|
20184
|
-
|
|
20117
|
+
sessionId,
|
|
20118
|
+
bufferedLength: session.chunkBuffer.text.length
|
|
20185
20119
|
}
|
|
20186
20120
|
);
|
|
20187
20121
|
}
|
|
20188
|
-
|
|
20189
|
-
"rebuild_conversation",
|
|
20190
|
-
() => Promise.resolve(this.rebuildConversation(entries))
|
|
20191
|
-
);
|
|
20192
|
-
const lastDevice = await this.readOnlyStep(
|
|
20193
|
-
"find_device",
|
|
20194
|
-
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
20195
|
-
);
|
|
20196
|
-
this.log.info("Resume state rebuilt", {
|
|
20197
|
-
turns: conversation.length,
|
|
20198
|
-
hasSnapshot: !!latestSnapshot,
|
|
20199
|
-
snapshotApplied,
|
|
20200
|
-
interrupted: latestSnapshot?.interrupted ?? false
|
|
20201
|
-
});
|
|
20202
|
-
return {
|
|
20203
|
-
conversation,
|
|
20204
|
-
latestSnapshot,
|
|
20205
|
-
latestGitCheckpoint,
|
|
20206
|
-
snapshotApplied,
|
|
20207
|
-
interrupted: latestSnapshot?.interrupted ?? false,
|
|
20208
|
-
lastDevice,
|
|
20209
|
-
logEntryCount: entries.length
|
|
20210
|
-
};
|
|
20122
|
+
return session.currentTurnMessages.join("\n\n");
|
|
20211
20123
|
}
|
|
20212
|
-
|
|
20213
|
-
|
|
20214
|
-
|
|
20215
|
-
|
|
20216
|
-
|
|
20217
|
-
snapshotApplied: false,
|
|
20218
|
-
interrupted: false,
|
|
20219
|
-
logEntryCount: 0
|
|
20220
|
-
};
|
|
20124
|
+
resetTurnMessages(sessionId) {
|
|
20125
|
+
const session = this.sessions.get(sessionId);
|
|
20126
|
+
if (session) {
|
|
20127
|
+
session.currentTurnMessages = [];
|
|
20128
|
+
}
|
|
20221
20129
|
}
|
|
20222
|
-
|
|
20223
|
-
|
|
20224
|
-
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
|
|
20229
|
-
|
|
20230
|
-
|
|
20231
|
-
|
|
20130
|
+
extractAgentMessageText(message) {
|
|
20131
|
+
if (message.method !== "session/update") {
|
|
20132
|
+
return null;
|
|
20133
|
+
}
|
|
20134
|
+
const params = message.params;
|
|
20135
|
+
const update = params?.update;
|
|
20136
|
+
if (update?.sessionUpdate !== "agent_message") {
|
|
20137
|
+
return null;
|
|
20138
|
+
}
|
|
20139
|
+
const content = update.content;
|
|
20140
|
+
if (content?.type === "text" && typeof content.text === "string") {
|
|
20141
|
+
const trimmed2 = content.text.trim();
|
|
20142
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20143
|
+
}
|
|
20144
|
+
if (typeof update.message === "string") {
|
|
20145
|
+
const trimmed2 = update.message.trim();
|
|
20146
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20147
|
+
}
|
|
20148
|
+
return null;
|
|
20149
|
+
}
|
|
20150
|
+
scheduleFlush(sessionId) {
|
|
20151
|
+
const existing = this.flushTimeouts.get(sessionId);
|
|
20152
|
+
if (existing) clearTimeout(existing);
|
|
20153
|
+
const retryCount = this.retryCounts.get(sessionId) ?? 0;
|
|
20154
|
+
const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
|
|
20155
|
+
const elapsed = Date.now() - lastAttempt;
|
|
20156
|
+
let delay3;
|
|
20157
|
+
if (retryCount > 0) {
|
|
20158
|
+
delay3 = Math.min(
|
|
20159
|
+
_SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
|
|
20160
|
+
_SessionLogWriter.MAX_RETRY_DELAY_MS
|
|
20161
|
+
);
|
|
20162
|
+
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20163
|
+
delay3 = 0;
|
|
20164
|
+
} else {
|
|
20165
|
+
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
20166
|
+
}
|
|
20167
|
+
const timeout = setTimeout(() => this.flush(sessionId), delay3);
|
|
20168
|
+
this.flushTimeouts.set(sessionId, timeout);
|
|
20169
|
+
}
|
|
20170
|
+
writeToLocalCache(sessionId, entry) {
|
|
20171
|
+
if (!this.localCachePath) return;
|
|
20172
|
+
const session = this.sessions.get(sessionId);
|
|
20173
|
+
if (!session) return;
|
|
20174
|
+
const logPath = path15.join(
|
|
20175
|
+
this.localCachePath,
|
|
20176
|
+
"sessions",
|
|
20177
|
+
session.context.runId,
|
|
20178
|
+
"logs.ndjson"
|
|
20179
|
+
);
|
|
20180
|
+
try {
|
|
20181
|
+
fs12.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
20182
|
+
`);
|
|
20183
|
+
} catch (error) {
|
|
20184
|
+
this.logger.warn("Failed to write to local cache", {
|
|
20185
|
+
taskId: session.context.taskId,
|
|
20186
|
+
runId: session.context.runId,
|
|
20187
|
+
logPath,
|
|
20188
|
+
error
|
|
20189
|
+
});
|
|
20190
|
+
}
|
|
20191
|
+
}
|
|
20192
|
+
static async cleanupOldSessions(localCachePath) {
|
|
20193
|
+
const sessionsDir = path15.join(localCachePath, "sessions");
|
|
20194
|
+
let deleted = 0;
|
|
20195
|
+
try {
|
|
20196
|
+
const entries = await fsp.readdir(sessionsDir);
|
|
20197
|
+
const now = Date.now();
|
|
20198
|
+
for (const entry of entries) {
|
|
20199
|
+
const entryPath = path15.join(sessionsDir, entry);
|
|
20200
|
+
try {
|
|
20201
|
+
const stats = await fsp.stat(entryPath);
|
|
20202
|
+
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
20203
|
+
await fsp.rm(entryPath, { recursive: true, force: true });
|
|
20204
|
+
deleted++;
|
|
20205
|
+
}
|
|
20206
|
+
} catch {
|
|
20232
20207
|
}
|
|
20233
20208
|
}
|
|
20209
|
+
} catch {
|
|
20234
20210
|
}
|
|
20235
|
-
return
|
|
20211
|
+
return deleted;
|
|
20236
20212
|
}
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
|
|
20244
|
-
|
|
20245
|
-
|
|
20213
|
+
};
|
|
20214
|
+
|
|
20215
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20216
|
+
import { mkdir as mkdir7, rm as rm6, writeFile as writeFile5 } from "fs/promises";
|
|
20217
|
+
import { join as join12 } from "path";
|
|
20218
|
+
|
|
20219
|
+
// ../git/dist/sagas/tree.js
|
|
20220
|
+
import { existsSync as existsSync5 } from "fs";
|
|
20221
|
+
import * as fs13 from "fs/promises";
|
|
20222
|
+
import * as path16 from "path";
|
|
20223
|
+
import * as tar from "tar";
|
|
20224
|
+
var CaptureTreeSaga = class extends GitSaga {
|
|
20225
|
+
sagaName = "CaptureTreeSaga";
|
|
20226
|
+
tempIndexPath = null;
|
|
20227
|
+
async executeGitOperations(input) {
|
|
20228
|
+
const { baseDir, lastTreeHash, archivePath, signal } = input;
|
|
20229
|
+
const tmpDir = path16.join(baseDir, ".git", "posthog-code-tmp");
|
|
20230
|
+
await this.step({
|
|
20231
|
+
name: "create_tmp_dir",
|
|
20232
|
+
execute: () => fs13.mkdir(tmpDir, { recursive: true }),
|
|
20233
|
+
rollback: async () => {
|
|
20234
|
+
}
|
|
20235
|
+
});
|
|
20236
|
+
this.tempIndexPath = path16.join(tmpDir, `index-${Date.now()}`);
|
|
20237
|
+
const tempIndexGit = this.git.env({
|
|
20238
|
+
...process.env,
|
|
20239
|
+
GIT_INDEX_FILE: this.tempIndexPath
|
|
20240
|
+
});
|
|
20241
|
+
await this.step({
|
|
20242
|
+
name: "init_temp_index",
|
|
20243
|
+
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
20244
|
+
rollback: async () => {
|
|
20245
|
+
if (this.tempIndexPath) {
|
|
20246
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20247
|
+
});
|
|
20246
20248
|
}
|
|
20247
20249
|
}
|
|
20250
|
+
});
|
|
20251
|
+
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
20252
|
+
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
20253
|
+
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
20254
|
+
this.log.debug("No changes since last capture", { treeHash });
|
|
20255
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20256
|
+
});
|
|
20257
|
+
return { snapshot: null, changed: false };
|
|
20248
20258
|
}
|
|
20249
|
-
|
|
20259
|
+
const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
|
|
20260
|
+
try {
|
|
20261
|
+
return await getHeadSha(baseDir, { abortSignal: signal });
|
|
20262
|
+
} catch {
|
|
20263
|
+
return null;
|
|
20264
|
+
}
|
|
20265
|
+
});
|
|
20266
|
+
const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
|
|
20267
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20268
|
+
});
|
|
20269
|
+
const snapshot = {
|
|
20270
|
+
treeHash,
|
|
20271
|
+
baseCommit,
|
|
20272
|
+
changes,
|
|
20273
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20274
|
+
};
|
|
20275
|
+
let createdArchivePath;
|
|
20276
|
+
if (archivePath) {
|
|
20277
|
+
createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
|
|
20278
|
+
}
|
|
20279
|
+
this.log.info("Tree captured", {
|
|
20280
|
+
treeHash,
|
|
20281
|
+
changes: changes.length,
|
|
20282
|
+
archived: !!createdArchivePath
|
|
20283
|
+
});
|
|
20284
|
+
return { snapshot, archivePath: createdArchivePath, changed: true };
|
|
20250
20285
|
}
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20256
|
-
|
|
20286
|
+
async createArchive(baseDir, archivePath, changes) {
|
|
20287
|
+
const filesToArchive = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20288
|
+
if (filesToArchive.length === 0) {
|
|
20289
|
+
return void 0;
|
|
20290
|
+
}
|
|
20291
|
+
const existingFiles = filesToArchive.filter((f) => existsSync5(path16.join(baseDir, f)));
|
|
20292
|
+
if (existingFiles.length === 0) {
|
|
20293
|
+
return void 0;
|
|
20294
|
+
}
|
|
20295
|
+
await this.step({
|
|
20296
|
+
name: "create_archive",
|
|
20297
|
+
execute: async () => {
|
|
20298
|
+
const archiveDir = path16.dirname(archivePath);
|
|
20299
|
+
await fs13.mkdir(archiveDir, { recursive: true });
|
|
20300
|
+
await tar.create({
|
|
20301
|
+
gzip: true,
|
|
20302
|
+
file: archivePath,
|
|
20303
|
+
cwd: baseDir
|
|
20304
|
+
}, existingFiles);
|
|
20305
|
+
},
|
|
20306
|
+
rollback: async () => {
|
|
20307
|
+
await fs13.rm(archivePath, { force: true }).catch(() => {
|
|
20308
|
+
});
|
|
20257
20309
|
}
|
|
20310
|
+
});
|
|
20311
|
+
return archivePath;
|
|
20312
|
+
}
|
|
20313
|
+
async getChanges(git, fromRef, toRef) {
|
|
20314
|
+
if (!fromRef) {
|
|
20315
|
+
const stdout2 = await git.raw(["ls-tree", "-r", "--name-only", toRef]);
|
|
20316
|
+
return stdout2.split("\n").filter((p) => p.trim()).map((p) => ({ path: p, status: "A" }));
|
|
20258
20317
|
}
|
|
20259
|
-
|
|
20318
|
+
const stdout = await git.raw([
|
|
20319
|
+
"diff-tree",
|
|
20320
|
+
"-r",
|
|
20321
|
+
"--name-status",
|
|
20322
|
+
fromRef,
|
|
20323
|
+
toRef
|
|
20324
|
+
]);
|
|
20325
|
+
const changes = [];
|
|
20326
|
+
for (const line of stdout.split("\n")) {
|
|
20327
|
+
if (!line.trim())
|
|
20328
|
+
continue;
|
|
20329
|
+
const [status, filePath] = line.split(" ");
|
|
20330
|
+
if (!filePath)
|
|
20331
|
+
continue;
|
|
20332
|
+
let normalizedStatus;
|
|
20333
|
+
if (status === "D") {
|
|
20334
|
+
normalizedStatus = "D";
|
|
20335
|
+
} else if (status === "A") {
|
|
20336
|
+
normalizedStatus = "A";
|
|
20337
|
+
} else {
|
|
20338
|
+
normalizedStatus = "M";
|
|
20339
|
+
}
|
|
20340
|
+
changes.push({ path: filePath, status: normalizedStatus });
|
|
20341
|
+
}
|
|
20342
|
+
return changes;
|
|
20260
20343
|
}
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
20268
|
-
|
|
20269
|
-
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20273
|
-
|
|
20274
|
-
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20281
|
-
|
|
20282
|
-
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
|
|
20287
|
-
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
|
|
20291
|
-
|
|
20292
|
-
|
|
20293
|
-
|
|
20294
|
-
|
|
20295
|
-
|
|
20296
|
-
|
|
20297
|
-
|
|
20298
|
-
|
|
20299
|
-
|
|
20300
|
-
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
|
|
20304
|
-
|
|
20305
|
-
|
|
20306
|
-
|
|
20307
|
-
|
|
20308
|
-
|
|
20309
|
-
|
|
20310
|
-
|
|
20311
|
-
|
|
20312
|
-
|
|
20313
|
-
break;
|
|
20314
|
-
}
|
|
20315
|
-
case "tool_call":
|
|
20316
|
-
case "tool_call_update": {
|
|
20317
|
-
const meta = update._meta?.claudeCode;
|
|
20318
|
-
if (meta) {
|
|
20319
|
-
const toolCallId = meta.toolCallId;
|
|
20320
|
-
const toolName = meta.toolName;
|
|
20321
|
-
const toolInput = meta.toolInput;
|
|
20322
|
-
const toolResponse = meta.toolResponse;
|
|
20323
|
-
if (toolCallId && toolName) {
|
|
20324
|
-
let toolCall = currentToolCalls.find(
|
|
20325
|
-
(tc) => tc.toolCallId === toolCallId
|
|
20326
|
-
);
|
|
20327
|
-
if (!toolCall) {
|
|
20328
|
-
toolCall = {
|
|
20329
|
-
toolCallId,
|
|
20330
|
-
toolName,
|
|
20331
|
-
input: toolInput
|
|
20332
|
-
};
|
|
20333
|
-
currentToolCalls.push(toolCall);
|
|
20334
|
-
}
|
|
20335
|
-
if (toolResponse !== void 0) {
|
|
20336
|
-
toolCall.result = toolResponse;
|
|
20337
|
-
}
|
|
20338
|
-
}
|
|
20344
|
+
};
|
|
20345
|
+
var ApplyTreeSaga = class extends GitSaga {
|
|
20346
|
+
sagaName = "ApplyTreeSaga";
|
|
20347
|
+
originalHead = null;
|
|
20348
|
+
originalBranch = null;
|
|
20349
|
+
extractedFiles = [];
|
|
20350
|
+
fileBackups = /* @__PURE__ */ new Map();
|
|
20351
|
+
async executeGitOperations(input) {
|
|
20352
|
+
const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
|
|
20353
|
+
const headInfo = await this.readOnlyStep("get_current_head", async () => {
|
|
20354
|
+
let head = null;
|
|
20355
|
+
let branch = null;
|
|
20356
|
+
try {
|
|
20357
|
+
head = await this.git.revparse(["HEAD"]);
|
|
20358
|
+
} catch {
|
|
20359
|
+
head = null;
|
|
20360
|
+
}
|
|
20361
|
+
try {
|
|
20362
|
+
branch = await this.git.raw(["symbolic-ref", "--short", "HEAD"]);
|
|
20363
|
+
} catch {
|
|
20364
|
+
branch = null;
|
|
20365
|
+
}
|
|
20366
|
+
return { head, branch };
|
|
20367
|
+
});
|
|
20368
|
+
this.originalHead = headInfo.head;
|
|
20369
|
+
this.originalBranch = headInfo.branch;
|
|
20370
|
+
let checkoutPerformed = false;
|
|
20371
|
+
if (baseCommit && baseCommit !== this.originalHead) {
|
|
20372
|
+
await this.readOnlyStep("check_working_tree", async () => {
|
|
20373
|
+
const status = await this.git.status();
|
|
20374
|
+
if (!status.isClean()) {
|
|
20375
|
+
const changedFiles = status.modified.length + status.staged.length + status.deleted.length;
|
|
20376
|
+
throw new Error(`Cannot apply tree: ${changedFiles} uncommitted change(s) exist. Commit or stash your changes first.`);
|
|
20377
|
+
}
|
|
20378
|
+
});
|
|
20379
|
+
await this.step({
|
|
20380
|
+
name: "checkout_base",
|
|
20381
|
+
execute: async () => {
|
|
20382
|
+
await this.git.checkout(baseCommit);
|
|
20383
|
+
checkoutPerformed = true;
|
|
20384
|
+
this.log.warn("Applied tree from different commit - now in detached HEAD state", {
|
|
20385
|
+
originalHead: this.originalHead,
|
|
20386
|
+
originalBranch: this.originalBranch,
|
|
20387
|
+
baseCommit
|
|
20388
|
+
});
|
|
20389
|
+
},
|
|
20390
|
+
rollback: async () => {
|
|
20391
|
+
try {
|
|
20392
|
+
if (this.originalBranch) {
|
|
20393
|
+
await this.git.checkout(this.originalBranch);
|
|
20394
|
+
} else if (this.originalHead) {
|
|
20395
|
+
await this.git.checkout(this.originalHead);
|
|
20339
20396
|
}
|
|
20340
|
-
|
|
20397
|
+
} catch (error) {
|
|
20398
|
+
this.log.warn("Failed to rollback checkout", { error });
|
|
20341
20399
|
}
|
|
20342
|
-
|
|
20343
|
-
|
|
20344
|
-
|
|
20345
|
-
|
|
20346
|
-
|
|
20347
|
-
|
|
20348
|
-
|
|
20349
|
-
|
|
20350
|
-
|
|
20351
|
-
|
|
20352
|
-
|
|
20353
|
-
|
|
20354
|
-
|
|
20400
|
+
}
|
|
20401
|
+
});
|
|
20402
|
+
}
|
|
20403
|
+
if (archivePath) {
|
|
20404
|
+
const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20405
|
+
await this.readOnlyStep("backup_existing_files", async () => {
|
|
20406
|
+
for (const filePath of filesToExtract) {
|
|
20407
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20408
|
+
try {
|
|
20409
|
+
const content = await fs13.readFile(fullPath);
|
|
20410
|
+
this.fileBackups.set(filePath, content);
|
|
20411
|
+
} catch {
|
|
20412
|
+
}
|
|
20413
|
+
}
|
|
20414
|
+
});
|
|
20415
|
+
await this.step({
|
|
20416
|
+
name: "extract_archive",
|
|
20417
|
+
execute: async () => {
|
|
20418
|
+
await tar.extract({
|
|
20419
|
+
file: archivePath,
|
|
20420
|
+
cwd: baseDir
|
|
20421
|
+
});
|
|
20422
|
+
this.extractedFiles = filesToExtract;
|
|
20423
|
+
},
|
|
20424
|
+
rollback: async () => {
|
|
20425
|
+
for (const filePath of this.extractedFiles) {
|
|
20426
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20427
|
+
const backup = this.fileBackups.get(filePath);
|
|
20428
|
+
if (backup) {
|
|
20429
|
+
const dir = path16.dirname(fullPath);
|
|
20430
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20431
|
+
});
|
|
20432
|
+
await fs13.writeFile(fullPath, backup).catch(() => {
|
|
20433
|
+
});
|
|
20434
|
+
} else {
|
|
20435
|
+
await fs13.rm(fullPath, { force: true }).catch(() => {
|
|
20436
|
+
});
|
|
20355
20437
|
}
|
|
20356
|
-
break;
|
|
20357
20438
|
}
|
|
20358
20439
|
}
|
|
20359
|
-
}
|
|
20440
|
+
});
|
|
20360
20441
|
}
|
|
20361
|
-
|
|
20362
|
-
|
|
20363
|
-
|
|
20364
|
-
|
|
20365
|
-
|
|
20442
|
+
for (const change of changes.filter((c) => c.status === "D")) {
|
|
20443
|
+
const fullPath = path16.join(baseDir, change.path);
|
|
20444
|
+
const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
|
|
20445
|
+
try {
|
|
20446
|
+
return await fs13.readFile(fullPath);
|
|
20447
|
+
} catch {
|
|
20448
|
+
return null;
|
|
20449
|
+
}
|
|
20450
|
+
});
|
|
20451
|
+
await this.step({
|
|
20452
|
+
name: `delete_${change.path}`,
|
|
20453
|
+
execute: async () => {
|
|
20454
|
+
await fs13.rm(fullPath, { force: true });
|
|
20455
|
+
this.log.debug(`Deleted file: ${change.path}`);
|
|
20456
|
+
},
|
|
20457
|
+
rollback: async () => {
|
|
20458
|
+
if (backupContent) {
|
|
20459
|
+
const dir = path16.dirname(fullPath);
|
|
20460
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20461
|
+
});
|
|
20462
|
+
await fs13.writeFile(fullPath, backupContent).catch(() => {
|
|
20463
|
+
});
|
|
20464
|
+
}
|
|
20465
|
+
}
|
|
20366
20466
|
});
|
|
20367
20467
|
}
|
|
20368
|
-
|
|
20468
|
+
const deletedCount = changes.filter((c) => c.status === "D").length;
|
|
20469
|
+
this.log.info("Tree applied", {
|
|
20470
|
+
treeHash,
|
|
20471
|
+
totalChanges: changes.length,
|
|
20472
|
+
deletedFiles: deletedCount,
|
|
20473
|
+
checkoutPerformed
|
|
20474
|
+
});
|
|
20475
|
+
return { treeHash, checkoutPerformed };
|
|
20369
20476
|
}
|
|
20370
20477
|
};
|
|
20371
20478
|
|
|
20372
|
-
// src/
|
|
20373
|
-
|
|
20374
|
-
|
|
20375
|
-
|
|
20376
|
-
|
|
20377
|
-
runId
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
|
|
20381
|
-
taskId: config.taskId,
|
|
20382
|
-
runId: config.runId,
|
|
20383
|
-
repositoryPath: config.repositoryPath,
|
|
20384
|
-
apiClient: config.apiClient,
|
|
20385
|
-
logger
|
|
20386
|
-
});
|
|
20387
|
-
if (!result.success) {
|
|
20388
|
-
logger.error("Failed to resume from log", {
|
|
20389
|
-
error: result.error,
|
|
20390
|
-
failedStep: result.failedStep
|
|
20391
|
-
});
|
|
20392
|
-
throw new Error(
|
|
20393
|
-
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
20394
|
-
);
|
|
20395
|
-
}
|
|
20396
|
-
return {
|
|
20397
|
-
conversation: result.data.conversation,
|
|
20398
|
-
latestSnapshot: result.data.latestSnapshot,
|
|
20399
|
-
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
20400
|
-
snapshotApplied: result.data.snapshotApplied,
|
|
20401
|
-
interrupted: result.data.interrupted,
|
|
20402
|
-
lastDevice: result.data.lastDevice,
|
|
20403
|
-
logEntryCount: result.data.logEntryCount
|
|
20404
|
-
};
|
|
20405
|
-
}
|
|
20406
|
-
var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
|
|
20407
|
-
var TOOL_RESULT_MAX_CHARS = 2e3;
|
|
20408
|
-
function formatConversationForResume(conversation) {
|
|
20409
|
-
const selected = selectRecentTurns(conversation, RESUME_HISTORY_TOKEN_BUDGET);
|
|
20410
|
-
const parts2 = [];
|
|
20411
|
-
if (selected.length < conversation.length) {
|
|
20412
|
-
parts2.push(
|
|
20413
|
-
`*(${conversation.length - selected.length} earlier turns omitted)*`
|
|
20414
|
-
);
|
|
20415
|
-
}
|
|
20416
|
-
for (const turn of selected) {
|
|
20417
|
-
const role = turn.role === "user" ? "User" : "Assistant";
|
|
20418
|
-
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
20419
|
-
if (textParts.length > 0) {
|
|
20420
|
-
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
20479
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20480
|
+
var ApplySnapshotSaga = class extends Saga {
|
|
20481
|
+
sagaName = "ApplySnapshotSaga";
|
|
20482
|
+
archivePath = null;
|
|
20483
|
+
async execute(input) {
|
|
20484
|
+
const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
|
|
20485
|
+
const tmpDir = join12(repositoryPath, ".posthog", "tmp");
|
|
20486
|
+
if (!snapshot.archiveUrl) {
|
|
20487
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20421
20488
|
}
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20427
|
-
|
|
20489
|
+
const archiveUrl = snapshot.archiveUrl;
|
|
20490
|
+
await this.step({
|
|
20491
|
+
name: "create_tmp_dir",
|
|
20492
|
+
execute: () => mkdir7(tmpDir, { recursive: true }),
|
|
20493
|
+
rollback: async () => {
|
|
20494
|
+
}
|
|
20495
|
+
});
|
|
20496
|
+
const archivePath = join12(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
20497
|
+
this.archivePath = archivePath;
|
|
20498
|
+
await this.step({
|
|
20499
|
+
name: "download_archive",
|
|
20500
|
+
execute: async () => {
|
|
20501
|
+
const arrayBuffer = await apiClient.downloadArtifact(
|
|
20502
|
+
taskId,
|
|
20503
|
+
runId,
|
|
20504
|
+
archiveUrl
|
|
20505
|
+
);
|
|
20506
|
+
if (!arrayBuffer) {
|
|
20507
|
+
throw new Error("Failed to download archive");
|
|
20428
20508
|
}
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
|
|
20435
|
-
|
|
20436
|
-
|
|
20437
|
-
|
|
20438
|
-
// src/session-log-writer.ts
|
|
20439
|
-
import fs13 from "fs";
|
|
20440
|
-
import fsp from "fs/promises";
|
|
20441
|
-
import path16 from "path";
|
|
20442
|
-
var SessionLogWriter = class _SessionLogWriter {
|
|
20443
|
-
static FLUSH_DEBOUNCE_MS = 500;
|
|
20444
|
-
static FLUSH_MAX_INTERVAL_MS = 5e3;
|
|
20445
|
-
static MAX_FLUSH_RETRIES = 10;
|
|
20446
|
-
static MAX_RETRY_DELAY_MS = 3e4;
|
|
20447
|
-
static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
20448
|
-
posthogAPI;
|
|
20449
|
-
pendingEntries = /* @__PURE__ */ new Map();
|
|
20450
|
-
flushTimeouts = /* @__PURE__ */ new Map();
|
|
20451
|
-
lastFlushAttemptTime = /* @__PURE__ */ new Map();
|
|
20452
|
-
retryCounts = /* @__PURE__ */ new Map();
|
|
20453
|
-
sessions = /* @__PURE__ */ new Map();
|
|
20454
|
-
flushQueues = /* @__PURE__ */ new Map();
|
|
20455
|
-
logger;
|
|
20456
|
-
localCachePath;
|
|
20457
|
-
constructor(options = {}) {
|
|
20458
|
-
this.posthogAPI = options.posthogAPI;
|
|
20459
|
-
this.localCachePath = options.localCachePath;
|
|
20460
|
-
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
20461
|
-
}
|
|
20462
|
-
async flushAll() {
|
|
20463
|
-
const flushPromises = [];
|
|
20464
|
-
for (const [sessionId, session] of this.sessions) {
|
|
20465
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
20466
|
-
flushPromises.push(this.flush(sessionId));
|
|
20467
|
-
}
|
|
20468
|
-
await Promise.all(flushPromises);
|
|
20469
|
-
}
|
|
20470
|
-
register(sessionId, context) {
|
|
20471
|
-
if (this.sessions.has(sessionId)) {
|
|
20472
|
-
return;
|
|
20473
|
-
}
|
|
20474
|
-
this.sessions.set(sessionId, { context, currentTurnMessages: [] });
|
|
20475
|
-
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20476
|
-
if (this.localCachePath) {
|
|
20477
|
-
const sessionDir = path16.join(
|
|
20478
|
-
this.localCachePath,
|
|
20479
|
-
"sessions",
|
|
20480
|
-
context.runId
|
|
20481
|
-
);
|
|
20482
|
-
try {
|
|
20483
|
-
fs13.mkdirSync(sessionDir, { recursive: true });
|
|
20484
|
-
} catch (error) {
|
|
20485
|
-
this.logger.warn("Failed to create local cache directory", {
|
|
20486
|
-
sessionDir,
|
|
20487
|
-
error
|
|
20509
|
+
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
20510
|
+
const binaryContent = Buffer.from(base64Content, "base64");
|
|
20511
|
+
await writeFile5(archivePath, binaryContent);
|
|
20512
|
+
this.log.info("Tree archive downloaded", {
|
|
20513
|
+
treeHash: snapshot.treeHash,
|
|
20514
|
+
snapshotBytes: binaryContent.byteLength,
|
|
20515
|
+
snapshotWireBytes: arrayBuffer.byteLength,
|
|
20516
|
+
totalBytes: binaryContent.byteLength,
|
|
20517
|
+
totalWireBytes: arrayBuffer.byteLength
|
|
20488
20518
|
});
|
|
20489
|
-
}
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
}
|
|
20495
|
-
appendRawLine(sessionId, line) {
|
|
20496
|
-
const session = this.sessions.get(sessionId);
|
|
20497
|
-
if (!session) {
|
|
20498
|
-
this.logger.warn("appendRawLine called for unregistered session", {
|
|
20499
|
-
sessionId
|
|
20500
|
-
});
|
|
20501
|
-
return;
|
|
20502
|
-
}
|
|
20503
|
-
try {
|
|
20504
|
-
const message = JSON.parse(line);
|
|
20505
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
20506
|
-
if (this.isAgentMessageChunk(message)) {
|
|
20507
|
-
const text2 = this.extractChunkText(message);
|
|
20508
|
-
if (text2) {
|
|
20509
|
-
if (!session.chunkBuffer) {
|
|
20510
|
-
session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
|
|
20511
|
-
} else {
|
|
20512
|
-
session.chunkBuffer.text += text2;
|
|
20513
|
-
}
|
|
20519
|
+
},
|
|
20520
|
+
rollback: async () => {
|
|
20521
|
+
if (this.archivePath) {
|
|
20522
|
+
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
20523
|
+
});
|
|
20514
20524
|
}
|
|
20515
|
-
return;
|
|
20516
|
-
}
|
|
20517
|
-
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
20518
|
-
session.chunkBuffer = void 0;
|
|
20519
|
-
} else {
|
|
20520
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
20521
|
-
}
|
|
20522
|
-
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
20523
|
-
if (nonChunkAgentText) {
|
|
20524
|
-
session.lastAgentMessage = nonChunkAgentText;
|
|
20525
|
-
session.currentTurnMessages.push(nonChunkAgentText);
|
|
20526
|
-
}
|
|
20527
|
-
const entry = {
|
|
20528
|
-
type: "notification",
|
|
20529
|
-
timestamp,
|
|
20530
|
-
notification: message
|
|
20531
|
-
};
|
|
20532
|
-
this.writeToLocalCache(sessionId, entry);
|
|
20533
|
-
if (this.posthogAPI) {
|
|
20534
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20535
|
-
pending.push(entry);
|
|
20536
|
-
this.pendingEntries.set(sessionId, pending);
|
|
20537
|
-
this.scheduleFlush(sessionId);
|
|
20538
|
-
}
|
|
20539
|
-
} catch {
|
|
20540
|
-
this.logger.warn("Failed to parse raw line for persistence", {
|
|
20541
|
-
taskId: session.context.taskId,
|
|
20542
|
-
runId: session.context.runId,
|
|
20543
|
-
lineLength: line.length
|
|
20544
|
-
});
|
|
20545
|
-
}
|
|
20546
|
-
}
|
|
20547
|
-
async flush(sessionId, { coalesce = false } = {}) {
|
|
20548
|
-
if (coalesce) {
|
|
20549
|
-
const session = this.sessions.get(sessionId);
|
|
20550
|
-
if (session) {
|
|
20551
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
20552
20525
|
}
|
|
20526
|
+
});
|
|
20527
|
+
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
20528
|
+
const applyResult = await gitApplySaga.run({
|
|
20529
|
+
baseDir: repositoryPath,
|
|
20530
|
+
treeHash: snapshot.treeHash,
|
|
20531
|
+
baseCommit: snapshot.baseCommit,
|
|
20532
|
+
changes: snapshot.changes,
|
|
20533
|
+
archivePath: this.archivePath
|
|
20534
|
+
});
|
|
20535
|
+
if (!applyResult.success) {
|
|
20536
|
+
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
20553
20537
|
}
|
|
20554
|
-
|
|
20555
|
-
const next = prev.catch(() => {
|
|
20556
|
-
}).then(() => this._doFlush(sessionId));
|
|
20557
|
-
this.flushQueues.set(sessionId, next);
|
|
20558
|
-
next.finally(() => {
|
|
20559
|
-
if (this.flushQueues.get(sessionId) === next) {
|
|
20560
|
-
this.flushQueues.delete(sessionId);
|
|
20561
|
-
}
|
|
20538
|
+
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
20562
20539
|
});
|
|
20563
|
-
|
|
20540
|
+
this.log.info("Tree snapshot applied", {
|
|
20541
|
+
treeHash: snapshot.treeHash,
|
|
20542
|
+
totalChanges: snapshot.changes.length,
|
|
20543
|
+
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
20544
|
+
});
|
|
20545
|
+
return { treeHash: snapshot.treeHash };
|
|
20564
20546
|
}
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
20569
|
-
|
|
20547
|
+
};
|
|
20548
|
+
|
|
20549
|
+
// src/sagas/capture-tree-saga.ts
|
|
20550
|
+
import { existsSync as existsSync6 } from "fs";
|
|
20551
|
+
import { readFile as readFile6, rm as rm7 } from "fs/promises";
|
|
20552
|
+
import { join as join13 } from "path";
|
|
20553
|
+
var CaptureTreeSaga2 = class extends Saga {
|
|
20554
|
+
sagaName = "CaptureTreeSaga";
|
|
20555
|
+
async execute(input) {
|
|
20556
|
+
const {
|
|
20557
|
+
repositoryPath,
|
|
20558
|
+
lastTreeHash,
|
|
20559
|
+
interrupted,
|
|
20560
|
+
apiClient,
|
|
20561
|
+
taskId,
|
|
20562
|
+
runId
|
|
20563
|
+
} = input;
|
|
20564
|
+
const tmpDir = join13(repositoryPath, ".posthog", "tmp");
|
|
20565
|
+
if (existsSync6(join13(repositoryPath, ".gitmodules"))) {
|
|
20566
|
+
this.log.warn(
|
|
20567
|
+
"Repository has submodules - snapshot may not capture submodule state"
|
|
20568
|
+
);
|
|
20570
20569
|
}
|
|
20571
|
-
const
|
|
20572
|
-
|
|
20573
|
-
|
|
20570
|
+
const shouldArchive = !!apiClient;
|
|
20571
|
+
const archivePath = shouldArchive ? join13(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
20572
|
+
const gitCaptureSaga = new CaptureTreeSaga(this.log);
|
|
20573
|
+
const captureResult = await gitCaptureSaga.run({
|
|
20574
|
+
baseDir: repositoryPath,
|
|
20575
|
+
lastTreeHash,
|
|
20576
|
+
archivePath
|
|
20577
|
+
});
|
|
20578
|
+
if (!captureResult.success) {
|
|
20579
|
+
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
20574
20580
|
}
|
|
20575
|
-
|
|
20576
|
-
|
|
20577
|
-
|
|
20578
|
-
|
|
20579
|
-
|
|
20581
|
+
const {
|
|
20582
|
+
snapshot: gitSnapshot,
|
|
20583
|
+
archivePath: createdArchivePath,
|
|
20584
|
+
changed
|
|
20585
|
+
} = captureResult.data;
|
|
20586
|
+
if (!changed || !gitSnapshot) {
|
|
20587
|
+
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
20588
|
+
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
20580
20589
|
}
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20586
|
-
|
|
20587
|
-
|
|
20588
|
-
|
|
20589
|
-
|
|
20590
|
-
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
20591
|
-
this.retryCounts.set(sessionId, retryCount);
|
|
20592
|
-
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
20593
|
-
this.logger.error(
|
|
20594
|
-
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
20595
|
-
{
|
|
20596
|
-
taskId: session.context.taskId,
|
|
20597
|
-
runId: session.context.runId,
|
|
20598
|
-
error
|
|
20599
|
-
}
|
|
20590
|
+
let archiveUrl;
|
|
20591
|
+
if (apiClient && createdArchivePath) {
|
|
20592
|
+
try {
|
|
20593
|
+
archiveUrl = await this.uploadArchive(
|
|
20594
|
+
createdArchivePath,
|
|
20595
|
+
gitSnapshot.treeHash,
|
|
20596
|
+
apiClient,
|
|
20597
|
+
taskId,
|
|
20598
|
+
runId
|
|
20600
20599
|
);
|
|
20601
|
-
|
|
20602
|
-
|
|
20603
|
-
|
|
20604
|
-
this.logger.warn(
|
|
20605
|
-
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
20606
|
-
{
|
|
20607
|
-
taskId: session.context.taskId,
|
|
20608
|
-
runId: session.context.runId,
|
|
20609
|
-
error: error instanceof Error ? error.message : String(error)
|
|
20610
|
-
}
|
|
20611
|
-
);
|
|
20612
|
-
}
|
|
20613
|
-
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
20614
|
-
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
20615
|
-
this.scheduleFlush(sessionId);
|
|
20600
|
+
} finally {
|
|
20601
|
+
await rm7(createdArchivePath, { force: true }).catch(() => {
|
|
20602
|
+
});
|
|
20616
20603
|
}
|
|
20617
20604
|
}
|
|
20605
|
+
const snapshot = {
|
|
20606
|
+
treeHash: gitSnapshot.treeHash,
|
|
20607
|
+
baseCommit: gitSnapshot.baseCommit,
|
|
20608
|
+
changes: gitSnapshot.changes,
|
|
20609
|
+
timestamp: gitSnapshot.timestamp,
|
|
20610
|
+
interrupted,
|
|
20611
|
+
archiveUrl
|
|
20612
|
+
};
|
|
20613
|
+
this.log.info("Tree captured", {
|
|
20614
|
+
treeHash: snapshot.treeHash,
|
|
20615
|
+
changes: snapshot.changes.length,
|
|
20616
|
+
interrupted,
|
|
20617
|
+
archiveUrl
|
|
20618
|
+
});
|
|
20619
|
+
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
20618
20620
|
}
|
|
20619
|
-
|
|
20620
|
-
|
|
20621
|
-
|
|
20622
|
-
|
|
20623
|
-
|
|
20624
|
-
|
|
20625
|
-
|
|
20626
|
-
|
|
20627
|
-
|
|
20628
|
-
|
|
20629
|
-
|
|
20630
|
-
|
|
20631
|
-
|
|
20632
|
-
|
|
20633
|
-
const update = params?.update;
|
|
20634
|
-
const content = update?.content;
|
|
20635
|
-
if (content?.type === "text" && content.text) {
|
|
20636
|
-
return content.text;
|
|
20637
|
-
}
|
|
20638
|
-
return "";
|
|
20639
|
-
}
|
|
20640
|
-
emitCoalescedMessage(sessionId, session) {
|
|
20641
|
-
if (!session.chunkBuffer) return;
|
|
20642
|
-
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
20643
|
-
session.chunkBuffer = void 0;
|
|
20644
|
-
session.lastAgentMessage = text2;
|
|
20645
|
-
session.currentTurnMessages.push(text2);
|
|
20646
|
-
const entry = {
|
|
20647
|
-
type: "notification",
|
|
20648
|
-
timestamp: firstTimestamp,
|
|
20649
|
-
notification: {
|
|
20650
|
-
jsonrpc: "2.0",
|
|
20651
|
-
method: "session/update",
|
|
20652
|
-
params: {
|
|
20653
|
-
update: {
|
|
20654
|
-
sessionUpdate: "agent_message",
|
|
20655
|
-
content: { type: "text", text: text2 }
|
|
20621
|
+
async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
|
|
20622
|
+
const archiveUrl = await this.step({
|
|
20623
|
+
name: "upload_archive",
|
|
20624
|
+
execute: async () => {
|
|
20625
|
+
const archiveContent = await readFile6(archivePath);
|
|
20626
|
+
const base64Content = archiveContent.toString("base64");
|
|
20627
|
+
const snapshotBytes = archiveContent.byteLength;
|
|
20628
|
+
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
20629
|
+
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
20630
|
+
{
|
|
20631
|
+
name: `trees/${treeHash}.tar.gz`,
|
|
20632
|
+
type: "tree_snapshot",
|
|
20633
|
+
content: base64Content,
|
|
20634
|
+
content_type: "application/gzip"
|
|
20656
20635
|
}
|
|
20636
|
+
]);
|
|
20637
|
+
const uploadedArtifact = artifacts[0];
|
|
20638
|
+
if (uploadedArtifact?.storage_path) {
|
|
20639
|
+
this.log.info("Tree archive uploaded", {
|
|
20640
|
+
storagePath: uploadedArtifact.storage_path,
|
|
20641
|
+
treeHash,
|
|
20642
|
+
snapshotBytes,
|
|
20643
|
+
snapshotWireBytes,
|
|
20644
|
+
totalBytes: snapshotBytes,
|
|
20645
|
+
totalWireBytes: snapshotWireBytes
|
|
20646
|
+
});
|
|
20647
|
+
return uploadedArtifact.storage_path;
|
|
20657
20648
|
}
|
|
20649
|
+
return void 0;
|
|
20650
|
+
},
|
|
20651
|
+
rollback: async () => {
|
|
20652
|
+
await rm7(archivePath, { force: true }).catch(() => {
|
|
20653
|
+
});
|
|
20658
20654
|
}
|
|
20659
|
-
};
|
|
20660
|
-
|
|
20661
|
-
if (this.posthogAPI) {
|
|
20662
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20663
|
-
pending.push(entry);
|
|
20664
|
-
this.pendingEntries.set(sessionId, pending);
|
|
20665
|
-
this.scheduleFlush(sessionId);
|
|
20666
|
-
}
|
|
20655
|
+
});
|
|
20656
|
+
return archiveUrl;
|
|
20667
20657
|
}
|
|
20668
|
-
|
|
20669
|
-
|
|
20658
|
+
};
|
|
20659
|
+
|
|
20660
|
+
// src/tree-tracker.ts
|
|
20661
|
+
var TreeTracker = class {
|
|
20662
|
+
repositoryPath;
|
|
20663
|
+
taskId;
|
|
20664
|
+
runId;
|
|
20665
|
+
apiClient;
|
|
20666
|
+
logger;
|
|
20667
|
+
lastTreeHash = null;
|
|
20668
|
+
constructor(config) {
|
|
20669
|
+
this.repositoryPath = config.repositoryPath;
|
|
20670
|
+
this.taskId = config.taskId;
|
|
20671
|
+
this.runId = config.runId;
|
|
20672
|
+
this.apiClient = config.apiClient;
|
|
20673
|
+
this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
20670
20674
|
}
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
|
|
20675
|
-
|
|
20676
|
-
|
|
20677
|
-
|
|
20678
|
-
|
|
20679
|
-
|
|
20680
|
-
|
|
20675
|
+
/**
|
|
20676
|
+
* Capture current working tree state as a snapshot.
|
|
20677
|
+
* Uses a temporary index to avoid modifying user's staging area.
|
|
20678
|
+
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
20679
|
+
*/
|
|
20680
|
+
async captureTree(options) {
|
|
20681
|
+
const saga = new CaptureTreeSaga2(this.logger);
|
|
20682
|
+
const result = await saga.run({
|
|
20683
|
+
repositoryPath: this.repositoryPath,
|
|
20684
|
+
taskId: this.taskId,
|
|
20685
|
+
runId: this.runId,
|
|
20686
|
+
apiClient: this.apiClient,
|
|
20687
|
+
lastTreeHash: this.lastTreeHash,
|
|
20688
|
+
interrupted: options?.interrupted
|
|
20689
|
+
});
|
|
20690
|
+
if (!result.success) {
|
|
20691
|
+
this.logger.error("Failed to capture tree", {
|
|
20692
|
+
error: result.error,
|
|
20693
|
+
failedStep: result.failedStep
|
|
20694
|
+
});
|
|
20695
|
+
throw new Error(
|
|
20696
|
+
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
20681
20697
|
);
|
|
20682
20698
|
}
|
|
20683
|
-
|
|
20684
|
-
|
|
20685
|
-
resetTurnMessages(sessionId) {
|
|
20686
|
-
const session = this.sessions.get(sessionId);
|
|
20687
|
-
if (session) {
|
|
20688
|
-
session.currentTurnMessages = [];
|
|
20699
|
+
if (result.data.newTreeHash !== null) {
|
|
20700
|
+
this.lastTreeHash = result.data.newTreeHash;
|
|
20689
20701
|
}
|
|
20702
|
+
return result.data.snapshot;
|
|
20690
20703
|
}
|
|
20691
|
-
|
|
20692
|
-
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
|
|
20696
|
-
|
|
20697
|
-
|
|
20698
|
-
return null;
|
|
20699
|
-
}
|
|
20700
|
-
const content = update.content;
|
|
20701
|
-
if (content?.type === "text" && typeof content.text === "string") {
|
|
20702
|
-
const trimmed2 = content.text.trim();
|
|
20703
|
-
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20704
|
+
/**
|
|
20705
|
+
* Download and apply a tree snapshot.
|
|
20706
|
+
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
20707
|
+
*/
|
|
20708
|
+
async applyTreeSnapshot(snapshot) {
|
|
20709
|
+
if (!this.apiClient) {
|
|
20710
|
+
throw new Error("Cannot apply snapshot: API client not configured");
|
|
20704
20711
|
}
|
|
20705
|
-
if (
|
|
20706
|
-
|
|
20707
|
-
|
|
20712
|
+
if (!snapshot.archiveUrl) {
|
|
20713
|
+
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
20714
|
+
treeHash: snapshot.treeHash,
|
|
20715
|
+
changes: snapshot.changes.length
|
|
20716
|
+
});
|
|
20717
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20708
20718
|
}
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20719
|
+
const saga = new ApplySnapshotSaga(this.logger);
|
|
20720
|
+
const result = await saga.run({
|
|
20721
|
+
snapshot,
|
|
20722
|
+
repositoryPath: this.repositoryPath,
|
|
20723
|
+
apiClient: this.apiClient,
|
|
20724
|
+
taskId: this.taskId,
|
|
20725
|
+
runId: this.runId
|
|
20726
|
+
});
|
|
20727
|
+
if (!result.success) {
|
|
20728
|
+
this.logger.error("Failed to apply tree snapshot", {
|
|
20729
|
+
error: result.error,
|
|
20730
|
+
failedStep: result.failedStep,
|
|
20731
|
+
treeHash: snapshot.treeHash
|
|
20732
|
+
});
|
|
20733
|
+
throw new Error(
|
|
20734
|
+
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
20722
20735
|
);
|
|
20723
|
-
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20724
|
-
delay3 = 0;
|
|
20725
|
-
} else {
|
|
20726
|
-
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
20727
20736
|
}
|
|
20728
|
-
|
|
20729
|
-
this.flushTimeouts.set(sessionId, timeout);
|
|
20737
|
+
this.lastTreeHash = result.data.treeHash;
|
|
20730
20738
|
}
|
|
20731
|
-
|
|
20732
|
-
|
|
20733
|
-
|
|
20734
|
-
|
|
20735
|
-
|
|
20736
|
-
this.localCachePath,
|
|
20737
|
-
"sessions",
|
|
20738
|
-
session.context.runId,
|
|
20739
|
-
"logs.ndjson"
|
|
20740
|
-
);
|
|
20741
|
-
try {
|
|
20742
|
-
fs13.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
20743
|
-
`);
|
|
20744
|
-
} catch (error) {
|
|
20745
|
-
this.logger.warn("Failed to write to local cache", {
|
|
20746
|
-
taskId: session.context.taskId,
|
|
20747
|
-
runId: session.context.runId,
|
|
20748
|
-
logPath,
|
|
20749
|
-
error
|
|
20750
|
-
});
|
|
20751
|
-
}
|
|
20739
|
+
/**
|
|
20740
|
+
* Get the last captured tree hash.
|
|
20741
|
+
*/
|
|
20742
|
+
getLastTreeHash() {
|
|
20743
|
+
return this.lastTreeHash;
|
|
20752
20744
|
}
|
|
20753
|
-
|
|
20754
|
-
|
|
20755
|
-
|
|
20756
|
-
|
|
20757
|
-
|
|
20758
|
-
const now = Date.now();
|
|
20759
|
-
for (const entry of entries) {
|
|
20760
|
-
const entryPath = path16.join(sessionsDir, entry);
|
|
20761
|
-
try {
|
|
20762
|
-
const stats = await fsp.stat(entryPath);
|
|
20763
|
-
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
20764
|
-
await fsp.rm(entryPath, { recursive: true, force: true });
|
|
20765
|
-
deleted++;
|
|
20766
|
-
}
|
|
20767
|
-
} catch {
|
|
20768
|
-
}
|
|
20769
|
-
}
|
|
20770
|
-
} catch {
|
|
20771
|
-
}
|
|
20772
|
-
return deleted;
|
|
20745
|
+
/**
|
|
20746
|
+
* Set the last tree hash (used when resuming).
|
|
20747
|
+
*/
|
|
20748
|
+
setLastTreeHash(hash) {
|
|
20749
|
+
this.lastTreeHash = hash;
|
|
20773
20750
|
}
|
|
20774
20751
|
};
|
|
20775
20752
|
|
|
@@ -21245,45 +21222,29 @@ var AgentServer = class {
|
|
|
21245
21222
|
});
|
|
21246
21223
|
await this.autoInitializeSession();
|
|
21247
21224
|
}
|
|
21248
|
-
async
|
|
21249
|
-
|
|
21250
|
-
|
|
21251
|
-
|
|
21252
|
-
|
|
21253
|
-
|
|
21254
|
-
|
|
21255
|
-
|
|
21225
|
+
async loadResumeState(taskId, resumeRunId, currentRunId) {
|
|
21226
|
+
this.logger.debug("Loading resume state", { resumeRunId, currentRunId });
|
|
21227
|
+
try {
|
|
21228
|
+
this.resumeState = await resumeFromLog({
|
|
21229
|
+
taskId,
|
|
21230
|
+
runId: resumeRunId,
|
|
21231
|
+
repositoryPath: this.config.repositoryPath,
|
|
21232
|
+
apiClient: this.posthogAPI,
|
|
21233
|
+
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
21256
21234
|
});
|
|
21257
|
-
|
|
21258
|
-
this.resumeState
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
21264
|
-
|
|
21265
|
-
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
});
|
|
21270
|
-
} catch (error) {
|
|
21271
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21272
|
-
error
|
|
21273
|
-
});
|
|
21274
|
-
this.resumeState = null;
|
|
21275
|
-
}
|
|
21235
|
+
this.logger.debug("Resume state loaded", {
|
|
21236
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
21237
|
+
hasSnapshot: !!this.resumeState.latestSnapshot,
|
|
21238
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21239
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null,
|
|
21240
|
+
logEntries: this.resumeState.logEntryCount
|
|
21241
|
+
});
|
|
21242
|
+
} catch (error) {
|
|
21243
|
+
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21244
|
+
error
|
|
21245
|
+
});
|
|
21246
|
+
this.resumeState = null;
|
|
21276
21247
|
}
|
|
21277
|
-
const payload = {
|
|
21278
|
-
task_id: taskId,
|
|
21279
|
-
run_id: runId,
|
|
21280
|
-
team_id: projectId,
|
|
21281
|
-
user_id: 0,
|
|
21282
|
-
// System-initiated
|
|
21283
|
-
distinct_id: "agent-server",
|
|
21284
|
-
mode
|
|
21285
|
-
};
|
|
21286
|
-
await this.initializeSession(payload, null);
|
|
21287
21248
|
}
|
|
21288
21249
|
async stop() {
|
|
21289
21250
|
this.logger.debug("Stopping agent server...");
|
|
@@ -21682,29 +21643,11 @@ var AgentServer = class {
|
|
|
21682
21643
|
if (!this.resumeState) {
|
|
21683
21644
|
const resumeRunId = this.getResumeRunId(taskRun);
|
|
21684
21645
|
if (resumeRunId) {
|
|
21685
|
-
this.
|
|
21646
|
+
await this.loadResumeState(
|
|
21647
|
+
payload.task_id,
|
|
21686
21648
|
resumeRunId,
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
try {
|
|
21690
|
-
this.resumeState = await resumeFromLog({
|
|
21691
|
-
taskId: payload.task_id,
|
|
21692
|
-
runId: resumeRunId,
|
|
21693
|
-
repositoryPath: this.config.repositoryPath,
|
|
21694
|
-
apiClient: this.posthogAPI,
|
|
21695
|
-
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
21696
|
-
});
|
|
21697
|
-
this.logger.debug("Resume state loaded (via TaskRun state)", {
|
|
21698
|
-
conversationTurns: this.resumeState.conversation.length,
|
|
21699
|
-
snapshotApplied: this.resumeState.snapshotApplied,
|
|
21700
|
-
logEntries: this.resumeState.logEntryCount
|
|
21701
|
-
});
|
|
21702
|
-
} catch (error) {
|
|
21703
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21704
|
-
error
|
|
21705
|
-
});
|
|
21706
|
-
this.resumeState = null;
|
|
21707
|
-
}
|
|
21649
|
+
payload.run_id
|
|
21650
|
+
);
|
|
21708
21651
|
}
|
|
21709
21652
|
}
|
|
21710
21653
|
if (this.resumeState && this.resumeState.conversation.length > 0) {
|
|
@@ -21763,8 +21706,59 @@ var AgentServer = class {
|
|
|
21763
21706
|
const conversationSummary = formatConversationForResume(
|
|
21764
21707
|
this.resumeState.conversation
|
|
21765
21708
|
);
|
|
21709
|
+
let snapshotApplied = false;
|
|
21710
|
+
if (this.resumeState.latestSnapshot?.archiveUrl && this.config.repositoryPath && this.posthogAPI) {
|
|
21711
|
+
try {
|
|
21712
|
+
const treeTracker = new TreeTracker({
|
|
21713
|
+
repositoryPath: this.config.repositoryPath,
|
|
21714
|
+
taskId: payload.task_id,
|
|
21715
|
+
runId: payload.run_id,
|
|
21716
|
+
apiClient: this.posthogAPI,
|
|
21717
|
+
logger: this.logger.child("TreeTracker")
|
|
21718
|
+
});
|
|
21719
|
+
await treeTracker.applyTreeSnapshot(this.resumeState.latestSnapshot);
|
|
21720
|
+
treeTracker.setLastTreeHash(this.resumeState.latestSnapshot.treeHash);
|
|
21721
|
+
snapshotApplied = true;
|
|
21722
|
+
this.logger.info("Tree snapshot applied", {
|
|
21723
|
+
treeHash: this.resumeState.latestSnapshot.treeHash,
|
|
21724
|
+
changes: this.resumeState.latestSnapshot.changes?.length ?? 0,
|
|
21725
|
+
hasArchiveUrl: !!this.resumeState.latestSnapshot.archiveUrl
|
|
21726
|
+
});
|
|
21727
|
+
} catch (error) {
|
|
21728
|
+
this.logger.warn("Failed to apply tree snapshot", {
|
|
21729
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21730
|
+
treeHash: this.resumeState.latestSnapshot.treeHash
|
|
21731
|
+
});
|
|
21732
|
+
}
|
|
21733
|
+
}
|
|
21734
|
+
if (this.resumeState.latestGitCheckpoint && this.config.repositoryPath && this.posthogAPI) {
|
|
21735
|
+
try {
|
|
21736
|
+
const checkpointTracker = new HandoffCheckpointTracker({
|
|
21737
|
+
repositoryPath: this.config.repositoryPath,
|
|
21738
|
+
taskId: payload.task_id,
|
|
21739
|
+
runId: payload.run_id,
|
|
21740
|
+
apiClient: this.posthogAPI,
|
|
21741
|
+
logger: this.logger.child("HandoffCheckpoint")
|
|
21742
|
+
});
|
|
21743
|
+
const metrics = await checkpointTracker.applyFromHandoff(
|
|
21744
|
+
this.resumeState.latestGitCheckpoint
|
|
21745
|
+
);
|
|
21746
|
+
this.logger.info("Git checkpoint applied", {
|
|
21747
|
+
branch: this.resumeState.latestGitCheckpoint.branch,
|
|
21748
|
+
head: this.resumeState.latestGitCheckpoint.head,
|
|
21749
|
+
packBytes: metrics.packBytes,
|
|
21750
|
+
indexBytes: metrics.indexBytes,
|
|
21751
|
+
totalBytes: metrics.totalBytes
|
|
21752
|
+
});
|
|
21753
|
+
} catch (error) {
|
|
21754
|
+
this.logger.warn("Failed to apply git checkpoint", {
|
|
21755
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21756
|
+
branch: this.resumeState.latestGitCheckpoint.branch
|
|
21757
|
+
});
|
|
21758
|
+
}
|
|
21759
|
+
}
|
|
21766
21760
|
const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
|
|
21767
|
-
const sandboxContext =
|
|
21761
|
+
const sandboxContext = snapshotApplied ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.` : `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.`;
|
|
21768
21762
|
let resumePromptBlocks;
|
|
21769
21763
|
if (pendingUserPrompt?.length) {
|
|
21770
21764
|
resumePromptBlocks = [
|
|
@@ -21805,7 +21799,9 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21805
21799
|
conversationTurns: this.resumeState.conversation.length,
|
|
21806
21800
|
promptLength: promptBlocksToText(resumePromptBlocks).length,
|
|
21807
21801
|
hasPendingUserMessage: !!pendingUserPrompt?.length,
|
|
21808
|
-
snapshotApplied
|
|
21802
|
+
snapshotApplied,
|
|
21803
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21804
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null
|
|
21809
21805
|
});
|
|
21810
21806
|
this.resumeState = null;
|
|
21811
21807
|
this.session.logWriter.resetTurnMessages(payload.run_id);
|
|
@@ -21969,6 +21965,24 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21969
21965
|
const normalizedName = baseName.replace(/[^\w.-]/g, "_");
|
|
21970
21966
|
return normalizedName.length > 0 ? normalizedName : "attachment";
|
|
21971
21967
|
}
|
|
21968
|
+
async autoInitializeSession() {
|
|
21969
|
+
const { taskId, runId, mode, projectId } = this.config;
|
|
21970
|
+
this.logger.debug("Auto-initializing session", { taskId, runId, mode });
|
|
21971
|
+
const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21972
|
+
if (resumeRunId) {
|
|
21973
|
+
await this.loadResumeState(taskId, resumeRunId, runId);
|
|
21974
|
+
}
|
|
21975
|
+
const payload = {
|
|
21976
|
+
task_id: taskId,
|
|
21977
|
+
run_id: runId,
|
|
21978
|
+
team_id: projectId,
|
|
21979
|
+
user_id: 0,
|
|
21980
|
+
// System-initiated
|
|
21981
|
+
distinct_id: "agent-server",
|
|
21982
|
+
mode
|
|
21983
|
+
};
|
|
21984
|
+
await this.initializeSession(payload, null);
|
|
21985
|
+
}
|
|
21972
21986
|
getResumeRunId(taskRun) {
|
|
21973
21987
|
const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21974
21988
|
if (envRunId) return envRunId;
|