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