@posthog/agent 2.3.387 → 2.3.398
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/adapters/claude/mcp/tool-metadata.d.ts +24 -0
- package/dist/adapters/claude/mcp/tool-metadata.js +165 -0
- package/dist/adapters/claude/mcp/tool-metadata.js.map +1 -0
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/agent.js +120 -3
- 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/index.d.ts +7 -9
- package/dist/index.js.map +1 -1
- package/dist/posthog-api.d.ts +1 -0
- package/dist/posthog-api.js +12 -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 +1305 -1181
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1303 -1179
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +5 -1
- package/src/adapters/claude/claude-agent.ts +5 -0
- package/src/adapters/claude/mcp/tool-metadata.test.ts +93 -0
- package/src/adapters/claude/mcp/tool-metadata.ts +33 -0
- package/src/adapters/claude/permissions/permission-handlers.test.ts +165 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +105 -0
- package/src/adapters/claude/session/instructions.ts +9 -1
- package/src/adapters/claude/types.ts +2 -0
- 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.398",
|
|
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: {
|
|
@@ -8781,6 +8781,10 @@ var package_default = {
|
|
|
8781
8781
|
types: "./dist/adapters/reasoning-effort.d.ts",
|
|
8782
8782
|
import: "./dist/adapters/reasoning-effort.js"
|
|
8783
8783
|
},
|
|
8784
|
+
"./adapters/claude/mcp/tool-metadata": {
|
|
8785
|
+
types: "./dist/adapters/claude/mcp/tool-metadata.d.ts",
|
|
8786
|
+
import: "./dist/adapters/claude/mcp/tool-metadata.js"
|
|
8787
|
+
},
|
|
8784
8788
|
"./execution-mode": {
|
|
8785
8789
|
types: "./dist/execution-mode.d.ts",
|
|
8786
8790
|
import: "./dist/execution-mode.js"
|
|
@@ -13776,10 +13780,12 @@ async function fetchMcpToolMetadata(q, logger = new Logger({ debug: false, prefi
|
|
|
13776
13780
|
for (const tool of server.tools) {
|
|
13777
13781
|
const toolKey = buildToolKey(server.name, tool.name);
|
|
13778
13782
|
const readOnly = tool.annotations?.readOnly === true;
|
|
13783
|
+
const existing = mcpToolMetadataCache.get(toolKey);
|
|
13779
13784
|
mcpToolMetadataCache.set(toolKey, {
|
|
13780
13785
|
readOnly,
|
|
13781
13786
|
name: tool.name,
|
|
13782
|
-
description: tool.description
|
|
13787
|
+
description: tool.description,
|
|
13788
|
+
approvalState: existing?.approvalState
|
|
13783
13789
|
});
|
|
13784
13790
|
if (readOnly) readOnlyCount++;
|
|
13785
13791
|
}
|
|
@@ -13821,6 +13827,23 @@ function getConnectedMcpServerNames() {
|
|
|
13821
13827
|
}
|
|
13822
13828
|
return [...names];
|
|
13823
13829
|
}
|
|
13830
|
+
function getMcpToolApprovalState(toolName) {
|
|
13831
|
+
return mcpToolMetadataCache.get(toolName)?.approvalState;
|
|
13832
|
+
}
|
|
13833
|
+
function setMcpToolApprovalStates(approvals) {
|
|
13834
|
+
for (const [toolKey, approvalState] of Object.entries(approvals)) {
|
|
13835
|
+
const existing = mcpToolMetadataCache.get(toolKey);
|
|
13836
|
+
if (existing) {
|
|
13837
|
+
existing.approvalState = approvalState;
|
|
13838
|
+
} else {
|
|
13839
|
+
mcpToolMetadataCache.set(toolKey, {
|
|
13840
|
+
readOnly: false,
|
|
13841
|
+
name: toolKey,
|
|
13842
|
+
approvalState
|
|
13843
|
+
});
|
|
13844
|
+
}
|
|
13845
|
+
}
|
|
13846
|
+
}
|
|
13824
13847
|
|
|
13825
13848
|
// src/adapters/claude/conversion/tool-use-to-acp.ts
|
|
13826
13849
|
var SYSTEM_REMINDER_REGEX = /\s*<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -15516,6 +15539,72 @@ async function handleDefaultPermissionFlow(context) {
|
|
|
15516
15539
|
return { behavior: "deny", message, interrupt: !feedback };
|
|
15517
15540
|
}
|
|
15518
15541
|
}
|
|
15542
|
+
function parseMcpToolName(toolName) {
|
|
15543
|
+
const parts2 = toolName.split("__");
|
|
15544
|
+
return {
|
|
15545
|
+
serverName: parts2[1] ?? toolName,
|
|
15546
|
+
tool: parts2.slice(2).join("__") || toolName
|
|
15547
|
+
};
|
|
15548
|
+
}
|
|
15549
|
+
async function handleMcpApprovalFlow(context) {
|
|
15550
|
+
const { toolName, toolInput, toolUseID, client, sessionId } = context;
|
|
15551
|
+
const { serverName, tool: displayTool } = parseMcpToolName(toolName);
|
|
15552
|
+
const metadata2 = getMcpToolMetadata(toolName);
|
|
15553
|
+
const description = metadata2?.description ? `
|
|
15554
|
+
|
|
15555
|
+
${metadata2.description}` : "";
|
|
15556
|
+
const response = await client.requestPermission({
|
|
15557
|
+
options: [
|
|
15558
|
+
{ kind: "allow_once", name: "Yes", optionId: "allow" },
|
|
15559
|
+
{
|
|
15560
|
+
kind: "allow_always",
|
|
15561
|
+
name: "Yes, always allow",
|
|
15562
|
+
optionId: "allow_always"
|
|
15563
|
+
},
|
|
15564
|
+
{
|
|
15565
|
+
kind: "reject_once",
|
|
15566
|
+
name: "Type here to tell the agent what to do differently",
|
|
15567
|
+
optionId: "reject",
|
|
15568
|
+
_meta: { customInput: true }
|
|
15569
|
+
}
|
|
15570
|
+
],
|
|
15571
|
+
sessionId,
|
|
15572
|
+
toolCall: {
|
|
15573
|
+
toolCallId: toolUseID,
|
|
15574
|
+
title: `The agent wants to call ${displayTool} (${serverName})`,
|
|
15575
|
+
kind: "other",
|
|
15576
|
+
content: description ? [{ type: "content", content: text(description) }] : [],
|
|
15577
|
+
rawInput: { ...toolInput, toolName }
|
|
15578
|
+
}
|
|
15579
|
+
});
|
|
15580
|
+
if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
|
|
15581
|
+
throw new Error("Tool use aborted");
|
|
15582
|
+
}
|
|
15583
|
+
if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
|
|
15584
|
+
if (response.outcome.optionId === "allow_always") {
|
|
15585
|
+
return {
|
|
15586
|
+
behavior: "allow",
|
|
15587
|
+
updatedInput: toolInput,
|
|
15588
|
+
updatedPermissions: [
|
|
15589
|
+
{
|
|
15590
|
+
type: "addRules",
|
|
15591
|
+
rules: [{ toolName }],
|
|
15592
|
+
behavior: "allow",
|
|
15593
|
+
destination: "localSettings"
|
|
15594
|
+
}
|
|
15595
|
+
]
|
|
15596
|
+
};
|
|
15597
|
+
}
|
|
15598
|
+
return {
|
|
15599
|
+
behavior: "allow",
|
|
15600
|
+
updatedInput: toolInput
|
|
15601
|
+
};
|
|
15602
|
+
}
|
|
15603
|
+
const feedback = response._meta?.customInput?.trim();
|
|
15604
|
+
const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
|
|
15605
|
+
await emitToolDenial(context, message);
|
|
15606
|
+
return { behavior: "deny", message, interrupt: !feedback };
|
|
15607
|
+
}
|
|
15519
15608
|
function handlePlanFileException(context) {
|
|
15520
15609
|
const { session, toolName, toolInput } = context;
|
|
15521
15610
|
if (session.permissionMode !== "plan" || !WRITE_TOOLS.has(toolName)) {
|
|
@@ -15586,6 +15675,17 @@ async function canUseTool(context) {
|
|
|
15586
15675
|
}
|
|
15587
15676
|
}
|
|
15588
15677
|
}
|
|
15678
|
+
if (toolName.startsWith("mcp__")) {
|
|
15679
|
+
const approvalState = getMcpToolApprovalState(toolName);
|
|
15680
|
+
if (approvalState === "do_not_use") {
|
|
15681
|
+
const message = "This tool has been blocked. To re-enable it, go to Settings > MCP Servers in PostHog Code.";
|
|
15682
|
+
await emitToolDenial(context, message);
|
|
15683
|
+
return { behavior: "deny", message, interrupt: false };
|
|
15684
|
+
}
|
|
15685
|
+
if (approvalState === "needs_approval") {
|
|
15686
|
+
return handleMcpApprovalFlow(context);
|
|
15687
|
+
}
|
|
15688
|
+
}
|
|
15589
15689
|
if (isToolAllowedForMode(toolName, session.permissionMode)) {
|
|
15590
15690
|
return {
|
|
15591
15691
|
behavior: "allow",
|
|
@@ -15691,7 +15791,14 @@ Only enter plan mode (EnterPlanMode) when the user is requesting a significant c
|
|
|
15691
15791
|
|
|
15692
15792
|
When in doubt, continue executing and incorporate the feedback inline.
|
|
15693
15793
|
`;
|
|
15694
|
-
var
|
|
15794
|
+
var MCP_TOOLS = `
|
|
15795
|
+
# MCP Tool Access
|
|
15796
|
+
|
|
15797
|
+
If an MCP tool call is explicitly denied with a message, relay that denial message to the user exactly as given. Do NOT suggest checking "Claude Code settings."
|
|
15798
|
+
|
|
15799
|
+
If an MCP tool call returns an error, treat it as a normal tool error \u2014 troubleshoot, retry, or inform the user about the specific error. Do NOT assume it is a permissions issue and do NOT direct the user to any settings page.
|
|
15800
|
+
`;
|
|
15801
|
+
var APPENDED_INSTRUCTIONS = BRANCH_NAMING + PLAN_MODE + MCP_TOOLS;
|
|
15695
15802
|
|
|
15696
15803
|
// src/adapters/claude/session/options.ts
|
|
15697
15804
|
function buildSystemPrompt(customPrompt) {
|
|
@@ -17020,6 +17127,9 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
|
|
|
17020
17127
|
const earlyModelId = settingsManager.getSettings().model || meta?.model || "";
|
|
17021
17128
|
const mcpServers = supportsMcpInjection(earlyModelId) ? parseMcpServers(params) : {};
|
|
17022
17129
|
const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
|
|
17130
|
+
if (meta?.mcpToolApprovals) {
|
|
17131
|
+
setMcpToolApprovalStates(meta.mcpToolApprovals);
|
|
17132
|
+
}
|
|
17023
17133
|
const outputFormat = meta?.jsonSchema && this.options?.onStructuredOutput ? { type: "json_schema", schema: meta.jsonSchema } : void 0;
|
|
17024
17134
|
this.logger.debug(isResume ? "Resuming session" : "Creating new session", {
|
|
17025
17135
|
sessionId,
|
|
@@ -19165,6 +19275,11 @@ var HandoffCheckpointTracker = class {
|
|
|
19165
19275
|
onDivergedBranch: options?.onDivergedBranch
|
|
19166
19276
|
});
|
|
19167
19277
|
this.logApplyMetrics(checkpoint, downloads, applyResult.totalBytes);
|
|
19278
|
+
return {
|
|
19279
|
+
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
19280
|
+
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
19281
|
+
totalBytes: applyResult.totalBytes
|
|
19282
|
+
};
|
|
19168
19283
|
} finally {
|
|
19169
19284
|
await this.removeIfPresent(packPath);
|
|
19170
19285
|
await this.removeIfPresent(indexPath);
|
|
@@ -19211,22 +19326,22 @@ var HandoffCheckpointTracker = class {
|
|
|
19211
19326
|
};
|
|
19212
19327
|
}
|
|
19213
19328
|
async uploadArtifacts(specs) {
|
|
19214
|
-
const
|
|
19215
|
-
|
|
19216
|
-
|
|
19217
|
-
|
|
19218
|
-
|
|
19219
|
-
|
|
19220
|
-
|
|
19221
|
-
|
|
19222
|
-
|
|
19223
|
-
|
|
19224
|
-
|
|
19225
|
-
|
|
19226
|
-
|
|
19227
|
-
|
|
19228
|
-
|
|
19229
|
-
return Object.fromEntries(
|
|
19329
|
+
const results = [];
|
|
19330
|
+
for (const spec of specs) {
|
|
19331
|
+
if (!spec.filePath) {
|
|
19332
|
+
results.push([spec.key, void 0]);
|
|
19333
|
+
continue;
|
|
19334
|
+
}
|
|
19335
|
+
results.push([
|
|
19336
|
+
spec.key,
|
|
19337
|
+
await this.uploadArtifactFile(
|
|
19338
|
+
spec.filePath,
|
|
19339
|
+
spec.name,
|
|
19340
|
+
spec.contentType
|
|
19341
|
+
)
|
|
19342
|
+
]);
|
|
19343
|
+
}
|
|
19344
|
+
return Object.fromEntries(results);
|
|
19230
19345
|
}
|
|
19231
19346
|
async downloadArtifactToFile(artifactPath, filePath, label) {
|
|
19232
19347
|
if (!this.apiClient) {
|
|
@@ -19238,7 +19353,7 @@ var HandoffCheckpointTracker = class {
|
|
|
19238
19353
|
artifactPath
|
|
19239
19354
|
);
|
|
19240
19355
|
if (!arrayBuffer) {
|
|
19241
|
-
throw new Error(`Failed to download ${label}`);
|
|
19356
|
+
throw new Error(`Failed to download ${label} from ${artifactPath}`);
|
|
19242
19357
|
}
|
|
19243
19358
|
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19244
19359
|
const binaryContent = Buffer.from(base64Content, "base64");
|
|
@@ -19422,6 +19537,13 @@ var PostHogAPIClient = class {
|
|
|
19422
19537
|
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
19423
19538
|
);
|
|
19424
19539
|
}
|
|
19540
|
+
async resumeRunInCloud(taskId, runId) {
|
|
19541
|
+
const teamId = this.getTeamId();
|
|
19542
|
+
return this.apiRequest(
|
|
19543
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/resume_in_cloud/`,
|
|
19544
|
+
{ method: "POST" }
|
|
19545
|
+
);
|
|
19546
|
+
}
|
|
19425
19547
|
async updateTaskRun(taskId, runId, payload) {
|
|
19426
19548
|
const teamId = this.getTeamId();
|
|
19427
19549
|
return this.apiRequest(
|
|
@@ -19569,1215 +19691,1180 @@ function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
|
19569
19691
|
return turns.slice(startIndex);
|
|
19570
19692
|
}
|
|
19571
19693
|
|
|
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 };
|
|
19694
|
+
// src/sagas/resume-saga.ts
|
|
19695
|
+
var ResumeSaga = class extends Saga {
|
|
19696
|
+
sagaName = "ResumeSaga";
|
|
19697
|
+
async execute(input) {
|
|
19698
|
+
const { taskId, runId, apiClient } = input;
|
|
19699
|
+
const taskRun = await this.readOnlyStep(
|
|
19700
|
+
"fetch_task_run",
|
|
19701
|
+
() => apiClient.getTaskRun(taskId, runId)
|
|
19702
|
+
);
|
|
19703
|
+
if (!taskRun.log_url) {
|
|
19704
|
+
this.log.info("No log URL found, starting fresh");
|
|
19705
|
+
return this.emptyResult();
|
|
19615
19706
|
}
|
|
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);
|
|
19707
|
+
const entries = await this.readOnlyStep(
|
|
19708
|
+
"fetch_logs",
|
|
19709
|
+
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
19710
|
+
);
|
|
19711
|
+
if (entries.length === 0) {
|
|
19712
|
+
this.log.info("No log entries found, starting fresh");
|
|
19713
|
+
return this.emptyResult();
|
|
19635
19714
|
}
|
|
19636
|
-
this.log.info("
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
if (
|
|
19646
|
-
|
|
19715
|
+
this.log.info("Fetched log entries", { count: entries.length });
|
|
19716
|
+
const latestSnapshot = await this.readOnlyStep(
|
|
19717
|
+
"find_snapshot",
|
|
19718
|
+
() => Promise.resolve(this.findLatestTreeSnapshot(entries))
|
|
19719
|
+
);
|
|
19720
|
+
const latestGitCheckpoint = await this.readOnlyStep(
|
|
19721
|
+
"find_git_checkpoint",
|
|
19722
|
+
() => Promise.resolve(this.findLatestGitCheckpoint(entries))
|
|
19723
|
+
);
|
|
19724
|
+
if (latestSnapshot) {
|
|
19725
|
+
this.log.info("Found tree snapshot", {
|
|
19726
|
+
treeHash: latestSnapshot.treeHash,
|
|
19727
|
+
hasArchiveUrl: !!latestSnapshot.archiveUrl,
|
|
19728
|
+
changes: latestSnapshot.changes?.length ?? 0
|
|
19729
|
+
});
|
|
19647
19730
|
}
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
|
|
19731
|
+
if (latestGitCheckpoint) {
|
|
19732
|
+
this.log.info("Found git checkpoint", {
|
|
19733
|
+
checkpointId: latestGitCheckpoint.checkpointId,
|
|
19734
|
+
branch: latestGitCheckpoint.branch
|
|
19735
|
+
});
|
|
19651
19736
|
}
|
|
19652
|
-
await this.
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
|
|
19664
|
-
|
|
19665
|
-
});
|
|
19666
|
-
}
|
|
19737
|
+
const conversation = await this.readOnlyStep(
|
|
19738
|
+
"rebuild_conversation",
|
|
19739
|
+
() => Promise.resolve(this.rebuildConversation(entries))
|
|
19740
|
+
);
|
|
19741
|
+
const lastDevice = await this.readOnlyStep(
|
|
19742
|
+
"find_device",
|
|
19743
|
+
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
19744
|
+
);
|
|
19745
|
+
this.log.info("Resume state rebuilt", {
|
|
19746
|
+
turns: conversation.length,
|
|
19747
|
+
hasSnapshot: !!latestSnapshot,
|
|
19748
|
+
hasGitCheckpoint: !!latestGitCheckpoint,
|
|
19749
|
+
interrupted: latestSnapshot?.interrupted ?? false
|
|
19667
19750
|
});
|
|
19668
|
-
return
|
|
19751
|
+
return {
|
|
19752
|
+
conversation,
|
|
19753
|
+
latestSnapshot,
|
|
19754
|
+
latestGitCheckpoint,
|
|
19755
|
+
interrupted: latestSnapshot?.interrupted ?? false,
|
|
19756
|
+
lastDevice,
|
|
19757
|
+
logEntryCount: entries.length
|
|
19758
|
+
};
|
|
19669
19759
|
}
|
|
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";
|
|
19760
|
+
emptyResult() {
|
|
19761
|
+
return {
|
|
19762
|
+
conversation: [],
|
|
19763
|
+
latestSnapshot: null,
|
|
19764
|
+
latestGitCheckpoint: null,
|
|
19765
|
+
interrupted: false,
|
|
19766
|
+
logEntryCount: 0
|
|
19767
|
+
};
|
|
19768
|
+
}
|
|
19769
|
+
findLatestTreeSnapshot(entries) {
|
|
19770
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19771
|
+
const entry = entries[i2];
|
|
19772
|
+
if (isNotification(
|
|
19773
|
+
entry.notification?.method,
|
|
19774
|
+
POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
19775
|
+
)) {
|
|
19776
|
+
const params = entry.notification.params;
|
|
19777
|
+
if (params?.treeHash) {
|
|
19778
|
+
return params;
|
|
19779
|
+
}
|
|
19696
19780
|
}
|
|
19697
|
-
changes.push({ path: filePath, status: normalizedStatus });
|
|
19698
19781
|
}
|
|
19699
|
-
return
|
|
19782
|
+
return null;
|
|
19700
19783
|
}
|
|
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;
|
|
19784
|
+
findLatestGitCheckpoint(entries) {
|
|
19785
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
|
|
19786
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19787
|
+
const entry = entries[i2];
|
|
19788
|
+
const method = entry.notification?.method;
|
|
19789
|
+
if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
|
|
19790
|
+
const params = entry.notification?.params;
|
|
19791
|
+
if (params?.checkpointId && params?.checkpointRef) {
|
|
19792
|
+
return params;
|
|
19793
|
+
}
|
|
19717
19794
|
}
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19795
|
+
}
|
|
19796
|
+
return null;
|
|
19797
|
+
}
|
|
19798
|
+
findLastDeviceInfo(entries) {
|
|
19799
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19800
|
+
const entry = entries[i2];
|
|
19801
|
+
const params = entry.notification?.params;
|
|
19802
|
+
if (params?.device) {
|
|
19803
|
+
return params.device;
|
|
19722
19804
|
}
|
|
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);
|
|
19805
|
+
}
|
|
19806
|
+
return void 0;
|
|
19807
|
+
}
|
|
19808
|
+
rebuildConversation(entries) {
|
|
19809
|
+
const turns = [];
|
|
19810
|
+
let currentAssistantContent = [];
|
|
19811
|
+
let currentToolCalls = [];
|
|
19812
|
+
for (const entry of entries) {
|
|
19813
|
+
const method = entry.notification?.method;
|
|
19814
|
+
const params = entry.notification?.params;
|
|
19815
|
+
if (method === "session/update" && params?.update) {
|
|
19816
|
+
const update = params.update;
|
|
19817
|
+
const sessionUpdate = update.sessionUpdate;
|
|
19818
|
+
switch (sessionUpdate) {
|
|
19819
|
+
case "user_message":
|
|
19820
|
+
case "user_message_chunk": {
|
|
19821
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19822
|
+
turns.push({
|
|
19823
|
+
role: "assistant",
|
|
19824
|
+
content: currentAssistantContent,
|
|
19825
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19826
|
+
});
|
|
19827
|
+
currentAssistantContent = [];
|
|
19828
|
+
currentToolCalls = [];
|
|
19753
19829
|
}
|
|
19754
|
-
|
|
19755
|
-
|
|
19830
|
+
const content = update.content;
|
|
19831
|
+
const contentArray = Array.isArray(content) ? content : [content];
|
|
19832
|
+
turns.push({
|
|
19833
|
+
role: "user",
|
|
19834
|
+
content: contentArray
|
|
19835
|
+
});
|
|
19836
|
+
break;
|
|
19756
19837
|
}
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
|
|
19761
|
-
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
|
|
19765
|
-
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
} catch {
|
|
19838
|
+
case "agent_message": {
|
|
19839
|
+
const content = update.content;
|
|
19840
|
+
if (content) {
|
|
19841
|
+
if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
|
|
19842
|
+
const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
|
|
19843
|
+
lastBlock.text += content.text;
|
|
19844
|
+
} else {
|
|
19845
|
+
currentAssistantContent.push(content);
|
|
19846
|
+
}
|
|
19847
|
+
}
|
|
19848
|
+
break;
|
|
19769
19849
|
}
|
|
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
|
-
});
|
|
19850
|
+
case "agent_message_chunk": {
|
|
19851
|
+
const content = update.content;
|
|
19852
|
+
if (content) {
|
|
19853
|
+
if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
|
|
19854
|
+
const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
|
|
19855
|
+
lastBlock.text += content.text;
|
|
19856
|
+
} else {
|
|
19857
|
+
currentAssistantContent.push(content);
|
|
19858
|
+
}
|
|
19794
19859
|
}
|
|
19860
|
+
break;
|
|
19795
19861
|
}
|
|
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
|
-
}
|
|
19862
|
+
case "tool_call":
|
|
19863
|
+
case "tool_call_update": {
|
|
19864
|
+
const meta = update._meta?.claudeCode;
|
|
19865
|
+
if (meta) {
|
|
19866
|
+
const toolCallId = meta.toolCallId;
|
|
19867
|
+
const toolName = meta.toolName;
|
|
19868
|
+
const toolInput = meta.toolInput;
|
|
19869
|
+
const toolResponse = meta.toolResponse;
|
|
19870
|
+
if (toolCallId && toolName) {
|
|
19871
|
+
let toolCall = currentToolCalls.find(
|
|
19872
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19873
|
+
);
|
|
19874
|
+
if (!toolCall) {
|
|
19875
|
+
toolCall = {
|
|
19876
|
+
toolCallId,
|
|
19877
|
+
toolName,
|
|
19878
|
+
input: toolInput
|
|
19879
|
+
};
|
|
19880
|
+
currentToolCalls.push(toolCall);
|
|
19881
|
+
}
|
|
19882
|
+
if (toolResponse !== void 0) {
|
|
19883
|
+
toolCall.result = toolResponse;
|
|
19884
|
+
}
|
|
19885
|
+
}
|
|
19886
|
+
}
|
|
19887
|
+
break;
|
|
19888
|
+
}
|
|
19889
|
+
case "tool_result": {
|
|
19890
|
+
const meta = update._meta?.claudeCode;
|
|
19891
|
+
if (meta) {
|
|
19892
|
+
const toolCallId = meta.toolCallId;
|
|
19893
|
+
const toolResponse = meta.toolResponse;
|
|
19894
|
+
if (toolCallId) {
|
|
19895
|
+
const toolCall = currentToolCalls.find(
|
|
19896
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19897
|
+
);
|
|
19898
|
+
if (toolCall && toolResponse !== void 0) {
|
|
19899
|
+
toolCall.result = toolResponse;
|
|
19900
|
+
}
|
|
19901
|
+
}
|
|
19902
|
+
}
|
|
19903
|
+
break;
|
|
19821
19904
|
}
|
|
19822
19905
|
}
|
|
19906
|
+
}
|
|
19907
|
+
}
|
|
19908
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19909
|
+
turns.push({
|
|
19910
|
+
role: "assistant",
|
|
19911
|
+
content: currentAssistantContent,
|
|
19912
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19823
19913
|
});
|
|
19824
19914
|
}
|
|
19825
|
-
|
|
19826
|
-
this.log.info("Tree applied", {
|
|
19827
|
-
treeHash,
|
|
19828
|
-
totalChanges: changes.length,
|
|
19829
|
-
deletedFiles: deletedCount,
|
|
19830
|
-
checkoutPerformed
|
|
19831
|
-
});
|
|
19832
|
-
return { treeHash, checkoutPerformed };
|
|
19915
|
+
return turns;
|
|
19833
19916
|
}
|
|
19834
19917
|
};
|
|
19835
19918
|
|
|
19836
|
-
// src/
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
|
|
19919
|
+
// src/resume.ts
|
|
19920
|
+
async function resumeFromLog(config) {
|
|
19921
|
+
const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
19922
|
+
logger.info("Resuming from log", {
|
|
19923
|
+
taskId: config.taskId,
|
|
19924
|
+
runId: config.runId
|
|
19925
|
+
});
|
|
19926
|
+
const saga = new ResumeSaga(logger);
|
|
19927
|
+
const result = await saga.run({
|
|
19928
|
+
taskId: config.taskId,
|
|
19929
|
+
runId: config.runId,
|
|
19930
|
+
repositoryPath: config.repositoryPath,
|
|
19931
|
+
apiClient: config.apiClient,
|
|
19932
|
+
logger
|
|
19933
|
+
});
|
|
19934
|
+
if (!result.success) {
|
|
19935
|
+
logger.error("Failed to resume from log", {
|
|
19936
|
+
error: result.error,
|
|
19937
|
+
failedStep: result.failedStep
|
|
19852
19938
|
});
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
|
|
19859
|
-
|
|
19860
|
-
|
|
19861
|
-
|
|
19862
|
-
|
|
19863
|
-
|
|
19864
|
-
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
19868
|
-
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
|
|
19873
|
-
|
|
19874
|
-
|
|
19875
|
-
|
|
19876
|
-
|
|
19877
|
-
|
|
19878
|
-
|
|
19879
|
-
|
|
19880
|
-
|
|
19939
|
+
throw new Error(
|
|
19940
|
+
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
19941
|
+
);
|
|
19942
|
+
}
|
|
19943
|
+
return {
|
|
19944
|
+
conversation: result.data.conversation,
|
|
19945
|
+
latestSnapshot: result.data.latestSnapshot,
|
|
19946
|
+
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
19947
|
+
interrupted: result.data.interrupted,
|
|
19948
|
+
lastDevice: result.data.lastDevice,
|
|
19949
|
+
logEntryCount: result.data.logEntryCount
|
|
19950
|
+
};
|
|
19951
|
+
}
|
|
19952
|
+
var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
|
|
19953
|
+
var TOOL_RESULT_MAX_CHARS = 2e3;
|
|
19954
|
+
var RESUME_CONTEXT_MARKERS = [
|
|
19955
|
+
"You are resuming a previous conversation",
|
|
19956
|
+
"Here is the conversation history from the",
|
|
19957
|
+
"Continue from where you left off"
|
|
19958
|
+
];
|
|
19959
|
+
function isResumeContextTurn(turn) {
|
|
19960
|
+
if (turn.role !== "user") return false;
|
|
19961
|
+
const text2 = turn.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
19962
|
+
return RESUME_CONTEXT_MARKERS.some((marker) => text2.includes(marker));
|
|
19963
|
+
}
|
|
19964
|
+
function formatConversationForResume(conversation) {
|
|
19965
|
+
const filtered = conversation.filter((turn) => !isResumeContextTurn(turn));
|
|
19966
|
+
const selected = selectRecentTurns(filtered, RESUME_HISTORY_TOKEN_BUDGET);
|
|
19967
|
+
const parts2 = [];
|
|
19968
|
+
if (selected.length < filtered.length) {
|
|
19969
|
+
parts2.push(
|
|
19970
|
+
`*(${filtered.length - selected.length} earlier turns omitted)*`
|
|
19971
|
+
);
|
|
19972
|
+
}
|
|
19973
|
+
for (const turn of selected) {
|
|
19974
|
+
const role = turn.role === "user" ? "User" : "Assistant";
|
|
19975
|
+
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
19976
|
+
if (textParts.length > 0) {
|
|
19977
|
+
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
19978
|
+
}
|
|
19979
|
+
if (turn.toolCalls?.length) {
|
|
19980
|
+
const toolSummary = turn.toolCalls.map((tc) => {
|
|
19981
|
+
let resultStr = "";
|
|
19982
|
+
if (tc.result !== void 0) {
|
|
19983
|
+
const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
|
|
19984
|
+
resultStr = raw.length > TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
|
|
19881
19985
|
}
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
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}`);
|
|
19986
|
+
return ` - ${tc.toolName}${resultStr}`;
|
|
19987
|
+
}).join("\n");
|
|
19988
|
+
parts2.push(`**${role} (tools)**:
|
|
19989
|
+
${toolSummary}`);
|
|
19894
19990
|
}
|
|
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
|
|
19901
|
-
});
|
|
19902
|
-
return { treeHash: snapshot.treeHash };
|
|
19903
19991
|
}
|
|
19904
|
-
|
|
19992
|
+
return parts2.join("\n\n");
|
|
19993
|
+
}
|
|
19905
19994
|
|
|
19906
|
-
// src/
|
|
19907
|
-
var
|
|
19908
|
-
var
|
|
19909
|
-
var
|
|
19910
|
-
var
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
|
|
19928
|
-
|
|
19929
|
-
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
|
|
19935
|
-
if (!captureResult.success) {
|
|
19936
|
-
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
19995
|
+
// src/session-log-writer.ts
|
|
19996
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
19997
|
+
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
19998
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
19999
|
+
var SessionLogWriter = class _SessionLogWriter {
|
|
20000
|
+
static FLUSH_DEBOUNCE_MS = 500;
|
|
20001
|
+
static FLUSH_MAX_INTERVAL_MS = 5e3;
|
|
20002
|
+
static MAX_FLUSH_RETRIES = 10;
|
|
20003
|
+
static MAX_RETRY_DELAY_MS = 3e4;
|
|
20004
|
+
static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
20005
|
+
posthogAPI;
|
|
20006
|
+
pendingEntries = /* @__PURE__ */ new Map();
|
|
20007
|
+
flushTimeouts = /* @__PURE__ */ new Map();
|
|
20008
|
+
lastFlushAttemptTime = /* @__PURE__ */ new Map();
|
|
20009
|
+
retryCounts = /* @__PURE__ */ new Map();
|
|
20010
|
+
sessions = /* @__PURE__ */ new Map();
|
|
20011
|
+
flushQueues = /* @__PURE__ */ new Map();
|
|
20012
|
+
logger;
|
|
20013
|
+
localCachePath;
|
|
20014
|
+
constructor(options = {}) {
|
|
20015
|
+
this.posthogAPI = options.posthogAPI;
|
|
20016
|
+
this.localCachePath = options.localCachePath;
|
|
20017
|
+
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
20018
|
+
}
|
|
20019
|
+
async flushAll() {
|
|
20020
|
+
const flushPromises = [];
|
|
20021
|
+
for (const [sessionId, session] of this.sessions) {
|
|
20022
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
20023
|
+
flushPromises.push(this.flush(sessionId));
|
|
19937
20024
|
}
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
19942
|
-
|
|
19943
|
-
if (!changed || !gitSnapshot) {
|
|
19944
|
-
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
19945
|
-
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
20025
|
+
await Promise.all(flushPromises);
|
|
20026
|
+
}
|
|
20027
|
+
register(sessionId, context) {
|
|
20028
|
+
if (this.sessions.has(sessionId)) {
|
|
20029
|
+
return;
|
|
19946
20030
|
}
|
|
19947
|
-
|
|
19948
|
-
|
|
20031
|
+
this.sessions.set(sessionId, { context, currentTurnMessages: [] });
|
|
20032
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20033
|
+
if (this.localCachePath) {
|
|
20034
|
+
const sessionDir = import_node_path7.default.join(
|
|
20035
|
+
this.localCachePath,
|
|
20036
|
+
"sessions",
|
|
20037
|
+
context.runId
|
|
20038
|
+
);
|
|
19949
20039
|
try {
|
|
19950
|
-
|
|
19951
|
-
|
|
19952
|
-
|
|
19953
|
-
|
|
19954
|
-
|
|
19955
|
-
runId
|
|
19956
|
-
);
|
|
19957
|
-
} finally {
|
|
19958
|
-
await (0, import_promises5.rm)(createdArchivePath, { force: true }).catch(() => {
|
|
20040
|
+
import_node_fs3.default.mkdirSync(sessionDir, { recursive: true });
|
|
20041
|
+
} catch (error) {
|
|
20042
|
+
this.logger.warn("Failed to create local cache directory", {
|
|
20043
|
+
sessionDir,
|
|
20044
|
+
error
|
|
19959
20045
|
});
|
|
19960
20046
|
}
|
|
19961
20047
|
}
|
|
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 };
|
|
19977
20048
|
}
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
20049
|
+
isRegistered(sessionId) {
|
|
20050
|
+
return this.sessions.has(sessionId);
|
|
20051
|
+
}
|
|
20052
|
+
appendRawLine(sessionId, line) {
|
|
20053
|
+
const session = this.sessions.get(sessionId);
|
|
20054
|
+
if (!session) {
|
|
20055
|
+
this.logger.warn("appendRawLine called for unregistered session", {
|
|
20056
|
+
sessionId
|
|
20057
|
+
});
|
|
20058
|
+
return;
|
|
20059
|
+
}
|
|
20060
|
+
try {
|
|
20061
|
+
const message = JSON.parse(line);
|
|
20062
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
20063
|
+
if (this.isAgentMessageChunk(message)) {
|
|
20064
|
+
const text2 = this.extractChunkText(message);
|
|
20065
|
+
if (text2) {
|
|
20066
|
+
if (!session.chunkBuffer) {
|
|
20067
|
+
session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
|
|
20068
|
+
} else {
|
|
20069
|
+
session.chunkBuffer.text += text2;
|
|
19992
20070
|
}
|
|
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;
|
|
20005
20071
|
}
|
|
20006
|
-
return
|
|
20007
|
-
},
|
|
20008
|
-
rollback: async () => {
|
|
20009
|
-
await (0, import_promises5.rm)(archivePath, { force: true }).catch(() => {
|
|
20010
|
-
});
|
|
20072
|
+
return;
|
|
20011
20073
|
}
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
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
|
|
20074
|
+
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
20075
|
+
session.chunkBuffer = void 0;
|
|
20076
|
+
} else {
|
|
20077
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
20078
|
+
}
|
|
20079
|
+
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
20080
|
+
if (nonChunkAgentText) {
|
|
20081
|
+
session.lastAgentMessage = nonChunkAgentText;
|
|
20082
|
+
session.currentTurnMessages.push(nonChunkAgentText);
|
|
20083
|
+
}
|
|
20084
|
+
const entry = {
|
|
20085
|
+
type: "notification",
|
|
20086
|
+
timestamp,
|
|
20087
|
+
notification: message
|
|
20088
|
+
};
|
|
20089
|
+
this.writeToLocalCache(sessionId, entry);
|
|
20090
|
+
if (this.posthogAPI) {
|
|
20091
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20092
|
+
pending.push(entry);
|
|
20093
|
+
this.pendingEntries.set(sessionId, pending);
|
|
20094
|
+
this.scheduleFlush(sessionId);
|
|
20095
|
+
}
|
|
20096
|
+
} catch {
|
|
20097
|
+
this.logger.warn("Failed to parse raw line for persistence", {
|
|
20098
|
+
taskId: session.context.taskId,
|
|
20099
|
+
runId: session.context.runId,
|
|
20100
|
+
lineLength: line.length
|
|
20051
20101
|
});
|
|
20052
|
-
throw new Error(
|
|
20053
|
-
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
20054
|
-
);
|
|
20055
20102
|
}
|
|
20056
|
-
|
|
20057
|
-
|
|
20103
|
+
}
|
|
20104
|
+
async flush(sessionId, { coalesce = false } = {}) {
|
|
20105
|
+
if (coalesce) {
|
|
20106
|
+
const session = this.sessions.get(sessionId);
|
|
20107
|
+
if (session) {
|
|
20108
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
20109
|
+
}
|
|
20058
20110
|
}
|
|
20059
|
-
|
|
20111
|
+
const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
|
|
20112
|
+
const next = prev.catch(() => {
|
|
20113
|
+
}).then(() => this._doFlush(sessionId));
|
|
20114
|
+
this.flushQueues.set(sessionId, next);
|
|
20115
|
+
next.finally(() => {
|
|
20116
|
+
if (this.flushQueues.get(sessionId) === next) {
|
|
20117
|
+
this.flushQueues.delete(sessionId);
|
|
20118
|
+
}
|
|
20119
|
+
});
|
|
20120
|
+
return next;
|
|
20060
20121
|
}
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
if (!this.apiClient) {
|
|
20067
|
-
throw new Error("Cannot apply snapshot: API client not configured");
|
|
20122
|
+
async _doFlush(sessionId) {
|
|
20123
|
+
const session = this.sessions.get(sessionId);
|
|
20124
|
+
if (!session) {
|
|
20125
|
+
this.logger.warn("flush: no session found", { sessionId });
|
|
20126
|
+
return;
|
|
20068
20127
|
}
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
changes: snapshot.changes.length
|
|
20073
|
-
});
|
|
20074
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20128
|
+
const pending = this.pendingEntries.get(sessionId);
|
|
20129
|
+
if (!this.posthogAPI || !pending?.length) {
|
|
20130
|
+
return;
|
|
20075
20131
|
}
|
|
20076
|
-
|
|
20077
|
-
const
|
|
20078
|
-
|
|
20079
|
-
|
|
20080
|
-
|
|
20081
|
-
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
treeHash: snapshot.treeHash
|
|
20089
|
-
});
|
|
20090
|
-
throw new Error(
|
|
20091
|
-
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
20132
|
+
this.pendingEntries.delete(sessionId);
|
|
20133
|
+
const timeout = this.flushTimeouts.get(sessionId);
|
|
20134
|
+
if (timeout) {
|
|
20135
|
+
clearTimeout(timeout);
|
|
20136
|
+
this.flushTimeouts.delete(sessionId);
|
|
20137
|
+
}
|
|
20138
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20139
|
+
try {
|
|
20140
|
+
await this.posthogAPI.appendTaskRunLog(
|
|
20141
|
+
session.context.taskId,
|
|
20142
|
+
session.context.runId,
|
|
20143
|
+
pending
|
|
20092
20144
|
);
|
|
20145
|
+
this.retryCounts.set(sessionId, 0);
|
|
20146
|
+
} catch (error) {
|
|
20147
|
+
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
20148
|
+
this.retryCounts.set(sessionId, retryCount);
|
|
20149
|
+
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
20150
|
+
this.logger.error(
|
|
20151
|
+
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
20152
|
+
{
|
|
20153
|
+
taskId: session.context.taskId,
|
|
20154
|
+
runId: session.context.runId,
|
|
20155
|
+
error
|
|
20156
|
+
}
|
|
20157
|
+
);
|
|
20158
|
+
this.retryCounts.set(sessionId, 0);
|
|
20159
|
+
} else {
|
|
20160
|
+
if (retryCount === 1) {
|
|
20161
|
+
this.logger.warn(
|
|
20162
|
+
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
20163
|
+
{
|
|
20164
|
+
taskId: session.context.taskId,
|
|
20165
|
+
runId: session.context.runId,
|
|
20166
|
+
error: error instanceof Error ? error.message : String(error)
|
|
20167
|
+
}
|
|
20168
|
+
);
|
|
20169
|
+
}
|
|
20170
|
+
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
20171
|
+
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
20172
|
+
this.scheduleFlush(sessionId);
|
|
20173
|
+
}
|
|
20093
20174
|
}
|
|
20094
|
-
this.lastTreeHash = result.data.treeHash;
|
|
20095
20175
|
}
|
|
20096
|
-
|
|
20097
|
-
|
|
20098
|
-
|
|
20099
|
-
|
|
20100
|
-
return
|
|
20176
|
+
getSessionUpdateType(message) {
|
|
20177
|
+
if (message.method !== "session/update") return void 0;
|
|
20178
|
+
const params = message.params;
|
|
20179
|
+
const update = params?.update;
|
|
20180
|
+
return update?.sessionUpdate;
|
|
20101
20181
|
}
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
*/
|
|
20105
|
-
setLastTreeHash(hash) {
|
|
20106
|
-
this.lastTreeHash = hash;
|
|
20182
|
+
isDirectAgentMessage(message) {
|
|
20183
|
+
return this.getSessionUpdateType(message) === "agent_message";
|
|
20107
20184
|
}
|
|
20108
|
-
|
|
20109
|
-
|
|
20110
|
-
|
|
20111
|
-
|
|
20112
|
-
|
|
20113
|
-
|
|
20114
|
-
const
|
|
20115
|
-
|
|
20116
|
-
|
|
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();
|
|
20185
|
+
isAgentMessageChunk(message) {
|
|
20186
|
+
return this.getSessionUpdateType(message) === "agent_message_chunk";
|
|
20187
|
+
}
|
|
20188
|
+
extractChunkText(message) {
|
|
20189
|
+
const params = message.params;
|
|
20190
|
+
const update = params?.update;
|
|
20191
|
+
const content = update?.content;
|
|
20192
|
+
if (content?.type === "text" && content.text) {
|
|
20193
|
+
return content.text;
|
|
20131
20194
|
}
|
|
20132
|
-
|
|
20133
|
-
|
|
20134
|
-
|
|
20135
|
-
|
|
20136
|
-
|
|
20137
|
-
|
|
20138
|
-
|
|
20139
|
-
|
|
20140
|
-
|
|
20141
|
-
|
|
20142
|
-
|
|
20143
|
-
|
|
20144
|
-
|
|
20145
|
-
|
|
20146
|
-
|
|
20147
|
-
|
|
20148
|
-
|
|
20149
|
-
|
|
20150
|
-
name: "apply_snapshot",
|
|
20151
|
-
execute: async () => {
|
|
20152
|
-
const treeTracker = new TreeTracker({
|
|
20153
|
-
repositoryPath,
|
|
20154
|
-
taskId,
|
|
20155
|
-
runId,
|
|
20156
|
-
apiClient,
|
|
20157
|
-
logger: logger.child("TreeTracker")
|
|
20158
|
-
});
|
|
20159
|
-
try {
|
|
20160
|
-
await treeTracker.applyTreeSnapshot(latestSnapshot);
|
|
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
|
-
);
|
|
20195
|
+
return "";
|
|
20196
|
+
}
|
|
20197
|
+
emitCoalescedMessage(sessionId, session) {
|
|
20198
|
+
if (!session.chunkBuffer) return;
|
|
20199
|
+
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
20200
|
+
session.chunkBuffer = void 0;
|
|
20201
|
+
session.lastAgentMessage = text2;
|
|
20202
|
+
session.currentTurnMessages.push(text2);
|
|
20203
|
+
const entry = {
|
|
20204
|
+
type: "notification",
|
|
20205
|
+
timestamp: firstTimestamp,
|
|
20206
|
+
notification: {
|
|
20207
|
+
jsonrpc: "2.0",
|
|
20208
|
+
method: "session/update",
|
|
20209
|
+
params: {
|
|
20210
|
+
update: {
|
|
20211
|
+
sessionUpdate: "agent_message",
|
|
20212
|
+
content: { type: "text", text: text2 }
|
|
20174
20213
|
}
|
|
20175
|
-
},
|
|
20176
|
-
rollback: async () => {
|
|
20177
|
-
}
|
|
20178
|
-
});
|
|
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
20214
|
}
|
|
20194
|
-
|
|
20195
|
-
}
|
|
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
|
|
20215
|
+
}
|
|
20218
20216
|
};
|
|
20217
|
+
this.writeToLocalCache(sessionId, entry);
|
|
20218
|
+
if (this.posthogAPI) {
|
|
20219
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20220
|
+
pending.push(entry);
|
|
20221
|
+
this.pendingEntries.set(sessionId, pending);
|
|
20222
|
+
this.scheduleFlush(sessionId);
|
|
20223
|
+
}
|
|
20219
20224
|
}
|
|
20220
|
-
|
|
20221
|
-
return
|
|
20222
|
-
conversation: [],
|
|
20223
|
-
latestSnapshot: null,
|
|
20224
|
-
latestGitCheckpoint: null,
|
|
20225
|
-
snapshotApplied: false,
|
|
20226
|
-
interrupted: false,
|
|
20227
|
-
logEntryCount: 0
|
|
20228
|
-
};
|
|
20225
|
+
getLastAgentMessage(sessionId) {
|
|
20226
|
+
return this.sessions.get(sessionId)?.lastAgentMessage;
|
|
20229
20227
|
}
|
|
20230
|
-
|
|
20231
|
-
|
|
20232
|
-
|
|
20233
|
-
|
|
20234
|
-
|
|
20235
|
-
|
|
20236
|
-
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
return params;
|
|
20228
|
+
getFullAgentResponse(sessionId) {
|
|
20229
|
+
const session = this.sessions.get(sessionId);
|
|
20230
|
+
if (!session || session.currentTurnMessages.length === 0) return void 0;
|
|
20231
|
+
if (session.chunkBuffer) {
|
|
20232
|
+
this.logger.warn(
|
|
20233
|
+
"getFullAgentResponse called with non-empty chunk buffer",
|
|
20234
|
+
{
|
|
20235
|
+
sessionId,
|
|
20236
|
+
bufferedLength: session.chunkBuffer.text.length
|
|
20240
20237
|
}
|
|
20241
|
-
|
|
20238
|
+
);
|
|
20242
20239
|
}
|
|
20243
|
-
return
|
|
20240
|
+
return session.currentTurnMessages.join("\n\n");
|
|
20244
20241
|
}
|
|
20245
|
-
|
|
20246
|
-
const
|
|
20247
|
-
|
|
20248
|
-
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20242
|
+
resetTurnMessages(sessionId) {
|
|
20243
|
+
const session = this.sessions.get(sessionId);
|
|
20244
|
+
if (session) {
|
|
20245
|
+
session.currentTurnMessages = [];
|
|
20246
|
+
}
|
|
20247
|
+
}
|
|
20248
|
+
extractAgentMessageText(message) {
|
|
20249
|
+
if (message.method !== "session/update") {
|
|
20250
|
+
return null;
|
|
20251
|
+
}
|
|
20252
|
+
const params = message.params;
|
|
20253
|
+
const update = params?.update;
|
|
20254
|
+
if (update?.sessionUpdate !== "agent_message") {
|
|
20255
|
+
return null;
|
|
20256
|
+
}
|
|
20257
|
+
const content = update.content;
|
|
20258
|
+
if (content?.type === "text" && typeof content.text === "string") {
|
|
20259
|
+
const trimmed2 = content.text.trim();
|
|
20260
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20261
|
+
}
|
|
20262
|
+
if (typeof update.message === "string") {
|
|
20263
|
+
const trimmed2 = update.message.trim();
|
|
20264
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20256
20265
|
}
|
|
20257
20266
|
return null;
|
|
20258
20267
|
}
|
|
20259
|
-
|
|
20260
|
-
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20268
|
+
scheduleFlush(sessionId) {
|
|
20269
|
+
const existing = this.flushTimeouts.get(sessionId);
|
|
20270
|
+
if (existing) clearTimeout(existing);
|
|
20271
|
+
const retryCount = this.retryCounts.get(sessionId) ?? 0;
|
|
20272
|
+
const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
|
|
20273
|
+
const elapsed = Date.now() - lastAttempt;
|
|
20274
|
+
let delay3;
|
|
20275
|
+
if (retryCount > 0) {
|
|
20276
|
+
delay3 = Math.min(
|
|
20277
|
+
_SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
|
|
20278
|
+
_SessionLogWriter.MAX_RETRY_DELAY_MS
|
|
20279
|
+
);
|
|
20280
|
+
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20281
|
+
delay3 = 0;
|
|
20282
|
+
} else {
|
|
20283
|
+
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
20266
20284
|
}
|
|
20267
|
-
|
|
20285
|
+
const timeout = setTimeout(() => this.flush(sessionId), delay3);
|
|
20286
|
+
this.flushTimeouts.set(sessionId, timeout);
|
|
20268
20287
|
}
|
|
20269
|
-
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20273
|
-
|
|
20274
|
-
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20281
|
-
|
|
20282
|
-
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
|
|
20287
|
-
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
|
|
20291
|
-
|
|
20292
|
-
|
|
20293
|
-
|
|
20294
|
-
|
|
20295
|
-
|
|
20296
|
-
|
|
20297
|
-
|
|
20298
|
-
|
|
20299
|
-
|
|
20300
|
-
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
|
|
20304
|
-
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;
|
|
20288
|
+
writeToLocalCache(sessionId, entry) {
|
|
20289
|
+
if (!this.localCachePath) return;
|
|
20290
|
+
const session = this.sessions.get(sessionId);
|
|
20291
|
+
if (!session) return;
|
|
20292
|
+
const logPath = import_node_path7.default.join(
|
|
20293
|
+
this.localCachePath,
|
|
20294
|
+
"sessions",
|
|
20295
|
+
session.context.runId,
|
|
20296
|
+
"logs.ndjson"
|
|
20297
|
+
);
|
|
20298
|
+
try {
|
|
20299
|
+
import_node_fs3.default.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
20300
|
+
`);
|
|
20301
|
+
} catch (error) {
|
|
20302
|
+
this.logger.warn("Failed to write to local cache", {
|
|
20303
|
+
taskId: session.context.taskId,
|
|
20304
|
+
runId: session.context.runId,
|
|
20305
|
+
logPath,
|
|
20306
|
+
error
|
|
20307
|
+
});
|
|
20308
|
+
}
|
|
20309
|
+
}
|
|
20310
|
+
static async cleanupOldSessions(localCachePath) {
|
|
20311
|
+
const sessionsDir = import_node_path7.default.join(localCachePath, "sessions");
|
|
20312
|
+
let deleted = 0;
|
|
20313
|
+
try {
|
|
20314
|
+
const entries = await import_promises4.default.readdir(sessionsDir);
|
|
20315
|
+
const now = Date.now();
|
|
20316
|
+
for (const entry of entries) {
|
|
20317
|
+
const entryPath = import_node_path7.default.join(sessionsDir, entry);
|
|
20318
|
+
try {
|
|
20319
|
+
const stats = await import_promises4.default.stat(entryPath);
|
|
20320
|
+
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
20321
|
+
await import_promises4.default.rm(entryPath, { recursive: true, force: true });
|
|
20322
|
+
deleted++;
|
|
20365
20323
|
}
|
|
20324
|
+
} catch {
|
|
20366
20325
|
}
|
|
20367
20326
|
}
|
|
20327
|
+
} catch {
|
|
20368
20328
|
}
|
|
20369
|
-
|
|
20370
|
-
turns.push({
|
|
20371
|
-
role: "assistant",
|
|
20372
|
-
content: currentAssistantContent,
|
|
20373
|
-
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
20374
|
-
});
|
|
20375
|
-
}
|
|
20376
|
-
return turns;
|
|
20329
|
+
return deleted;
|
|
20377
20330
|
}
|
|
20378
20331
|
};
|
|
20379
20332
|
|
|
20380
|
-
// src/
|
|
20381
|
-
|
|
20382
|
-
|
|
20383
|
-
|
|
20384
|
-
|
|
20385
|
-
|
|
20386
|
-
|
|
20387
|
-
|
|
20388
|
-
|
|
20389
|
-
|
|
20390
|
-
|
|
20391
|
-
|
|
20392
|
-
|
|
20393
|
-
|
|
20394
|
-
|
|
20395
|
-
|
|
20396
|
-
|
|
20397
|
-
|
|
20398
|
-
|
|
20333
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20334
|
+
var import_promises5 = require("fs/promises");
|
|
20335
|
+
var import_node_path8 = require("path");
|
|
20336
|
+
|
|
20337
|
+
// ../git/dist/sagas/tree.js
|
|
20338
|
+
var import_node_fs4 = require("fs");
|
|
20339
|
+
var fs13 = __toESM(require("fs/promises"), 1);
|
|
20340
|
+
var path16 = __toESM(require("path"), 1);
|
|
20341
|
+
var tar = __toESM(require("tar"), 1);
|
|
20342
|
+
var CaptureTreeSaga = class extends GitSaga {
|
|
20343
|
+
sagaName = "CaptureTreeSaga";
|
|
20344
|
+
tempIndexPath = null;
|
|
20345
|
+
async executeGitOperations(input) {
|
|
20346
|
+
const { baseDir, lastTreeHash, archivePath, signal } = input;
|
|
20347
|
+
const tmpDir = path16.join(baseDir, ".git", "posthog-code-tmp");
|
|
20348
|
+
await this.step({
|
|
20349
|
+
name: "create_tmp_dir",
|
|
20350
|
+
execute: () => fs13.mkdir(tmpDir, { recursive: true }),
|
|
20351
|
+
rollback: async () => {
|
|
20352
|
+
}
|
|
20399
20353
|
});
|
|
20400
|
-
|
|
20401
|
-
|
|
20402
|
-
|
|
20403
|
-
|
|
20404
|
-
|
|
20405
|
-
|
|
20406
|
-
|
|
20407
|
-
|
|
20408
|
-
|
|
20409
|
-
|
|
20410
|
-
|
|
20411
|
-
|
|
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
|
-
);
|
|
20423
|
-
}
|
|
20424
|
-
for (const turn of selected) {
|
|
20425
|
-
const role = turn.role === "user" ? "User" : "Assistant";
|
|
20426
|
-
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
20427
|
-
if (textParts.length > 0) {
|
|
20428
|
-
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
20429
|
-
}
|
|
20430
|
-
if (turn.toolCalls?.length) {
|
|
20431
|
-
const toolSummary = turn.toolCalls.map((tc) => {
|
|
20432
|
-
let resultStr = "";
|
|
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}`;
|
|
20354
|
+
this.tempIndexPath = path16.join(tmpDir, `index-${Date.now()}`);
|
|
20355
|
+
const tempIndexGit = this.git.env({
|
|
20356
|
+
...process.env,
|
|
20357
|
+
GIT_INDEX_FILE: this.tempIndexPath
|
|
20358
|
+
});
|
|
20359
|
+
await this.step({
|
|
20360
|
+
name: "init_temp_index",
|
|
20361
|
+
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
20362
|
+
rollback: async () => {
|
|
20363
|
+
if (this.tempIndexPath) {
|
|
20364
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20365
|
+
});
|
|
20436
20366
|
}
|
|
20437
|
-
|
|
20438
|
-
|
|
20439
|
-
|
|
20440
|
-
|
|
20367
|
+
}
|
|
20368
|
+
});
|
|
20369
|
+
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
20370
|
+
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
20371
|
+
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
20372
|
+
this.log.debug("No changes since last capture", { treeHash });
|
|
20373
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20374
|
+
});
|
|
20375
|
+
return { snapshot: null, changed: false };
|
|
20441
20376
|
}
|
|
20442
|
-
|
|
20443
|
-
|
|
20444
|
-
}
|
|
20445
|
-
|
|
20446
|
-
|
|
20447
|
-
|
|
20448
|
-
|
|
20449
|
-
|
|
20450
|
-
|
|
20451
|
-
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
|
|
20458
|
-
|
|
20459
|
-
|
|
20460
|
-
|
|
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));
|
|
20377
|
+
const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
|
|
20378
|
+
try {
|
|
20379
|
+
return await getHeadSha(baseDir, { abortSignal: signal });
|
|
20380
|
+
} catch {
|
|
20381
|
+
return null;
|
|
20382
|
+
}
|
|
20383
|
+
});
|
|
20384
|
+
const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
|
|
20385
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20386
|
+
});
|
|
20387
|
+
const snapshot = {
|
|
20388
|
+
treeHash,
|
|
20389
|
+
baseCommit,
|
|
20390
|
+
changes,
|
|
20391
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20392
|
+
};
|
|
20393
|
+
let createdArchivePath;
|
|
20394
|
+
if (archivePath) {
|
|
20395
|
+
createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
|
|
20475
20396
|
}
|
|
20476
|
-
|
|
20397
|
+
this.log.info("Tree captured", {
|
|
20398
|
+
treeHash,
|
|
20399
|
+
changes: changes.length,
|
|
20400
|
+
archived: !!createdArchivePath
|
|
20401
|
+
});
|
|
20402
|
+
return { snapshot, archivePath: createdArchivePath, changed: true };
|
|
20477
20403
|
}
|
|
20478
|
-
|
|
20479
|
-
|
|
20480
|
-
|
|
20404
|
+
async createArchive(baseDir, archivePath, changes) {
|
|
20405
|
+
const filesToArchive = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20406
|
+
if (filesToArchive.length === 0) {
|
|
20407
|
+
return void 0;
|
|
20481
20408
|
}
|
|
20482
|
-
|
|
20483
|
-
|
|
20484
|
-
|
|
20485
|
-
|
|
20486
|
-
|
|
20487
|
-
|
|
20488
|
-
|
|
20489
|
-
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
|
|
20495
|
-
|
|
20409
|
+
const existingFiles = filesToArchive.filter((f) => (0, import_node_fs4.existsSync)(path16.join(baseDir, f)));
|
|
20410
|
+
if (existingFiles.length === 0) {
|
|
20411
|
+
return void 0;
|
|
20412
|
+
}
|
|
20413
|
+
await this.step({
|
|
20414
|
+
name: "create_archive",
|
|
20415
|
+
execute: async () => {
|
|
20416
|
+
const archiveDir = path16.dirname(archivePath);
|
|
20417
|
+
await fs13.mkdir(archiveDir, { recursive: true });
|
|
20418
|
+
await tar.create({
|
|
20419
|
+
gzip: true,
|
|
20420
|
+
file: archivePath,
|
|
20421
|
+
cwd: baseDir
|
|
20422
|
+
}, existingFiles);
|
|
20423
|
+
},
|
|
20424
|
+
rollback: async () => {
|
|
20425
|
+
await fs13.rm(archivePath, { force: true }).catch(() => {
|
|
20496
20426
|
});
|
|
20497
20427
|
}
|
|
20498
|
-
}
|
|
20499
|
-
|
|
20500
|
-
isRegistered(sessionId) {
|
|
20501
|
-
return this.sessions.has(sessionId);
|
|
20428
|
+
});
|
|
20429
|
+
return archivePath;
|
|
20502
20430
|
}
|
|
20503
|
-
|
|
20504
|
-
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
sessionId
|
|
20508
|
-
});
|
|
20509
|
-
return;
|
|
20431
|
+
async getChanges(git, fromRef, toRef) {
|
|
20432
|
+
if (!fromRef) {
|
|
20433
|
+
const stdout2 = await git.raw(["ls-tree", "-r", "--name-only", toRef]);
|
|
20434
|
+
return stdout2.split("\n").filter((p) => p.trim()).map((p) => ({ path: p, status: "A" }));
|
|
20510
20435
|
}
|
|
20511
|
-
|
|
20512
|
-
|
|
20513
|
-
|
|
20514
|
-
|
|
20515
|
-
|
|
20516
|
-
|
|
20517
|
-
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
|
|
20526
|
-
|
|
20436
|
+
const stdout = await git.raw([
|
|
20437
|
+
"diff-tree",
|
|
20438
|
+
"-r",
|
|
20439
|
+
"--name-status",
|
|
20440
|
+
fromRef,
|
|
20441
|
+
toRef
|
|
20442
|
+
]);
|
|
20443
|
+
const changes = [];
|
|
20444
|
+
for (const line of stdout.split("\n")) {
|
|
20445
|
+
if (!line.trim())
|
|
20446
|
+
continue;
|
|
20447
|
+
const [status, filePath] = line.split(" ");
|
|
20448
|
+
if (!filePath)
|
|
20449
|
+
continue;
|
|
20450
|
+
let normalizedStatus;
|
|
20451
|
+
if (status === "D") {
|
|
20452
|
+
normalizedStatus = "D";
|
|
20453
|
+
} else if (status === "A") {
|
|
20454
|
+
normalizedStatus = "A";
|
|
20527
20455
|
} else {
|
|
20528
|
-
|
|
20456
|
+
normalizedStatus = "M";
|
|
20529
20457
|
}
|
|
20530
|
-
|
|
20531
|
-
|
|
20532
|
-
|
|
20533
|
-
|
|
20458
|
+
changes.push({ path: filePath, status: normalizedStatus });
|
|
20459
|
+
}
|
|
20460
|
+
return changes;
|
|
20461
|
+
}
|
|
20462
|
+
};
|
|
20463
|
+
var ApplyTreeSaga = class extends GitSaga {
|
|
20464
|
+
sagaName = "ApplyTreeSaga";
|
|
20465
|
+
originalHead = null;
|
|
20466
|
+
originalBranch = null;
|
|
20467
|
+
extractedFiles = [];
|
|
20468
|
+
fileBackups = /* @__PURE__ */ new Map();
|
|
20469
|
+
async executeGitOperations(input) {
|
|
20470
|
+
const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
|
|
20471
|
+
const headInfo = await this.readOnlyStep("get_current_head", async () => {
|
|
20472
|
+
let head = null;
|
|
20473
|
+
let branch = null;
|
|
20474
|
+
try {
|
|
20475
|
+
head = await this.git.revparse(["HEAD"]);
|
|
20476
|
+
} catch {
|
|
20477
|
+
head = null;
|
|
20534
20478
|
}
|
|
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);
|
|
20479
|
+
try {
|
|
20480
|
+
branch = await this.git.raw(["symbolic-ref", "--short", "HEAD"]);
|
|
20481
|
+
} catch {
|
|
20482
|
+
branch = null;
|
|
20546
20483
|
}
|
|
20547
|
-
|
|
20548
|
-
|
|
20549
|
-
|
|
20550
|
-
|
|
20551
|
-
|
|
20484
|
+
return { head, branch };
|
|
20485
|
+
});
|
|
20486
|
+
this.originalHead = headInfo.head;
|
|
20487
|
+
this.originalBranch = headInfo.branch;
|
|
20488
|
+
let checkoutPerformed = false;
|
|
20489
|
+
if (baseCommit && baseCommit !== this.originalHead) {
|
|
20490
|
+
await this.readOnlyStep("check_working_tree", async () => {
|
|
20491
|
+
const status = await this.git.status();
|
|
20492
|
+
if (!status.isClean()) {
|
|
20493
|
+
const changedFiles = status.modified.length + status.staged.length + status.deleted.length;
|
|
20494
|
+
throw new Error(`Cannot apply tree: ${changedFiles} uncommitted change(s) exist. Commit or stash your changes first.`);
|
|
20495
|
+
}
|
|
20496
|
+
});
|
|
20497
|
+
await this.step({
|
|
20498
|
+
name: "checkout_base",
|
|
20499
|
+
execute: async () => {
|
|
20500
|
+
await this.git.checkout(baseCommit);
|
|
20501
|
+
checkoutPerformed = true;
|
|
20502
|
+
this.log.warn("Applied tree from different commit - now in detached HEAD state", {
|
|
20503
|
+
originalHead: this.originalHead,
|
|
20504
|
+
originalBranch: this.originalBranch,
|
|
20505
|
+
baseCommit
|
|
20506
|
+
});
|
|
20507
|
+
},
|
|
20508
|
+
rollback: async () => {
|
|
20509
|
+
try {
|
|
20510
|
+
if (this.originalBranch) {
|
|
20511
|
+
await this.git.checkout(this.originalBranch);
|
|
20512
|
+
} else if (this.originalHead) {
|
|
20513
|
+
await this.git.checkout(this.originalHead);
|
|
20514
|
+
}
|
|
20515
|
+
} catch (error) {
|
|
20516
|
+
this.log.warn("Failed to rollback checkout", { error });
|
|
20517
|
+
}
|
|
20518
|
+
}
|
|
20519
|
+
});
|
|
20520
|
+
}
|
|
20521
|
+
if (archivePath) {
|
|
20522
|
+
const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20523
|
+
await this.readOnlyStep("backup_existing_files", async () => {
|
|
20524
|
+
for (const filePath of filesToExtract) {
|
|
20525
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20526
|
+
try {
|
|
20527
|
+
const content = await fs13.readFile(fullPath);
|
|
20528
|
+
this.fileBackups.set(filePath, content);
|
|
20529
|
+
} catch {
|
|
20530
|
+
}
|
|
20531
|
+
}
|
|
20532
|
+
});
|
|
20533
|
+
await this.step({
|
|
20534
|
+
name: "extract_archive",
|
|
20535
|
+
execute: async () => {
|
|
20536
|
+
await tar.extract({
|
|
20537
|
+
file: archivePath,
|
|
20538
|
+
cwd: baseDir
|
|
20539
|
+
});
|
|
20540
|
+
this.extractedFiles = filesToExtract;
|
|
20541
|
+
},
|
|
20542
|
+
rollback: async () => {
|
|
20543
|
+
for (const filePath of this.extractedFiles) {
|
|
20544
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20545
|
+
const backup = this.fileBackups.get(filePath);
|
|
20546
|
+
if (backup) {
|
|
20547
|
+
const dir = path16.dirname(fullPath);
|
|
20548
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20549
|
+
});
|
|
20550
|
+
await fs13.writeFile(fullPath, backup).catch(() => {
|
|
20551
|
+
});
|
|
20552
|
+
} else {
|
|
20553
|
+
await fs13.rm(fullPath, { force: true }).catch(() => {
|
|
20554
|
+
});
|
|
20555
|
+
}
|
|
20556
|
+
}
|
|
20557
|
+
}
|
|
20558
|
+
});
|
|
20559
|
+
}
|
|
20560
|
+
for (const change of changes.filter((c) => c.status === "D")) {
|
|
20561
|
+
const fullPath = path16.join(baseDir, change.path);
|
|
20562
|
+
const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
|
|
20563
|
+
try {
|
|
20564
|
+
return await fs13.readFile(fullPath);
|
|
20565
|
+
} catch {
|
|
20566
|
+
return null;
|
|
20567
|
+
}
|
|
20552
20568
|
});
|
|
20569
|
+
await this.step({
|
|
20570
|
+
name: `delete_${change.path}`,
|
|
20571
|
+
execute: async () => {
|
|
20572
|
+
await fs13.rm(fullPath, { force: true });
|
|
20573
|
+
this.log.debug(`Deleted file: ${change.path}`);
|
|
20574
|
+
},
|
|
20575
|
+
rollback: async () => {
|
|
20576
|
+
if (backupContent) {
|
|
20577
|
+
const dir = path16.dirname(fullPath);
|
|
20578
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20579
|
+
});
|
|
20580
|
+
await fs13.writeFile(fullPath, backupContent).catch(() => {
|
|
20581
|
+
});
|
|
20582
|
+
}
|
|
20583
|
+
}
|
|
20584
|
+
});
|
|
20585
|
+
}
|
|
20586
|
+
const deletedCount = changes.filter((c) => c.status === "D").length;
|
|
20587
|
+
this.log.info("Tree applied", {
|
|
20588
|
+
treeHash,
|
|
20589
|
+
totalChanges: changes.length,
|
|
20590
|
+
deletedFiles: deletedCount,
|
|
20591
|
+
checkoutPerformed
|
|
20592
|
+
});
|
|
20593
|
+
return { treeHash, checkoutPerformed };
|
|
20594
|
+
}
|
|
20595
|
+
};
|
|
20596
|
+
|
|
20597
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20598
|
+
var ApplySnapshotSaga = class extends Saga {
|
|
20599
|
+
sagaName = "ApplySnapshotSaga";
|
|
20600
|
+
archivePath = null;
|
|
20601
|
+
async execute(input) {
|
|
20602
|
+
const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
|
|
20603
|
+
const tmpDir = (0, import_node_path8.join)(repositoryPath, ".posthog", "tmp");
|
|
20604
|
+
if (!snapshot.archiveUrl) {
|
|
20605
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20553
20606
|
}
|
|
20554
|
-
|
|
20555
|
-
|
|
20556
|
-
|
|
20557
|
-
|
|
20558
|
-
|
|
20559
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
20607
|
+
const archiveUrl = snapshot.archiveUrl;
|
|
20608
|
+
await this.step({
|
|
20609
|
+
name: "create_tmp_dir",
|
|
20610
|
+
execute: () => (0, import_promises5.mkdir)(tmpDir, { recursive: true }),
|
|
20611
|
+
rollback: async () => {
|
|
20560
20612
|
}
|
|
20561
|
-
}
|
|
20562
|
-
const
|
|
20563
|
-
|
|
20564
|
-
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
20613
|
+
});
|
|
20614
|
+
const archivePath = (0, import_node_path8.join)(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
20615
|
+
this.archivePath = archivePath;
|
|
20616
|
+
await this.step({
|
|
20617
|
+
name: "download_archive",
|
|
20618
|
+
execute: async () => {
|
|
20619
|
+
const arrayBuffer = await apiClient.downloadArtifact(
|
|
20620
|
+
taskId,
|
|
20621
|
+
runId,
|
|
20622
|
+
archiveUrl
|
|
20623
|
+
);
|
|
20624
|
+
if (!arrayBuffer) {
|
|
20625
|
+
throw new Error("Failed to download archive");
|
|
20626
|
+
}
|
|
20627
|
+
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
20628
|
+
const binaryContent = Buffer.from(base64Content, "base64");
|
|
20629
|
+
await (0, import_promises5.writeFile)(archivePath, binaryContent);
|
|
20630
|
+
this.log.info("Tree archive downloaded", {
|
|
20631
|
+
treeHash: snapshot.treeHash,
|
|
20632
|
+
snapshotBytes: binaryContent.byteLength,
|
|
20633
|
+
snapshotWireBytes: arrayBuffer.byteLength,
|
|
20634
|
+
totalBytes: binaryContent.byteLength,
|
|
20635
|
+
totalWireBytes: arrayBuffer.byteLength
|
|
20636
|
+
});
|
|
20637
|
+
},
|
|
20638
|
+
rollback: async () => {
|
|
20639
|
+
if (this.archivePath) {
|
|
20640
|
+
await (0, import_promises5.rm)(this.archivePath, { force: true }).catch(() => {
|
|
20641
|
+
});
|
|
20642
|
+
}
|
|
20569
20643
|
}
|
|
20570
20644
|
});
|
|
20571
|
-
|
|
20645
|
+
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
20646
|
+
const applyResult = await gitApplySaga.run({
|
|
20647
|
+
baseDir: repositoryPath,
|
|
20648
|
+
treeHash: snapshot.treeHash,
|
|
20649
|
+
baseCommit: snapshot.baseCommit,
|
|
20650
|
+
changes: snapshot.changes,
|
|
20651
|
+
archivePath: this.archivePath
|
|
20652
|
+
});
|
|
20653
|
+
if (!applyResult.success) {
|
|
20654
|
+
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
20655
|
+
}
|
|
20656
|
+
await (0, import_promises5.rm)(this.archivePath, { force: true }).catch(() => {
|
|
20657
|
+
});
|
|
20658
|
+
this.log.info("Tree snapshot applied", {
|
|
20659
|
+
treeHash: snapshot.treeHash,
|
|
20660
|
+
totalChanges: snapshot.changes.length,
|
|
20661
|
+
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
20662
|
+
});
|
|
20663
|
+
return { treeHash: snapshot.treeHash };
|
|
20572
20664
|
}
|
|
20573
|
-
|
|
20574
|
-
|
|
20575
|
-
|
|
20576
|
-
|
|
20577
|
-
|
|
20665
|
+
};
|
|
20666
|
+
|
|
20667
|
+
// src/sagas/capture-tree-saga.ts
|
|
20668
|
+
var import_node_fs5 = require("fs");
|
|
20669
|
+
var import_promises6 = require("fs/promises");
|
|
20670
|
+
var import_node_path9 = require("path");
|
|
20671
|
+
var CaptureTreeSaga2 = class extends Saga {
|
|
20672
|
+
sagaName = "CaptureTreeSaga";
|
|
20673
|
+
async execute(input) {
|
|
20674
|
+
const {
|
|
20675
|
+
repositoryPath,
|
|
20676
|
+
lastTreeHash,
|
|
20677
|
+
interrupted,
|
|
20678
|
+
apiClient,
|
|
20679
|
+
taskId,
|
|
20680
|
+
runId
|
|
20681
|
+
} = input;
|
|
20682
|
+
const tmpDir = (0, import_node_path9.join)(repositoryPath, ".posthog", "tmp");
|
|
20683
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path9.join)(repositoryPath, ".gitmodules"))) {
|
|
20684
|
+
this.log.warn(
|
|
20685
|
+
"Repository has submodules - snapshot may not capture submodule state"
|
|
20686
|
+
);
|
|
20578
20687
|
}
|
|
20579
|
-
const
|
|
20580
|
-
|
|
20581
|
-
|
|
20688
|
+
const shouldArchive = !!apiClient;
|
|
20689
|
+
const archivePath = shouldArchive ? (0, import_node_path9.join)(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
20690
|
+
const gitCaptureSaga = new CaptureTreeSaga(this.log);
|
|
20691
|
+
const captureResult = await gitCaptureSaga.run({
|
|
20692
|
+
baseDir: repositoryPath,
|
|
20693
|
+
lastTreeHash,
|
|
20694
|
+
archivePath
|
|
20695
|
+
});
|
|
20696
|
+
if (!captureResult.success) {
|
|
20697
|
+
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
20582
20698
|
}
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20586
|
-
|
|
20587
|
-
|
|
20699
|
+
const {
|
|
20700
|
+
snapshot: gitSnapshot,
|
|
20701
|
+
archivePath: createdArchivePath,
|
|
20702
|
+
changed
|
|
20703
|
+
} = captureResult.data;
|
|
20704
|
+
if (!changed || !gitSnapshot) {
|
|
20705
|
+
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
20706
|
+
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
20588
20707
|
}
|
|
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
|
-
}
|
|
20708
|
+
let archiveUrl;
|
|
20709
|
+
if (apiClient && createdArchivePath) {
|
|
20710
|
+
try {
|
|
20711
|
+
archiveUrl = await this.uploadArchive(
|
|
20712
|
+
createdArchivePath,
|
|
20713
|
+
gitSnapshot.treeHash,
|
|
20714
|
+
apiClient,
|
|
20715
|
+
taskId,
|
|
20716
|
+
runId
|
|
20608
20717
|
);
|
|
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);
|
|
20718
|
+
} finally {
|
|
20719
|
+
await (0, import_promises6.rm)(createdArchivePath, { force: true }).catch(() => {
|
|
20720
|
+
});
|
|
20624
20721
|
}
|
|
20625
20722
|
}
|
|
20723
|
+
const snapshot = {
|
|
20724
|
+
treeHash: gitSnapshot.treeHash,
|
|
20725
|
+
baseCommit: gitSnapshot.baseCommit,
|
|
20726
|
+
changes: gitSnapshot.changes,
|
|
20727
|
+
timestamp: gitSnapshot.timestamp,
|
|
20728
|
+
interrupted,
|
|
20729
|
+
archiveUrl
|
|
20730
|
+
};
|
|
20731
|
+
this.log.info("Tree captured", {
|
|
20732
|
+
treeHash: snapshot.treeHash,
|
|
20733
|
+
changes: snapshot.changes.length,
|
|
20734
|
+
interrupted,
|
|
20735
|
+
archiveUrl
|
|
20736
|
+
});
|
|
20737
|
+
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
20626
20738
|
}
|
|
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 }
|
|
20739
|
+
async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
|
|
20740
|
+
const archiveUrl = await this.step({
|
|
20741
|
+
name: "upload_archive",
|
|
20742
|
+
execute: async () => {
|
|
20743
|
+
const archiveContent = await (0, import_promises6.readFile)(archivePath);
|
|
20744
|
+
const base64Content = archiveContent.toString("base64");
|
|
20745
|
+
const snapshotBytes = archiveContent.byteLength;
|
|
20746
|
+
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
20747
|
+
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
20748
|
+
{
|
|
20749
|
+
name: `trees/${treeHash}.tar.gz`,
|
|
20750
|
+
type: "tree_snapshot",
|
|
20751
|
+
content: base64Content,
|
|
20752
|
+
content_type: "application/gzip"
|
|
20664
20753
|
}
|
|
20754
|
+
]);
|
|
20755
|
+
const uploadedArtifact = artifacts[0];
|
|
20756
|
+
if (uploadedArtifact?.storage_path) {
|
|
20757
|
+
this.log.info("Tree archive uploaded", {
|
|
20758
|
+
storagePath: uploadedArtifact.storage_path,
|
|
20759
|
+
treeHash,
|
|
20760
|
+
snapshotBytes,
|
|
20761
|
+
snapshotWireBytes,
|
|
20762
|
+
totalBytes: snapshotBytes,
|
|
20763
|
+
totalWireBytes: snapshotWireBytes
|
|
20764
|
+
});
|
|
20765
|
+
return uploadedArtifact.storage_path;
|
|
20665
20766
|
}
|
|
20666
|
-
|
|
20667
|
-
|
|
20668
|
-
|
|
20669
|
-
|
|
20670
|
-
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
}
|
|
20767
|
+
return void 0;
|
|
20768
|
+
},
|
|
20769
|
+
rollback: async () => {
|
|
20770
|
+
await (0, import_promises6.rm)(archivePath, { force: true }).catch(() => {
|
|
20771
|
+
});
|
|
20772
|
+
}
|
|
20773
|
+
});
|
|
20774
|
+
return archiveUrl;
|
|
20675
20775
|
}
|
|
20676
|
-
|
|
20677
|
-
|
|
20776
|
+
};
|
|
20777
|
+
|
|
20778
|
+
// src/tree-tracker.ts
|
|
20779
|
+
var TreeTracker = class {
|
|
20780
|
+
repositoryPath;
|
|
20781
|
+
taskId;
|
|
20782
|
+
runId;
|
|
20783
|
+
apiClient;
|
|
20784
|
+
logger;
|
|
20785
|
+
lastTreeHash = null;
|
|
20786
|
+
constructor(config) {
|
|
20787
|
+
this.repositoryPath = config.repositoryPath;
|
|
20788
|
+
this.taskId = config.taskId;
|
|
20789
|
+
this.runId = config.runId;
|
|
20790
|
+
this.apiClient = config.apiClient;
|
|
20791
|
+
this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
20678
20792
|
}
|
|
20679
|
-
|
|
20680
|
-
|
|
20681
|
-
|
|
20682
|
-
|
|
20683
|
-
|
|
20684
|
-
|
|
20685
|
-
|
|
20686
|
-
|
|
20687
|
-
|
|
20688
|
-
|
|
20793
|
+
/**
|
|
20794
|
+
* Capture current working tree state as a snapshot.
|
|
20795
|
+
* Uses a temporary index to avoid modifying user's staging area.
|
|
20796
|
+
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
20797
|
+
*/
|
|
20798
|
+
async captureTree(options) {
|
|
20799
|
+
const saga = new CaptureTreeSaga2(this.logger);
|
|
20800
|
+
const result = await saga.run({
|
|
20801
|
+
repositoryPath: this.repositoryPath,
|
|
20802
|
+
taskId: this.taskId,
|
|
20803
|
+
runId: this.runId,
|
|
20804
|
+
apiClient: this.apiClient,
|
|
20805
|
+
lastTreeHash: this.lastTreeHash,
|
|
20806
|
+
interrupted: options?.interrupted
|
|
20807
|
+
});
|
|
20808
|
+
if (!result.success) {
|
|
20809
|
+
this.logger.error("Failed to capture tree", {
|
|
20810
|
+
error: result.error,
|
|
20811
|
+
failedStep: result.failedStep
|
|
20812
|
+
});
|
|
20813
|
+
throw new Error(
|
|
20814
|
+
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
20689
20815
|
);
|
|
20690
20816
|
}
|
|
20691
|
-
|
|
20692
|
-
|
|
20693
|
-
resetTurnMessages(sessionId) {
|
|
20694
|
-
const session = this.sessions.get(sessionId);
|
|
20695
|
-
if (session) {
|
|
20696
|
-
session.currentTurnMessages = [];
|
|
20817
|
+
if (result.data.newTreeHash !== null) {
|
|
20818
|
+
this.lastTreeHash = result.data.newTreeHash;
|
|
20697
20819
|
}
|
|
20820
|
+
return result.data.snapshot;
|
|
20698
20821
|
}
|
|
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;
|
|
20822
|
+
/**
|
|
20823
|
+
* Download and apply a tree snapshot.
|
|
20824
|
+
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
20825
|
+
*/
|
|
20826
|
+
async applyTreeSnapshot(snapshot) {
|
|
20827
|
+
if (!this.apiClient) {
|
|
20828
|
+
throw new Error("Cannot apply snapshot: API client not configured");
|
|
20712
20829
|
}
|
|
20713
|
-
if (
|
|
20714
|
-
|
|
20715
|
-
|
|
20830
|
+
if (!snapshot.archiveUrl) {
|
|
20831
|
+
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
20832
|
+
treeHash: snapshot.treeHash,
|
|
20833
|
+
changes: snapshot.changes.length
|
|
20834
|
+
});
|
|
20835
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20716
20836
|
}
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
20727
|
-
|
|
20728
|
-
|
|
20729
|
-
|
|
20837
|
+
const saga = new ApplySnapshotSaga(this.logger);
|
|
20838
|
+
const result = await saga.run({
|
|
20839
|
+
snapshot,
|
|
20840
|
+
repositoryPath: this.repositoryPath,
|
|
20841
|
+
apiClient: this.apiClient,
|
|
20842
|
+
taskId: this.taskId,
|
|
20843
|
+
runId: this.runId
|
|
20844
|
+
});
|
|
20845
|
+
if (!result.success) {
|
|
20846
|
+
this.logger.error("Failed to apply tree snapshot", {
|
|
20847
|
+
error: result.error,
|
|
20848
|
+
failedStep: result.failedStep,
|
|
20849
|
+
treeHash: snapshot.treeHash
|
|
20850
|
+
});
|
|
20851
|
+
throw new Error(
|
|
20852
|
+
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
20730
20853
|
);
|
|
20731
|
-
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20732
|
-
delay3 = 0;
|
|
20733
|
-
} else {
|
|
20734
|
-
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
20735
20854
|
}
|
|
20736
|
-
|
|
20737
|
-
this.flushTimeouts.set(sessionId, timeout);
|
|
20855
|
+
this.lastTreeHash = result.data.treeHash;
|
|
20738
20856
|
}
|
|
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
|
-
}
|
|
20857
|
+
/**
|
|
20858
|
+
* Get the last captured tree hash.
|
|
20859
|
+
*/
|
|
20860
|
+
getLastTreeHash() {
|
|
20861
|
+
return this.lastTreeHash;
|
|
20760
20862
|
}
|
|
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;
|
|
20863
|
+
/**
|
|
20864
|
+
* Set the last tree hash (used when resuming).
|
|
20865
|
+
*/
|
|
20866
|
+
setLastTreeHash(hash) {
|
|
20867
|
+
this.lastTreeHash = hash;
|
|
20781
20868
|
}
|
|
20782
20869
|
};
|
|
20783
20870
|
|
|
@@ -21253,45 +21340,29 @@ var AgentServer = class {
|
|
|
21253
21340
|
});
|
|
21254
21341
|
await this.autoInitializeSession();
|
|
21255
21342
|
}
|
|
21256
|
-
async
|
|
21257
|
-
|
|
21258
|
-
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
21343
|
+
async loadResumeState(taskId, resumeRunId, currentRunId) {
|
|
21344
|
+
this.logger.debug("Loading resume state", { resumeRunId, currentRunId });
|
|
21345
|
+
try {
|
|
21346
|
+
this.resumeState = await resumeFromLog({
|
|
21347
|
+
taskId,
|
|
21348
|
+
runId: resumeRunId,
|
|
21349
|
+
repositoryPath: this.config.repositoryPath,
|
|
21350
|
+
apiClient: this.posthogAPI,
|
|
21351
|
+
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
21264
21352
|
});
|
|
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
|
-
}
|
|
21353
|
+
this.logger.debug("Resume state loaded", {
|
|
21354
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
21355
|
+
hasSnapshot: !!this.resumeState.latestSnapshot,
|
|
21356
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21357
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null,
|
|
21358
|
+
logEntries: this.resumeState.logEntryCount
|
|
21359
|
+
});
|
|
21360
|
+
} catch (error) {
|
|
21361
|
+
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21362
|
+
error
|
|
21363
|
+
});
|
|
21364
|
+
this.resumeState = null;
|
|
21284
21365
|
}
|
|
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
21366
|
}
|
|
21296
21367
|
async stop() {
|
|
21297
21368
|
this.logger.debug("Stopping agent server...");
|
|
@@ -21690,29 +21761,11 @@ var AgentServer = class {
|
|
|
21690
21761
|
if (!this.resumeState) {
|
|
21691
21762
|
const resumeRunId = this.getResumeRunId(taskRun);
|
|
21692
21763
|
if (resumeRunId) {
|
|
21693
|
-
this.
|
|
21764
|
+
await this.loadResumeState(
|
|
21765
|
+
payload.task_id,
|
|
21694
21766
|
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
|
-
}
|
|
21767
|
+
payload.run_id
|
|
21768
|
+
);
|
|
21716
21769
|
}
|
|
21717
21770
|
}
|
|
21718
21771
|
if (this.resumeState && this.resumeState.conversation.length > 0) {
|
|
@@ -21771,8 +21824,59 @@ var AgentServer = class {
|
|
|
21771
21824
|
const conversationSummary = formatConversationForResume(
|
|
21772
21825
|
this.resumeState.conversation
|
|
21773
21826
|
);
|
|
21827
|
+
let snapshotApplied = false;
|
|
21828
|
+
if (this.resumeState.latestSnapshot?.archiveUrl && this.config.repositoryPath && this.posthogAPI) {
|
|
21829
|
+
try {
|
|
21830
|
+
const treeTracker = new TreeTracker({
|
|
21831
|
+
repositoryPath: this.config.repositoryPath,
|
|
21832
|
+
taskId: payload.task_id,
|
|
21833
|
+
runId: payload.run_id,
|
|
21834
|
+
apiClient: this.posthogAPI,
|
|
21835
|
+
logger: this.logger.child("TreeTracker")
|
|
21836
|
+
});
|
|
21837
|
+
await treeTracker.applyTreeSnapshot(this.resumeState.latestSnapshot);
|
|
21838
|
+
treeTracker.setLastTreeHash(this.resumeState.latestSnapshot.treeHash);
|
|
21839
|
+
snapshotApplied = true;
|
|
21840
|
+
this.logger.info("Tree snapshot applied", {
|
|
21841
|
+
treeHash: this.resumeState.latestSnapshot.treeHash,
|
|
21842
|
+
changes: this.resumeState.latestSnapshot.changes?.length ?? 0,
|
|
21843
|
+
hasArchiveUrl: !!this.resumeState.latestSnapshot.archiveUrl
|
|
21844
|
+
});
|
|
21845
|
+
} catch (error) {
|
|
21846
|
+
this.logger.warn("Failed to apply tree snapshot", {
|
|
21847
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21848
|
+
treeHash: this.resumeState.latestSnapshot.treeHash
|
|
21849
|
+
});
|
|
21850
|
+
}
|
|
21851
|
+
}
|
|
21852
|
+
if (this.resumeState.latestGitCheckpoint && this.config.repositoryPath && this.posthogAPI) {
|
|
21853
|
+
try {
|
|
21854
|
+
const checkpointTracker = new HandoffCheckpointTracker({
|
|
21855
|
+
repositoryPath: this.config.repositoryPath,
|
|
21856
|
+
taskId: payload.task_id,
|
|
21857
|
+
runId: payload.run_id,
|
|
21858
|
+
apiClient: this.posthogAPI,
|
|
21859
|
+
logger: this.logger.child("HandoffCheckpoint")
|
|
21860
|
+
});
|
|
21861
|
+
const metrics = await checkpointTracker.applyFromHandoff(
|
|
21862
|
+
this.resumeState.latestGitCheckpoint
|
|
21863
|
+
);
|
|
21864
|
+
this.logger.info("Git checkpoint applied", {
|
|
21865
|
+
branch: this.resumeState.latestGitCheckpoint.branch,
|
|
21866
|
+
head: this.resumeState.latestGitCheckpoint.head,
|
|
21867
|
+
packBytes: metrics.packBytes,
|
|
21868
|
+
indexBytes: metrics.indexBytes,
|
|
21869
|
+
totalBytes: metrics.totalBytes
|
|
21870
|
+
});
|
|
21871
|
+
} catch (error) {
|
|
21872
|
+
this.logger.warn("Failed to apply git checkpoint", {
|
|
21873
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21874
|
+
branch: this.resumeState.latestGitCheckpoint.branch
|
|
21875
|
+
});
|
|
21876
|
+
}
|
|
21877
|
+
}
|
|
21774
21878
|
const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
|
|
21775
|
-
const sandboxContext =
|
|
21879
|
+
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
21880
|
let resumePromptBlocks;
|
|
21777
21881
|
if (pendingUserPrompt?.length) {
|
|
21778
21882
|
resumePromptBlocks = [
|
|
@@ -21813,7 +21917,9 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21813
21917
|
conversationTurns: this.resumeState.conversation.length,
|
|
21814
21918
|
promptLength: promptBlocksToText(resumePromptBlocks).length,
|
|
21815
21919
|
hasPendingUserMessage: !!pendingUserPrompt?.length,
|
|
21816
|
-
snapshotApplied
|
|
21920
|
+
snapshotApplied,
|
|
21921
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21922
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null
|
|
21817
21923
|
});
|
|
21818
21924
|
this.resumeState = null;
|
|
21819
21925
|
this.session.logWriter.resetTurnMessages(payload.run_id);
|
|
@@ -21977,6 +22083,24 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21977
22083
|
const normalizedName = baseName.replace(/[^\w.-]/g, "_");
|
|
21978
22084
|
return normalizedName.length > 0 ? normalizedName : "attachment";
|
|
21979
22085
|
}
|
|
22086
|
+
async autoInitializeSession() {
|
|
22087
|
+
const { taskId, runId, mode, projectId } = this.config;
|
|
22088
|
+
this.logger.debug("Auto-initializing session", { taskId, runId, mode });
|
|
22089
|
+
const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
22090
|
+
if (resumeRunId) {
|
|
22091
|
+
await this.loadResumeState(taskId, resumeRunId, runId);
|
|
22092
|
+
}
|
|
22093
|
+
const payload = {
|
|
22094
|
+
task_id: taskId,
|
|
22095
|
+
run_id: runId,
|
|
22096
|
+
team_id: projectId,
|
|
22097
|
+
user_id: 0,
|
|
22098
|
+
// System-initiated
|
|
22099
|
+
distinct_id: "agent-server",
|
|
22100
|
+
mode
|
|
22101
|
+
};
|
|
22102
|
+
await this.initializeSession(payload, null);
|
|
22103
|
+
}
|
|
21980
22104
|
getResumeRunId(taskRun) {
|
|
21981
22105
|
const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21982
22106
|
if (envRunId) return envRunId;
|