@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
|
@@ -8605,7 +8605,7 @@ import { z as z4 } from "zod";
|
|
|
8605
8605
|
// package.json
|
|
8606
8606
|
var package_default = {
|
|
8607
8607
|
name: "@posthog/agent",
|
|
8608
|
-
version: "2.3.
|
|
8608
|
+
version: "2.3.398",
|
|
8609
8609
|
repository: "https://github.com/PostHog/code",
|
|
8610
8610
|
description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
8611
8611
|
exports: {
|
|
@@ -8657,6 +8657,10 @@ var package_default = {
|
|
|
8657
8657
|
types: "./dist/adapters/reasoning-effort.d.ts",
|
|
8658
8658
|
import: "./dist/adapters/reasoning-effort.js"
|
|
8659
8659
|
},
|
|
8660
|
+
"./adapters/claude/mcp/tool-metadata": {
|
|
8661
|
+
types: "./dist/adapters/claude/mcp/tool-metadata.d.ts",
|
|
8662
|
+
import: "./dist/adapters/claude/mcp/tool-metadata.js"
|
|
8663
|
+
},
|
|
8660
8664
|
"./execution-mode": {
|
|
8661
8665
|
types: "./dist/execution-mode.d.ts",
|
|
8662
8666
|
import: "./dist/execution-mode.js"
|
|
@@ -13657,10 +13661,12 @@ async function fetchMcpToolMetadata(q, logger = new Logger({ debug: false, prefi
|
|
|
13657
13661
|
for (const tool of server.tools) {
|
|
13658
13662
|
const toolKey = buildToolKey(server.name, tool.name);
|
|
13659
13663
|
const readOnly = tool.annotations?.readOnly === true;
|
|
13664
|
+
const existing = mcpToolMetadataCache.get(toolKey);
|
|
13660
13665
|
mcpToolMetadataCache.set(toolKey, {
|
|
13661
13666
|
readOnly,
|
|
13662
13667
|
name: tool.name,
|
|
13663
|
-
description: tool.description
|
|
13668
|
+
description: tool.description,
|
|
13669
|
+
approvalState: existing?.approvalState
|
|
13664
13670
|
});
|
|
13665
13671
|
if (readOnly) readOnlyCount++;
|
|
13666
13672
|
}
|
|
@@ -13702,6 +13708,23 @@ function getConnectedMcpServerNames() {
|
|
|
13702
13708
|
}
|
|
13703
13709
|
return [...names];
|
|
13704
13710
|
}
|
|
13711
|
+
function getMcpToolApprovalState(toolName) {
|
|
13712
|
+
return mcpToolMetadataCache.get(toolName)?.approvalState;
|
|
13713
|
+
}
|
|
13714
|
+
function setMcpToolApprovalStates(approvals) {
|
|
13715
|
+
for (const [toolKey, approvalState] of Object.entries(approvals)) {
|
|
13716
|
+
const existing = mcpToolMetadataCache.get(toolKey);
|
|
13717
|
+
if (existing) {
|
|
13718
|
+
existing.approvalState = approvalState;
|
|
13719
|
+
} else {
|
|
13720
|
+
mcpToolMetadataCache.set(toolKey, {
|
|
13721
|
+
readOnly: false,
|
|
13722
|
+
name: toolKey,
|
|
13723
|
+
approvalState
|
|
13724
|
+
});
|
|
13725
|
+
}
|
|
13726
|
+
}
|
|
13727
|
+
}
|
|
13705
13728
|
|
|
13706
13729
|
// src/adapters/claude/conversion/tool-use-to-acp.ts
|
|
13707
13730
|
var SYSTEM_REMINDER_REGEX = /\s*<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -15397,6 +15420,72 @@ async function handleDefaultPermissionFlow(context) {
|
|
|
15397
15420
|
return { behavior: "deny", message, interrupt: !feedback };
|
|
15398
15421
|
}
|
|
15399
15422
|
}
|
|
15423
|
+
function parseMcpToolName(toolName) {
|
|
15424
|
+
const parts2 = toolName.split("__");
|
|
15425
|
+
return {
|
|
15426
|
+
serverName: parts2[1] ?? toolName,
|
|
15427
|
+
tool: parts2.slice(2).join("__") || toolName
|
|
15428
|
+
};
|
|
15429
|
+
}
|
|
15430
|
+
async function handleMcpApprovalFlow(context) {
|
|
15431
|
+
const { toolName, toolInput, toolUseID, client, sessionId } = context;
|
|
15432
|
+
const { serverName, tool: displayTool } = parseMcpToolName(toolName);
|
|
15433
|
+
const metadata2 = getMcpToolMetadata(toolName);
|
|
15434
|
+
const description = metadata2?.description ? `
|
|
15435
|
+
|
|
15436
|
+
${metadata2.description}` : "";
|
|
15437
|
+
const response = await client.requestPermission({
|
|
15438
|
+
options: [
|
|
15439
|
+
{ kind: "allow_once", name: "Yes", optionId: "allow" },
|
|
15440
|
+
{
|
|
15441
|
+
kind: "allow_always",
|
|
15442
|
+
name: "Yes, always allow",
|
|
15443
|
+
optionId: "allow_always"
|
|
15444
|
+
},
|
|
15445
|
+
{
|
|
15446
|
+
kind: "reject_once",
|
|
15447
|
+
name: "Type here to tell the agent what to do differently",
|
|
15448
|
+
optionId: "reject",
|
|
15449
|
+
_meta: { customInput: true }
|
|
15450
|
+
}
|
|
15451
|
+
],
|
|
15452
|
+
sessionId,
|
|
15453
|
+
toolCall: {
|
|
15454
|
+
toolCallId: toolUseID,
|
|
15455
|
+
title: `The agent wants to call ${displayTool} (${serverName})`,
|
|
15456
|
+
kind: "other",
|
|
15457
|
+
content: description ? [{ type: "content", content: text(description) }] : [],
|
|
15458
|
+
rawInput: { ...toolInput, toolName }
|
|
15459
|
+
}
|
|
15460
|
+
});
|
|
15461
|
+
if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
|
|
15462
|
+
throw new Error("Tool use aborted");
|
|
15463
|
+
}
|
|
15464
|
+
if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
|
|
15465
|
+
if (response.outcome.optionId === "allow_always") {
|
|
15466
|
+
return {
|
|
15467
|
+
behavior: "allow",
|
|
15468
|
+
updatedInput: toolInput,
|
|
15469
|
+
updatedPermissions: [
|
|
15470
|
+
{
|
|
15471
|
+
type: "addRules",
|
|
15472
|
+
rules: [{ toolName }],
|
|
15473
|
+
behavior: "allow",
|
|
15474
|
+
destination: "localSettings"
|
|
15475
|
+
}
|
|
15476
|
+
]
|
|
15477
|
+
};
|
|
15478
|
+
}
|
|
15479
|
+
return {
|
|
15480
|
+
behavior: "allow",
|
|
15481
|
+
updatedInput: toolInput
|
|
15482
|
+
};
|
|
15483
|
+
}
|
|
15484
|
+
const feedback = response._meta?.customInput?.trim();
|
|
15485
|
+
const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
|
|
15486
|
+
await emitToolDenial(context, message);
|
|
15487
|
+
return { behavior: "deny", message, interrupt: !feedback };
|
|
15488
|
+
}
|
|
15400
15489
|
function handlePlanFileException(context) {
|
|
15401
15490
|
const { session, toolName, toolInput } = context;
|
|
15402
15491
|
if (session.permissionMode !== "plan" || !WRITE_TOOLS.has(toolName)) {
|
|
@@ -15467,6 +15556,17 @@ async function canUseTool(context) {
|
|
|
15467
15556
|
}
|
|
15468
15557
|
}
|
|
15469
15558
|
}
|
|
15559
|
+
if (toolName.startsWith("mcp__")) {
|
|
15560
|
+
const approvalState = getMcpToolApprovalState(toolName);
|
|
15561
|
+
if (approvalState === "do_not_use") {
|
|
15562
|
+
const message = "This tool has been blocked. To re-enable it, go to Settings > MCP Servers in PostHog Code.";
|
|
15563
|
+
await emitToolDenial(context, message);
|
|
15564
|
+
return { behavior: "deny", message, interrupt: false };
|
|
15565
|
+
}
|
|
15566
|
+
if (approvalState === "needs_approval") {
|
|
15567
|
+
return handleMcpApprovalFlow(context);
|
|
15568
|
+
}
|
|
15569
|
+
}
|
|
15470
15570
|
if (isToolAllowedForMode(toolName, session.permissionMode)) {
|
|
15471
15571
|
return {
|
|
15472
15572
|
behavior: "allow",
|
|
@@ -15679,7 +15779,14 @@ Only enter plan mode (EnterPlanMode) when the user is requesting a significant c
|
|
|
15679
15779
|
|
|
15680
15780
|
When in doubt, continue executing and incorporate the feedback inline.
|
|
15681
15781
|
`;
|
|
15682
|
-
var
|
|
15782
|
+
var MCP_TOOLS = `
|
|
15783
|
+
# MCP Tool Access
|
|
15784
|
+
|
|
15785
|
+
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."
|
|
15786
|
+
|
|
15787
|
+
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.
|
|
15788
|
+
`;
|
|
15789
|
+
var APPENDED_INSTRUCTIONS = BRANCH_NAMING + PLAN_MODE + MCP_TOOLS;
|
|
15683
15790
|
|
|
15684
15791
|
// src/adapters/claude/session/options.ts
|
|
15685
15792
|
function buildSystemPrompt(customPrompt) {
|
|
@@ -17008,6 +17115,9 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
|
|
|
17008
17115
|
const earlyModelId = settingsManager.getSettings().model || meta?.model || "";
|
|
17009
17116
|
const mcpServers = supportsMcpInjection(earlyModelId) ? parseMcpServers(params) : {};
|
|
17010
17117
|
const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
|
|
17118
|
+
if (meta?.mcpToolApprovals) {
|
|
17119
|
+
setMcpToolApprovalStates(meta.mcpToolApprovals);
|
|
17120
|
+
}
|
|
17011
17121
|
const outputFormat = meta?.jsonSchema && this.options?.onStructuredOutput ? { type: "json_schema", schema: meta.jsonSchema } : void 0;
|
|
17012
17122
|
this.logger.debug(isResume ? "Resuming session" : "Creating new session", {
|
|
17013
17123
|
sessionId,
|
|
@@ -19157,6 +19267,11 @@ var HandoffCheckpointTracker = class {
|
|
|
19157
19267
|
onDivergedBranch: options?.onDivergedBranch
|
|
19158
19268
|
});
|
|
19159
19269
|
this.logApplyMetrics(checkpoint, downloads, applyResult.totalBytes);
|
|
19270
|
+
return {
|
|
19271
|
+
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
19272
|
+
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
19273
|
+
totalBytes: applyResult.totalBytes
|
|
19274
|
+
};
|
|
19160
19275
|
} finally {
|
|
19161
19276
|
await this.removeIfPresent(packPath);
|
|
19162
19277
|
await this.removeIfPresent(indexPath);
|
|
@@ -19203,22 +19318,22 @@ var HandoffCheckpointTracker = class {
|
|
|
19203
19318
|
};
|
|
19204
19319
|
}
|
|
19205
19320
|
async uploadArtifacts(specs) {
|
|
19206
|
-
const
|
|
19207
|
-
|
|
19208
|
-
|
|
19209
|
-
|
|
19210
|
-
|
|
19211
|
-
|
|
19212
|
-
|
|
19213
|
-
|
|
19214
|
-
|
|
19215
|
-
|
|
19216
|
-
|
|
19217
|
-
|
|
19218
|
-
|
|
19219
|
-
|
|
19220
|
-
|
|
19221
|
-
return Object.fromEntries(
|
|
19321
|
+
const results = [];
|
|
19322
|
+
for (const spec of specs) {
|
|
19323
|
+
if (!spec.filePath) {
|
|
19324
|
+
results.push([spec.key, void 0]);
|
|
19325
|
+
continue;
|
|
19326
|
+
}
|
|
19327
|
+
results.push([
|
|
19328
|
+
spec.key,
|
|
19329
|
+
await this.uploadArtifactFile(
|
|
19330
|
+
spec.filePath,
|
|
19331
|
+
spec.name,
|
|
19332
|
+
spec.contentType
|
|
19333
|
+
)
|
|
19334
|
+
]);
|
|
19335
|
+
}
|
|
19336
|
+
return Object.fromEntries(results);
|
|
19222
19337
|
}
|
|
19223
19338
|
async downloadArtifactToFile(artifactPath, filePath, label) {
|
|
19224
19339
|
if (!this.apiClient) {
|
|
@@ -19230,7 +19345,7 @@ var HandoffCheckpointTracker = class {
|
|
|
19230
19345
|
artifactPath
|
|
19231
19346
|
);
|
|
19232
19347
|
if (!arrayBuffer) {
|
|
19233
|
-
throw new Error(`Failed to download ${label}`);
|
|
19348
|
+
throw new Error(`Failed to download ${label} from ${artifactPath}`);
|
|
19234
19349
|
}
|
|
19235
19350
|
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19236
19351
|
const binaryContent = Buffer.from(base64Content, "base64");
|
|
@@ -19414,6 +19529,13 @@ var PostHogAPIClient = class {
|
|
|
19414
19529
|
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
19415
19530
|
);
|
|
19416
19531
|
}
|
|
19532
|
+
async resumeRunInCloud(taskId, runId) {
|
|
19533
|
+
const teamId = this.getTeamId();
|
|
19534
|
+
return this.apiRequest(
|
|
19535
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/resume_in_cloud/`,
|
|
19536
|
+
{ method: "POST" }
|
|
19537
|
+
);
|
|
19538
|
+
}
|
|
19417
19539
|
async updateTaskRun(taskId, runId, payload) {
|
|
19418
19540
|
const teamId = this.getTeamId();
|
|
19419
19541
|
return this.apiRequest(
|
|
@@ -19561,1215 +19683,1180 @@ function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
|
19561
19683
|
return turns.slice(startIndex);
|
|
19562
19684
|
}
|
|
19563
19685
|
|
|
19564
|
-
// src/sagas/
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
|
|
19571
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
19575
|
-
|
|
19576
|
-
async executeGitOperations(input) {
|
|
19577
|
-
const { baseDir, lastTreeHash, archivePath, signal } = input;
|
|
19578
|
-
const tmpDir = path15.join(baseDir, ".git", "posthog-code-tmp");
|
|
19579
|
-
await this.step({
|
|
19580
|
-
name: "create_tmp_dir",
|
|
19581
|
-
execute: () => fs12.mkdir(tmpDir, { recursive: true }),
|
|
19582
|
-
rollback: async () => {
|
|
19583
|
-
}
|
|
19584
|
-
});
|
|
19585
|
-
this.tempIndexPath = path15.join(tmpDir, `index-${Date.now()}`);
|
|
19586
|
-
const tempIndexGit = this.git.env({
|
|
19587
|
-
...process.env,
|
|
19588
|
-
GIT_INDEX_FILE: this.tempIndexPath
|
|
19589
|
-
});
|
|
19590
|
-
await this.step({
|
|
19591
|
-
name: "init_temp_index",
|
|
19592
|
-
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
19593
|
-
rollback: async () => {
|
|
19594
|
-
if (this.tempIndexPath) {
|
|
19595
|
-
await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
19596
|
-
});
|
|
19597
|
-
}
|
|
19598
|
-
}
|
|
19599
|
-
});
|
|
19600
|
-
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
19601
|
-
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
19602
|
-
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
19603
|
-
this.log.debug("No changes since last capture", { treeHash });
|
|
19604
|
-
await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
19605
|
-
});
|
|
19606
|
-
return { snapshot: null, changed: false };
|
|
19686
|
+
// src/sagas/resume-saga.ts
|
|
19687
|
+
var ResumeSaga = class extends Saga {
|
|
19688
|
+
sagaName = "ResumeSaga";
|
|
19689
|
+
async execute(input) {
|
|
19690
|
+
const { taskId, runId, apiClient } = input;
|
|
19691
|
+
const taskRun = await this.readOnlyStep(
|
|
19692
|
+
"fetch_task_run",
|
|
19693
|
+
() => apiClient.getTaskRun(taskId, runId)
|
|
19694
|
+
);
|
|
19695
|
+
if (!taskRun.log_url) {
|
|
19696
|
+
this.log.info("No log URL found, starting fresh");
|
|
19697
|
+
return this.emptyResult();
|
|
19607
19698
|
}
|
|
19608
|
-
const
|
|
19609
|
-
|
|
19610
|
-
|
|
19611
|
-
|
|
19612
|
-
|
|
19613
|
-
|
|
19614
|
-
|
|
19615
|
-
const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
|
|
19616
|
-
await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
19617
|
-
});
|
|
19618
|
-
const snapshot = {
|
|
19619
|
-
treeHash,
|
|
19620
|
-
baseCommit,
|
|
19621
|
-
changes,
|
|
19622
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
19623
|
-
};
|
|
19624
|
-
let createdArchivePath;
|
|
19625
|
-
if (archivePath) {
|
|
19626
|
-
createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
|
|
19699
|
+
const entries = await this.readOnlyStep(
|
|
19700
|
+
"fetch_logs",
|
|
19701
|
+
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
19702
|
+
);
|
|
19703
|
+
if (entries.length === 0) {
|
|
19704
|
+
this.log.info("No log entries found, starting fresh");
|
|
19705
|
+
return this.emptyResult();
|
|
19627
19706
|
}
|
|
19628
|
-
this.log.info("
|
|
19629
|
-
|
|
19630
|
-
|
|
19631
|
-
|
|
19632
|
-
|
|
19633
|
-
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
|
|
19637
|
-
if (
|
|
19638
|
-
|
|
19707
|
+
this.log.info("Fetched log entries", { count: entries.length });
|
|
19708
|
+
const latestSnapshot = await this.readOnlyStep(
|
|
19709
|
+
"find_snapshot",
|
|
19710
|
+
() => Promise.resolve(this.findLatestTreeSnapshot(entries))
|
|
19711
|
+
);
|
|
19712
|
+
const latestGitCheckpoint = await this.readOnlyStep(
|
|
19713
|
+
"find_git_checkpoint",
|
|
19714
|
+
() => Promise.resolve(this.findLatestGitCheckpoint(entries))
|
|
19715
|
+
);
|
|
19716
|
+
if (latestSnapshot) {
|
|
19717
|
+
this.log.info("Found tree snapshot", {
|
|
19718
|
+
treeHash: latestSnapshot.treeHash,
|
|
19719
|
+
hasArchiveUrl: !!latestSnapshot.archiveUrl,
|
|
19720
|
+
changes: latestSnapshot.changes?.length ?? 0
|
|
19721
|
+
});
|
|
19639
19722
|
}
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19723
|
+
if (latestGitCheckpoint) {
|
|
19724
|
+
this.log.info("Found git checkpoint", {
|
|
19725
|
+
checkpointId: latestGitCheckpoint.checkpointId,
|
|
19726
|
+
branch: latestGitCheckpoint.branch
|
|
19727
|
+
});
|
|
19643
19728
|
}
|
|
19644
|
-
await this.
|
|
19645
|
-
|
|
19646
|
-
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
});
|
|
19658
|
-
}
|
|
19729
|
+
const conversation = await this.readOnlyStep(
|
|
19730
|
+
"rebuild_conversation",
|
|
19731
|
+
() => Promise.resolve(this.rebuildConversation(entries))
|
|
19732
|
+
);
|
|
19733
|
+
const lastDevice = await this.readOnlyStep(
|
|
19734
|
+
"find_device",
|
|
19735
|
+
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
19736
|
+
);
|
|
19737
|
+
this.log.info("Resume state rebuilt", {
|
|
19738
|
+
turns: conversation.length,
|
|
19739
|
+
hasSnapshot: !!latestSnapshot,
|
|
19740
|
+
hasGitCheckpoint: !!latestGitCheckpoint,
|
|
19741
|
+
interrupted: latestSnapshot?.interrupted ?? false
|
|
19659
19742
|
});
|
|
19660
|
-
return
|
|
19743
|
+
return {
|
|
19744
|
+
conversation,
|
|
19745
|
+
latestSnapshot,
|
|
19746
|
+
latestGitCheckpoint,
|
|
19747
|
+
interrupted: latestSnapshot?.interrupted ?? false,
|
|
19748
|
+
lastDevice,
|
|
19749
|
+
logEntryCount: entries.length
|
|
19750
|
+
};
|
|
19661
19751
|
}
|
|
19662
|
-
|
|
19663
|
-
|
|
19664
|
-
|
|
19665
|
-
|
|
19666
|
-
|
|
19667
|
-
|
|
19668
|
-
|
|
19669
|
-
|
|
19670
|
-
|
|
19671
|
-
|
|
19672
|
-
|
|
19673
|
-
|
|
19674
|
-
|
|
19675
|
-
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
|
|
19681
|
-
|
|
19682
|
-
if (status === "D") {
|
|
19683
|
-
normalizedStatus = "D";
|
|
19684
|
-
} else if (status === "A") {
|
|
19685
|
-
normalizedStatus = "A";
|
|
19686
|
-
} else {
|
|
19687
|
-
normalizedStatus = "M";
|
|
19752
|
+
emptyResult() {
|
|
19753
|
+
return {
|
|
19754
|
+
conversation: [],
|
|
19755
|
+
latestSnapshot: null,
|
|
19756
|
+
latestGitCheckpoint: null,
|
|
19757
|
+
interrupted: false,
|
|
19758
|
+
logEntryCount: 0
|
|
19759
|
+
};
|
|
19760
|
+
}
|
|
19761
|
+
findLatestTreeSnapshot(entries) {
|
|
19762
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19763
|
+
const entry = entries[i2];
|
|
19764
|
+
if (isNotification(
|
|
19765
|
+
entry.notification?.method,
|
|
19766
|
+
POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
19767
|
+
)) {
|
|
19768
|
+
const params = entry.notification.params;
|
|
19769
|
+
if (params?.treeHash) {
|
|
19770
|
+
return params;
|
|
19771
|
+
}
|
|
19688
19772
|
}
|
|
19689
|
-
changes.push({ path: filePath, status: normalizedStatus });
|
|
19690
19773
|
}
|
|
19691
|
-
return
|
|
19774
|
+
return null;
|
|
19692
19775
|
}
|
|
19693
|
-
|
|
19694
|
-
|
|
19695
|
-
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
|
|
19699
|
-
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
|
|
19703
|
-
let head = null;
|
|
19704
|
-
let branch = null;
|
|
19705
|
-
try {
|
|
19706
|
-
head = await this.git.revparse(["HEAD"]);
|
|
19707
|
-
} catch {
|
|
19708
|
-
head = null;
|
|
19776
|
+
findLatestGitCheckpoint(entries) {
|
|
19777
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
|
|
19778
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19779
|
+
const entry = entries[i2];
|
|
19780
|
+
const method = entry.notification?.method;
|
|
19781
|
+
if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
|
|
19782
|
+
const params = entry.notification?.params;
|
|
19783
|
+
if (params?.checkpointId && params?.checkpointRef) {
|
|
19784
|
+
return params;
|
|
19785
|
+
}
|
|
19709
19786
|
}
|
|
19710
|
-
|
|
19711
|
-
|
|
19712
|
-
|
|
19713
|
-
|
|
19787
|
+
}
|
|
19788
|
+
return null;
|
|
19789
|
+
}
|
|
19790
|
+
findLastDeviceInfo(entries) {
|
|
19791
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19792
|
+
const entry = entries[i2];
|
|
19793
|
+
const params = entry.notification?.params;
|
|
19794
|
+
if (params?.device) {
|
|
19795
|
+
return params.device;
|
|
19714
19796
|
}
|
|
19715
|
-
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
|
|
19727
|
-
|
|
19728
|
-
|
|
19729
|
-
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
|
|
19734
|
-
|
|
19735
|
-
|
|
19736
|
-
|
|
19737
|
-
|
|
19738
|
-
|
|
19739
|
-
rollback: async () => {
|
|
19740
|
-
try {
|
|
19741
|
-
if (this.originalBranch) {
|
|
19742
|
-
await this.git.checkout(this.originalBranch);
|
|
19743
|
-
} else if (this.originalHead) {
|
|
19744
|
-
await this.git.checkout(this.originalHead);
|
|
19797
|
+
}
|
|
19798
|
+
return void 0;
|
|
19799
|
+
}
|
|
19800
|
+
rebuildConversation(entries) {
|
|
19801
|
+
const turns = [];
|
|
19802
|
+
let currentAssistantContent = [];
|
|
19803
|
+
let currentToolCalls = [];
|
|
19804
|
+
for (const entry of entries) {
|
|
19805
|
+
const method = entry.notification?.method;
|
|
19806
|
+
const params = entry.notification?.params;
|
|
19807
|
+
if (method === "session/update" && params?.update) {
|
|
19808
|
+
const update = params.update;
|
|
19809
|
+
const sessionUpdate = update.sessionUpdate;
|
|
19810
|
+
switch (sessionUpdate) {
|
|
19811
|
+
case "user_message":
|
|
19812
|
+
case "user_message_chunk": {
|
|
19813
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19814
|
+
turns.push({
|
|
19815
|
+
role: "assistant",
|
|
19816
|
+
content: currentAssistantContent,
|
|
19817
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19818
|
+
});
|
|
19819
|
+
currentAssistantContent = [];
|
|
19820
|
+
currentToolCalls = [];
|
|
19745
19821
|
}
|
|
19746
|
-
|
|
19747
|
-
|
|
19822
|
+
const content = update.content;
|
|
19823
|
+
const contentArray = Array.isArray(content) ? content : [content];
|
|
19824
|
+
turns.push({
|
|
19825
|
+
role: "user",
|
|
19826
|
+
content: contentArray
|
|
19827
|
+
});
|
|
19828
|
+
break;
|
|
19748
19829
|
}
|
|
19749
|
-
|
|
19750
|
-
|
|
19751
|
-
|
|
19752
|
-
|
|
19753
|
-
|
|
19754
|
-
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
} catch {
|
|
19830
|
+
case "agent_message": {
|
|
19831
|
+
const content = update.content;
|
|
19832
|
+
if (content) {
|
|
19833
|
+
if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
|
|
19834
|
+
const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
|
|
19835
|
+
lastBlock.text += content.text;
|
|
19836
|
+
} else {
|
|
19837
|
+
currentAssistantContent.push(content);
|
|
19838
|
+
}
|
|
19839
|
+
}
|
|
19840
|
+
break;
|
|
19761
19841
|
}
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
|
|
19765
|
-
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
this.extractedFiles = filesToExtract;
|
|
19772
|
-
},
|
|
19773
|
-
rollback: async () => {
|
|
19774
|
-
for (const filePath of this.extractedFiles) {
|
|
19775
|
-
const fullPath = path15.join(baseDir, filePath);
|
|
19776
|
-
const backup = this.fileBackups.get(filePath);
|
|
19777
|
-
if (backup) {
|
|
19778
|
-
const dir = path15.dirname(fullPath);
|
|
19779
|
-
await fs12.mkdir(dir, { recursive: true }).catch(() => {
|
|
19780
|
-
});
|
|
19781
|
-
await fs12.writeFile(fullPath, backup).catch(() => {
|
|
19782
|
-
});
|
|
19783
|
-
} else {
|
|
19784
|
-
await fs12.rm(fullPath, { force: true }).catch(() => {
|
|
19785
|
-
});
|
|
19842
|
+
case "agent_message_chunk": {
|
|
19843
|
+
const content = update.content;
|
|
19844
|
+
if (content) {
|
|
19845
|
+
if (content.type === "text" && currentAssistantContent.length > 0 && currentAssistantContent[currentAssistantContent.length - 1].type === "text") {
|
|
19846
|
+
const lastBlock = currentAssistantContent[currentAssistantContent.length - 1];
|
|
19847
|
+
lastBlock.text += content.text;
|
|
19848
|
+
} else {
|
|
19849
|
+
currentAssistantContent.push(content);
|
|
19850
|
+
}
|
|
19786
19851
|
}
|
|
19852
|
+
break;
|
|
19787
19853
|
}
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
19812
|
-
}
|
|
19854
|
+
case "tool_call":
|
|
19855
|
+
case "tool_call_update": {
|
|
19856
|
+
const meta = update._meta?.claudeCode;
|
|
19857
|
+
if (meta) {
|
|
19858
|
+
const toolCallId = meta.toolCallId;
|
|
19859
|
+
const toolName = meta.toolName;
|
|
19860
|
+
const toolInput = meta.toolInput;
|
|
19861
|
+
const toolResponse = meta.toolResponse;
|
|
19862
|
+
if (toolCallId && toolName) {
|
|
19863
|
+
let toolCall = currentToolCalls.find(
|
|
19864
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19865
|
+
);
|
|
19866
|
+
if (!toolCall) {
|
|
19867
|
+
toolCall = {
|
|
19868
|
+
toolCallId,
|
|
19869
|
+
toolName,
|
|
19870
|
+
input: toolInput
|
|
19871
|
+
};
|
|
19872
|
+
currentToolCalls.push(toolCall);
|
|
19873
|
+
}
|
|
19874
|
+
if (toolResponse !== void 0) {
|
|
19875
|
+
toolCall.result = toolResponse;
|
|
19876
|
+
}
|
|
19877
|
+
}
|
|
19878
|
+
}
|
|
19879
|
+
break;
|
|
19880
|
+
}
|
|
19881
|
+
case "tool_result": {
|
|
19882
|
+
const meta = update._meta?.claudeCode;
|
|
19883
|
+
if (meta) {
|
|
19884
|
+
const toolCallId = meta.toolCallId;
|
|
19885
|
+
const toolResponse = meta.toolResponse;
|
|
19886
|
+
if (toolCallId) {
|
|
19887
|
+
const toolCall = currentToolCalls.find(
|
|
19888
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19889
|
+
);
|
|
19890
|
+
if (toolCall && toolResponse !== void 0) {
|
|
19891
|
+
toolCall.result = toolResponse;
|
|
19892
|
+
}
|
|
19893
|
+
}
|
|
19894
|
+
}
|
|
19895
|
+
break;
|
|
19813
19896
|
}
|
|
19814
19897
|
}
|
|
19898
|
+
}
|
|
19899
|
+
}
|
|
19900
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19901
|
+
turns.push({
|
|
19902
|
+
role: "assistant",
|
|
19903
|
+
content: currentAssistantContent,
|
|
19904
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19815
19905
|
});
|
|
19816
19906
|
}
|
|
19817
|
-
|
|
19818
|
-
this.log.info("Tree applied", {
|
|
19819
|
-
treeHash,
|
|
19820
|
-
totalChanges: changes.length,
|
|
19821
|
-
deletedFiles: deletedCount,
|
|
19822
|
-
checkoutPerformed
|
|
19823
|
-
});
|
|
19824
|
-
return { treeHash, checkoutPerformed };
|
|
19907
|
+
return turns;
|
|
19825
19908
|
}
|
|
19826
19909
|
};
|
|
19827
19910
|
|
|
19828
|
-
// src/
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
await this.step({
|
|
19848
|
-
name: "download_archive",
|
|
19849
|
-
execute: async () => {
|
|
19850
|
-
const arrayBuffer = await apiClient.downloadArtifact(
|
|
19851
|
-
taskId,
|
|
19852
|
-
runId,
|
|
19853
|
-
archiveUrl
|
|
19854
|
-
);
|
|
19855
|
-
if (!arrayBuffer) {
|
|
19856
|
-
throw new Error("Failed to download archive");
|
|
19857
|
-
}
|
|
19858
|
-
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19859
|
-
const binaryContent = Buffer.from(base64Content, "base64");
|
|
19860
|
-
await writeFile5(archivePath, binaryContent);
|
|
19861
|
-
this.log.info("Tree archive downloaded", {
|
|
19862
|
-
treeHash: snapshot.treeHash,
|
|
19863
|
-
snapshotBytes: binaryContent.byteLength,
|
|
19864
|
-
snapshotWireBytes: arrayBuffer.byteLength,
|
|
19865
|
-
totalBytes: binaryContent.byteLength,
|
|
19866
|
-
totalWireBytes: arrayBuffer.byteLength
|
|
19867
|
-
});
|
|
19868
|
-
},
|
|
19869
|
-
rollback: async () => {
|
|
19870
|
-
if (this.archivePath) {
|
|
19871
|
-
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
19872
|
-
});
|
|
19873
|
-
}
|
|
19874
|
-
}
|
|
19875
|
-
});
|
|
19876
|
-
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
19877
|
-
const applyResult = await gitApplySaga.run({
|
|
19878
|
-
baseDir: repositoryPath,
|
|
19879
|
-
treeHash: snapshot.treeHash,
|
|
19880
|
-
baseCommit: snapshot.baseCommit,
|
|
19881
|
-
changes: snapshot.changes,
|
|
19882
|
-
archivePath: this.archivePath
|
|
19911
|
+
// src/resume.ts
|
|
19912
|
+
async function resumeFromLog(config) {
|
|
19913
|
+
const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
19914
|
+
logger.info("Resuming from log", {
|
|
19915
|
+
taskId: config.taskId,
|
|
19916
|
+
runId: config.runId
|
|
19917
|
+
});
|
|
19918
|
+
const saga = new ResumeSaga(logger);
|
|
19919
|
+
const result = await saga.run({
|
|
19920
|
+
taskId: config.taskId,
|
|
19921
|
+
runId: config.runId,
|
|
19922
|
+
repositoryPath: config.repositoryPath,
|
|
19923
|
+
apiClient: config.apiClient,
|
|
19924
|
+
logger
|
|
19925
|
+
});
|
|
19926
|
+
if (!result.success) {
|
|
19927
|
+
logger.error("Failed to resume from log", {
|
|
19928
|
+
error: result.error,
|
|
19929
|
+
failedStep: result.failedStep
|
|
19883
19930
|
});
|
|
19884
|
-
|
|
19885
|
-
|
|
19931
|
+
throw new Error(
|
|
19932
|
+
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
19933
|
+
);
|
|
19934
|
+
}
|
|
19935
|
+
return {
|
|
19936
|
+
conversation: result.data.conversation,
|
|
19937
|
+
latestSnapshot: result.data.latestSnapshot,
|
|
19938
|
+
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
19939
|
+
interrupted: result.data.interrupted,
|
|
19940
|
+
lastDevice: result.data.lastDevice,
|
|
19941
|
+
logEntryCount: result.data.logEntryCount
|
|
19942
|
+
};
|
|
19943
|
+
}
|
|
19944
|
+
var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
|
|
19945
|
+
var TOOL_RESULT_MAX_CHARS = 2e3;
|
|
19946
|
+
var RESUME_CONTEXT_MARKERS = [
|
|
19947
|
+
"You are resuming a previous conversation",
|
|
19948
|
+
"Here is the conversation history from the",
|
|
19949
|
+
"Continue from where you left off"
|
|
19950
|
+
];
|
|
19951
|
+
function isResumeContextTurn(turn) {
|
|
19952
|
+
if (turn.role !== "user") return false;
|
|
19953
|
+
const text2 = turn.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
19954
|
+
return RESUME_CONTEXT_MARKERS.some((marker) => text2.includes(marker));
|
|
19955
|
+
}
|
|
19956
|
+
function formatConversationForResume(conversation) {
|
|
19957
|
+
const filtered = conversation.filter((turn) => !isResumeContextTurn(turn));
|
|
19958
|
+
const selected = selectRecentTurns(filtered, RESUME_HISTORY_TOKEN_BUDGET);
|
|
19959
|
+
const parts2 = [];
|
|
19960
|
+
if (selected.length < filtered.length) {
|
|
19961
|
+
parts2.push(
|
|
19962
|
+
`*(${filtered.length - selected.length} earlier turns omitted)*`
|
|
19963
|
+
);
|
|
19964
|
+
}
|
|
19965
|
+
for (const turn of selected) {
|
|
19966
|
+
const role = turn.role === "user" ? "User" : "Assistant";
|
|
19967
|
+
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
19968
|
+
if (textParts.length > 0) {
|
|
19969
|
+
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
19970
|
+
}
|
|
19971
|
+
if (turn.toolCalls?.length) {
|
|
19972
|
+
const toolSummary = turn.toolCalls.map((tc) => {
|
|
19973
|
+
let resultStr = "";
|
|
19974
|
+
if (tc.result !== void 0) {
|
|
19975
|
+
const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
|
|
19976
|
+
resultStr = raw.length > TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
|
|
19977
|
+
}
|
|
19978
|
+
return ` - ${tc.toolName}${resultStr}`;
|
|
19979
|
+
}).join("\n");
|
|
19980
|
+
parts2.push(`**${role} (tools)**:
|
|
19981
|
+
${toolSummary}`);
|
|
19886
19982
|
}
|
|
19887
|
-
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
19888
|
-
});
|
|
19889
|
-
this.log.info("Tree snapshot applied", {
|
|
19890
|
-
treeHash: snapshot.treeHash,
|
|
19891
|
-
totalChanges: snapshot.changes.length,
|
|
19892
|
-
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
19893
|
-
});
|
|
19894
|
-
return { treeHash: snapshot.treeHash };
|
|
19895
19983
|
}
|
|
19896
|
-
|
|
19984
|
+
return parts2.join("\n\n");
|
|
19985
|
+
}
|
|
19897
19986
|
|
|
19898
|
-
// src/
|
|
19899
|
-
import
|
|
19900
|
-
import
|
|
19901
|
-
import
|
|
19902
|
-
var
|
|
19903
|
-
|
|
19904
|
-
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
19908
|
-
|
|
19909
|
-
|
|
19910
|
-
|
|
19911
|
-
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
if (!captureResult.success) {
|
|
19928
|
-
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
19987
|
+
// src/session-log-writer.ts
|
|
19988
|
+
import fs12 from "fs";
|
|
19989
|
+
import fsp from "fs/promises";
|
|
19990
|
+
import path15 from "path";
|
|
19991
|
+
var SessionLogWriter = class _SessionLogWriter {
|
|
19992
|
+
static FLUSH_DEBOUNCE_MS = 500;
|
|
19993
|
+
static FLUSH_MAX_INTERVAL_MS = 5e3;
|
|
19994
|
+
static MAX_FLUSH_RETRIES = 10;
|
|
19995
|
+
static MAX_RETRY_DELAY_MS = 3e4;
|
|
19996
|
+
static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
19997
|
+
posthogAPI;
|
|
19998
|
+
pendingEntries = /* @__PURE__ */ new Map();
|
|
19999
|
+
flushTimeouts = /* @__PURE__ */ new Map();
|
|
20000
|
+
lastFlushAttemptTime = /* @__PURE__ */ new Map();
|
|
20001
|
+
retryCounts = /* @__PURE__ */ new Map();
|
|
20002
|
+
sessions = /* @__PURE__ */ new Map();
|
|
20003
|
+
flushQueues = /* @__PURE__ */ new Map();
|
|
20004
|
+
logger;
|
|
20005
|
+
localCachePath;
|
|
20006
|
+
constructor(options = {}) {
|
|
20007
|
+
this.posthogAPI = options.posthogAPI;
|
|
20008
|
+
this.localCachePath = options.localCachePath;
|
|
20009
|
+
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
20010
|
+
}
|
|
20011
|
+
async flushAll() {
|
|
20012
|
+
const flushPromises = [];
|
|
20013
|
+
for (const [sessionId, session] of this.sessions) {
|
|
20014
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
20015
|
+
flushPromises.push(this.flush(sessionId));
|
|
19929
20016
|
}
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
|
|
19935
|
-
if (!changed || !gitSnapshot) {
|
|
19936
|
-
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
19937
|
-
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
20017
|
+
await Promise.all(flushPromises);
|
|
20018
|
+
}
|
|
20019
|
+
register(sessionId, context) {
|
|
20020
|
+
if (this.sessions.has(sessionId)) {
|
|
20021
|
+
return;
|
|
19938
20022
|
}
|
|
19939
|
-
|
|
19940
|
-
|
|
20023
|
+
this.sessions.set(sessionId, { context, currentTurnMessages: [] });
|
|
20024
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20025
|
+
if (this.localCachePath) {
|
|
20026
|
+
const sessionDir = path15.join(
|
|
20027
|
+
this.localCachePath,
|
|
20028
|
+
"sessions",
|
|
20029
|
+
context.runId
|
|
20030
|
+
);
|
|
19941
20031
|
try {
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
runId
|
|
19948
|
-
);
|
|
19949
|
-
} finally {
|
|
19950
|
-
await rm7(createdArchivePath, { force: true }).catch(() => {
|
|
20032
|
+
fs12.mkdirSync(sessionDir, { recursive: true });
|
|
20033
|
+
} catch (error) {
|
|
20034
|
+
this.logger.warn("Failed to create local cache directory", {
|
|
20035
|
+
sessionDir,
|
|
20036
|
+
error
|
|
19951
20037
|
});
|
|
19952
20038
|
}
|
|
19953
20039
|
}
|
|
19954
|
-
const snapshot = {
|
|
19955
|
-
treeHash: gitSnapshot.treeHash,
|
|
19956
|
-
baseCommit: gitSnapshot.baseCommit,
|
|
19957
|
-
changes: gitSnapshot.changes,
|
|
19958
|
-
timestamp: gitSnapshot.timestamp,
|
|
19959
|
-
interrupted,
|
|
19960
|
-
archiveUrl
|
|
19961
|
-
};
|
|
19962
|
-
this.log.info("Tree captured", {
|
|
19963
|
-
treeHash: snapshot.treeHash,
|
|
19964
|
-
changes: snapshot.changes.length,
|
|
19965
|
-
interrupted,
|
|
19966
|
-
archiveUrl
|
|
19967
|
-
});
|
|
19968
|
-
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
19969
20040
|
}
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
20041
|
+
isRegistered(sessionId) {
|
|
20042
|
+
return this.sessions.has(sessionId);
|
|
20043
|
+
}
|
|
20044
|
+
appendRawLine(sessionId, line) {
|
|
20045
|
+
const session = this.sessions.get(sessionId);
|
|
20046
|
+
if (!session) {
|
|
20047
|
+
this.logger.warn("appendRawLine called for unregistered session", {
|
|
20048
|
+
sessionId
|
|
20049
|
+
});
|
|
20050
|
+
return;
|
|
20051
|
+
}
|
|
20052
|
+
try {
|
|
20053
|
+
const message = JSON.parse(line);
|
|
20054
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
20055
|
+
if (this.isAgentMessageChunk(message)) {
|
|
20056
|
+
const text2 = this.extractChunkText(message);
|
|
20057
|
+
if (text2) {
|
|
20058
|
+
if (!session.chunkBuffer) {
|
|
20059
|
+
session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
|
|
20060
|
+
} else {
|
|
20061
|
+
session.chunkBuffer.text += text2;
|
|
19984
20062
|
}
|
|
19985
|
-
]);
|
|
19986
|
-
const uploadedArtifact = artifacts[0];
|
|
19987
|
-
if (uploadedArtifact?.storage_path) {
|
|
19988
|
-
this.log.info("Tree archive uploaded", {
|
|
19989
|
-
storagePath: uploadedArtifact.storage_path,
|
|
19990
|
-
treeHash,
|
|
19991
|
-
snapshotBytes,
|
|
19992
|
-
snapshotWireBytes,
|
|
19993
|
-
totalBytes: snapshotBytes,
|
|
19994
|
-
totalWireBytes: snapshotWireBytes
|
|
19995
|
-
});
|
|
19996
|
-
return uploadedArtifact.storage_path;
|
|
19997
20063
|
}
|
|
19998
|
-
return
|
|
19999
|
-
},
|
|
20000
|
-
rollback: async () => {
|
|
20001
|
-
await rm7(archivePath, { force: true }).catch(() => {
|
|
20002
|
-
});
|
|
20064
|
+
return;
|
|
20003
20065
|
}
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
const result = await saga.run({
|
|
20032
|
-
repositoryPath: this.repositoryPath,
|
|
20033
|
-
taskId: this.taskId,
|
|
20034
|
-
runId: this.runId,
|
|
20035
|
-
apiClient: this.apiClient,
|
|
20036
|
-
lastTreeHash: this.lastTreeHash,
|
|
20037
|
-
interrupted: options?.interrupted
|
|
20038
|
-
});
|
|
20039
|
-
if (!result.success) {
|
|
20040
|
-
this.logger.error("Failed to capture tree", {
|
|
20041
|
-
error: result.error,
|
|
20042
|
-
failedStep: result.failedStep
|
|
20066
|
+
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
20067
|
+
session.chunkBuffer = void 0;
|
|
20068
|
+
} else {
|
|
20069
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
20070
|
+
}
|
|
20071
|
+
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
20072
|
+
if (nonChunkAgentText) {
|
|
20073
|
+
session.lastAgentMessage = nonChunkAgentText;
|
|
20074
|
+
session.currentTurnMessages.push(nonChunkAgentText);
|
|
20075
|
+
}
|
|
20076
|
+
const entry = {
|
|
20077
|
+
type: "notification",
|
|
20078
|
+
timestamp,
|
|
20079
|
+
notification: message
|
|
20080
|
+
};
|
|
20081
|
+
this.writeToLocalCache(sessionId, entry);
|
|
20082
|
+
if (this.posthogAPI) {
|
|
20083
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20084
|
+
pending.push(entry);
|
|
20085
|
+
this.pendingEntries.set(sessionId, pending);
|
|
20086
|
+
this.scheduleFlush(sessionId);
|
|
20087
|
+
}
|
|
20088
|
+
} catch {
|
|
20089
|
+
this.logger.warn("Failed to parse raw line for persistence", {
|
|
20090
|
+
taskId: session.context.taskId,
|
|
20091
|
+
runId: session.context.runId,
|
|
20092
|
+
lineLength: line.length
|
|
20043
20093
|
});
|
|
20044
|
-
throw new Error(
|
|
20045
|
-
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
20046
|
-
);
|
|
20047
20094
|
}
|
|
20048
|
-
|
|
20049
|
-
|
|
20095
|
+
}
|
|
20096
|
+
async flush(sessionId, { coalesce = false } = {}) {
|
|
20097
|
+
if (coalesce) {
|
|
20098
|
+
const session = this.sessions.get(sessionId);
|
|
20099
|
+
if (session) {
|
|
20100
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
20101
|
+
}
|
|
20050
20102
|
}
|
|
20051
|
-
|
|
20103
|
+
const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
|
|
20104
|
+
const next = prev.catch(() => {
|
|
20105
|
+
}).then(() => this._doFlush(sessionId));
|
|
20106
|
+
this.flushQueues.set(sessionId, next);
|
|
20107
|
+
next.finally(() => {
|
|
20108
|
+
if (this.flushQueues.get(sessionId) === next) {
|
|
20109
|
+
this.flushQueues.delete(sessionId);
|
|
20110
|
+
}
|
|
20111
|
+
});
|
|
20112
|
+
return next;
|
|
20052
20113
|
}
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
if (!this.apiClient) {
|
|
20059
|
-
throw new Error("Cannot apply snapshot: API client not configured");
|
|
20114
|
+
async _doFlush(sessionId) {
|
|
20115
|
+
const session = this.sessions.get(sessionId);
|
|
20116
|
+
if (!session) {
|
|
20117
|
+
this.logger.warn("flush: no session found", { sessionId });
|
|
20118
|
+
return;
|
|
20060
20119
|
}
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
changes: snapshot.changes.length
|
|
20065
|
-
});
|
|
20066
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20120
|
+
const pending = this.pendingEntries.get(sessionId);
|
|
20121
|
+
if (!this.posthogAPI || !pending?.length) {
|
|
20122
|
+
return;
|
|
20067
20123
|
}
|
|
20068
|
-
|
|
20069
|
-
const
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
20074
|
-
|
|
20075
|
-
|
|
20076
|
-
|
|
20077
|
-
|
|
20078
|
-
|
|
20079
|
-
|
|
20080
|
-
treeHash: snapshot.treeHash
|
|
20081
|
-
});
|
|
20082
|
-
throw new Error(
|
|
20083
|
-
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
20124
|
+
this.pendingEntries.delete(sessionId);
|
|
20125
|
+
const timeout = this.flushTimeouts.get(sessionId);
|
|
20126
|
+
if (timeout) {
|
|
20127
|
+
clearTimeout(timeout);
|
|
20128
|
+
this.flushTimeouts.delete(sessionId);
|
|
20129
|
+
}
|
|
20130
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20131
|
+
try {
|
|
20132
|
+
await this.posthogAPI.appendTaskRunLog(
|
|
20133
|
+
session.context.taskId,
|
|
20134
|
+
session.context.runId,
|
|
20135
|
+
pending
|
|
20084
20136
|
);
|
|
20137
|
+
this.retryCounts.set(sessionId, 0);
|
|
20138
|
+
} catch (error) {
|
|
20139
|
+
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
20140
|
+
this.retryCounts.set(sessionId, retryCount);
|
|
20141
|
+
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
20142
|
+
this.logger.error(
|
|
20143
|
+
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
20144
|
+
{
|
|
20145
|
+
taskId: session.context.taskId,
|
|
20146
|
+
runId: session.context.runId,
|
|
20147
|
+
error
|
|
20148
|
+
}
|
|
20149
|
+
);
|
|
20150
|
+
this.retryCounts.set(sessionId, 0);
|
|
20151
|
+
} else {
|
|
20152
|
+
if (retryCount === 1) {
|
|
20153
|
+
this.logger.warn(
|
|
20154
|
+
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
20155
|
+
{
|
|
20156
|
+
taskId: session.context.taskId,
|
|
20157
|
+
runId: session.context.runId,
|
|
20158
|
+
error: error instanceof Error ? error.message : String(error)
|
|
20159
|
+
}
|
|
20160
|
+
);
|
|
20161
|
+
}
|
|
20162
|
+
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
20163
|
+
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
20164
|
+
this.scheduleFlush(sessionId);
|
|
20165
|
+
}
|
|
20085
20166
|
}
|
|
20086
|
-
this.lastTreeHash = result.data.treeHash;
|
|
20087
20167
|
}
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
return
|
|
20168
|
+
getSessionUpdateType(message) {
|
|
20169
|
+
if (message.method !== "session/update") return void 0;
|
|
20170
|
+
const params = message.params;
|
|
20171
|
+
const update = params?.update;
|
|
20172
|
+
return update?.sessionUpdate;
|
|
20093
20173
|
}
|
|
20094
|
-
|
|
20095
|
-
|
|
20096
|
-
*/
|
|
20097
|
-
setLastTreeHash(hash) {
|
|
20098
|
-
this.lastTreeHash = hash;
|
|
20174
|
+
isDirectAgentMessage(message) {
|
|
20175
|
+
return this.getSessionUpdateType(message) === "agent_message";
|
|
20099
20176
|
}
|
|
20100
|
-
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
const
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
"fetch_task_run",
|
|
20110
|
-
() => apiClient.getTaskRun(taskId, runId)
|
|
20111
|
-
);
|
|
20112
|
-
if (!taskRun.log_url) {
|
|
20113
|
-
this.log.info("No log URL found, starting fresh");
|
|
20114
|
-
return this.emptyResult();
|
|
20177
|
+
isAgentMessageChunk(message) {
|
|
20178
|
+
return this.getSessionUpdateType(message) === "agent_message_chunk";
|
|
20179
|
+
}
|
|
20180
|
+
extractChunkText(message) {
|
|
20181
|
+
const params = message.params;
|
|
20182
|
+
const update = params?.update;
|
|
20183
|
+
const content = update?.content;
|
|
20184
|
+
if (content?.type === "text" && content.text) {
|
|
20185
|
+
return content.text;
|
|
20115
20186
|
}
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20119
|
-
);
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20187
|
+
return "";
|
|
20188
|
+
}
|
|
20189
|
+
emitCoalescedMessage(sessionId, session) {
|
|
20190
|
+
if (!session.chunkBuffer) return;
|
|
20191
|
+
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
20192
|
+
session.chunkBuffer = void 0;
|
|
20193
|
+
session.lastAgentMessage = text2;
|
|
20194
|
+
session.currentTurnMessages.push(text2);
|
|
20195
|
+
const entry = {
|
|
20196
|
+
type: "notification",
|
|
20197
|
+
timestamp: firstTimestamp,
|
|
20198
|
+
notification: {
|
|
20199
|
+
jsonrpc: "2.0",
|
|
20200
|
+
method: "session/update",
|
|
20201
|
+
params: {
|
|
20202
|
+
update: {
|
|
20203
|
+
sessionUpdate: "agent_message",
|
|
20204
|
+
content: { type: "text", text: text2 }
|
|
20205
|
+
}
|
|
20206
|
+
}
|
|
20207
|
+
}
|
|
20208
|
+
};
|
|
20209
|
+
this.writeToLocalCache(sessionId, entry);
|
|
20210
|
+
if (this.posthogAPI) {
|
|
20211
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20212
|
+
pending.push(entry);
|
|
20213
|
+
this.pendingEntries.set(sessionId, pending);
|
|
20214
|
+
this.scheduleFlush(sessionId);
|
|
20123
20215
|
}
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
20129
|
-
const
|
|
20130
|
-
|
|
20131
|
-
|
|
20132
|
-
|
|
20133
|
-
|
|
20134
|
-
if (latestSnapshot?.archiveUrl && repositoryPath) {
|
|
20135
|
-
this.log.info("Found tree snapshot", {
|
|
20136
|
-
treeHash: latestSnapshot.treeHash,
|
|
20137
|
-
hasArchiveUrl: true,
|
|
20138
|
-
changes: latestSnapshot.changes?.length ?? 0,
|
|
20139
|
-
interrupted: latestSnapshot.interrupted
|
|
20140
|
-
});
|
|
20141
|
-
await this.step({
|
|
20142
|
-
name: "apply_snapshot",
|
|
20143
|
-
execute: async () => {
|
|
20144
|
-
const treeTracker = new TreeTracker({
|
|
20145
|
-
repositoryPath,
|
|
20146
|
-
taskId,
|
|
20147
|
-
runId,
|
|
20148
|
-
apiClient,
|
|
20149
|
-
logger: logger.child("TreeTracker")
|
|
20150
|
-
});
|
|
20151
|
-
try {
|
|
20152
|
-
await treeTracker.applyTreeSnapshot(latestSnapshot);
|
|
20153
|
-
treeTracker.setLastTreeHash(latestSnapshot.treeHash);
|
|
20154
|
-
snapshotApplied = true;
|
|
20155
|
-
this.log.info("Tree snapshot applied successfully", {
|
|
20156
|
-
treeHash: latestSnapshot.treeHash
|
|
20157
|
-
});
|
|
20158
|
-
} catch (error) {
|
|
20159
|
-
this.log.warn(
|
|
20160
|
-
"Failed to apply tree snapshot, continuing without it",
|
|
20161
|
-
{
|
|
20162
|
-
error: error instanceof Error ? error.message : String(error),
|
|
20163
|
-
treeHash: latestSnapshot.treeHash
|
|
20164
|
-
}
|
|
20165
|
-
);
|
|
20166
|
-
}
|
|
20167
|
-
},
|
|
20168
|
-
rollback: async () => {
|
|
20169
|
-
}
|
|
20170
|
-
});
|
|
20171
|
-
} else if (latestSnapshot?.archiveUrl && !repositoryPath) {
|
|
20172
|
-
this.log.warn(
|
|
20173
|
-
"Snapshot found but no repositoryPath configured - files cannot be restored",
|
|
20216
|
+
}
|
|
20217
|
+
getLastAgentMessage(sessionId) {
|
|
20218
|
+
return this.sessions.get(sessionId)?.lastAgentMessage;
|
|
20219
|
+
}
|
|
20220
|
+
getFullAgentResponse(sessionId) {
|
|
20221
|
+
const session = this.sessions.get(sessionId);
|
|
20222
|
+
if (!session || session.currentTurnMessages.length === 0) return void 0;
|
|
20223
|
+
if (session.chunkBuffer) {
|
|
20224
|
+
this.logger.warn(
|
|
20225
|
+
"getFullAgentResponse called with non-empty chunk buffer",
|
|
20174
20226
|
{
|
|
20175
|
-
|
|
20176
|
-
|
|
20227
|
+
sessionId,
|
|
20228
|
+
bufferedLength: session.chunkBuffer.text.length
|
|
20177
20229
|
}
|
|
20178
20230
|
);
|
|
20179
|
-
}
|
|
20180
|
-
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
|
|
20184
|
-
|
|
20185
|
-
|
|
20231
|
+
}
|
|
20232
|
+
return session.currentTurnMessages.join("\n\n");
|
|
20233
|
+
}
|
|
20234
|
+
resetTurnMessages(sessionId) {
|
|
20235
|
+
const session = this.sessions.get(sessionId);
|
|
20236
|
+
if (session) {
|
|
20237
|
+
session.currentTurnMessages = [];
|
|
20238
|
+
}
|
|
20239
|
+
}
|
|
20240
|
+
extractAgentMessageText(message) {
|
|
20241
|
+
if (message.method !== "session/update") {
|
|
20242
|
+
return null;
|
|
20243
|
+
}
|
|
20244
|
+
const params = message.params;
|
|
20245
|
+
const update = params?.update;
|
|
20246
|
+
if (update?.sessionUpdate !== "agent_message") {
|
|
20247
|
+
return null;
|
|
20248
|
+
}
|
|
20249
|
+
const content = update.content;
|
|
20250
|
+
if (content?.type === "text" && typeof content.text === "string") {
|
|
20251
|
+
const trimmed2 = content.text.trim();
|
|
20252
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20253
|
+
}
|
|
20254
|
+
if (typeof update.message === "string") {
|
|
20255
|
+
const trimmed2 = update.message.trim();
|
|
20256
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20257
|
+
}
|
|
20258
|
+
return null;
|
|
20259
|
+
}
|
|
20260
|
+
scheduleFlush(sessionId) {
|
|
20261
|
+
const existing = this.flushTimeouts.get(sessionId);
|
|
20262
|
+
if (existing) clearTimeout(existing);
|
|
20263
|
+
const retryCount = this.retryCounts.get(sessionId) ?? 0;
|
|
20264
|
+
const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
|
|
20265
|
+
const elapsed = Date.now() - lastAttempt;
|
|
20266
|
+
let delay3;
|
|
20267
|
+
if (retryCount > 0) {
|
|
20268
|
+
delay3 = Math.min(
|
|
20269
|
+
_SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
|
|
20270
|
+
_SessionLogWriter.MAX_RETRY_DELAY_MS
|
|
20186
20271
|
);
|
|
20272
|
+
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20273
|
+
delay3 = 0;
|
|
20274
|
+
} else {
|
|
20275
|
+
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
20187
20276
|
}
|
|
20188
|
-
const
|
|
20189
|
-
|
|
20190
|
-
() => Promise.resolve(this.rebuildConversation(entries))
|
|
20191
|
-
);
|
|
20192
|
-
const lastDevice = await this.readOnlyStep(
|
|
20193
|
-
"find_device",
|
|
20194
|
-
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
20195
|
-
);
|
|
20196
|
-
this.log.info("Resume state rebuilt", {
|
|
20197
|
-
turns: conversation.length,
|
|
20198
|
-
hasSnapshot: !!latestSnapshot,
|
|
20199
|
-
snapshotApplied,
|
|
20200
|
-
interrupted: latestSnapshot?.interrupted ?? false
|
|
20201
|
-
});
|
|
20202
|
-
return {
|
|
20203
|
-
conversation,
|
|
20204
|
-
latestSnapshot,
|
|
20205
|
-
latestGitCheckpoint,
|
|
20206
|
-
snapshotApplied,
|
|
20207
|
-
interrupted: latestSnapshot?.interrupted ?? false,
|
|
20208
|
-
lastDevice,
|
|
20209
|
-
logEntryCount: entries.length
|
|
20210
|
-
};
|
|
20277
|
+
const timeout = setTimeout(() => this.flush(sessionId), delay3);
|
|
20278
|
+
this.flushTimeouts.set(sessionId, timeout);
|
|
20211
20279
|
}
|
|
20212
|
-
|
|
20213
|
-
return
|
|
20214
|
-
|
|
20215
|
-
|
|
20216
|
-
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
|
|
20280
|
+
writeToLocalCache(sessionId, entry) {
|
|
20281
|
+
if (!this.localCachePath) return;
|
|
20282
|
+
const session = this.sessions.get(sessionId);
|
|
20283
|
+
if (!session) return;
|
|
20284
|
+
const logPath = path15.join(
|
|
20285
|
+
this.localCachePath,
|
|
20286
|
+
"sessions",
|
|
20287
|
+
session.context.runId,
|
|
20288
|
+
"logs.ndjson"
|
|
20289
|
+
);
|
|
20290
|
+
try {
|
|
20291
|
+
fs12.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
20292
|
+
`);
|
|
20293
|
+
} catch (error) {
|
|
20294
|
+
this.logger.warn("Failed to write to local cache", {
|
|
20295
|
+
taskId: session.context.taskId,
|
|
20296
|
+
runId: session.context.runId,
|
|
20297
|
+
logPath,
|
|
20298
|
+
error
|
|
20299
|
+
});
|
|
20300
|
+
}
|
|
20221
20301
|
}
|
|
20222
|
-
|
|
20223
|
-
|
|
20224
|
-
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
)
|
|
20229
|
-
const
|
|
20230
|
-
|
|
20231
|
-
|
|
20302
|
+
static async cleanupOldSessions(localCachePath) {
|
|
20303
|
+
const sessionsDir = path15.join(localCachePath, "sessions");
|
|
20304
|
+
let deleted = 0;
|
|
20305
|
+
try {
|
|
20306
|
+
const entries = await fsp.readdir(sessionsDir);
|
|
20307
|
+
const now = Date.now();
|
|
20308
|
+
for (const entry of entries) {
|
|
20309
|
+
const entryPath = path15.join(sessionsDir, entry);
|
|
20310
|
+
try {
|
|
20311
|
+
const stats = await fsp.stat(entryPath);
|
|
20312
|
+
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
20313
|
+
await fsp.rm(entryPath, { recursive: true, force: true });
|
|
20314
|
+
deleted++;
|
|
20315
|
+
}
|
|
20316
|
+
} catch {
|
|
20232
20317
|
}
|
|
20233
20318
|
}
|
|
20319
|
+
} catch {
|
|
20234
20320
|
}
|
|
20235
|
-
return
|
|
20321
|
+
return deleted;
|
|
20236
20322
|
}
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
|
|
20244
|
-
|
|
20245
|
-
|
|
20323
|
+
};
|
|
20324
|
+
|
|
20325
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20326
|
+
import { mkdir as mkdir7, rm as rm6, writeFile as writeFile5 } from "fs/promises";
|
|
20327
|
+
import { join as join12 } from "path";
|
|
20328
|
+
|
|
20329
|
+
// ../git/dist/sagas/tree.js
|
|
20330
|
+
import { existsSync as existsSync5 } from "fs";
|
|
20331
|
+
import * as fs13 from "fs/promises";
|
|
20332
|
+
import * as path16 from "path";
|
|
20333
|
+
import * as tar from "tar";
|
|
20334
|
+
var CaptureTreeSaga = class extends GitSaga {
|
|
20335
|
+
sagaName = "CaptureTreeSaga";
|
|
20336
|
+
tempIndexPath = null;
|
|
20337
|
+
async executeGitOperations(input) {
|
|
20338
|
+
const { baseDir, lastTreeHash, archivePath, signal } = input;
|
|
20339
|
+
const tmpDir = path16.join(baseDir, ".git", "posthog-code-tmp");
|
|
20340
|
+
await this.step({
|
|
20341
|
+
name: "create_tmp_dir",
|
|
20342
|
+
execute: () => fs13.mkdir(tmpDir, { recursive: true }),
|
|
20343
|
+
rollback: async () => {
|
|
20344
|
+
}
|
|
20345
|
+
});
|
|
20346
|
+
this.tempIndexPath = path16.join(tmpDir, `index-${Date.now()}`);
|
|
20347
|
+
const tempIndexGit = this.git.env({
|
|
20348
|
+
...process.env,
|
|
20349
|
+
GIT_INDEX_FILE: this.tempIndexPath
|
|
20350
|
+
});
|
|
20351
|
+
await this.step({
|
|
20352
|
+
name: "init_temp_index",
|
|
20353
|
+
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
20354
|
+
rollback: async () => {
|
|
20355
|
+
if (this.tempIndexPath) {
|
|
20356
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20357
|
+
});
|
|
20246
20358
|
}
|
|
20247
20359
|
}
|
|
20360
|
+
});
|
|
20361
|
+
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
20362
|
+
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
20363
|
+
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
20364
|
+
this.log.debug("No changes since last capture", { treeHash });
|
|
20365
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20366
|
+
});
|
|
20367
|
+
return { snapshot: null, changed: false };
|
|
20248
20368
|
}
|
|
20249
|
-
|
|
20369
|
+
const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
|
|
20370
|
+
try {
|
|
20371
|
+
return await getHeadSha(baseDir, { abortSignal: signal });
|
|
20372
|
+
} catch {
|
|
20373
|
+
return null;
|
|
20374
|
+
}
|
|
20375
|
+
});
|
|
20376
|
+
const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
|
|
20377
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20378
|
+
});
|
|
20379
|
+
const snapshot = {
|
|
20380
|
+
treeHash,
|
|
20381
|
+
baseCommit,
|
|
20382
|
+
changes,
|
|
20383
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20384
|
+
};
|
|
20385
|
+
let createdArchivePath;
|
|
20386
|
+
if (archivePath) {
|
|
20387
|
+
createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
|
|
20388
|
+
}
|
|
20389
|
+
this.log.info("Tree captured", {
|
|
20390
|
+
treeHash,
|
|
20391
|
+
changes: changes.length,
|
|
20392
|
+
archived: !!createdArchivePath
|
|
20393
|
+
});
|
|
20394
|
+
return { snapshot, archivePath: createdArchivePath, changed: true };
|
|
20250
20395
|
}
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
|
|
20256
|
-
|
|
20396
|
+
async createArchive(baseDir, archivePath, changes) {
|
|
20397
|
+
const filesToArchive = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20398
|
+
if (filesToArchive.length === 0) {
|
|
20399
|
+
return void 0;
|
|
20400
|
+
}
|
|
20401
|
+
const existingFiles = filesToArchive.filter((f) => existsSync5(path16.join(baseDir, f)));
|
|
20402
|
+
if (existingFiles.length === 0) {
|
|
20403
|
+
return void 0;
|
|
20404
|
+
}
|
|
20405
|
+
await this.step({
|
|
20406
|
+
name: "create_archive",
|
|
20407
|
+
execute: async () => {
|
|
20408
|
+
const archiveDir = path16.dirname(archivePath);
|
|
20409
|
+
await fs13.mkdir(archiveDir, { recursive: true });
|
|
20410
|
+
await tar.create({
|
|
20411
|
+
gzip: true,
|
|
20412
|
+
file: archivePath,
|
|
20413
|
+
cwd: baseDir
|
|
20414
|
+
}, existingFiles);
|
|
20415
|
+
},
|
|
20416
|
+
rollback: async () => {
|
|
20417
|
+
await fs13.rm(archivePath, { force: true }).catch(() => {
|
|
20418
|
+
});
|
|
20257
20419
|
}
|
|
20420
|
+
});
|
|
20421
|
+
return archivePath;
|
|
20422
|
+
}
|
|
20423
|
+
async getChanges(git, fromRef, toRef) {
|
|
20424
|
+
if (!fromRef) {
|
|
20425
|
+
const stdout2 = await git.raw(["ls-tree", "-r", "--name-only", toRef]);
|
|
20426
|
+
return stdout2.split("\n").filter((p) => p.trim()).map((p) => ({ path: p, status: "A" }));
|
|
20258
20427
|
}
|
|
20259
|
-
|
|
20428
|
+
const stdout = await git.raw([
|
|
20429
|
+
"diff-tree",
|
|
20430
|
+
"-r",
|
|
20431
|
+
"--name-status",
|
|
20432
|
+
fromRef,
|
|
20433
|
+
toRef
|
|
20434
|
+
]);
|
|
20435
|
+
const changes = [];
|
|
20436
|
+
for (const line of stdout.split("\n")) {
|
|
20437
|
+
if (!line.trim())
|
|
20438
|
+
continue;
|
|
20439
|
+
const [status, filePath] = line.split(" ");
|
|
20440
|
+
if (!filePath)
|
|
20441
|
+
continue;
|
|
20442
|
+
let normalizedStatus;
|
|
20443
|
+
if (status === "D") {
|
|
20444
|
+
normalizedStatus = "D";
|
|
20445
|
+
} else if (status === "A") {
|
|
20446
|
+
normalizedStatus = "A";
|
|
20447
|
+
} else {
|
|
20448
|
+
normalizedStatus = "M";
|
|
20449
|
+
}
|
|
20450
|
+
changes.push({ path: filePath, status: normalizedStatus });
|
|
20451
|
+
}
|
|
20452
|
+
return changes;
|
|
20260
20453
|
}
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
20268
|
-
|
|
20269
|
-
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20273
|
-
|
|
20274
|
-
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20281
|
-
|
|
20282
|
-
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20286
|
-
|
|
20287
|
-
|
|
20288
|
-
|
|
20289
|
-
|
|
20290
|
-
|
|
20291
|
-
|
|
20292
|
-
|
|
20293
|
-
|
|
20294
|
-
|
|
20295
|
-
|
|
20296
|
-
|
|
20297
|
-
|
|
20298
|
-
|
|
20299
|
-
|
|
20300
|
-
|
|
20301
|
-
|
|
20302
|
-
|
|
20303
|
-
|
|
20304
|
-
|
|
20305
|
-
|
|
20306
|
-
|
|
20307
|
-
|
|
20308
|
-
|
|
20309
|
-
|
|
20310
|
-
|
|
20311
|
-
|
|
20312
|
-
|
|
20313
|
-
break;
|
|
20314
|
-
}
|
|
20315
|
-
case "tool_call":
|
|
20316
|
-
case "tool_call_update": {
|
|
20317
|
-
const meta = update._meta?.claudeCode;
|
|
20318
|
-
if (meta) {
|
|
20319
|
-
const toolCallId = meta.toolCallId;
|
|
20320
|
-
const toolName = meta.toolName;
|
|
20321
|
-
const toolInput = meta.toolInput;
|
|
20322
|
-
const toolResponse = meta.toolResponse;
|
|
20323
|
-
if (toolCallId && toolName) {
|
|
20324
|
-
let toolCall = currentToolCalls.find(
|
|
20325
|
-
(tc) => tc.toolCallId === toolCallId
|
|
20326
|
-
);
|
|
20327
|
-
if (!toolCall) {
|
|
20328
|
-
toolCall = {
|
|
20329
|
-
toolCallId,
|
|
20330
|
-
toolName,
|
|
20331
|
-
input: toolInput
|
|
20332
|
-
};
|
|
20333
|
-
currentToolCalls.push(toolCall);
|
|
20334
|
-
}
|
|
20335
|
-
if (toolResponse !== void 0) {
|
|
20336
|
-
toolCall.result = toolResponse;
|
|
20337
|
-
}
|
|
20338
|
-
}
|
|
20454
|
+
};
|
|
20455
|
+
var ApplyTreeSaga = class extends GitSaga {
|
|
20456
|
+
sagaName = "ApplyTreeSaga";
|
|
20457
|
+
originalHead = null;
|
|
20458
|
+
originalBranch = null;
|
|
20459
|
+
extractedFiles = [];
|
|
20460
|
+
fileBackups = /* @__PURE__ */ new Map();
|
|
20461
|
+
async executeGitOperations(input) {
|
|
20462
|
+
const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
|
|
20463
|
+
const headInfo = await this.readOnlyStep("get_current_head", async () => {
|
|
20464
|
+
let head = null;
|
|
20465
|
+
let branch = null;
|
|
20466
|
+
try {
|
|
20467
|
+
head = await this.git.revparse(["HEAD"]);
|
|
20468
|
+
} catch {
|
|
20469
|
+
head = null;
|
|
20470
|
+
}
|
|
20471
|
+
try {
|
|
20472
|
+
branch = await this.git.raw(["symbolic-ref", "--short", "HEAD"]);
|
|
20473
|
+
} catch {
|
|
20474
|
+
branch = null;
|
|
20475
|
+
}
|
|
20476
|
+
return { head, branch };
|
|
20477
|
+
});
|
|
20478
|
+
this.originalHead = headInfo.head;
|
|
20479
|
+
this.originalBranch = headInfo.branch;
|
|
20480
|
+
let checkoutPerformed = false;
|
|
20481
|
+
if (baseCommit && baseCommit !== this.originalHead) {
|
|
20482
|
+
await this.readOnlyStep("check_working_tree", async () => {
|
|
20483
|
+
const status = await this.git.status();
|
|
20484
|
+
if (!status.isClean()) {
|
|
20485
|
+
const changedFiles = status.modified.length + status.staged.length + status.deleted.length;
|
|
20486
|
+
throw new Error(`Cannot apply tree: ${changedFiles} uncommitted change(s) exist. Commit or stash your changes first.`);
|
|
20487
|
+
}
|
|
20488
|
+
});
|
|
20489
|
+
await this.step({
|
|
20490
|
+
name: "checkout_base",
|
|
20491
|
+
execute: async () => {
|
|
20492
|
+
await this.git.checkout(baseCommit);
|
|
20493
|
+
checkoutPerformed = true;
|
|
20494
|
+
this.log.warn("Applied tree from different commit - now in detached HEAD state", {
|
|
20495
|
+
originalHead: this.originalHead,
|
|
20496
|
+
originalBranch: this.originalBranch,
|
|
20497
|
+
baseCommit
|
|
20498
|
+
});
|
|
20499
|
+
},
|
|
20500
|
+
rollback: async () => {
|
|
20501
|
+
try {
|
|
20502
|
+
if (this.originalBranch) {
|
|
20503
|
+
await this.git.checkout(this.originalBranch);
|
|
20504
|
+
} else if (this.originalHead) {
|
|
20505
|
+
await this.git.checkout(this.originalHead);
|
|
20339
20506
|
}
|
|
20340
|
-
|
|
20507
|
+
} catch (error) {
|
|
20508
|
+
this.log.warn("Failed to rollback checkout", { error });
|
|
20341
20509
|
}
|
|
20342
|
-
|
|
20343
|
-
|
|
20344
|
-
|
|
20345
|
-
|
|
20346
|
-
|
|
20347
|
-
|
|
20348
|
-
|
|
20349
|
-
|
|
20350
|
-
|
|
20351
|
-
|
|
20352
|
-
|
|
20353
|
-
|
|
20354
|
-
|
|
20510
|
+
}
|
|
20511
|
+
});
|
|
20512
|
+
}
|
|
20513
|
+
if (archivePath) {
|
|
20514
|
+
const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20515
|
+
await this.readOnlyStep("backup_existing_files", async () => {
|
|
20516
|
+
for (const filePath of filesToExtract) {
|
|
20517
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20518
|
+
try {
|
|
20519
|
+
const content = await fs13.readFile(fullPath);
|
|
20520
|
+
this.fileBackups.set(filePath, content);
|
|
20521
|
+
} catch {
|
|
20522
|
+
}
|
|
20523
|
+
}
|
|
20524
|
+
});
|
|
20525
|
+
await this.step({
|
|
20526
|
+
name: "extract_archive",
|
|
20527
|
+
execute: async () => {
|
|
20528
|
+
await tar.extract({
|
|
20529
|
+
file: archivePath,
|
|
20530
|
+
cwd: baseDir
|
|
20531
|
+
});
|
|
20532
|
+
this.extractedFiles = filesToExtract;
|
|
20533
|
+
},
|
|
20534
|
+
rollback: async () => {
|
|
20535
|
+
for (const filePath of this.extractedFiles) {
|
|
20536
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20537
|
+
const backup = this.fileBackups.get(filePath);
|
|
20538
|
+
if (backup) {
|
|
20539
|
+
const dir = path16.dirname(fullPath);
|
|
20540
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20541
|
+
});
|
|
20542
|
+
await fs13.writeFile(fullPath, backup).catch(() => {
|
|
20543
|
+
});
|
|
20544
|
+
} else {
|
|
20545
|
+
await fs13.rm(fullPath, { force: true }).catch(() => {
|
|
20546
|
+
});
|
|
20355
20547
|
}
|
|
20356
|
-
break;
|
|
20357
20548
|
}
|
|
20358
20549
|
}
|
|
20359
|
-
}
|
|
20550
|
+
});
|
|
20360
20551
|
}
|
|
20361
|
-
|
|
20362
|
-
|
|
20363
|
-
|
|
20364
|
-
|
|
20365
|
-
|
|
20552
|
+
for (const change of changes.filter((c) => c.status === "D")) {
|
|
20553
|
+
const fullPath = path16.join(baseDir, change.path);
|
|
20554
|
+
const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
|
|
20555
|
+
try {
|
|
20556
|
+
return await fs13.readFile(fullPath);
|
|
20557
|
+
} catch {
|
|
20558
|
+
return null;
|
|
20559
|
+
}
|
|
20560
|
+
});
|
|
20561
|
+
await this.step({
|
|
20562
|
+
name: `delete_${change.path}`,
|
|
20563
|
+
execute: async () => {
|
|
20564
|
+
await fs13.rm(fullPath, { force: true });
|
|
20565
|
+
this.log.debug(`Deleted file: ${change.path}`);
|
|
20566
|
+
},
|
|
20567
|
+
rollback: async () => {
|
|
20568
|
+
if (backupContent) {
|
|
20569
|
+
const dir = path16.dirname(fullPath);
|
|
20570
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20571
|
+
});
|
|
20572
|
+
await fs13.writeFile(fullPath, backupContent).catch(() => {
|
|
20573
|
+
});
|
|
20574
|
+
}
|
|
20575
|
+
}
|
|
20366
20576
|
});
|
|
20367
20577
|
}
|
|
20368
|
-
|
|
20578
|
+
const deletedCount = changes.filter((c) => c.status === "D").length;
|
|
20579
|
+
this.log.info("Tree applied", {
|
|
20580
|
+
treeHash,
|
|
20581
|
+
totalChanges: changes.length,
|
|
20582
|
+
deletedFiles: deletedCount,
|
|
20583
|
+
checkoutPerformed
|
|
20584
|
+
});
|
|
20585
|
+
return { treeHash, checkoutPerformed };
|
|
20369
20586
|
}
|
|
20370
20587
|
};
|
|
20371
20588
|
|
|
20372
|
-
// src/
|
|
20373
|
-
|
|
20374
|
-
|
|
20375
|
-
|
|
20376
|
-
|
|
20377
|
-
runId
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
|
|
20381
|
-
taskId: config.taskId,
|
|
20382
|
-
runId: config.runId,
|
|
20383
|
-
repositoryPath: config.repositoryPath,
|
|
20384
|
-
apiClient: config.apiClient,
|
|
20385
|
-
logger
|
|
20386
|
-
});
|
|
20387
|
-
if (!result.success) {
|
|
20388
|
-
logger.error("Failed to resume from log", {
|
|
20389
|
-
error: result.error,
|
|
20390
|
-
failedStep: result.failedStep
|
|
20391
|
-
});
|
|
20392
|
-
throw new Error(
|
|
20393
|
-
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
20394
|
-
);
|
|
20395
|
-
}
|
|
20396
|
-
return {
|
|
20397
|
-
conversation: result.data.conversation,
|
|
20398
|
-
latestSnapshot: result.data.latestSnapshot,
|
|
20399
|
-
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
20400
|
-
snapshotApplied: result.data.snapshotApplied,
|
|
20401
|
-
interrupted: result.data.interrupted,
|
|
20402
|
-
lastDevice: result.data.lastDevice,
|
|
20403
|
-
logEntryCount: result.data.logEntryCount
|
|
20404
|
-
};
|
|
20405
|
-
}
|
|
20406
|
-
var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
|
|
20407
|
-
var TOOL_RESULT_MAX_CHARS = 2e3;
|
|
20408
|
-
function formatConversationForResume(conversation) {
|
|
20409
|
-
const selected = selectRecentTurns(conversation, RESUME_HISTORY_TOKEN_BUDGET);
|
|
20410
|
-
const parts2 = [];
|
|
20411
|
-
if (selected.length < conversation.length) {
|
|
20412
|
-
parts2.push(
|
|
20413
|
-
`*(${conversation.length - selected.length} earlier turns omitted)*`
|
|
20414
|
-
);
|
|
20415
|
-
}
|
|
20416
|
-
for (const turn of selected) {
|
|
20417
|
-
const role = turn.role === "user" ? "User" : "Assistant";
|
|
20418
|
-
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
20419
|
-
if (textParts.length > 0) {
|
|
20420
|
-
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
20589
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20590
|
+
var ApplySnapshotSaga = class extends Saga {
|
|
20591
|
+
sagaName = "ApplySnapshotSaga";
|
|
20592
|
+
archivePath = null;
|
|
20593
|
+
async execute(input) {
|
|
20594
|
+
const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
|
|
20595
|
+
const tmpDir = join12(repositoryPath, ".posthog", "tmp");
|
|
20596
|
+
if (!snapshot.archiveUrl) {
|
|
20597
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20421
20598
|
}
|
|
20422
|
-
|
|
20423
|
-
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20427
|
-
|
|
20599
|
+
const archiveUrl = snapshot.archiveUrl;
|
|
20600
|
+
await this.step({
|
|
20601
|
+
name: "create_tmp_dir",
|
|
20602
|
+
execute: () => mkdir7(tmpDir, { recursive: true }),
|
|
20603
|
+
rollback: async () => {
|
|
20604
|
+
}
|
|
20605
|
+
});
|
|
20606
|
+
const archivePath = join12(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
20607
|
+
this.archivePath = archivePath;
|
|
20608
|
+
await this.step({
|
|
20609
|
+
name: "download_archive",
|
|
20610
|
+
execute: async () => {
|
|
20611
|
+
const arrayBuffer = await apiClient.downloadArtifact(
|
|
20612
|
+
taskId,
|
|
20613
|
+
runId,
|
|
20614
|
+
archiveUrl
|
|
20615
|
+
);
|
|
20616
|
+
if (!arrayBuffer) {
|
|
20617
|
+
throw new Error("Failed to download archive");
|
|
20428
20618
|
}
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
|
|
20435
|
-
|
|
20436
|
-
|
|
20437
|
-
|
|
20438
|
-
// src/session-log-writer.ts
|
|
20439
|
-
import fs13 from "fs";
|
|
20440
|
-
import fsp from "fs/promises";
|
|
20441
|
-
import path16 from "path";
|
|
20442
|
-
var SessionLogWriter = class _SessionLogWriter {
|
|
20443
|
-
static FLUSH_DEBOUNCE_MS = 500;
|
|
20444
|
-
static FLUSH_MAX_INTERVAL_MS = 5e3;
|
|
20445
|
-
static MAX_FLUSH_RETRIES = 10;
|
|
20446
|
-
static MAX_RETRY_DELAY_MS = 3e4;
|
|
20447
|
-
static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
20448
|
-
posthogAPI;
|
|
20449
|
-
pendingEntries = /* @__PURE__ */ new Map();
|
|
20450
|
-
flushTimeouts = /* @__PURE__ */ new Map();
|
|
20451
|
-
lastFlushAttemptTime = /* @__PURE__ */ new Map();
|
|
20452
|
-
retryCounts = /* @__PURE__ */ new Map();
|
|
20453
|
-
sessions = /* @__PURE__ */ new Map();
|
|
20454
|
-
flushQueues = /* @__PURE__ */ new Map();
|
|
20455
|
-
logger;
|
|
20456
|
-
localCachePath;
|
|
20457
|
-
constructor(options = {}) {
|
|
20458
|
-
this.posthogAPI = options.posthogAPI;
|
|
20459
|
-
this.localCachePath = options.localCachePath;
|
|
20460
|
-
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
20461
|
-
}
|
|
20462
|
-
async flushAll() {
|
|
20463
|
-
const flushPromises = [];
|
|
20464
|
-
for (const [sessionId, session] of this.sessions) {
|
|
20465
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
20466
|
-
flushPromises.push(this.flush(sessionId));
|
|
20467
|
-
}
|
|
20468
|
-
await Promise.all(flushPromises);
|
|
20469
|
-
}
|
|
20470
|
-
register(sessionId, context) {
|
|
20471
|
-
if (this.sessions.has(sessionId)) {
|
|
20472
|
-
return;
|
|
20473
|
-
}
|
|
20474
|
-
this.sessions.set(sessionId, { context, currentTurnMessages: [] });
|
|
20475
|
-
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20476
|
-
if (this.localCachePath) {
|
|
20477
|
-
const sessionDir = path16.join(
|
|
20478
|
-
this.localCachePath,
|
|
20479
|
-
"sessions",
|
|
20480
|
-
context.runId
|
|
20481
|
-
);
|
|
20482
|
-
try {
|
|
20483
|
-
fs13.mkdirSync(sessionDir, { recursive: true });
|
|
20484
|
-
} catch (error) {
|
|
20485
|
-
this.logger.warn("Failed to create local cache directory", {
|
|
20486
|
-
sessionDir,
|
|
20487
|
-
error
|
|
20619
|
+
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
20620
|
+
const binaryContent = Buffer.from(base64Content, "base64");
|
|
20621
|
+
await writeFile5(archivePath, binaryContent);
|
|
20622
|
+
this.log.info("Tree archive downloaded", {
|
|
20623
|
+
treeHash: snapshot.treeHash,
|
|
20624
|
+
snapshotBytes: binaryContent.byteLength,
|
|
20625
|
+
snapshotWireBytes: arrayBuffer.byteLength,
|
|
20626
|
+
totalBytes: binaryContent.byteLength,
|
|
20627
|
+
totalWireBytes: arrayBuffer.byteLength
|
|
20488
20628
|
});
|
|
20489
|
-
}
|
|
20490
|
-
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
}
|
|
20495
|
-
appendRawLine(sessionId, line) {
|
|
20496
|
-
const session = this.sessions.get(sessionId);
|
|
20497
|
-
if (!session) {
|
|
20498
|
-
this.logger.warn("appendRawLine called for unregistered session", {
|
|
20499
|
-
sessionId
|
|
20500
|
-
});
|
|
20501
|
-
return;
|
|
20502
|
-
}
|
|
20503
|
-
try {
|
|
20504
|
-
const message = JSON.parse(line);
|
|
20505
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
20506
|
-
if (this.isAgentMessageChunk(message)) {
|
|
20507
|
-
const text2 = this.extractChunkText(message);
|
|
20508
|
-
if (text2) {
|
|
20509
|
-
if (!session.chunkBuffer) {
|
|
20510
|
-
session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
|
|
20511
|
-
} else {
|
|
20512
|
-
session.chunkBuffer.text += text2;
|
|
20513
|
-
}
|
|
20629
|
+
},
|
|
20630
|
+
rollback: async () => {
|
|
20631
|
+
if (this.archivePath) {
|
|
20632
|
+
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
20633
|
+
});
|
|
20514
20634
|
}
|
|
20515
|
-
return;
|
|
20516
|
-
}
|
|
20517
|
-
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
20518
|
-
session.chunkBuffer = void 0;
|
|
20519
|
-
} else {
|
|
20520
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
20521
|
-
}
|
|
20522
|
-
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
20523
|
-
if (nonChunkAgentText) {
|
|
20524
|
-
session.lastAgentMessage = nonChunkAgentText;
|
|
20525
|
-
session.currentTurnMessages.push(nonChunkAgentText);
|
|
20526
|
-
}
|
|
20527
|
-
const entry = {
|
|
20528
|
-
type: "notification",
|
|
20529
|
-
timestamp,
|
|
20530
|
-
notification: message
|
|
20531
|
-
};
|
|
20532
|
-
this.writeToLocalCache(sessionId, entry);
|
|
20533
|
-
if (this.posthogAPI) {
|
|
20534
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20535
|
-
pending.push(entry);
|
|
20536
|
-
this.pendingEntries.set(sessionId, pending);
|
|
20537
|
-
this.scheduleFlush(sessionId);
|
|
20538
|
-
}
|
|
20539
|
-
} catch {
|
|
20540
|
-
this.logger.warn("Failed to parse raw line for persistence", {
|
|
20541
|
-
taskId: session.context.taskId,
|
|
20542
|
-
runId: session.context.runId,
|
|
20543
|
-
lineLength: line.length
|
|
20544
|
-
});
|
|
20545
|
-
}
|
|
20546
|
-
}
|
|
20547
|
-
async flush(sessionId, { coalesce = false } = {}) {
|
|
20548
|
-
if (coalesce) {
|
|
20549
|
-
const session = this.sessions.get(sessionId);
|
|
20550
|
-
if (session) {
|
|
20551
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
20552
20635
|
}
|
|
20636
|
+
});
|
|
20637
|
+
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
20638
|
+
const applyResult = await gitApplySaga.run({
|
|
20639
|
+
baseDir: repositoryPath,
|
|
20640
|
+
treeHash: snapshot.treeHash,
|
|
20641
|
+
baseCommit: snapshot.baseCommit,
|
|
20642
|
+
changes: snapshot.changes,
|
|
20643
|
+
archivePath: this.archivePath
|
|
20644
|
+
});
|
|
20645
|
+
if (!applyResult.success) {
|
|
20646
|
+
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
20553
20647
|
}
|
|
20554
|
-
|
|
20555
|
-
const next = prev.catch(() => {
|
|
20556
|
-
}).then(() => this._doFlush(sessionId));
|
|
20557
|
-
this.flushQueues.set(sessionId, next);
|
|
20558
|
-
next.finally(() => {
|
|
20559
|
-
if (this.flushQueues.get(sessionId) === next) {
|
|
20560
|
-
this.flushQueues.delete(sessionId);
|
|
20561
|
-
}
|
|
20648
|
+
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
20562
20649
|
});
|
|
20563
|
-
|
|
20650
|
+
this.log.info("Tree snapshot applied", {
|
|
20651
|
+
treeHash: snapshot.treeHash,
|
|
20652
|
+
totalChanges: snapshot.changes.length,
|
|
20653
|
+
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
20654
|
+
});
|
|
20655
|
+
return { treeHash: snapshot.treeHash };
|
|
20564
20656
|
}
|
|
20565
|
-
|
|
20566
|
-
|
|
20567
|
-
|
|
20568
|
-
|
|
20569
|
-
|
|
20657
|
+
};
|
|
20658
|
+
|
|
20659
|
+
// src/sagas/capture-tree-saga.ts
|
|
20660
|
+
import { existsSync as existsSync6 } from "fs";
|
|
20661
|
+
import { readFile as readFile6, rm as rm7 } from "fs/promises";
|
|
20662
|
+
import { join as join13 } from "path";
|
|
20663
|
+
var CaptureTreeSaga2 = class extends Saga {
|
|
20664
|
+
sagaName = "CaptureTreeSaga";
|
|
20665
|
+
async execute(input) {
|
|
20666
|
+
const {
|
|
20667
|
+
repositoryPath,
|
|
20668
|
+
lastTreeHash,
|
|
20669
|
+
interrupted,
|
|
20670
|
+
apiClient,
|
|
20671
|
+
taskId,
|
|
20672
|
+
runId
|
|
20673
|
+
} = input;
|
|
20674
|
+
const tmpDir = join13(repositoryPath, ".posthog", "tmp");
|
|
20675
|
+
if (existsSync6(join13(repositoryPath, ".gitmodules"))) {
|
|
20676
|
+
this.log.warn(
|
|
20677
|
+
"Repository has submodules - snapshot may not capture submodule state"
|
|
20678
|
+
);
|
|
20570
20679
|
}
|
|
20571
|
-
const
|
|
20572
|
-
|
|
20573
|
-
|
|
20680
|
+
const shouldArchive = !!apiClient;
|
|
20681
|
+
const archivePath = shouldArchive ? join13(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
20682
|
+
const gitCaptureSaga = new CaptureTreeSaga(this.log);
|
|
20683
|
+
const captureResult = await gitCaptureSaga.run({
|
|
20684
|
+
baseDir: repositoryPath,
|
|
20685
|
+
lastTreeHash,
|
|
20686
|
+
archivePath
|
|
20687
|
+
});
|
|
20688
|
+
if (!captureResult.success) {
|
|
20689
|
+
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
20574
20690
|
}
|
|
20575
|
-
|
|
20576
|
-
|
|
20577
|
-
|
|
20578
|
-
|
|
20579
|
-
|
|
20691
|
+
const {
|
|
20692
|
+
snapshot: gitSnapshot,
|
|
20693
|
+
archivePath: createdArchivePath,
|
|
20694
|
+
changed
|
|
20695
|
+
} = captureResult.data;
|
|
20696
|
+
if (!changed || !gitSnapshot) {
|
|
20697
|
+
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
20698
|
+
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
20580
20699
|
}
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20585
|
-
|
|
20586
|
-
|
|
20587
|
-
|
|
20588
|
-
|
|
20589
|
-
|
|
20590
|
-
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
20591
|
-
this.retryCounts.set(sessionId, retryCount);
|
|
20592
|
-
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
20593
|
-
this.logger.error(
|
|
20594
|
-
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
20595
|
-
{
|
|
20596
|
-
taskId: session.context.taskId,
|
|
20597
|
-
runId: session.context.runId,
|
|
20598
|
-
error
|
|
20599
|
-
}
|
|
20700
|
+
let archiveUrl;
|
|
20701
|
+
if (apiClient && createdArchivePath) {
|
|
20702
|
+
try {
|
|
20703
|
+
archiveUrl = await this.uploadArchive(
|
|
20704
|
+
createdArchivePath,
|
|
20705
|
+
gitSnapshot.treeHash,
|
|
20706
|
+
apiClient,
|
|
20707
|
+
taskId,
|
|
20708
|
+
runId
|
|
20600
20709
|
);
|
|
20601
|
-
|
|
20602
|
-
|
|
20603
|
-
|
|
20604
|
-
this.logger.warn(
|
|
20605
|
-
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
20606
|
-
{
|
|
20607
|
-
taskId: session.context.taskId,
|
|
20608
|
-
runId: session.context.runId,
|
|
20609
|
-
error: error instanceof Error ? error.message : String(error)
|
|
20610
|
-
}
|
|
20611
|
-
);
|
|
20612
|
-
}
|
|
20613
|
-
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
20614
|
-
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
20615
|
-
this.scheduleFlush(sessionId);
|
|
20710
|
+
} finally {
|
|
20711
|
+
await rm7(createdArchivePath, { force: true }).catch(() => {
|
|
20712
|
+
});
|
|
20616
20713
|
}
|
|
20617
20714
|
}
|
|
20715
|
+
const snapshot = {
|
|
20716
|
+
treeHash: gitSnapshot.treeHash,
|
|
20717
|
+
baseCommit: gitSnapshot.baseCommit,
|
|
20718
|
+
changes: gitSnapshot.changes,
|
|
20719
|
+
timestamp: gitSnapshot.timestamp,
|
|
20720
|
+
interrupted,
|
|
20721
|
+
archiveUrl
|
|
20722
|
+
};
|
|
20723
|
+
this.log.info("Tree captured", {
|
|
20724
|
+
treeHash: snapshot.treeHash,
|
|
20725
|
+
changes: snapshot.changes.length,
|
|
20726
|
+
interrupted,
|
|
20727
|
+
archiveUrl
|
|
20728
|
+
});
|
|
20729
|
+
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
20618
20730
|
}
|
|
20619
|
-
|
|
20620
|
-
|
|
20621
|
-
|
|
20622
|
-
|
|
20623
|
-
|
|
20624
|
-
|
|
20625
|
-
|
|
20626
|
-
|
|
20627
|
-
|
|
20628
|
-
|
|
20629
|
-
|
|
20630
|
-
|
|
20631
|
-
|
|
20632
|
-
|
|
20633
|
-
const update = params?.update;
|
|
20634
|
-
const content = update?.content;
|
|
20635
|
-
if (content?.type === "text" && content.text) {
|
|
20636
|
-
return content.text;
|
|
20637
|
-
}
|
|
20638
|
-
return "";
|
|
20639
|
-
}
|
|
20640
|
-
emitCoalescedMessage(sessionId, session) {
|
|
20641
|
-
if (!session.chunkBuffer) return;
|
|
20642
|
-
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
20643
|
-
session.chunkBuffer = void 0;
|
|
20644
|
-
session.lastAgentMessage = text2;
|
|
20645
|
-
session.currentTurnMessages.push(text2);
|
|
20646
|
-
const entry = {
|
|
20647
|
-
type: "notification",
|
|
20648
|
-
timestamp: firstTimestamp,
|
|
20649
|
-
notification: {
|
|
20650
|
-
jsonrpc: "2.0",
|
|
20651
|
-
method: "session/update",
|
|
20652
|
-
params: {
|
|
20653
|
-
update: {
|
|
20654
|
-
sessionUpdate: "agent_message",
|
|
20655
|
-
content: { type: "text", text: text2 }
|
|
20731
|
+
async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
|
|
20732
|
+
const archiveUrl = await this.step({
|
|
20733
|
+
name: "upload_archive",
|
|
20734
|
+
execute: async () => {
|
|
20735
|
+
const archiveContent = await readFile6(archivePath);
|
|
20736
|
+
const base64Content = archiveContent.toString("base64");
|
|
20737
|
+
const snapshotBytes = archiveContent.byteLength;
|
|
20738
|
+
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
20739
|
+
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
20740
|
+
{
|
|
20741
|
+
name: `trees/${treeHash}.tar.gz`,
|
|
20742
|
+
type: "tree_snapshot",
|
|
20743
|
+
content: base64Content,
|
|
20744
|
+
content_type: "application/gzip"
|
|
20656
20745
|
}
|
|
20746
|
+
]);
|
|
20747
|
+
const uploadedArtifact = artifacts[0];
|
|
20748
|
+
if (uploadedArtifact?.storage_path) {
|
|
20749
|
+
this.log.info("Tree archive uploaded", {
|
|
20750
|
+
storagePath: uploadedArtifact.storage_path,
|
|
20751
|
+
treeHash,
|
|
20752
|
+
snapshotBytes,
|
|
20753
|
+
snapshotWireBytes,
|
|
20754
|
+
totalBytes: snapshotBytes,
|
|
20755
|
+
totalWireBytes: snapshotWireBytes
|
|
20756
|
+
});
|
|
20757
|
+
return uploadedArtifact.storage_path;
|
|
20657
20758
|
}
|
|
20759
|
+
return void 0;
|
|
20760
|
+
},
|
|
20761
|
+
rollback: async () => {
|
|
20762
|
+
await rm7(archivePath, { force: true }).catch(() => {
|
|
20763
|
+
});
|
|
20658
20764
|
}
|
|
20659
|
-
};
|
|
20660
|
-
|
|
20661
|
-
if (this.posthogAPI) {
|
|
20662
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20663
|
-
pending.push(entry);
|
|
20664
|
-
this.pendingEntries.set(sessionId, pending);
|
|
20665
|
-
this.scheduleFlush(sessionId);
|
|
20666
|
-
}
|
|
20765
|
+
});
|
|
20766
|
+
return archiveUrl;
|
|
20667
20767
|
}
|
|
20668
|
-
|
|
20669
|
-
|
|
20768
|
+
};
|
|
20769
|
+
|
|
20770
|
+
// src/tree-tracker.ts
|
|
20771
|
+
var TreeTracker = class {
|
|
20772
|
+
repositoryPath;
|
|
20773
|
+
taskId;
|
|
20774
|
+
runId;
|
|
20775
|
+
apiClient;
|
|
20776
|
+
logger;
|
|
20777
|
+
lastTreeHash = null;
|
|
20778
|
+
constructor(config) {
|
|
20779
|
+
this.repositoryPath = config.repositoryPath;
|
|
20780
|
+
this.taskId = config.taskId;
|
|
20781
|
+
this.runId = config.runId;
|
|
20782
|
+
this.apiClient = config.apiClient;
|
|
20783
|
+
this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
20670
20784
|
}
|
|
20671
|
-
|
|
20672
|
-
|
|
20673
|
-
|
|
20674
|
-
|
|
20675
|
-
|
|
20676
|
-
|
|
20677
|
-
|
|
20678
|
-
|
|
20679
|
-
|
|
20680
|
-
|
|
20785
|
+
/**
|
|
20786
|
+
* Capture current working tree state as a snapshot.
|
|
20787
|
+
* Uses a temporary index to avoid modifying user's staging area.
|
|
20788
|
+
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
20789
|
+
*/
|
|
20790
|
+
async captureTree(options) {
|
|
20791
|
+
const saga = new CaptureTreeSaga2(this.logger);
|
|
20792
|
+
const result = await saga.run({
|
|
20793
|
+
repositoryPath: this.repositoryPath,
|
|
20794
|
+
taskId: this.taskId,
|
|
20795
|
+
runId: this.runId,
|
|
20796
|
+
apiClient: this.apiClient,
|
|
20797
|
+
lastTreeHash: this.lastTreeHash,
|
|
20798
|
+
interrupted: options?.interrupted
|
|
20799
|
+
});
|
|
20800
|
+
if (!result.success) {
|
|
20801
|
+
this.logger.error("Failed to capture tree", {
|
|
20802
|
+
error: result.error,
|
|
20803
|
+
failedStep: result.failedStep
|
|
20804
|
+
});
|
|
20805
|
+
throw new Error(
|
|
20806
|
+
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
20681
20807
|
);
|
|
20682
20808
|
}
|
|
20683
|
-
|
|
20684
|
-
|
|
20685
|
-
resetTurnMessages(sessionId) {
|
|
20686
|
-
const session = this.sessions.get(sessionId);
|
|
20687
|
-
if (session) {
|
|
20688
|
-
session.currentTurnMessages = [];
|
|
20809
|
+
if (result.data.newTreeHash !== null) {
|
|
20810
|
+
this.lastTreeHash = result.data.newTreeHash;
|
|
20689
20811
|
}
|
|
20812
|
+
return result.data.snapshot;
|
|
20690
20813
|
}
|
|
20691
|
-
|
|
20692
|
-
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
|
|
20696
|
-
|
|
20697
|
-
|
|
20698
|
-
return null;
|
|
20699
|
-
}
|
|
20700
|
-
const content = update.content;
|
|
20701
|
-
if (content?.type === "text" && typeof content.text === "string") {
|
|
20702
|
-
const trimmed2 = content.text.trim();
|
|
20703
|
-
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20814
|
+
/**
|
|
20815
|
+
* Download and apply a tree snapshot.
|
|
20816
|
+
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
20817
|
+
*/
|
|
20818
|
+
async applyTreeSnapshot(snapshot) {
|
|
20819
|
+
if (!this.apiClient) {
|
|
20820
|
+
throw new Error("Cannot apply snapshot: API client not configured");
|
|
20704
20821
|
}
|
|
20705
|
-
if (
|
|
20706
|
-
|
|
20707
|
-
|
|
20822
|
+
if (!snapshot.archiveUrl) {
|
|
20823
|
+
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
20824
|
+
treeHash: snapshot.treeHash,
|
|
20825
|
+
changes: snapshot.changes.length
|
|
20826
|
+
});
|
|
20827
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
20708
20828
|
}
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20829
|
+
const saga = new ApplySnapshotSaga(this.logger);
|
|
20830
|
+
const result = await saga.run({
|
|
20831
|
+
snapshot,
|
|
20832
|
+
repositoryPath: this.repositoryPath,
|
|
20833
|
+
apiClient: this.apiClient,
|
|
20834
|
+
taskId: this.taskId,
|
|
20835
|
+
runId: this.runId
|
|
20836
|
+
});
|
|
20837
|
+
if (!result.success) {
|
|
20838
|
+
this.logger.error("Failed to apply tree snapshot", {
|
|
20839
|
+
error: result.error,
|
|
20840
|
+
failedStep: result.failedStep,
|
|
20841
|
+
treeHash: snapshot.treeHash
|
|
20842
|
+
});
|
|
20843
|
+
throw new Error(
|
|
20844
|
+
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
20722
20845
|
);
|
|
20723
|
-
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20724
|
-
delay3 = 0;
|
|
20725
|
-
} else {
|
|
20726
|
-
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
20727
20846
|
}
|
|
20728
|
-
|
|
20729
|
-
this.flushTimeouts.set(sessionId, timeout);
|
|
20847
|
+
this.lastTreeHash = result.data.treeHash;
|
|
20730
20848
|
}
|
|
20731
|
-
|
|
20732
|
-
|
|
20733
|
-
|
|
20734
|
-
|
|
20735
|
-
|
|
20736
|
-
this.localCachePath,
|
|
20737
|
-
"sessions",
|
|
20738
|
-
session.context.runId,
|
|
20739
|
-
"logs.ndjson"
|
|
20740
|
-
);
|
|
20741
|
-
try {
|
|
20742
|
-
fs13.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
20743
|
-
`);
|
|
20744
|
-
} catch (error) {
|
|
20745
|
-
this.logger.warn("Failed to write to local cache", {
|
|
20746
|
-
taskId: session.context.taskId,
|
|
20747
|
-
runId: session.context.runId,
|
|
20748
|
-
logPath,
|
|
20749
|
-
error
|
|
20750
|
-
});
|
|
20751
|
-
}
|
|
20849
|
+
/**
|
|
20850
|
+
* Get the last captured tree hash.
|
|
20851
|
+
*/
|
|
20852
|
+
getLastTreeHash() {
|
|
20853
|
+
return this.lastTreeHash;
|
|
20752
20854
|
}
|
|
20753
|
-
|
|
20754
|
-
|
|
20755
|
-
|
|
20756
|
-
|
|
20757
|
-
|
|
20758
|
-
const now = Date.now();
|
|
20759
|
-
for (const entry of entries) {
|
|
20760
|
-
const entryPath = path16.join(sessionsDir, entry);
|
|
20761
|
-
try {
|
|
20762
|
-
const stats = await fsp.stat(entryPath);
|
|
20763
|
-
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
20764
|
-
await fsp.rm(entryPath, { recursive: true, force: true });
|
|
20765
|
-
deleted++;
|
|
20766
|
-
}
|
|
20767
|
-
} catch {
|
|
20768
|
-
}
|
|
20769
|
-
}
|
|
20770
|
-
} catch {
|
|
20771
|
-
}
|
|
20772
|
-
return deleted;
|
|
20855
|
+
/**
|
|
20856
|
+
* Set the last tree hash (used when resuming).
|
|
20857
|
+
*/
|
|
20858
|
+
setLastTreeHash(hash) {
|
|
20859
|
+
this.lastTreeHash = hash;
|
|
20773
20860
|
}
|
|
20774
20861
|
};
|
|
20775
20862
|
|
|
@@ -21245,45 +21332,29 @@ var AgentServer = class {
|
|
|
21245
21332
|
});
|
|
21246
21333
|
await this.autoInitializeSession();
|
|
21247
21334
|
}
|
|
21248
|
-
async
|
|
21249
|
-
|
|
21250
|
-
|
|
21251
|
-
|
|
21252
|
-
|
|
21253
|
-
|
|
21254
|
-
|
|
21255
|
-
|
|
21335
|
+
async loadResumeState(taskId, resumeRunId, currentRunId) {
|
|
21336
|
+
this.logger.debug("Loading resume state", { resumeRunId, currentRunId });
|
|
21337
|
+
try {
|
|
21338
|
+
this.resumeState = await resumeFromLog({
|
|
21339
|
+
taskId,
|
|
21340
|
+
runId: resumeRunId,
|
|
21341
|
+
repositoryPath: this.config.repositoryPath,
|
|
21342
|
+
apiClient: this.posthogAPI,
|
|
21343
|
+
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
21256
21344
|
});
|
|
21257
|
-
|
|
21258
|
-
this.resumeState
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
21264
|
-
|
|
21265
|
-
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
});
|
|
21270
|
-
} catch (error) {
|
|
21271
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21272
|
-
error
|
|
21273
|
-
});
|
|
21274
|
-
this.resumeState = null;
|
|
21275
|
-
}
|
|
21345
|
+
this.logger.debug("Resume state loaded", {
|
|
21346
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
21347
|
+
hasSnapshot: !!this.resumeState.latestSnapshot,
|
|
21348
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21349
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null,
|
|
21350
|
+
logEntries: this.resumeState.logEntryCount
|
|
21351
|
+
});
|
|
21352
|
+
} catch (error) {
|
|
21353
|
+
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21354
|
+
error
|
|
21355
|
+
});
|
|
21356
|
+
this.resumeState = null;
|
|
21276
21357
|
}
|
|
21277
|
-
const payload = {
|
|
21278
|
-
task_id: taskId,
|
|
21279
|
-
run_id: runId,
|
|
21280
|
-
team_id: projectId,
|
|
21281
|
-
user_id: 0,
|
|
21282
|
-
// System-initiated
|
|
21283
|
-
distinct_id: "agent-server",
|
|
21284
|
-
mode
|
|
21285
|
-
};
|
|
21286
|
-
await this.initializeSession(payload, null);
|
|
21287
21358
|
}
|
|
21288
21359
|
async stop() {
|
|
21289
21360
|
this.logger.debug("Stopping agent server...");
|
|
@@ -21682,29 +21753,11 @@ var AgentServer = class {
|
|
|
21682
21753
|
if (!this.resumeState) {
|
|
21683
21754
|
const resumeRunId = this.getResumeRunId(taskRun);
|
|
21684
21755
|
if (resumeRunId) {
|
|
21685
|
-
this.
|
|
21756
|
+
await this.loadResumeState(
|
|
21757
|
+
payload.task_id,
|
|
21686
21758
|
resumeRunId,
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
try {
|
|
21690
|
-
this.resumeState = await resumeFromLog({
|
|
21691
|
-
taskId: payload.task_id,
|
|
21692
|
-
runId: resumeRunId,
|
|
21693
|
-
repositoryPath: this.config.repositoryPath,
|
|
21694
|
-
apiClient: this.posthogAPI,
|
|
21695
|
-
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
21696
|
-
});
|
|
21697
|
-
this.logger.debug("Resume state loaded (via TaskRun state)", {
|
|
21698
|
-
conversationTurns: this.resumeState.conversation.length,
|
|
21699
|
-
snapshotApplied: this.resumeState.snapshotApplied,
|
|
21700
|
-
logEntries: this.resumeState.logEntryCount
|
|
21701
|
-
});
|
|
21702
|
-
} catch (error) {
|
|
21703
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21704
|
-
error
|
|
21705
|
-
});
|
|
21706
|
-
this.resumeState = null;
|
|
21707
|
-
}
|
|
21759
|
+
payload.run_id
|
|
21760
|
+
);
|
|
21708
21761
|
}
|
|
21709
21762
|
}
|
|
21710
21763
|
if (this.resumeState && this.resumeState.conversation.length > 0) {
|
|
@@ -21763,8 +21816,59 @@ var AgentServer = class {
|
|
|
21763
21816
|
const conversationSummary = formatConversationForResume(
|
|
21764
21817
|
this.resumeState.conversation
|
|
21765
21818
|
);
|
|
21819
|
+
let snapshotApplied = false;
|
|
21820
|
+
if (this.resumeState.latestSnapshot?.archiveUrl && this.config.repositoryPath && this.posthogAPI) {
|
|
21821
|
+
try {
|
|
21822
|
+
const treeTracker = new TreeTracker({
|
|
21823
|
+
repositoryPath: this.config.repositoryPath,
|
|
21824
|
+
taskId: payload.task_id,
|
|
21825
|
+
runId: payload.run_id,
|
|
21826
|
+
apiClient: this.posthogAPI,
|
|
21827
|
+
logger: this.logger.child("TreeTracker")
|
|
21828
|
+
});
|
|
21829
|
+
await treeTracker.applyTreeSnapshot(this.resumeState.latestSnapshot);
|
|
21830
|
+
treeTracker.setLastTreeHash(this.resumeState.latestSnapshot.treeHash);
|
|
21831
|
+
snapshotApplied = true;
|
|
21832
|
+
this.logger.info("Tree snapshot applied", {
|
|
21833
|
+
treeHash: this.resumeState.latestSnapshot.treeHash,
|
|
21834
|
+
changes: this.resumeState.latestSnapshot.changes?.length ?? 0,
|
|
21835
|
+
hasArchiveUrl: !!this.resumeState.latestSnapshot.archiveUrl
|
|
21836
|
+
});
|
|
21837
|
+
} catch (error) {
|
|
21838
|
+
this.logger.warn("Failed to apply tree snapshot", {
|
|
21839
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21840
|
+
treeHash: this.resumeState.latestSnapshot.treeHash
|
|
21841
|
+
});
|
|
21842
|
+
}
|
|
21843
|
+
}
|
|
21844
|
+
if (this.resumeState.latestGitCheckpoint && this.config.repositoryPath && this.posthogAPI) {
|
|
21845
|
+
try {
|
|
21846
|
+
const checkpointTracker = new HandoffCheckpointTracker({
|
|
21847
|
+
repositoryPath: this.config.repositoryPath,
|
|
21848
|
+
taskId: payload.task_id,
|
|
21849
|
+
runId: payload.run_id,
|
|
21850
|
+
apiClient: this.posthogAPI,
|
|
21851
|
+
logger: this.logger.child("HandoffCheckpoint")
|
|
21852
|
+
});
|
|
21853
|
+
const metrics = await checkpointTracker.applyFromHandoff(
|
|
21854
|
+
this.resumeState.latestGitCheckpoint
|
|
21855
|
+
);
|
|
21856
|
+
this.logger.info("Git checkpoint applied", {
|
|
21857
|
+
branch: this.resumeState.latestGitCheckpoint.branch,
|
|
21858
|
+
head: this.resumeState.latestGitCheckpoint.head,
|
|
21859
|
+
packBytes: metrics.packBytes,
|
|
21860
|
+
indexBytes: metrics.indexBytes,
|
|
21861
|
+
totalBytes: metrics.totalBytes
|
|
21862
|
+
});
|
|
21863
|
+
} catch (error) {
|
|
21864
|
+
this.logger.warn("Failed to apply git checkpoint", {
|
|
21865
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21866
|
+
branch: this.resumeState.latestGitCheckpoint.branch
|
|
21867
|
+
});
|
|
21868
|
+
}
|
|
21869
|
+
}
|
|
21766
21870
|
const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
|
|
21767
|
-
const sandboxContext =
|
|
21871
|
+
const sandboxContext = snapshotApplied ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.` : `The workspace files from the previous session were not restored (the file snapshot may have expired), so you are starting with a fresh environment. Your conversation history is fully preserved below.`;
|
|
21768
21872
|
let resumePromptBlocks;
|
|
21769
21873
|
if (pendingUserPrompt?.length) {
|
|
21770
21874
|
resumePromptBlocks = [
|
|
@@ -21805,7 +21909,9 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21805
21909
|
conversationTurns: this.resumeState.conversation.length,
|
|
21806
21910
|
promptLength: promptBlocksToText(resumePromptBlocks).length,
|
|
21807
21911
|
hasPendingUserMessage: !!pendingUserPrompt?.length,
|
|
21808
|
-
snapshotApplied
|
|
21912
|
+
snapshotApplied,
|
|
21913
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21914
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null
|
|
21809
21915
|
});
|
|
21810
21916
|
this.resumeState = null;
|
|
21811
21917
|
this.session.logWriter.resetTurnMessages(payload.run_id);
|
|
@@ -21969,6 +22075,24 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21969
22075
|
const normalizedName = baseName.replace(/[^\w.-]/g, "_");
|
|
21970
22076
|
return normalizedName.length > 0 ? normalizedName : "attachment";
|
|
21971
22077
|
}
|
|
22078
|
+
async autoInitializeSession() {
|
|
22079
|
+
const { taskId, runId, mode, projectId } = this.config;
|
|
22080
|
+
this.logger.debug("Auto-initializing session", { taskId, runId, mode });
|
|
22081
|
+
const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
22082
|
+
if (resumeRunId) {
|
|
22083
|
+
await this.loadResumeState(taskId, resumeRunId, runId);
|
|
22084
|
+
}
|
|
22085
|
+
const payload = {
|
|
22086
|
+
task_id: taskId,
|
|
22087
|
+
run_id: runId,
|
|
22088
|
+
team_id: projectId,
|
|
22089
|
+
user_id: 0,
|
|
22090
|
+
// System-initiated
|
|
22091
|
+
distinct_id: "agent-server",
|
|
22092
|
+
mode
|
|
22093
|
+
};
|
|
22094
|
+
await this.initializeSession(payload, null);
|
|
22095
|
+
}
|
|
21972
22096
|
getResumeRunId(taskRun) {
|
|
21973
22097
|
const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21974
22098
|
if (envRunId) return envRunId;
|