@posthog/agent 2.3.386 → 2.3.388
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude/session/jsonl-hydration.d.ts +1 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +20 -2
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +43 -0
- package/dist/handoff-checkpoint.js +6684 -0
- package/dist/handoff-checkpoint.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/posthog-api.d.ts +2 -0
- package/dist/posthog-api.js +18 -2
- package/dist/posthog-api.js.map +1 -1
- package/dist/resume.d.ts +4 -8
- package/dist/resume.js +266 -6491
- package/dist/resume.js.map +1 -1
- package/dist/server/agent-server.d.ts +7 -16
- package/dist/server/agent-server.js +2333 -1383
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +2332 -1382
- package/dist/server/bin.cjs.map +1 -1
- package/dist/server/schemas.d.ts +191 -0
- package/dist/server/schemas.js +108 -0
- package/dist/server/schemas.js.map +1 -0
- package/dist/tree-tracker.d.ts +1 -0
- package/dist/tree-tracker.js +18 -4
- package/dist/tree-tracker.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +10 -2
- package/src/acp-extensions.ts +3 -0
- package/src/handoff-checkpoint.test.ts +183 -0
- package/src/handoff-checkpoint.ts +367 -0
- package/src/posthog-api.test.ts +29 -0
- package/src/posthog-api.ts +13 -1
- package/src/resume.ts +27 -12
- package/src/sagas/apply-snapshot-saga.ts +7 -0
- package/src/sagas/capture-tree-saga.ts +10 -3
- package/src/sagas/resume-saga.test.ts +7 -47
- package/src/sagas/resume-saga.ts +42 -64
- package/src/sagas/test-fixtures.ts +46 -0
- package/src/server/agent-server.ts +193 -70
- package/src/server/schemas.ts +21 -2
- package/src/types.ts +24 -0
|
@@ -809,15 +809,15 @@ var require_src2 = __commonJS({
|
|
|
809
809
|
var fs_1 = __require("fs");
|
|
810
810
|
var debug_1 = __importDefault(require_src());
|
|
811
811
|
var log = debug_1.default("@kwsites/file-exists");
|
|
812
|
-
function check(
|
|
813
|
-
log(`checking %s`,
|
|
812
|
+
function check(path17, isFile2, isDirectory) {
|
|
813
|
+
log(`checking %s`, path17);
|
|
814
814
|
try {
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
815
|
+
const stat4 = fs_1.statSync(path17);
|
|
816
|
+
if (stat4.isFile() && isFile2) {
|
|
817
817
|
log(`[OK] path represents a file`);
|
|
818
818
|
return true;
|
|
819
819
|
}
|
|
820
|
-
if (
|
|
820
|
+
if (stat4.isDirectory() && isDirectory) {
|
|
821
821
|
log(`[OK] path represents a directory`);
|
|
822
822
|
return true;
|
|
823
823
|
}
|
|
@@ -832,8 +832,8 @@ var require_src2 = __commonJS({
|
|
|
832
832
|
throw e;
|
|
833
833
|
}
|
|
834
834
|
}
|
|
835
|
-
function exists2(
|
|
836
|
-
return check(
|
|
835
|
+
function exists2(path17, type = exports2.READABLE) {
|
|
836
|
+
return check(path17, (type & exports2.FILE) > 0, (type & exports2.FOLDER) > 0);
|
|
837
837
|
}
|
|
838
838
|
exports2.exists = exists2;
|
|
839
839
|
exports2.FILE = 1;
|
|
@@ -929,11 +929,11 @@ var require_tree_sitter = __commonJS({
|
|
|
929
929
|
throw toThrow;
|
|
930
930
|
};
|
|
931
931
|
var scriptDirectory = "";
|
|
932
|
-
function locateFile(
|
|
932
|
+
function locateFile(path17) {
|
|
933
933
|
if (Module["locateFile"]) {
|
|
934
|
-
return Module["locateFile"](
|
|
934
|
+
return Module["locateFile"](path17, scriptDirectory);
|
|
935
935
|
}
|
|
936
|
-
return scriptDirectory +
|
|
936
|
+
return scriptDirectory + path17;
|
|
937
937
|
}
|
|
938
938
|
var readAsync, readBinary;
|
|
939
939
|
if (ENVIRONMENT_IS_NODE) {
|
|
@@ -947,10 +947,10 @@ var require_tree_sitter = __commonJS({
|
|
|
947
947
|
};
|
|
948
948
|
readAsync = (filename, binary2 = true) => {
|
|
949
949
|
filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename);
|
|
950
|
-
return new Promise((
|
|
950
|
+
return new Promise((resolve7, reject) => {
|
|
951
951
|
fs.readFile(filename, binary2 ? void 0 : "utf8", (err2, data) => {
|
|
952
952
|
if (err2) reject(err2);
|
|
953
|
-
else
|
|
953
|
+
else resolve7(binary2 ? data.buffer : data);
|
|
954
954
|
});
|
|
955
955
|
});
|
|
956
956
|
};
|
|
@@ -991,13 +991,13 @@ var require_tree_sitter = __commonJS({
|
|
|
991
991
|
}
|
|
992
992
|
readAsync = (url) => {
|
|
993
993
|
if (isFileURI(url)) {
|
|
994
|
-
return new Promise((reject,
|
|
994
|
+
return new Promise((reject, resolve7) => {
|
|
995
995
|
var xhr = new XMLHttpRequest();
|
|
996
996
|
xhr.open("GET", url, true);
|
|
997
997
|
xhr.responseType = "arraybuffer";
|
|
998
998
|
xhr.onload = () => {
|
|
999
999
|
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
|
|
1000
|
-
|
|
1000
|
+
resolve7(xhr.response);
|
|
1001
1001
|
}
|
|
1002
1002
|
reject(xhr.status);
|
|
1003
1003
|
};
|
|
@@ -1957,8 +1957,8 @@ var require_tree_sitter = __commonJS({
|
|
|
1957
1957
|
}
|
|
1958
1958
|
var libFile = locateFile(libName2);
|
|
1959
1959
|
if (flags2.loadAsync) {
|
|
1960
|
-
return new Promise(function(
|
|
1961
|
-
asyncLoad(libFile,
|
|
1960
|
+
return new Promise(function(resolve7, reject) {
|
|
1961
|
+
asyncLoad(libFile, resolve7, reject);
|
|
1962
1962
|
});
|
|
1963
1963
|
}
|
|
1964
1964
|
if (!readBinary) {
|
|
@@ -3454,8 +3454,8 @@ var require_tree_sitter = __commonJS({
|
|
|
3454
3454
|
} else {
|
|
3455
3455
|
const url = input;
|
|
3456
3456
|
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
3457
|
-
const
|
|
3458
|
-
bytes = Promise.resolve(
|
|
3457
|
+
const fs14 = __require("fs");
|
|
3458
|
+
bytes = Promise.resolve(fs14.readFileSync(url));
|
|
3459
3459
|
} else {
|
|
3460
3460
|
bytes = fetch(url).then((response) => response.arrayBuffer().then((buffer) => {
|
|
3461
3461
|
if (response.ok) {
|
|
@@ -3784,8 +3784,8 @@ ${JSON.stringify(symbolNames, null, 2)}`);
|
|
|
3784
3784
|
});
|
|
3785
3785
|
|
|
3786
3786
|
// src/server/agent-server.ts
|
|
3787
|
-
import { mkdir as
|
|
3788
|
-
import { basename as basename2, join as
|
|
3787
|
+
import { mkdir as mkdir8, writeFile as writeFile6 } from "fs/promises";
|
|
3788
|
+
import { basename as basename2, join as join14 } from "path";
|
|
3789
3789
|
import { pathToFileURL } from "url";
|
|
3790
3790
|
import {
|
|
3791
3791
|
ClientSideConnection as ClientSideConnection2,
|
|
@@ -3835,8 +3835,8 @@ function pathspec(...paths) {
|
|
|
3835
3835
|
cache.set(key, paths);
|
|
3836
3836
|
return key;
|
|
3837
3837
|
}
|
|
3838
|
-
function isPathSpec(
|
|
3839
|
-
return
|
|
3838
|
+
function isPathSpec(path17) {
|
|
3839
|
+
return path17 instanceof String && cache.has(path17);
|
|
3840
3840
|
}
|
|
3841
3841
|
function toPaths(pathSpec) {
|
|
3842
3842
|
return cache.get(pathSpec) || [];
|
|
@@ -3925,8 +3925,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
|
|
|
3925
3925
|
function forEachLineWithContent(input, callback) {
|
|
3926
3926
|
return toLinesWithContent(input, true).map((line) => callback(line));
|
|
3927
3927
|
}
|
|
3928
|
-
function folderExists(
|
|
3929
|
-
return (0, import_file_exists.exists)(
|
|
3928
|
+
function folderExists(path17) {
|
|
3929
|
+
return (0, import_file_exists.exists)(path17, import_file_exists.FOLDER);
|
|
3930
3930
|
}
|
|
3931
3931
|
function append(target, item) {
|
|
3932
3932
|
if (Array.isArray(target)) {
|
|
@@ -4330,8 +4330,8 @@ function checkIsRepoRootTask() {
|
|
|
4330
4330
|
commands,
|
|
4331
4331
|
format: "utf-8",
|
|
4332
4332
|
onError,
|
|
4333
|
-
parser(
|
|
4334
|
-
return /^\.(git)?$/.test(
|
|
4333
|
+
parser(path17) {
|
|
4334
|
+
return /^\.(git)?$/.test(path17.trim());
|
|
4335
4335
|
}
|
|
4336
4336
|
};
|
|
4337
4337
|
}
|
|
@@ -4765,11 +4765,11 @@ function parseGrep(grep) {
|
|
|
4765
4765
|
const paths = /* @__PURE__ */ new Set();
|
|
4766
4766
|
const results = {};
|
|
4767
4767
|
forEachLineWithContent(grep, (input) => {
|
|
4768
|
-
const [
|
|
4769
|
-
paths.add(
|
|
4770
|
-
(results[
|
|
4768
|
+
const [path17, line, preview] = input.split(NULL);
|
|
4769
|
+
paths.add(path17);
|
|
4770
|
+
(results[path17] = results[path17] || []).push({
|
|
4771
4771
|
line: asNumber(line),
|
|
4772
|
-
path:
|
|
4772
|
+
path: path17,
|
|
4773
4773
|
preview
|
|
4774
4774
|
});
|
|
4775
4775
|
});
|
|
@@ -5534,14 +5534,14 @@ var init_hash_object = __esm({
|
|
|
5534
5534
|
init_task();
|
|
5535
5535
|
}
|
|
5536
5536
|
});
|
|
5537
|
-
function parseInit(bare,
|
|
5537
|
+
function parseInit(bare, path17, text2) {
|
|
5538
5538
|
const response = String(text2).trim();
|
|
5539
5539
|
let result;
|
|
5540
5540
|
if (result = initResponseRegex.exec(response)) {
|
|
5541
|
-
return new InitSummary(bare,
|
|
5541
|
+
return new InitSummary(bare, path17, false, result[1]);
|
|
5542
5542
|
}
|
|
5543
5543
|
if (result = reInitResponseRegex.exec(response)) {
|
|
5544
|
-
return new InitSummary(bare,
|
|
5544
|
+
return new InitSummary(bare, path17, true, result[1]);
|
|
5545
5545
|
}
|
|
5546
5546
|
let gitDir = "";
|
|
5547
5547
|
const tokens = response.split(" ");
|
|
@@ -5552,7 +5552,7 @@ function parseInit(bare, path15, text2) {
|
|
|
5552
5552
|
break;
|
|
5553
5553
|
}
|
|
5554
5554
|
}
|
|
5555
|
-
return new InitSummary(bare,
|
|
5555
|
+
return new InitSummary(bare, path17, /^re/i.test(response), gitDir);
|
|
5556
5556
|
}
|
|
5557
5557
|
var InitSummary;
|
|
5558
5558
|
var initResponseRegex;
|
|
@@ -5561,9 +5561,9 @@ var init_InitSummary = __esm({
|
|
|
5561
5561
|
"src/lib/responses/InitSummary.ts"() {
|
|
5562
5562
|
"use strict";
|
|
5563
5563
|
InitSummary = class {
|
|
5564
|
-
constructor(bare,
|
|
5564
|
+
constructor(bare, path17, existing, gitDir) {
|
|
5565
5565
|
this.bare = bare;
|
|
5566
|
-
this.path =
|
|
5566
|
+
this.path = path17;
|
|
5567
5567
|
this.existing = existing;
|
|
5568
5568
|
this.gitDir = gitDir;
|
|
5569
5569
|
}
|
|
@@ -5575,7 +5575,7 @@ var init_InitSummary = __esm({
|
|
|
5575
5575
|
function hasBareCommand(command) {
|
|
5576
5576
|
return command.includes(bareCommand);
|
|
5577
5577
|
}
|
|
5578
|
-
function initTask(bare = false,
|
|
5578
|
+
function initTask(bare = false, path17, customArgs) {
|
|
5579
5579
|
const commands = ["init", ...customArgs];
|
|
5580
5580
|
if (bare && !hasBareCommand(commands)) {
|
|
5581
5581
|
commands.splice(1, 0, bareCommand);
|
|
@@ -5584,7 +5584,7 @@ function initTask(bare = false, path15, customArgs) {
|
|
|
5584
5584
|
commands,
|
|
5585
5585
|
format: "utf-8",
|
|
5586
5586
|
parser(text2) {
|
|
5587
|
-
return parseInit(commands.includes("--bare"),
|
|
5587
|
+
return parseInit(commands.includes("--bare"), path17, text2);
|
|
5588
5588
|
}
|
|
5589
5589
|
};
|
|
5590
5590
|
}
|
|
@@ -6400,12 +6400,12 @@ var init_FileStatusSummary = __esm({
|
|
|
6400
6400
|
"use strict";
|
|
6401
6401
|
fromPathRegex = /^(.+)\0(.+)$/;
|
|
6402
6402
|
FileStatusSummary = class {
|
|
6403
|
-
constructor(
|
|
6404
|
-
this.path =
|
|
6403
|
+
constructor(path17, index, working_dir) {
|
|
6404
|
+
this.path = path17;
|
|
6405
6405
|
this.index = index;
|
|
6406
6406
|
this.working_dir = working_dir;
|
|
6407
6407
|
if (index === "R" || working_dir === "R") {
|
|
6408
|
-
const detail = fromPathRegex.exec(
|
|
6408
|
+
const detail = fromPathRegex.exec(path17) || [null, path17, path17];
|
|
6409
6409
|
this.from = detail[2] || "";
|
|
6410
6410
|
this.path = detail[1] || "";
|
|
6411
6411
|
}
|
|
@@ -6436,14 +6436,14 @@ function splitLine(result, lineStr) {
|
|
|
6436
6436
|
default:
|
|
6437
6437
|
return;
|
|
6438
6438
|
}
|
|
6439
|
-
function data(index, workingDir,
|
|
6439
|
+
function data(index, workingDir, path17) {
|
|
6440
6440
|
const raw = `${index}${workingDir}`;
|
|
6441
6441
|
const handler = parsers6.get(raw);
|
|
6442
6442
|
if (handler) {
|
|
6443
|
-
handler(result,
|
|
6443
|
+
handler(result, path17);
|
|
6444
6444
|
}
|
|
6445
6445
|
if (raw !== "##" && raw !== "!!") {
|
|
6446
|
-
result.files.push(new FileStatusSummary(
|
|
6446
|
+
result.files.push(new FileStatusSummary(path17, index, workingDir));
|
|
6447
6447
|
}
|
|
6448
6448
|
}
|
|
6449
6449
|
}
|
|
@@ -6756,9 +6756,9 @@ var init_simple_git_api = __esm({
|
|
|
6756
6756
|
next
|
|
6757
6757
|
);
|
|
6758
6758
|
}
|
|
6759
|
-
hashObject(
|
|
6759
|
+
hashObject(path17, write) {
|
|
6760
6760
|
return this._runTask(
|
|
6761
|
-
hashObjectTask(
|
|
6761
|
+
hashObjectTask(path17, write === true),
|
|
6762
6762
|
trailingFunctionArgument(arguments)
|
|
6763
6763
|
);
|
|
6764
6764
|
}
|
|
@@ -7111,8 +7111,8 @@ var init_branch = __esm({
|
|
|
7111
7111
|
}
|
|
7112
7112
|
});
|
|
7113
7113
|
function toPath(input) {
|
|
7114
|
-
const
|
|
7115
|
-
return
|
|
7114
|
+
const path17 = input.trim().replace(/^["']|["']$/g, "");
|
|
7115
|
+
return path17 && normalize(path17);
|
|
7116
7116
|
}
|
|
7117
7117
|
var parseCheckIgnore;
|
|
7118
7118
|
var init_CheckIgnore = __esm({
|
|
@@ -7426,8 +7426,8 @@ __export(sub_module_exports, {
|
|
|
7426
7426
|
subModuleTask: () => subModuleTask,
|
|
7427
7427
|
updateSubModuleTask: () => updateSubModuleTask
|
|
7428
7428
|
});
|
|
7429
|
-
function addSubModuleTask(repo,
|
|
7430
|
-
return subModuleTask(["add", repo,
|
|
7429
|
+
function addSubModuleTask(repo, path17) {
|
|
7430
|
+
return subModuleTask(["add", repo, path17]);
|
|
7431
7431
|
}
|
|
7432
7432
|
function initSubModuleTask(customArgs) {
|
|
7433
7433
|
return subModuleTask(["init", ...customArgs]);
|
|
@@ -7757,8 +7757,8 @@ var require_git = __commonJS2({
|
|
|
7757
7757
|
}
|
|
7758
7758
|
return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
|
|
7759
7759
|
};
|
|
7760
|
-
Git2.prototype.submoduleAdd = function(repo,
|
|
7761
|
-
return this._runTask(addSubModuleTask2(repo,
|
|
7760
|
+
Git2.prototype.submoduleAdd = function(repo, path17, then) {
|
|
7761
|
+
return this._runTask(addSubModuleTask2(repo, path17), trailingFunctionArgument2(arguments));
|
|
7762
7762
|
};
|
|
7763
7763
|
Git2.prototype.submoduleUpdate = function(args2, then) {
|
|
7764
7764
|
return this._runTask(
|
|
@@ -8374,10 +8374,10 @@ async function getIndexLockPath(repoPath) {
|
|
|
8374
8374
|
async function getLockInfo(repoPath) {
|
|
8375
8375
|
const lockPath = await getIndexLockPath(repoPath);
|
|
8376
8376
|
try {
|
|
8377
|
-
const
|
|
8377
|
+
const stat4 = await fs2.stat(lockPath);
|
|
8378
8378
|
return {
|
|
8379
8379
|
path: lockPath,
|
|
8380
|
-
ageMs: Date.now() -
|
|
8380
|
+
ageMs: Date.now() - stat4.mtimeMs
|
|
8381
8381
|
};
|
|
8382
8382
|
} catch {
|
|
8383
8383
|
return null;
|
|
@@ -8412,10 +8412,10 @@ var AsyncReaderWriterLock = class {
|
|
|
8412
8412
|
this.readers++;
|
|
8413
8413
|
return;
|
|
8414
8414
|
}
|
|
8415
|
-
return new Promise((
|
|
8415
|
+
return new Promise((resolve7) => {
|
|
8416
8416
|
this.readQueue.push(() => {
|
|
8417
8417
|
this.readers++;
|
|
8418
|
-
|
|
8418
|
+
resolve7();
|
|
8419
8419
|
});
|
|
8420
8420
|
});
|
|
8421
8421
|
}
|
|
@@ -8429,11 +8429,11 @@ var AsyncReaderWriterLock = class {
|
|
|
8429
8429
|
return;
|
|
8430
8430
|
}
|
|
8431
8431
|
this.writerWaiting = true;
|
|
8432
|
-
return new Promise((
|
|
8432
|
+
return new Promise((resolve7) => {
|
|
8433
8433
|
this.writeQueue.push(() => {
|
|
8434
8434
|
this.writerWaiting = this.writeQueue.length > 0;
|
|
8435
8435
|
this.writer = true;
|
|
8436
|
-
|
|
8436
|
+
resolve7();
|
|
8437
8437
|
});
|
|
8438
8438
|
});
|
|
8439
8439
|
}
|
|
@@ -8605,7 +8605,7 @@ import { z as z4 } from "zod";
|
|
|
8605
8605
|
// package.json
|
|
8606
8606
|
var package_default = {
|
|
8607
8607
|
name: "@posthog/agent",
|
|
8608
|
-
version: "2.3.
|
|
8608
|
+
version: "2.3.388",
|
|
8609
8609
|
repository: "https://github.com/PostHog/code",
|
|
8610
8610
|
description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
8611
8611
|
exports: {
|
|
@@ -8665,6 +8665,10 @@ var package_default = {
|
|
|
8665
8665
|
types: "./dist/resume.d.ts",
|
|
8666
8666
|
import: "./dist/resume.js"
|
|
8667
8667
|
},
|
|
8668
|
+
"./handoff-checkpoint": {
|
|
8669
|
+
types: "./dist/handoff-checkpoint.d.ts",
|
|
8670
|
+
import: "./dist/handoff-checkpoint.js"
|
|
8671
|
+
},
|
|
8668
8672
|
"./tree-tracker": {
|
|
8669
8673
|
types: "./dist/tree-tracker.d.ts",
|
|
8670
8674
|
import: "./dist/tree-tracker.js"
|
|
@@ -8672,6 +8676,10 @@ var package_default = {
|
|
|
8672
8676
|
"./server": {
|
|
8673
8677
|
types: "./dist/server/agent-server.d.ts",
|
|
8674
8678
|
import: "./dist/server/agent-server.js"
|
|
8679
|
+
},
|
|
8680
|
+
"./server/schemas": {
|
|
8681
|
+
types: "./dist/server/schemas.d.ts",
|
|
8682
|
+
import: "./dist/server/schemas.js"
|
|
8675
8683
|
}
|
|
8676
8684
|
},
|
|
8677
8685
|
bin: {
|
|
@@ -8762,6 +8770,8 @@ var POSTHOG_NOTIFICATIONS = {
|
|
|
8762
8770
|
SDK_SESSION: "_posthog/sdk_session",
|
|
8763
8771
|
/** Tree state snapshot captured (git tree hash + file archive) */
|
|
8764
8772
|
TREE_SNAPSHOT: "_posthog/tree_snapshot",
|
|
8773
|
+
/** Git checkpoint captured for handoff */
|
|
8774
|
+
GIT_CHECKPOINT: "_posthog/git_checkpoint",
|
|
8765
8775
|
/** Agent mode changed (interactive/background) */
|
|
8766
8776
|
MODE_CHANGE: "_posthog/mode_change",
|
|
8767
8777
|
/** Request to resume a session from previous state */
|
|
@@ -8868,17 +8878,17 @@ var Pushable = class {
|
|
|
8868
8878
|
resolvers = [];
|
|
8869
8879
|
done = false;
|
|
8870
8880
|
push(item) {
|
|
8871
|
-
const
|
|
8872
|
-
if (
|
|
8873
|
-
|
|
8881
|
+
const resolve7 = this.resolvers.shift();
|
|
8882
|
+
if (resolve7) {
|
|
8883
|
+
resolve7({ value: item, done: false });
|
|
8874
8884
|
} else {
|
|
8875
8885
|
this.queue.push(item);
|
|
8876
8886
|
}
|
|
8877
8887
|
}
|
|
8878
8888
|
end() {
|
|
8879
8889
|
this.done = true;
|
|
8880
|
-
for (const
|
|
8881
|
-
|
|
8890
|
+
for (const resolve7 of this.resolvers) {
|
|
8891
|
+
resolve7({ value: void 0, done: true });
|
|
8882
8892
|
}
|
|
8883
8893
|
this.resolvers = [];
|
|
8884
8894
|
}
|
|
@@ -8895,8 +8905,8 @@ var Pushable = class {
|
|
|
8895
8905
|
done: true
|
|
8896
8906
|
});
|
|
8897
8907
|
}
|
|
8898
|
-
return new Promise((
|
|
8899
|
-
this.resolvers.push(
|
|
8908
|
+
return new Promise((resolve7) => {
|
|
8909
|
+
this.resolvers.push(resolve7);
|
|
8900
8910
|
});
|
|
8901
8911
|
}
|
|
8902
8912
|
};
|
|
@@ -9010,20 +9020,20 @@ function nodeReadableToWebReadable(nodeStream) {
|
|
|
9010
9020
|
function nodeWritableToWebWritable(nodeStream) {
|
|
9011
9021
|
return new WritableStream2({
|
|
9012
9022
|
write(chunk) {
|
|
9013
|
-
return new Promise((
|
|
9023
|
+
return new Promise((resolve7, reject) => {
|
|
9014
9024
|
const ok = nodeStream.write(Buffer.from(chunk), (err2) => {
|
|
9015
9025
|
if (err2) reject(err2);
|
|
9016
9026
|
});
|
|
9017
9027
|
if (ok) {
|
|
9018
|
-
|
|
9028
|
+
resolve7();
|
|
9019
9029
|
} else {
|
|
9020
|
-
nodeStream.once("drain",
|
|
9030
|
+
nodeStream.once("drain", resolve7);
|
|
9021
9031
|
}
|
|
9022
9032
|
});
|
|
9023
9033
|
},
|
|
9024
9034
|
close() {
|
|
9025
|
-
return new Promise((
|
|
9026
|
-
nodeStream.end(
|
|
9035
|
+
return new Promise((resolve7) => {
|
|
9036
|
+
nodeStream.end(resolve7);
|
|
9027
9037
|
});
|
|
9028
9038
|
},
|
|
9029
9039
|
abort(reason) {
|
|
@@ -12970,9 +12980,9 @@ var PostHogEnricher = class {
|
|
|
12970
12980
|
}
|
|
12971
12981
|
let mtimeMs = 0;
|
|
12972
12982
|
try {
|
|
12973
|
-
const
|
|
12974
|
-
mtimeMs =
|
|
12975
|
-
if (
|
|
12983
|
+
const stat22 = await fs4.stat(absPath);
|
|
12984
|
+
mtimeMs = stat22.mtimeMs;
|
|
12985
|
+
if (stat22.size > MAX_WRAPPER_SOURCE_BYTES) {
|
|
12976
12986
|
return this.setWrapperCache(absPath, mtimeMs, []);
|
|
12977
12987
|
}
|
|
12978
12988
|
} catch {
|
|
@@ -13138,7 +13148,7 @@ async function buildWrapperContext(deps, content, langId, absPath) {
|
|
|
13138
13148
|
// src/utils/common.ts
|
|
13139
13149
|
async function withTimeout(operation, timeoutMs) {
|
|
13140
13150
|
const timeoutPromise = new Promise(
|
|
13141
|
-
(
|
|
13151
|
+
(resolve7) => setTimeout(() => resolve7({ result: "timeout" }), timeoutMs)
|
|
13142
13152
|
);
|
|
13143
13153
|
const operationPromise = operation.then((value) => ({
|
|
13144
13154
|
result: "success",
|
|
@@ -13436,8 +13446,8 @@ var ToolContentBuilder = class {
|
|
|
13436
13446
|
this.items.push({ type: "content", content: image(data, mimeType, uri) });
|
|
13437
13447
|
return this;
|
|
13438
13448
|
}
|
|
13439
|
-
diff(
|
|
13440
|
-
this.items.push({ type: "diff", path:
|
|
13449
|
+
diff(path17, oldText, newText) {
|
|
13450
|
+
this.items.push({ type: "diff", path: path17, oldText, newText });
|
|
13441
13451
|
return this;
|
|
13442
13452
|
}
|
|
13443
13453
|
build() {
|
|
@@ -13624,7 +13634,7 @@ function buildToolKey(serverName, toolName) {
|
|
|
13624
13634
|
return `mcp__${serverName}__${toolName}`;
|
|
13625
13635
|
}
|
|
13626
13636
|
function delay2(ms) {
|
|
13627
|
-
return new Promise((
|
|
13637
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
13628
13638
|
}
|
|
13629
13639
|
async function fetchMcpToolMetadata(q, logger = new Logger({ debug: false, prefix: "[McpToolMetadata]" })) {
|
|
13630
13640
|
let retries = 0;
|
|
@@ -15945,8 +15955,8 @@ var AsyncMutex = class {
|
|
|
15945
15955
|
this.locked = true;
|
|
15946
15956
|
return;
|
|
15947
15957
|
}
|
|
15948
|
-
return new Promise((
|
|
15949
|
-
this.queue.push(
|
|
15958
|
+
return new Promise((resolve7) => {
|
|
15959
|
+
this.queue.push(resolve7);
|
|
15950
15960
|
});
|
|
15951
15961
|
}
|
|
15952
15962
|
release() {
|
|
@@ -16473,8 +16483,8 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
|
|
|
16473
16483
|
if (this.session.promptRunning) {
|
|
16474
16484
|
this.session.input.push(userMessage);
|
|
16475
16485
|
const order = this.session.nextPendingOrder++;
|
|
16476
|
-
const cancelled = await new Promise((
|
|
16477
|
-
this.session.pendingMessages.set(promptUuid, { resolve:
|
|
16486
|
+
const cancelled = await new Promise((resolve7) => {
|
|
16487
|
+
this.session.pendingMessages.set(promptUuid, { resolve: resolve7, order });
|
|
16478
16488
|
});
|
|
16479
16489
|
if (cancelled) {
|
|
16480
16490
|
return { stopReason: "cancelled" };
|
|
@@ -17355,7 +17365,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
|
|
|
17355
17365
|
*/
|
|
17356
17366
|
deferBackgroundFetches(q) {
|
|
17357
17367
|
Promise.all([
|
|
17358
|
-
new Promise((
|
|
17368
|
+
new Promise((resolve7) => setTimeout(resolve7, 10)).then(
|
|
17359
17369
|
() => this.sendAvailableCommandsUpdate()
|
|
17360
17370
|
),
|
|
17361
17371
|
fetchMcpToolMetadata(q, this.logger).then(() => {
|
|
@@ -18323,245 +18333,19 @@ function createCodexConnection(config) {
|
|
|
18323
18333
|
};
|
|
18324
18334
|
}
|
|
18325
18335
|
|
|
18326
|
-
// src/
|
|
18327
|
-
|
|
18328
|
-
|
|
18329
|
-
const hostname = url.hostname;
|
|
18330
|
-
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
18331
|
-
return `${url.protocol}//localhost:3308`;
|
|
18332
|
-
}
|
|
18333
|
-
if (hostname === "host.docker.internal") {
|
|
18334
|
-
return `${url.protocol}//host.docker.internal:3308`;
|
|
18335
|
-
}
|
|
18336
|
-
const region = hostname.match(/^(us|eu)\.posthog\.com$/)?.[1] ?? "us";
|
|
18337
|
-
return `https://gateway.${region}.posthog.com`;
|
|
18338
|
-
}
|
|
18339
|
-
function getLlmGatewayUrl(posthogHost, product = "posthog_code") {
|
|
18340
|
-
return `${getGatewayBaseUrl(posthogHost)}/${product}`;
|
|
18341
|
-
}
|
|
18336
|
+
// src/handoff-checkpoint.ts
|
|
18337
|
+
import { mkdir as mkdir4, readFile as readFile4, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
18338
|
+
import { join as join9 } from "path";
|
|
18342
18339
|
|
|
18343
|
-
//
|
|
18344
|
-
|
|
18345
|
-
|
|
18346
|
-
|
|
18347
|
-
constructor(config) {
|
|
18348
|
-
this.config = config;
|
|
18349
|
-
}
|
|
18350
|
-
get baseUrl() {
|
|
18351
|
-
const host = this.config.apiUrl.endsWith("/") ? this.config.apiUrl.slice(0, -1) : this.config.apiUrl;
|
|
18352
|
-
return host;
|
|
18353
|
-
}
|
|
18354
|
-
isAuthFailure(status) {
|
|
18355
|
-
return status === 401 || status === 403;
|
|
18356
|
-
}
|
|
18357
|
-
async resolveApiKey(forceRefresh = false) {
|
|
18358
|
-
if (forceRefresh && this.config.refreshApiKey) {
|
|
18359
|
-
return this.config.refreshApiKey();
|
|
18360
|
-
}
|
|
18361
|
-
return this.config.getApiKey();
|
|
18362
|
-
}
|
|
18363
|
-
async buildHeaders(options, forceRefresh = false) {
|
|
18364
|
-
const headers = new Headers(options.headers);
|
|
18365
|
-
headers.set(
|
|
18366
|
-
"Authorization",
|
|
18367
|
-
`Bearer ${await this.resolveApiKey(forceRefresh)}`
|
|
18368
|
-
);
|
|
18369
|
-
headers.set("Content-Type", "application/json");
|
|
18370
|
-
headers.set("User-Agent", this.config.userAgent ?? DEFAULT_USER_AGENT);
|
|
18371
|
-
return headers;
|
|
18372
|
-
}
|
|
18373
|
-
async performRequest(endpoint, options, forceRefresh = false) {
|
|
18374
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
18375
|
-
return fetch(url, {
|
|
18376
|
-
...options,
|
|
18377
|
-
headers: await this.buildHeaders(options, forceRefresh)
|
|
18378
|
-
});
|
|
18379
|
-
}
|
|
18380
|
-
async performRequestWithRetry(endpoint, options = {}) {
|
|
18381
|
-
let response = await this.performRequest(endpoint, options);
|
|
18382
|
-
if (!response.ok && this.isAuthFailure(response.status)) {
|
|
18383
|
-
response = await this.performRequest(endpoint, options, true);
|
|
18384
|
-
}
|
|
18385
|
-
return response;
|
|
18386
|
-
}
|
|
18387
|
-
async apiRequest(endpoint, options = {}) {
|
|
18388
|
-
const response = await this.performRequestWithRetry(endpoint, options);
|
|
18389
|
-
if (!response.ok) {
|
|
18390
|
-
let errorMessage;
|
|
18391
|
-
try {
|
|
18392
|
-
const errorResponse = await response.json();
|
|
18393
|
-
errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
|
|
18394
|
-
} catch {
|
|
18395
|
-
errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
|
|
18396
|
-
}
|
|
18397
|
-
throw new Error(errorMessage);
|
|
18398
|
-
}
|
|
18399
|
-
return response.json();
|
|
18400
|
-
}
|
|
18401
|
-
getTeamId() {
|
|
18402
|
-
return this.config.projectId;
|
|
18403
|
-
}
|
|
18404
|
-
async getApiKey(forceRefresh = false) {
|
|
18405
|
-
return this.resolveApiKey(forceRefresh);
|
|
18406
|
-
}
|
|
18407
|
-
getLlmGatewayUrl() {
|
|
18408
|
-
return getLlmGatewayUrl(this.baseUrl);
|
|
18409
|
-
}
|
|
18410
|
-
async getTask(taskId) {
|
|
18411
|
-
const teamId = this.getTeamId();
|
|
18412
|
-
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
|
|
18413
|
-
}
|
|
18414
|
-
async getTaskRun(taskId, runId) {
|
|
18415
|
-
const teamId = this.getTeamId();
|
|
18416
|
-
return this.apiRequest(
|
|
18417
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
18418
|
-
);
|
|
18419
|
-
}
|
|
18420
|
-
async updateTaskRun(taskId, runId, payload) {
|
|
18421
|
-
const teamId = this.getTeamId();
|
|
18422
|
-
return this.apiRequest(
|
|
18423
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
|
|
18424
|
-
{
|
|
18425
|
-
method: "PATCH",
|
|
18426
|
-
body: JSON.stringify(payload)
|
|
18427
|
-
}
|
|
18428
|
-
);
|
|
18429
|
-
}
|
|
18430
|
-
async setTaskRunOutput(taskId, runId, output) {
|
|
18431
|
-
return this.apiRequest(
|
|
18432
|
-
`/api/projects/${this.getTeamId()}/tasks/${taskId}/runs/${runId}/set_output/`,
|
|
18433
|
-
{
|
|
18434
|
-
method: "PATCH",
|
|
18435
|
-
body: JSON.stringify(output)
|
|
18436
|
-
}
|
|
18437
|
-
);
|
|
18438
|
-
}
|
|
18439
|
-
async appendTaskRunLog(taskId, runId, entries) {
|
|
18440
|
-
const teamId = this.getTeamId();
|
|
18441
|
-
return this.apiRequest(
|
|
18442
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
|
|
18443
|
-
{
|
|
18444
|
-
method: "POST",
|
|
18445
|
-
body: JSON.stringify({ entries })
|
|
18446
|
-
}
|
|
18447
|
-
);
|
|
18448
|
-
}
|
|
18449
|
-
async relayMessage(taskId, runId, text2) {
|
|
18450
|
-
const teamId = this.getTeamId();
|
|
18451
|
-
await this.apiRequest(
|
|
18452
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
|
|
18453
|
-
{
|
|
18454
|
-
method: "POST",
|
|
18455
|
-
body: JSON.stringify({ text: text2 })
|
|
18456
|
-
}
|
|
18457
|
-
);
|
|
18458
|
-
}
|
|
18459
|
-
async uploadTaskArtifacts(taskId, runId, artifacts) {
|
|
18460
|
-
if (!artifacts.length) {
|
|
18461
|
-
return [];
|
|
18462
|
-
}
|
|
18463
|
-
const teamId = this.getTeamId();
|
|
18464
|
-
const response = await this.apiRequest(
|
|
18465
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
|
|
18466
|
-
{
|
|
18467
|
-
method: "POST",
|
|
18468
|
-
body: JSON.stringify({ artifacts })
|
|
18469
|
-
}
|
|
18470
|
-
);
|
|
18471
|
-
return response.artifacts ?? [];
|
|
18472
|
-
}
|
|
18473
|
-
/**
|
|
18474
|
-
* Download artifact content by storage path
|
|
18475
|
-
* Streams the file through the PostHog backend so the sandbox does not need
|
|
18476
|
-
* direct access to object storage.
|
|
18477
|
-
*/
|
|
18478
|
-
async downloadArtifact(taskId, runId, storagePath) {
|
|
18479
|
-
const teamId = this.getTeamId();
|
|
18480
|
-
try {
|
|
18481
|
-
const response = await this.performRequestWithRetry(
|
|
18482
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
|
|
18483
|
-
{
|
|
18484
|
-
method: "POST",
|
|
18485
|
-
body: JSON.stringify({ storage_path: storagePath })
|
|
18486
|
-
}
|
|
18487
|
-
);
|
|
18488
|
-
if (!response.ok) {
|
|
18489
|
-
throw new Error(`Failed to download artifact: ${response.status}`);
|
|
18490
|
-
}
|
|
18491
|
-
return response.arrayBuffer();
|
|
18492
|
-
} catch {
|
|
18493
|
-
return null;
|
|
18494
|
-
}
|
|
18495
|
-
}
|
|
18496
|
-
/**
|
|
18497
|
-
* Fetch logs for a task run via the logs API endpoint
|
|
18498
|
-
* @param taskRun - The task run to fetch logs for
|
|
18499
|
-
* @returns Array of stored entries, or empty array if no logs available
|
|
18500
|
-
*/
|
|
18501
|
-
async fetchTaskRunLogs(taskRun) {
|
|
18502
|
-
const teamId = this.getTeamId();
|
|
18503
|
-
const endpoint = `/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`;
|
|
18504
|
-
try {
|
|
18505
|
-
const response = await this.performRequestWithRetry(endpoint);
|
|
18506
|
-
if (!response.ok) {
|
|
18507
|
-
if (response.status === 404) {
|
|
18508
|
-
return [];
|
|
18509
|
-
}
|
|
18510
|
-
throw new Error(
|
|
18511
|
-
`Failed to fetch logs: ${response.status} ${response.statusText}`
|
|
18512
|
-
);
|
|
18513
|
-
}
|
|
18514
|
-
const content = await response.text();
|
|
18515
|
-
if (!content.trim()) {
|
|
18516
|
-
return [];
|
|
18517
|
-
}
|
|
18518
|
-
return content.trim().split("\n").map((line) => JSON.parse(line));
|
|
18519
|
-
} catch (error) {
|
|
18520
|
-
throw new Error(
|
|
18521
|
-
`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`
|
|
18522
|
-
);
|
|
18523
|
-
}
|
|
18524
|
-
}
|
|
18525
|
-
};
|
|
18340
|
+
// ../git/dist/handoff.js
|
|
18341
|
+
import { spawn as spawn4 } from "child_process";
|
|
18342
|
+
import { copyFile, mkdir as mkdir3, readFile as readFile3, rm as rm3, stat as stat3 } from "fs/promises";
|
|
18343
|
+
import path13 from "path";
|
|
18526
18344
|
|
|
18527
|
-
//
|
|
18345
|
+
// ../git/dist/sagas/checkpoint.js
|
|
18528
18346
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
18529
18347
|
import * as fs10 from "fs/promises";
|
|
18530
|
-
import * as os6 from "os";
|
|
18531
18348
|
import * as path12 from "path";
|
|
18532
|
-
var CHARS_PER_TOKEN = 4;
|
|
18533
|
-
var DEFAULT_MAX_TOKENS = 15e4;
|
|
18534
|
-
function estimateTurnTokens(turn) {
|
|
18535
|
-
let chars = 0;
|
|
18536
|
-
for (const block of turn.content) {
|
|
18537
|
-
if ("text" in block && typeof block.text === "string") {
|
|
18538
|
-
chars += block.text.length;
|
|
18539
|
-
}
|
|
18540
|
-
}
|
|
18541
|
-
if (turn.toolCalls) {
|
|
18542
|
-
for (const tc of turn.toolCalls) {
|
|
18543
|
-
chars += JSON.stringify(tc.input ?? "").length;
|
|
18544
|
-
if (tc.result !== void 0) {
|
|
18545
|
-
chars += typeof tc.result === "string" ? tc.result.length : JSON.stringify(tc.result).length;
|
|
18546
|
-
}
|
|
18547
|
-
}
|
|
18548
|
-
}
|
|
18549
|
-
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
18550
|
-
}
|
|
18551
|
-
function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
18552
|
-
let budget = maxTokens;
|
|
18553
|
-
let startIndex = turns.length;
|
|
18554
|
-
for (let i2 = turns.length - 1; i2 >= 0; i2--) {
|
|
18555
|
-
const cost = estimateTurnTokens(turns[i2]);
|
|
18556
|
-
if (cost > budget) break;
|
|
18557
|
-
budget -= cost;
|
|
18558
|
-
startIndex = i2;
|
|
18559
|
-
}
|
|
18560
|
-
while (startIndex < turns.length && turns[startIndex].role !== "user") {
|
|
18561
|
-
startIndex++;
|
|
18562
|
-
}
|
|
18563
|
-
return turns.slice(startIndex);
|
|
18564
|
-
}
|
|
18565
18349
|
|
|
18566
18350
|
// ../shared/dist/index.js
|
|
18567
18351
|
var CLOUD_PROMPT_PREFIX = "__twig_cloud_prompt_v1__:";
|
|
@@ -18705,16 +18489,6 @@ var Saga = class {
|
|
|
18705
18489
|
}
|
|
18706
18490
|
};
|
|
18707
18491
|
|
|
18708
|
-
// src/sagas/apply-snapshot-saga.ts
|
|
18709
|
-
import { mkdir as mkdir4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
18710
|
-
import { join as join10 } from "path";
|
|
18711
|
-
|
|
18712
|
-
// ../git/dist/sagas/tree.js
|
|
18713
|
-
import { existsSync as existsSync5 } from "fs";
|
|
18714
|
-
import * as fs11 from "fs/promises";
|
|
18715
|
-
import * as path13 from "path";
|
|
18716
|
-
import * as tar from "tar";
|
|
18717
|
-
|
|
18718
18492
|
// ../git/dist/git-saga.js
|
|
18719
18493
|
var GitSaga = class extends Saga {
|
|
18720
18494
|
_git = null;
|
|
@@ -18733,660 +18507,1185 @@ var GitSaga = class extends Saga {
|
|
|
18733
18507
|
}
|
|
18734
18508
|
};
|
|
18735
18509
|
|
|
18736
|
-
// ../git/dist/sagas/
|
|
18737
|
-
var
|
|
18738
|
-
|
|
18739
|
-
|
|
18510
|
+
// ../git/dist/sagas/checkpoint.js
|
|
18511
|
+
var CHECKPOINT_REF_PREFIX = "refs/posthog-code-checkpoint/";
|
|
18512
|
+
var CHECKPOINT_VERSION = "v1";
|
|
18513
|
+
var UNMERGED_INDEX_ERROR = "Cannot capture checkpoint with unresolved merge conflicts in the index";
|
|
18514
|
+
var GIT_BUSY_ERROR = "Cannot capture checkpoint while git operation is in progress";
|
|
18515
|
+
var CHECKPOINT_AUTHOR = {
|
|
18516
|
+
name: "PostHog Code",
|
|
18517
|
+
email: "posthog-code@local"
|
|
18518
|
+
};
|
|
18519
|
+
var CaptureCheckpointSaga = class extends GitSaga {
|
|
18520
|
+
sagaName = "CaptureCheckpointSaga";
|
|
18740
18521
|
async executeGitOperations(input) {
|
|
18741
|
-
const { baseDir
|
|
18742
|
-
const
|
|
18743
|
-
await this.
|
|
18744
|
-
|
|
18745
|
-
|
|
18522
|
+
const { baseDir } = input;
|
|
18523
|
+
const headInfo = await this.readOnlyStep("get_head_info", () => getHeadInfo(this.git));
|
|
18524
|
+
const busyState = await this.readOnlyStep("check_git_busy", () => getGitBusyState(this.git));
|
|
18525
|
+
if (busyState.busy) {
|
|
18526
|
+
throw new Error(`${GIT_BUSY_ERROR}: ${busyState.operation}`);
|
|
18527
|
+
}
|
|
18528
|
+
const hasUnmerged = await this.readOnlyStep("check_unmerged_index", () => hasUnmergedEntries(this.git));
|
|
18529
|
+
if (hasUnmerged) {
|
|
18530
|
+
throw new Error(UNMERGED_INDEX_ERROR);
|
|
18531
|
+
}
|
|
18532
|
+
const indexTree = await this.readOnlyStep("write_index_tree", () => this.git.raw(["write-tree"]));
|
|
18533
|
+
const worktreeTree = await this.readOnlyStep("write_worktree_tree", () => createWorktreeTree(this.git, baseDir, headInfo.head));
|
|
18534
|
+
const metaTree = await this.readOnlyStep("write_meta_tree", () => createMetaTree(this.git, baseDir, indexTree.trim(), worktreeTree.trim()));
|
|
18535
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
18536
|
+
const message = formatCheckpointMessage({
|
|
18537
|
+
head: headInfo.head,
|
|
18538
|
+
branch: headInfo.branch,
|
|
18539
|
+
indexTree: indexTree.trim(),
|
|
18540
|
+
worktreeTree: worktreeTree.trim(),
|
|
18541
|
+
timestamp
|
|
18542
|
+
});
|
|
18543
|
+
const commitHash = await this.step({
|
|
18544
|
+
name: "create_checkpoint_commit",
|
|
18545
|
+
execute: async () => {
|
|
18546
|
+
const commitGit = this.git.env({
|
|
18547
|
+
...process.env,
|
|
18548
|
+
GIT_AUTHOR_NAME: CHECKPOINT_AUTHOR.name,
|
|
18549
|
+
GIT_AUTHOR_EMAIL: CHECKPOINT_AUTHOR.email,
|
|
18550
|
+
GIT_COMMITTER_NAME: CHECKPOINT_AUTHOR.name,
|
|
18551
|
+
GIT_COMMITTER_EMAIL: CHECKPOINT_AUTHOR.email
|
|
18552
|
+
});
|
|
18553
|
+
const rawCommit = await commitGit.raw([
|
|
18554
|
+
"commit-tree",
|
|
18555
|
+
metaTree.trim(),
|
|
18556
|
+
...headInfo.head ? ["-p", headInfo.head] : [],
|
|
18557
|
+
"-m",
|
|
18558
|
+
message
|
|
18559
|
+
]);
|
|
18560
|
+
return rawCommit.trim();
|
|
18561
|
+
},
|
|
18746
18562
|
rollback: async () => {
|
|
18747
18563
|
}
|
|
18748
18564
|
});
|
|
18749
|
-
|
|
18750
|
-
const
|
|
18751
|
-
|
|
18752
|
-
GIT_INDEX_FILE: this.tempIndexPath
|
|
18753
|
-
});
|
|
18754
|
-
await this.step({
|
|
18755
|
-
name: "init_temp_index",
|
|
18756
|
-
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
18757
|
-
rollback: async () => {
|
|
18758
|
-
if (this.tempIndexPath) {
|
|
18759
|
-
await fs11.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
18760
|
-
});
|
|
18761
|
-
}
|
|
18762
|
-
}
|
|
18763
|
-
});
|
|
18764
|
-
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
18765
|
-
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
18766
|
-
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
18767
|
-
this.log.debug("No changes since last capture", { treeHash });
|
|
18768
|
-
await fs11.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
18769
|
-
});
|
|
18770
|
-
return { snapshot: null, changed: false };
|
|
18771
|
-
}
|
|
18772
|
-
const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
|
|
18565
|
+
const checkpointId = input.checkpointId ?? randomUUID2();
|
|
18566
|
+
const refName = `${CHECKPOINT_REF_PREFIX}${checkpointId}`;
|
|
18567
|
+
const existingRef = await this.readOnlyStep("check_existing_ref", async () => {
|
|
18773
18568
|
try {
|
|
18774
|
-
|
|
18569
|
+
await this.git.revparse(["--verify", refName]);
|
|
18570
|
+
return true;
|
|
18775
18571
|
} catch {
|
|
18776
|
-
return
|
|
18572
|
+
return false;
|
|
18777
18573
|
}
|
|
18778
18574
|
});
|
|
18779
|
-
|
|
18780
|
-
|
|
18575
|
+
if (existingRef) {
|
|
18576
|
+
throw new Error(`Checkpoint ref already exists: ${refName}`);
|
|
18577
|
+
}
|
|
18578
|
+
await this.step({
|
|
18579
|
+
name: "update_checkpoint_ref",
|
|
18580
|
+
execute: () => this.git.raw(["update-ref", refName, commitHash]),
|
|
18581
|
+
rollback: async () => {
|
|
18582
|
+
await this.git.raw(["update-ref", "-d", refName]).catch(() => {
|
|
18583
|
+
});
|
|
18584
|
+
}
|
|
18781
18585
|
});
|
|
18782
|
-
|
|
18783
|
-
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18586
|
+
return {
|
|
18587
|
+
checkpointId,
|
|
18588
|
+
commit: commitHash,
|
|
18589
|
+
head: headInfo.head,
|
|
18590
|
+
branch: headInfo.branch,
|
|
18591
|
+
indexTree: indexTree.trim(),
|
|
18592
|
+
worktreeTree: worktreeTree.trim(),
|
|
18593
|
+
timestamp
|
|
18787
18594
|
};
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18595
|
+
}
|
|
18596
|
+
};
|
|
18597
|
+
async function getHeadInfo(git) {
|
|
18598
|
+
let head = null;
|
|
18599
|
+
let branch = null;
|
|
18600
|
+
try {
|
|
18601
|
+
head = (await git.revparse(["HEAD"]))?.trim() || null;
|
|
18602
|
+
} catch {
|
|
18603
|
+
head = null;
|
|
18604
|
+
}
|
|
18605
|
+
try {
|
|
18606
|
+
const rawBranch = await git.raw(["symbolic-ref", "--short", "HEAD"]);
|
|
18607
|
+
branch = rawBranch.trim() || null;
|
|
18608
|
+
} catch {
|
|
18609
|
+
branch = null;
|
|
18610
|
+
}
|
|
18611
|
+
return { head, branch };
|
|
18612
|
+
}
|
|
18613
|
+
async function hasUnmergedEntries(git) {
|
|
18614
|
+
const output = await git.raw(["ls-files", "--unmerged"]);
|
|
18615
|
+
return output.trim().length > 0;
|
|
18616
|
+
}
|
|
18617
|
+
async function getGitBusyState(git) {
|
|
18618
|
+
const toplevel = (await git.raw(["rev-parse", "--show-toplevel"])).trim();
|
|
18619
|
+
const resolveGitPath = async (gitPath) => {
|
|
18620
|
+
const relative = (await git.raw(["rev-parse", "--git-path", gitPath])).trim();
|
|
18621
|
+
return path12.isAbsolute(relative) ? relative : path12.resolve(toplevel, relative);
|
|
18622
|
+
};
|
|
18623
|
+
const pathExists = async (gitPath) => {
|
|
18624
|
+
const resolved = await resolveGitPath(gitPath);
|
|
18625
|
+
try {
|
|
18626
|
+
await fs10.access(resolved);
|
|
18627
|
+
return true;
|
|
18628
|
+
} catch {
|
|
18629
|
+
return false;
|
|
18791
18630
|
}
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18631
|
+
};
|
|
18632
|
+
const dirExists = async (gitPath) => {
|
|
18633
|
+
const resolved = await resolveGitPath(gitPath);
|
|
18634
|
+
try {
|
|
18635
|
+
const stat4 = await fs10.stat(resolved);
|
|
18636
|
+
return stat4.isDirectory();
|
|
18637
|
+
} catch {
|
|
18638
|
+
return false;
|
|
18639
|
+
}
|
|
18640
|
+
};
|
|
18641
|
+
if (await dirExists("rebase-merge") || await dirExists("rebase-apply")) {
|
|
18642
|
+
return { busy: true, operation: "rebase" };
|
|
18643
|
+
}
|
|
18644
|
+
if (await pathExists("MERGE_HEAD")) {
|
|
18645
|
+
return { busy: true, operation: "merge" };
|
|
18646
|
+
}
|
|
18647
|
+
if (await pathExists("CHERRY_PICK_HEAD")) {
|
|
18648
|
+
return { busy: true, operation: "cherry-pick" };
|
|
18649
|
+
}
|
|
18650
|
+
if (await pathExists("REVERT_HEAD")) {
|
|
18651
|
+
return { busy: true, operation: "revert" };
|
|
18652
|
+
}
|
|
18653
|
+
return { busy: false };
|
|
18654
|
+
}
|
|
18655
|
+
async function createWorktreeTree(git, baseDir, head) {
|
|
18656
|
+
const { tempGit, tempIndexPath } = await createTempIndexGit(git, baseDir, "checkpoint-worktree");
|
|
18657
|
+
try {
|
|
18658
|
+
if (head) {
|
|
18659
|
+
await tempGit.raw(["read-tree", head]);
|
|
18660
|
+
} else {
|
|
18661
|
+
await tempGit.raw(["read-tree", "--empty"]);
|
|
18662
|
+
}
|
|
18663
|
+
await tempGit.raw(["add", "-A", "--", "."]);
|
|
18664
|
+
const treeHash = await tempGit.raw(["write-tree"]);
|
|
18665
|
+
return treeHash.trim();
|
|
18666
|
+
} finally {
|
|
18667
|
+
await fs10.rm(tempIndexPath, { force: true }).catch(() => {
|
|
18796
18668
|
});
|
|
18797
|
-
return { snapshot, archivePath: createdArchivePath, changed: true };
|
|
18798
18669
|
}
|
|
18799
|
-
|
|
18800
|
-
|
|
18801
|
-
|
|
18802
|
-
|
|
18670
|
+
}
|
|
18671
|
+
async function createMetaTree(git, baseDir, indexTree, worktreeTree) {
|
|
18672
|
+
const { tempGit, tempIndexPath } = await createTempIndexGit(git, baseDir, "checkpoint-meta");
|
|
18673
|
+
try {
|
|
18674
|
+
await tempGit.raw(["read-tree", "--empty"]);
|
|
18675
|
+
await tempGit.raw([
|
|
18676
|
+
"update-index",
|
|
18677
|
+
"--add",
|
|
18678
|
+
"--cacheinfo",
|
|
18679
|
+
"040000",
|
|
18680
|
+
indexTree,
|
|
18681
|
+
"index"
|
|
18682
|
+
]);
|
|
18683
|
+
await tempGit.raw([
|
|
18684
|
+
"update-index",
|
|
18685
|
+
"--add",
|
|
18686
|
+
"--cacheinfo",
|
|
18687
|
+
"040000",
|
|
18688
|
+
worktreeTree,
|
|
18689
|
+
"worktree"
|
|
18690
|
+
]);
|
|
18691
|
+
const metaTree = await tempGit.raw(["write-tree"]);
|
|
18692
|
+
return metaTree.trim();
|
|
18693
|
+
} finally {
|
|
18694
|
+
await fs10.rm(tempIndexPath, { force: true }).catch(() => {
|
|
18695
|
+
});
|
|
18696
|
+
}
|
|
18697
|
+
}
|
|
18698
|
+
function formatCheckpointMessage(meta) {
|
|
18699
|
+
return [
|
|
18700
|
+
`POSTHOG-CODE-CHECKPOINT ${CHECKPOINT_VERSION}`,
|
|
18701
|
+
`head=${meta.head ?? "null"}`,
|
|
18702
|
+
`branch=${meta.branch ?? "null"}`,
|
|
18703
|
+
`index=${meta.indexTree}`,
|
|
18704
|
+
`worktree=${meta.worktreeTree}`,
|
|
18705
|
+
`timestamp=${meta.timestamp}`
|
|
18706
|
+
].join("\n");
|
|
18707
|
+
}
|
|
18708
|
+
async function getGitCommonDir(git, baseDir) {
|
|
18709
|
+
const raw = await git.raw(["rev-parse", "--git-common-dir"]);
|
|
18710
|
+
const dir = raw.trim() || ".git";
|
|
18711
|
+
return path12.isAbsolute(dir) ? dir : path12.resolve(baseDir, dir);
|
|
18712
|
+
}
|
|
18713
|
+
async function createTempIndexGit(git, baseDir, label) {
|
|
18714
|
+
const tmpDir = path12.join(await getGitCommonDir(git, baseDir), "posthog-code-tmp");
|
|
18715
|
+
await fs10.mkdir(tmpDir, { recursive: true });
|
|
18716
|
+
const tempIndexPath = path12.join(tmpDir, `${label}-${Date.now()}-${randomUUID2()}`);
|
|
18717
|
+
const tempGit = createGitClient(baseDir).env({
|
|
18718
|
+
...process.env,
|
|
18719
|
+
GIT_INDEX_FILE: tempIndexPath
|
|
18720
|
+
});
|
|
18721
|
+
return { tempGit, tempIndexPath };
|
|
18722
|
+
}
|
|
18723
|
+
async function refExists(git, refName) {
|
|
18724
|
+
try {
|
|
18725
|
+
await git.revparse(["--verify", refName]);
|
|
18726
|
+
return true;
|
|
18727
|
+
} catch {
|
|
18728
|
+
return false;
|
|
18729
|
+
}
|
|
18730
|
+
}
|
|
18731
|
+
async function deleteCheckpoint(git, checkpointId) {
|
|
18732
|
+
const refName = `${CHECKPOINT_REF_PREFIX}${checkpointId}`;
|
|
18733
|
+
const exists2 = await refExists(git, refName);
|
|
18734
|
+
if (!exists2) {
|
|
18735
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
18736
|
+
}
|
|
18737
|
+
await git.raw(["update-ref", "-d", refName]);
|
|
18738
|
+
}
|
|
18739
|
+
|
|
18740
|
+
// ../git/dist/handoff.js
|
|
18741
|
+
var HANDOFF_HEAD_REF_PREFIX = "refs/posthog-code-handoff/head/";
|
|
18742
|
+
var CHECKPOINT_REF_PREFIX2 = "refs/posthog-code-checkpoint/";
|
|
18743
|
+
var GitHandoffTracker = class {
|
|
18744
|
+
repositoryPath;
|
|
18745
|
+
logger;
|
|
18746
|
+
constructor(config) {
|
|
18747
|
+
this.repositoryPath = config.repositoryPath;
|
|
18748
|
+
this.logger = config.logger;
|
|
18749
|
+
}
|
|
18750
|
+
async captureForHandoff(localGitState) {
|
|
18751
|
+
const captureSaga = new CaptureCheckpointSaga(this.logger);
|
|
18752
|
+
const result = await captureSaga.run({ baseDir: this.repositoryPath });
|
|
18753
|
+
if (!result.success) {
|
|
18754
|
+
throw new Error(`Failed to capture checkpoint at step '${result.failedStep}': ${result.error}`);
|
|
18755
|
+
}
|
|
18756
|
+
const checkpoint = result.data;
|
|
18757
|
+
const git = createGitClient(this.repositoryPath);
|
|
18758
|
+
const tempDir = await this.getTempDir(git);
|
|
18759
|
+
const checkpointRef = `${CHECKPOINT_REF_PREFIX2}${checkpoint.checkpointId}`;
|
|
18760
|
+
const shouldIncludeHead = !!checkpoint.head && checkpoint.head !== localGitState?.head;
|
|
18761
|
+
const headRef = shouldIncludeHead ? `${HANDOFF_HEAD_REF_PREFIX}${checkpoint.checkpointId}` : void 0;
|
|
18762
|
+
const packPrefix = path13.join(tempDir, checkpoint.checkpointId);
|
|
18763
|
+
try {
|
|
18764
|
+
const [headPack, indexFile, tracking] = await Promise.all([
|
|
18765
|
+
shouldIncludeHead && checkpoint.head ? this.captureHeadPack(packPrefix, checkpoint.head) : Promise.resolve(void 0),
|
|
18766
|
+
this.copyIndexFile(git, checkpoint.checkpointId),
|
|
18767
|
+
getTrackingMetadata(git, checkpoint.branch)
|
|
18768
|
+
]);
|
|
18769
|
+
return {
|
|
18770
|
+
checkpoint: {
|
|
18771
|
+
checkpointId: checkpoint.checkpointId,
|
|
18772
|
+
commit: checkpoint.commit,
|
|
18773
|
+
checkpointRef,
|
|
18774
|
+
headRef,
|
|
18775
|
+
head: checkpoint.head,
|
|
18776
|
+
branch: checkpoint.branch,
|
|
18777
|
+
indexTree: checkpoint.indexTree,
|
|
18778
|
+
worktreeTree: checkpoint.worktreeTree,
|
|
18779
|
+
timestamp: checkpoint.timestamp,
|
|
18780
|
+
upstreamRemote: tracking.upstreamRemote,
|
|
18781
|
+
upstreamMergeRef: tracking.upstreamMergeRef,
|
|
18782
|
+
remoteUrl: tracking.remoteUrl
|
|
18783
|
+
},
|
|
18784
|
+
headPack,
|
|
18785
|
+
indexFile,
|
|
18786
|
+
totalBytes: (headPack?.rawBytes ?? 0) + indexFile.rawBytes
|
|
18787
|
+
};
|
|
18788
|
+
} finally {
|
|
18789
|
+
await deleteCheckpoint(git, checkpoint.checkpointId).catch(() => {
|
|
18790
|
+
});
|
|
18803
18791
|
}
|
|
18804
|
-
|
|
18805
|
-
|
|
18806
|
-
|
|
18792
|
+
}
|
|
18793
|
+
async applyFromHandoff(input) {
|
|
18794
|
+
const { checkpoint, headPackPath, indexPath, localGitState, onDivergedBranch } = input;
|
|
18795
|
+
const git = createGitClient(this.repositoryPath);
|
|
18796
|
+
if (headPackPath) {
|
|
18797
|
+
await this.unpackPackFile(headPackPath);
|
|
18807
18798
|
}
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
await tar.create({
|
|
18814
|
-
gzip: true,
|
|
18815
|
-
file: archivePath,
|
|
18816
|
-
cwd: baseDir
|
|
18817
|
-
}, existingFiles);
|
|
18818
|
-
},
|
|
18819
|
-
rollback: async () => {
|
|
18820
|
-
await fs11.rm(archivePath, { force: true }).catch(() => {
|
|
18821
|
-
});
|
|
18799
|
+
if (checkpoint.branch && checkpoint.head) {
|
|
18800
|
+
const branchStatus2 = await this.resolveBranchRestoreStatus(git, checkpoint.branch, checkpoint.head, localGitState);
|
|
18801
|
+
const tracking = this.getPreferredTracking(localGitState, checkpoint);
|
|
18802
|
+
if (branchStatus2.kind === "diverged" && !await onDivergedBranch?.(branchStatus2.divergence)) {
|
|
18803
|
+
throw new Error(`Handoff aborted: local branch '${checkpoint.branch}' has diverged`);
|
|
18822
18804
|
}
|
|
18805
|
+
await this.checkoutBranchAtHead(git, checkpoint.branch, checkpoint.head);
|
|
18806
|
+
if (this.shouldRestoreTracking(branchStatus2, localGitState, tracking)) {
|
|
18807
|
+
await this.ensureRemoteForTracking(git, tracking);
|
|
18808
|
+
await this.configureUpstream(git, checkpoint.branch, tracking);
|
|
18809
|
+
}
|
|
18810
|
+
} else if (checkpoint.head) {
|
|
18811
|
+
await git.checkout(checkpoint.head);
|
|
18812
|
+
}
|
|
18813
|
+
if (indexPath) {
|
|
18814
|
+
await this.restoreIndexFile(git, indexPath);
|
|
18815
|
+
}
|
|
18816
|
+
const packBytes = headPackPath ? await this.getFileSize(headPackPath) : 0;
|
|
18817
|
+
const indexBytes = indexPath ? await this.getFileSize(indexPath) : 0;
|
|
18818
|
+
return {
|
|
18819
|
+
packBytes,
|
|
18820
|
+
indexBytes,
|
|
18821
|
+
totalBytes: packBytes + indexBytes
|
|
18822
|
+
};
|
|
18823
|
+
}
|
|
18824
|
+
async captureHeadPack(packPrefix, headCommit) {
|
|
18825
|
+
const hash = await this.runGitWithInput(["pack-objects", packPrefix, "--revs"], `${headCommit}
|
|
18826
|
+
`);
|
|
18827
|
+
const packPath = `${packPrefix}-${hash.trim()}.pack`;
|
|
18828
|
+
const rawBytes = await this.getFileSize(packPath);
|
|
18829
|
+
await rm3(`${packPath}.idx`, { force: true }).catch(() => {
|
|
18823
18830
|
});
|
|
18824
|
-
return
|
|
18831
|
+
return { path: packPath, rawBytes };
|
|
18825
18832
|
}
|
|
18826
|
-
async
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
|
|
18833
|
+
async copyIndexFile(git, checkpointId) {
|
|
18834
|
+
const indexPath = await this.getGitPath(git, "index");
|
|
18835
|
+
const tempDir = await this.getTempDir(git);
|
|
18836
|
+
const copiedIndexPath = path13.join(tempDir, `${checkpointId}.index`);
|
|
18837
|
+
await copyFile(indexPath, copiedIndexPath);
|
|
18838
|
+
return {
|
|
18839
|
+
path: copiedIndexPath,
|
|
18840
|
+
rawBytes: await this.getFileSize(copiedIndexPath)
|
|
18841
|
+
};
|
|
18842
|
+
}
|
|
18843
|
+
async restoreIndexFile(git, indexPath) {
|
|
18844
|
+
const gitIndexPath = await this.getGitPath(git, "index");
|
|
18845
|
+
await copyFile(indexPath, gitIndexPath);
|
|
18846
|
+
}
|
|
18847
|
+
async unpackPackFile(packPath) {
|
|
18848
|
+
const content = await readFile3(packPath);
|
|
18849
|
+
await this.runGitWithBuffer(["unpack-objects", "-r"], content);
|
|
18850
|
+
}
|
|
18851
|
+
getPreferredTracking(localGitState, checkpoint) {
|
|
18852
|
+
const state = localGitState;
|
|
18853
|
+
if (state && hasTrackingConfig(state)) {
|
|
18854
|
+
return {
|
|
18855
|
+
upstreamRemote: state.upstreamRemote ?? null,
|
|
18856
|
+
upstreamMergeRef: state.upstreamMergeRef ?? null,
|
|
18857
|
+
remoteUrl: state.upstreamRemote && state.upstreamRemote === checkpoint.upstreamRemote ? checkpoint.remoteUrl : null
|
|
18858
|
+
};
|
|
18830
18859
|
}
|
|
18831
|
-
|
|
18832
|
-
|
|
18833
|
-
|
|
18834
|
-
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18860
|
+
return {
|
|
18861
|
+
upstreamRemote: checkpoint.upstreamRemote,
|
|
18862
|
+
upstreamMergeRef: checkpoint.upstreamMergeRef,
|
|
18863
|
+
remoteUrl: checkpoint.remoteUrl
|
|
18864
|
+
};
|
|
18865
|
+
}
|
|
18866
|
+
shouldRestoreTracking(branchStatus2, localGitState, tracking) {
|
|
18867
|
+
return branchStatus2.kind === "missing" || !hasTrackingConfig(localGitState) && (tracking.upstreamRemote !== null || tracking.upstreamMergeRef !== null);
|
|
18868
|
+
}
|
|
18869
|
+
async ensureRemoteForTracking(git, tracking) {
|
|
18870
|
+
if (!tracking.upstreamRemote || !tracking.remoteUrl)
|
|
18871
|
+
return;
|
|
18872
|
+
const remotes = await git.getRemotes(true);
|
|
18873
|
+
const existing = remotes.find((remote) => remote.name === tracking.upstreamRemote);
|
|
18874
|
+
if (!existing) {
|
|
18875
|
+
await git.addRemote(tracking.upstreamRemote, tracking.remoteUrl);
|
|
18876
|
+
}
|
|
18877
|
+
}
|
|
18878
|
+
async configureUpstream(git, branch, tracking) {
|
|
18879
|
+
if (tracking.upstreamRemote) {
|
|
18880
|
+
await git.raw([
|
|
18881
|
+
"config",
|
|
18882
|
+
`branch.${branch}.remote`,
|
|
18883
|
+
tracking.upstreamRemote
|
|
18884
|
+
]);
|
|
18885
|
+
}
|
|
18886
|
+
if (tracking.upstreamMergeRef) {
|
|
18887
|
+
await git.raw([
|
|
18888
|
+
"config",
|
|
18889
|
+
`branch.${branch}.merge`,
|
|
18890
|
+
tracking.upstreamMergeRef
|
|
18891
|
+
]);
|
|
18892
|
+
}
|
|
18893
|
+
}
|
|
18894
|
+
async resolveBranchRestoreStatus(git, branch, cloudHead, localGitState) {
|
|
18895
|
+
const branchRef = `refs/heads/${branch}`;
|
|
18896
|
+
const branchExists = await this.refExists(git, branchRef);
|
|
18897
|
+
if (!branchExists) {
|
|
18898
|
+
return { kind: "missing" };
|
|
18899
|
+
}
|
|
18900
|
+
const currentBranchHead = (await git.revparse([branchRef])).trim();
|
|
18901
|
+
const candidateHeads = [
|
|
18902
|
+
currentBranchHead,
|
|
18903
|
+
...localGitState?.branch === branch && localGitState.head ? [localGitState.head] : []
|
|
18904
|
+
].filter((value, index, array) => array.indexOf(value) === index);
|
|
18905
|
+
if (candidateHeads.every((head) => head === cloudHead)) {
|
|
18906
|
+
return { kind: "match" };
|
|
18907
|
+
}
|
|
18908
|
+
const nonAncestorHead = await this.findNonAncestorHead(git, candidateHeads, cloudHead);
|
|
18909
|
+
if (!nonAncestorHead) {
|
|
18910
|
+
return { kind: "fast_forward" };
|
|
18854
18911
|
}
|
|
18855
|
-
return
|
|
18912
|
+
return {
|
|
18913
|
+
kind: "diverged",
|
|
18914
|
+
divergence: {
|
|
18915
|
+
branch,
|
|
18916
|
+
localHead: nonAncestorHead,
|
|
18917
|
+
cloudHead
|
|
18918
|
+
}
|
|
18919
|
+
};
|
|
18856
18920
|
}
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
18860
|
-
|
|
18861
|
-
originalBranch = null;
|
|
18862
|
-
extractedFiles = [];
|
|
18863
|
-
fileBackups = /* @__PURE__ */ new Map();
|
|
18864
|
-
async executeGitOperations(input) {
|
|
18865
|
-
const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
|
|
18866
|
-
const headInfo = await this.readOnlyStep("get_current_head", async () => {
|
|
18867
|
-
let head = null;
|
|
18868
|
-
let branch = null;
|
|
18869
|
-
try {
|
|
18870
|
-
head = await this.git.revparse(["HEAD"]);
|
|
18871
|
-
} catch {
|
|
18872
|
-
head = null;
|
|
18921
|
+
async findNonAncestorHead(_git, heads, cloudHead) {
|
|
18922
|
+
for (const head of heads) {
|
|
18923
|
+
if (head === cloudHead) {
|
|
18924
|
+
continue;
|
|
18873
18925
|
}
|
|
18874
|
-
|
|
18875
|
-
|
|
18876
|
-
} catch {
|
|
18877
|
-
branch = null;
|
|
18926
|
+
if (!await this.isAncestor(head, cloudHead)) {
|
|
18927
|
+
return head;
|
|
18878
18928
|
}
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
if (
|
|
18885
|
-
await
|
|
18886
|
-
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18929
|
+
}
|
|
18930
|
+
return null;
|
|
18931
|
+
}
|
|
18932
|
+
async checkoutBranchAtHead(git, branch, head) {
|
|
18933
|
+
const currentBranch = await getCurrentBranchName(git);
|
|
18934
|
+
if (currentBranch === branch) {
|
|
18935
|
+
await git.reset(["--hard", head]);
|
|
18936
|
+
return;
|
|
18937
|
+
}
|
|
18938
|
+
const branchRef = `refs/heads/${branch}`;
|
|
18939
|
+
if (await this.refExists(git, branchRef)) {
|
|
18940
|
+
await git.branch(["-f", branch, head]);
|
|
18941
|
+
await git.checkout(branch);
|
|
18942
|
+
return;
|
|
18943
|
+
}
|
|
18944
|
+
await git.checkout(["-b", branch, head]);
|
|
18945
|
+
}
|
|
18946
|
+
async refExists(git, ref) {
|
|
18947
|
+
try {
|
|
18948
|
+
await git.revparse(["--verify", ref]);
|
|
18949
|
+
return true;
|
|
18950
|
+
} catch {
|
|
18951
|
+
return false;
|
|
18952
|
+
}
|
|
18953
|
+
}
|
|
18954
|
+
async isAncestor(ancestor, descendant) {
|
|
18955
|
+
const exitCode = await this.runGitProcessAllowingFailure([
|
|
18956
|
+
"merge-base",
|
|
18957
|
+
"--is-ancestor",
|
|
18958
|
+
ancestor,
|
|
18959
|
+
descendant
|
|
18960
|
+
]);
|
|
18961
|
+
return exitCode === 0;
|
|
18962
|
+
}
|
|
18963
|
+
async getTempDir(git) {
|
|
18964
|
+
const raw = await git.raw(["rev-parse", "--git-common-dir"]);
|
|
18965
|
+
const commonDir = raw.trim() || ".git";
|
|
18966
|
+
const resolved = path13.isAbsolute(commonDir) ? commonDir : path13.resolve(this.repositoryPath, commonDir);
|
|
18967
|
+
const tempDir = path13.join(resolved, "posthog-code-tmp");
|
|
18968
|
+
await mkdir3(tempDir, { recursive: true });
|
|
18969
|
+
return tempDir;
|
|
18970
|
+
}
|
|
18971
|
+
async getGitPath(git, gitPath) {
|
|
18972
|
+
const raw = await git.raw(["rev-parse", "--git-path", gitPath]);
|
|
18973
|
+
const resolved = raw.trim();
|
|
18974
|
+
return path13.isAbsolute(resolved) ? resolved : path13.resolve(this.repositoryPath, resolved);
|
|
18975
|
+
}
|
|
18976
|
+
async getFileSize(filePath) {
|
|
18977
|
+
return (await stat3(filePath)).size;
|
|
18978
|
+
}
|
|
18979
|
+
async runGitWithInput(args2, input) {
|
|
18980
|
+
const { stdout } = await this.runGitProcess(args2, input);
|
|
18981
|
+
return stdout;
|
|
18982
|
+
}
|
|
18983
|
+
async runGitWithBuffer(args2, input) {
|
|
18984
|
+
await this.runGitProcess(args2, input);
|
|
18985
|
+
}
|
|
18986
|
+
async runGitProcessAllowingFailure(args2) {
|
|
18987
|
+
return new Promise((resolve7, reject) => {
|
|
18988
|
+
const child = spawn4("git", args2, {
|
|
18989
|
+
cwd: this.repositoryPath,
|
|
18990
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
18991
|
+
});
|
|
18992
|
+
let stderr = "";
|
|
18993
|
+
child.stderr.on("data", (chunk) => {
|
|
18994
|
+
stderr += chunk.toString();
|
|
18995
|
+
});
|
|
18996
|
+
child.on("error", reject);
|
|
18997
|
+
child.on("close", (code) => {
|
|
18998
|
+
if (code === null) {
|
|
18999
|
+
reject(new Error(`git ${args2.join(" ")} exited unexpectedly`));
|
|
19000
|
+
return;
|
|
19001
|
+
}
|
|
19002
|
+
if (code > 1) {
|
|
19003
|
+
reject(new Error(stderr || `git ${args2.join(" ")} failed with code ${code}`));
|
|
19004
|
+
return;
|
|
18890
19005
|
}
|
|
19006
|
+
resolve7(code);
|
|
18891
19007
|
});
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
19008
|
+
});
|
|
19009
|
+
}
|
|
19010
|
+
runGitProcess(args2, input) {
|
|
19011
|
+
return new Promise((resolve7, reject) => {
|
|
19012
|
+
const child = spawn4("git", args2, {
|
|
19013
|
+
cwd: this.repositoryPath,
|
|
19014
|
+
stdio: "pipe"
|
|
19015
|
+
});
|
|
19016
|
+
let stdout = "";
|
|
19017
|
+
let stderr = "";
|
|
19018
|
+
child.stdout.on("data", (chunk) => {
|
|
19019
|
+
stdout += chunk.toString();
|
|
19020
|
+
});
|
|
19021
|
+
child.stderr.on("data", (chunk) => {
|
|
19022
|
+
stderr += chunk.toString();
|
|
19023
|
+
});
|
|
19024
|
+
child.on("error", reject);
|
|
19025
|
+
child.on("close", (code) => {
|
|
19026
|
+
if (code === 0) {
|
|
19027
|
+
resolve7({ stdout, stderr });
|
|
19028
|
+
return;
|
|
18913
19029
|
}
|
|
19030
|
+
reject(new Error(stderr || `git ${args2.join(" ")} failed with code ${code}`));
|
|
18914
19031
|
});
|
|
18915
|
-
|
|
18916
|
-
if (archivePath) {
|
|
18917
|
-
const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
18918
|
-
await this.readOnlyStep("backup_existing_files", async () => {
|
|
18919
|
-
for (const filePath of filesToExtract) {
|
|
18920
|
-
const fullPath = path13.join(baseDir, filePath);
|
|
18921
|
-
try {
|
|
18922
|
-
const content = await fs11.readFile(fullPath);
|
|
18923
|
-
this.fileBackups.set(filePath, content);
|
|
18924
|
-
} catch {
|
|
18925
|
-
}
|
|
18926
|
-
}
|
|
18927
|
-
});
|
|
18928
|
-
await this.step({
|
|
18929
|
-
name: "extract_archive",
|
|
18930
|
-
execute: async () => {
|
|
18931
|
-
await tar.extract({
|
|
18932
|
-
file: archivePath,
|
|
18933
|
-
cwd: baseDir
|
|
18934
|
-
});
|
|
18935
|
-
this.extractedFiles = filesToExtract;
|
|
18936
|
-
},
|
|
18937
|
-
rollback: async () => {
|
|
18938
|
-
for (const filePath of this.extractedFiles) {
|
|
18939
|
-
const fullPath = path13.join(baseDir, filePath);
|
|
18940
|
-
const backup = this.fileBackups.get(filePath);
|
|
18941
|
-
if (backup) {
|
|
18942
|
-
const dir = path13.dirname(fullPath);
|
|
18943
|
-
await fs11.mkdir(dir, { recursive: true }).catch(() => {
|
|
18944
|
-
});
|
|
18945
|
-
await fs11.writeFile(fullPath, backup).catch(() => {
|
|
18946
|
-
});
|
|
18947
|
-
} else {
|
|
18948
|
-
await fs11.rm(fullPath, { force: true }).catch(() => {
|
|
18949
|
-
});
|
|
18950
|
-
}
|
|
18951
|
-
}
|
|
18952
|
-
}
|
|
18953
|
-
});
|
|
18954
|
-
}
|
|
18955
|
-
for (const change of changes.filter((c) => c.status === "D")) {
|
|
18956
|
-
const fullPath = path13.join(baseDir, change.path);
|
|
18957
|
-
const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
|
|
18958
|
-
try {
|
|
18959
|
-
return await fs11.readFile(fullPath);
|
|
18960
|
-
} catch {
|
|
18961
|
-
return null;
|
|
18962
|
-
}
|
|
18963
|
-
});
|
|
18964
|
-
await this.step({
|
|
18965
|
-
name: `delete_${change.path}`,
|
|
18966
|
-
execute: async () => {
|
|
18967
|
-
await fs11.rm(fullPath, { force: true });
|
|
18968
|
-
this.log.debug(`Deleted file: ${change.path}`);
|
|
18969
|
-
},
|
|
18970
|
-
rollback: async () => {
|
|
18971
|
-
if (backupContent) {
|
|
18972
|
-
const dir = path13.dirname(fullPath);
|
|
18973
|
-
await fs11.mkdir(dir, { recursive: true }).catch(() => {
|
|
18974
|
-
});
|
|
18975
|
-
await fs11.writeFile(fullPath, backupContent).catch(() => {
|
|
18976
|
-
});
|
|
18977
|
-
}
|
|
18978
|
-
}
|
|
18979
|
-
});
|
|
18980
|
-
}
|
|
18981
|
-
const deletedCount = changes.filter((c) => c.status === "D").length;
|
|
18982
|
-
this.log.info("Tree applied", {
|
|
18983
|
-
treeHash,
|
|
18984
|
-
totalChanges: changes.length,
|
|
18985
|
-
deletedFiles: deletedCount,
|
|
18986
|
-
checkoutPerformed
|
|
19032
|
+
child.stdin.end(input);
|
|
18987
19033
|
});
|
|
18988
|
-
return { treeHash, checkoutPerformed };
|
|
18989
19034
|
}
|
|
18990
19035
|
};
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
const tmpDir = join10(repositoryPath, ".posthog", "tmp");
|
|
18999
|
-
if (!snapshot.archiveUrl) {
|
|
19000
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19001
|
-
}
|
|
19002
|
-
const archiveUrl = snapshot.archiveUrl;
|
|
19003
|
-
await this.step({
|
|
19004
|
-
name: "create_tmp_dir",
|
|
19005
|
-
execute: () => mkdir4(tmpDir, { recursive: true }),
|
|
19006
|
-
rollback: async () => {
|
|
19007
|
-
}
|
|
19008
|
-
});
|
|
19009
|
-
const archivePath = join10(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
19010
|
-
this.archivePath = archivePath;
|
|
19011
|
-
await this.step({
|
|
19012
|
-
name: "download_archive",
|
|
19013
|
-
execute: async () => {
|
|
19014
|
-
const arrayBuffer = await apiClient.downloadArtifact(
|
|
19015
|
-
taskId,
|
|
19016
|
-
runId,
|
|
19017
|
-
archiveUrl
|
|
19018
|
-
);
|
|
19019
|
-
if (!arrayBuffer) {
|
|
19020
|
-
throw new Error("Failed to download archive");
|
|
19021
|
-
}
|
|
19022
|
-
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19023
|
-
const binaryContent = Buffer.from(base64Content, "base64");
|
|
19024
|
-
await writeFile4(archivePath, binaryContent);
|
|
19025
|
-
},
|
|
19026
|
-
rollback: async () => {
|
|
19027
|
-
if (this.archivePath) {
|
|
19028
|
-
await rm3(this.archivePath, { force: true }).catch(() => {
|
|
19029
|
-
});
|
|
19030
|
-
}
|
|
19031
|
-
}
|
|
19032
|
-
});
|
|
19033
|
-
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
19034
|
-
const applyResult = await gitApplySaga.run({
|
|
19035
|
-
baseDir: repositoryPath,
|
|
19036
|
-
treeHash: snapshot.treeHash,
|
|
19037
|
-
baseCommit: snapshot.baseCommit,
|
|
19038
|
-
changes: snapshot.changes,
|
|
19039
|
-
archivePath: this.archivePath
|
|
19040
|
-
});
|
|
19041
|
-
if (!applyResult.success) {
|
|
19042
|
-
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
19043
|
-
}
|
|
19044
|
-
await rm3(this.archivePath, { force: true }).catch(() => {
|
|
19045
|
-
});
|
|
19046
|
-
this.log.info("Tree snapshot applied", {
|
|
19047
|
-
treeHash: snapshot.treeHash,
|
|
19048
|
-
totalChanges: snapshot.changes.length,
|
|
19049
|
-
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
19050
|
-
});
|
|
19051
|
-
return { treeHash: snapshot.treeHash };
|
|
19036
|
+
async function getCurrentBranchName(git) {
|
|
19037
|
+
try {
|
|
19038
|
+
const raw = await git.revparse(["--abbrev-ref", "HEAD"]);
|
|
19039
|
+
const branch = raw.trim();
|
|
19040
|
+
return branch === "HEAD" ? null : branch;
|
|
19041
|
+
} catch {
|
|
19042
|
+
return null;
|
|
19052
19043
|
}
|
|
19053
|
-
}
|
|
19054
|
-
|
|
19055
|
-
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
sagaName = "CaptureTreeSaga";
|
|
19061
|
-
async execute(input) {
|
|
19062
|
-
const {
|
|
19063
|
-
repositoryPath,
|
|
19064
|
-
lastTreeHash,
|
|
19065
|
-
interrupted,
|
|
19066
|
-
apiClient,
|
|
19067
|
-
taskId,
|
|
19068
|
-
runId
|
|
19069
|
-
} = input;
|
|
19070
|
-
const tmpDir = join11(repositoryPath, ".posthog", "tmp");
|
|
19071
|
-
if (existsSync6(join11(repositoryPath, ".gitmodules"))) {
|
|
19072
|
-
this.log.warn(
|
|
19073
|
-
"Repository has submodules - snapshot may not capture submodule state"
|
|
19074
|
-
);
|
|
19075
|
-
}
|
|
19076
|
-
const shouldArchive = !!apiClient;
|
|
19077
|
-
const archivePath = shouldArchive ? join11(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
19078
|
-
const gitCaptureSaga = new CaptureTreeSaga(this.log);
|
|
19079
|
-
const captureResult = await gitCaptureSaga.run({
|
|
19080
|
-
baseDir: repositoryPath,
|
|
19081
|
-
lastTreeHash,
|
|
19082
|
-
archivePath
|
|
19083
|
-
});
|
|
19084
|
-
if (!captureResult.success) {
|
|
19085
|
-
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
19086
|
-
}
|
|
19087
|
-
const {
|
|
19088
|
-
snapshot: gitSnapshot,
|
|
19089
|
-
archivePath: createdArchivePath,
|
|
19090
|
-
changed
|
|
19091
|
-
} = captureResult.data;
|
|
19092
|
-
if (!changed || !gitSnapshot) {
|
|
19093
|
-
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
19094
|
-
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
19095
|
-
}
|
|
19096
|
-
let archiveUrl;
|
|
19097
|
-
if (apiClient && createdArchivePath) {
|
|
19098
|
-
try {
|
|
19099
|
-
archiveUrl = await this.uploadArchive(
|
|
19100
|
-
createdArchivePath,
|
|
19101
|
-
gitSnapshot.treeHash,
|
|
19102
|
-
apiClient,
|
|
19103
|
-
taskId,
|
|
19104
|
-
runId
|
|
19105
|
-
);
|
|
19106
|
-
} finally {
|
|
19107
|
-
await rm4(createdArchivePath, { force: true }).catch(() => {
|
|
19108
|
-
});
|
|
19109
|
-
}
|
|
19110
|
-
}
|
|
19111
|
-
const snapshot = {
|
|
19112
|
-
treeHash: gitSnapshot.treeHash,
|
|
19113
|
-
baseCommit: gitSnapshot.baseCommit,
|
|
19114
|
-
changes: gitSnapshot.changes,
|
|
19115
|
-
timestamp: gitSnapshot.timestamp,
|
|
19116
|
-
interrupted,
|
|
19117
|
-
archiveUrl
|
|
19044
|
+
}
|
|
19045
|
+
async function getTrackingMetadata(git, branch) {
|
|
19046
|
+
if (!branch) {
|
|
19047
|
+
return {
|
|
19048
|
+
upstreamRemote: null,
|
|
19049
|
+
upstreamMergeRef: null,
|
|
19050
|
+
remoteUrl: null
|
|
19118
19051
|
};
|
|
19119
|
-
this.log.info("Tree captured", {
|
|
19120
|
-
treeHash: snapshot.treeHash,
|
|
19121
|
-
changes: snapshot.changes.length,
|
|
19122
|
-
interrupted,
|
|
19123
|
-
archiveUrl
|
|
19124
|
-
});
|
|
19125
|
-
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
19126
19052
|
}
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
|
|
19130
|
-
|
|
19131
|
-
|
|
19132
|
-
|
|
19133
|
-
|
|
19134
|
-
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
content_type: "application/gzip"
|
|
19139
|
-
}
|
|
19140
|
-
]);
|
|
19141
|
-
if (artifacts.length > 0 && artifacts[0].storage_path) {
|
|
19142
|
-
this.log.info("Tree archive uploaded", {
|
|
19143
|
-
storagePath: artifacts[0].storage_path,
|
|
19144
|
-
treeHash
|
|
19145
|
-
});
|
|
19146
|
-
return artifacts[0].storage_path;
|
|
19147
|
-
}
|
|
19148
|
-
return void 0;
|
|
19149
|
-
},
|
|
19150
|
-
rollback: async () => {
|
|
19151
|
-
await rm4(archivePath, { force: true }).catch(() => {
|
|
19152
|
-
});
|
|
19153
|
-
}
|
|
19154
|
-
});
|
|
19155
|
-
return archiveUrl;
|
|
19053
|
+
const upstreamRemote = await getGitConfigValue(git, `branch.${branch}.remote`);
|
|
19054
|
+
const upstreamMergeRef = await getGitConfigValue(git, `branch.${branch}.merge`);
|
|
19055
|
+
const remoteUrl = upstreamRemote ? await getRemoteUrl(git, upstreamRemote) : null;
|
|
19056
|
+
return { upstreamRemote, upstreamMergeRef, remoteUrl };
|
|
19057
|
+
}
|
|
19058
|
+
async function getGitConfigValue(git, key) {
|
|
19059
|
+
try {
|
|
19060
|
+
const value = await git.raw(["config", "--get", key]);
|
|
19061
|
+
return value.trim() || null;
|
|
19062
|
+
} catch {
|
|
19063
|
+
return null;
|
|
19156
19064
|
}
|
|
19157
|
-
}
|
|
19065
|
+
}
|
|
19066
|
+
async function getRemoteUrl(git, remote) {
|
|
19067
|
+
try {
|
|
19068
|
+
const value = await git.remote(["get-url", remote]);
|
|
19069
|
+
return typeof value === "string" ? value.trim() || null : null;
|
|
19070
|
+
} catch {
|
|
19071
|
+
return null;
|
|
19072
|
+
}
|
|
19073
|
+
}
|
|
19074
|
+
function hasTrackingConfig(localGitState) {
|
|
19075
|
+
return !!(localGitState?.upstreamRemote || localGitState?.upstreamMergeRef);
|
|
19076
|
+
}
|
|
19158
19077
|
|
|
19159
|
-
// src/
|
|
19160
|
-
var
|
|
19078
|
+
// src/handoff-checkpoint.ts
|
|
19079
|
+
var HandoffCheckpointTracker = class {
|
|
19161
19080
|
repositoryPath;
|
|
19162
19081
|
taskId;
|
|
19163
19082
|
runId;
|
|
19164
19083
|
apiClient;
|
|
19165
19084
|
logger;
|
|
19166
|
-
lastTreeHash = null;
|
|
19167
19085
|
constructor(config) {
|
|
19168
19086
|
this.repositoryPath = config.repositoryPath;
|
|
19169
19087
|
this.taskId = config.taskId;
|
|
19170
19088
|
this.runId = config.runId;
|
|
19171
19089
|
this.apiClient = config.apiClient;
|
|
19172
|
-
this.logger = config.logger || new Logger({ debug: false, prefix: "[
|
|
19090
|
+
this.logger = config.logger || new Logger({ debug: false, prefix: "[HandoffCheckpointTracker]" });
|
|
19173
19091
|
}
|
|
19174
|
-
|
|
19175
|
-
|
|
19176
|
-
* Uses a temporary index to avoid modifying user's staging area.
|
|
19177
|
-
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
19178
|
-
*/
|
|
19179
|
-
async captureTree(options) {
|
|
19180
|
-
const saga = new CaptureTreeSaga2(this.logger);
|
|
19181
|
-
const result = await saga.run({
|
|
19182
|
-
repositoryPath: this.repositoryPath,
|
|
19183
|
-
taskId: this.taskId,
|
|
19184
|
-
runId: this.runId,
|
|
19185
|
-
apiClient: this.apiClient,
|
|
19186
|
-
lastTreeHash: this.lastTreeHash,
|
|
19187
|
-
interrupted: options?.interrupted
|
|
19188
|
-
});
|
|
19189
|
-
if (!result.success) {
|
|
19190
|
-
this.logger.error("Failed to capture tree", {
|
|
19191
|
-
error: result.error,
|
|
19192
|
-
failedStep: result.failedStep
|
|
19193
|
-
});
|
|
19092
|
+
async captureForHandoff(localGitState) {
|
|
19093
|
+
if (!this.apiClient) {
|
|
19194
19094
|
throw new Error(
|
|
19195
|
-
|
|
19095
|
+
"Cannot capture handoff checkpoint: API client not configured"
|
|
19196
19096
|
);
|
|
19197
19097
|
}
|
|
19198
|
-
|
|
19199
|
-
|
|
19098
|
+
const gitTracker = this.createGitTracker();
|
|
19099
|
+
const capture = await gitTracker.captureForHandoff(localGitState);
|
|
19100
|
+
try {
|
|
19101
|
+
const uploads = await this.uploadArtifacts([
|
|
19102
|
+
{
|
|
19103
|
+
key: "pack",
|
|
19104
|
+
filePath: capture.headPack?.path,
|
|
19105
|
+
name: `handoff/${capture.checkpoint.checkpointId}.pack`,
|
|
19106
|
+
contentType: "application/x-git-packed-objects"
|
|
19107
|
+
},
|
|
19108
|
+
{
|
|
19109
|
+
key: "index",
|
|
19110
|
+
filePath: capture.indexFile.path,
|
|
19111
|
+
name: `handoff/${capture.checkpoint.checkpointId}.index`,
|
|
19112
|
+
contentType: "application/octet-stream"
|
|
19113
|
+
}
|
|
19114
|
+
]);
|
|
19115
|
+
this.logCaptureMetrics(capture.checkpoint, uploads);
|
|
19116
|
+
return {
|
|
19117
|
+
...capture.checkpoint,
|
|
19118
|
+
artifactPath: uploads.pack?.storagePath,
|
|
19119
|
+
indexArtifactPath: uploads.index?.storagePath
|
|
19120
|
+
};
|
|
19121
|
+
} finally {
|
|
19122
|
+
await this.removeIfPresent(capture.headPack?.path);
|
|
19123
|
+
await this.removeIfPresent(capture.indexFile.path);
|
|
19200
19124
|
}
|
|
19201
|
-
return result.data.snapshot;
|
|
19202
19125
|
}
|
|
19203
|
-
|
|
19204
|
-
* Download and apply a tree snapshot.
|
|
19205
|
-
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
19206
|
-
*/
|
|
19207
|
-
async applyTreeSnapshot(snapshot) {
|
|
19126
|
+
async applyFromHandoff(checkpoint, options) {
|
|
19208
19127
|
if (!this.apiClient) {
|
|
19209
|
-
throw new Error("Cannot apply snapshot: API client not configured");
|
|
19210
|
-
}
|
|
19211
|
-
if (!snapshot.archiveUrl) {
|
|
19212
|
-
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
19213
|
-
treeHash: snapshot.treeHash,
|
|
19214
|
-
changes: snapshot.changes.length
|
|
19215
|
-
});
|
|
19216
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19217
|
-
}
|
|
19218
|
-
const saga = new ApplySnapshotSaga(this.logger);
|
|
19219
|
-
const result = await saga.run({
|
|
19220
|
-
snapshot,
|
|
19221
|
-
repositoryPath: this.repositoryPath,
|
|
19222
|
-
apiClient: this.apiClient,
|
|
19223
|
-
taskId: this.taskId,
|
|
19224
|
-
runId: this.runId
|
|
19225
|
-
});
|
|
19226
|
-
if (!result.success) {
|
|
19227
|
-
this.logger.error("Failed to apply tree snapshot", {
|
|
19228
|
-
error: result.error,
|
|
19229
|
-
failedStep: result.failedStep,
|
|
19230
|
-
treeHash: snapshot.treeHash
|
|
19231
|
-
});
|
|
19232
19128
|
throw new Error(
|
|
19233
|
-
|
|
19129
|
+
"Cannot apply handoff checkpoint: API client not configured"
|
|
19234
19130
|
);
|
|
19235
19131
|
}
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
|
|
19241
|
-
|
|
19242
|
-
|
|
19243
|
-
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
|
|
19248
|
-
this.lastTreeHash = hash;
|
|
19249
|
-
}
|
|
19250
|
-
};
|
|
19251
|
-
|
|
19252
|
-
// src/sagas/resume-saga.ts
|
|
19253
|
-
var ResumeSaga = class extends Saga {
|
|
19254
|
-
sagaName = "ResumeSaga";
|
|
19255
|
-
async execute(input) {
|
|
19256
|
-
const { taskId, runId, repositoryPath, apiClient } = input;
|
|
19257
|
-
const logger = input.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
19258
|
-
const taskRun = await this.readOnlyStep(
|
|
19259
|
-
"fetch_task_run",
|
|
19260
|
-
() => apiClient.getTaskRun(taskId, runId)
|
|
19261
|
-
);
|
|
19262
|
-
if (!taskRun.log_url) {
|
|
19263
|
-
this.log.info("No log URL found, starting fresh");
|
|
19264
|
-
return this.emptyResult();
|
|
19265
|
-
}
|
|
19266
|
-
const entries = await this.readOnlyStep(
|
|
19267
|
-
"fetch_logs",
|
|
19268
|
-
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
19269
|
-
);
|
|
19270
|
-
if (entries.length === 0) {
|
|
19271
|
-
this.log.info("No log entries found, starting fresh");
|
|
19272
|
-
return this.emptyResult();
|
|
19273
|
-
}
|
|
19274
|
-
this.log.info("Fetched log entries", { count: entries.length });
|
|
19275
|
-
const latestSnapshot = await this.readOnlyStep(
|
|
19276
|
-
"find_snapshot",
|
|
19277
|
-
() => Promise.resolve(this.findLatestTreeSnapshot(entries))
|
|
19278
|
-
);
|
|
19279
|
-
let snapshotApplied = false;
|
|
19280
|
-
if (latestSnapshot?.archiveUrl && repositoryPath) {
|
|
19281
|
-
this.log.info("Found tree snapshot", {
|
|
19282
|
-
treeHash: latestSnapshot.treeHash,
|
|
19283
|
-
hasArchiveUrl: true,
|
|
19284
|
-
changes: latestSnapshot.changes?.length ?? 0,
|
|
19285
|
-
interrupted: latestSnapshot.interrupted
|
|
19286
|
-
});
|
|
19287
|
-
await this.step({
|
|
19288
|
-
name: "apply_snapshot",
|
|
19289
|
-
execute: async () => {
|
|
19290
|
-
const treeTracker = new TreeTracker({
|
|
19291
|
-
repositoryPath,
|
|
19292
|
-
taskId,
|
|
19293
|
-
runId,
|
|
19294
|
-
apiClient,
|
|
19295
|
-
logger: logger.child("TreeTracker")
|
|
19296
|
-
});
|
|
19297
|
-
try {
|
|
19298
|
-
await treeTracker.applyTreeSnapshot(latestSnapshot);
|
|
19299
|
-
treeTracker.setLastTreeHash(latestSnapshot.treeHash);
|
|
19300
|
-
snapshotApplied = true;
|
|
19301
|
-
this.log.info("Tree snapshot applied successfully", {
|
|
19302
|
-
treeHash: latestSnapshot.treeHash
|
|
19303
|
-
});
|
|
19304
|
-
} catch (error) {
|
|
19305
|
-
this.log.warn(
|
|
19306
|
-
"Failed to apply tree snapshot, continuing without it",
|
|
19307
|
-
{
|
|
19308
|
-
error: error instanceof Error ? error.message : String(error),
|
|
19309
|
-
treeHash: latestSnapshot.treeHash
|
|
19310
|
-
}
|
|
19311
|
-
);
|
|
19312
|
-
}
|
|
19132
|
+
const gitTracker = this.createGitTracker();
|
|
19133
|
+
const tmpDir = join9(this.repositoryPath, ".posthog", "tmp");
|
|
19134
|
+
await mkdir4(tmpDir, { recursive: true });
|
|
19135
|
+
const packPath = join9(tmpDir, `${checkpoint.checkpointId}.pack`);
|
|
19136
|
+
const indexPath = join9(tmpDir, `${checkpoint.checkpointId}.index`);
|
|
19137
|
+
try {
|
|
19138
|
+
const downloads = await this.downloadArtifacts([
|
|
19139
|
+
{
|
|
19140
|
+
key: "pack",
|
|
19141
|
+
storagePath: checkpoint.artifactPath,
|
|
19142
|
+
filePath: packPath,
|
|
19143
|
+
label: "handoff pack"
|
|
19313
19144
|
},
|
|
19314
|
-
rollback: async () => {
|
|
19315
|
-
}
|
|
19316
|
-
});
|
|
19317
|
-
} else if (latestSnapshot?.archiveUrl && !repositoryPath) {
|
|
19318
|
-
this.log.warn(
|
|
19319
|
-
"Snapshot found but no repositoryPath configured - files cannot be restored",
|
|
19320
19145
|
{
|
|
19321
|
-
|
|
19322
|
-
|
|
19146
|
+
key: "index",
|
|
19147
|
+
storagePath: checkpoint.indexArtifactPath,
|
|
19148
|
+
filePath: indexPath,
|
|
19149
|
+
label: "handoff index"
|
|
19323
19150
|
}
|
|
19324
|
-
);
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19151
|
+
]);
|
|
19152
|
+
const applyResult = await gitTracker.applyFromHandoff({
|
|
19153
|
+
checkpoint: this.toGitCheckpoint(checkpoint),
|
|
19154
|
+
headPackPath: downloads.pack?.filePath,
|
|
19155
|
+
indexPath: downloads.index?.filePath,
|
|
19156
|
+
localGitState: options?.localGitState,
|
|
19157
|
+
onDivergedBranch: options?.onDivergedBranch
|
|
19158
|
+
});
|
|
19159
|
+
this.logApplyMetrics(checkpoint, downloads, applyResult.totalBytes);
|
|
19160
|
+
return {
|
|
19161
|
+
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
19162
|
+
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
19163
|
+
totalBytes: applyResult.totalBytes
|
|
19164
|
+
};
|
|
19165
|
+
} finally {
|
|
19166
|
+
await this.removeIfPresent(packPath);
|
|
19167
|
+
await this.removeIfPresent(indexPath);
|
|
19168
|
+
}
|
|
19169
|
+
}
|
|
19170
|
+
toGitCheckpoint(checkpoint) {
|
|
19171
|
+
return {
|
|
19172
|
+
checkpointId: checkpoint.checkpointId,
|
|
19173
|
+
commit: checkpoint.commit,
|
|
19174
|
+
checkpointRef: checkpoint.checkpointRef,
|
|
19175
|
+
headRef: checkpoint.headRef,
|
|
19176
|
+
head: checkpoint.head,
|
|
19177
|
+
branch: checkpoint.branch,
|
|
19178
|
+
indexTree: checkpoint.indexTree,
|
|
19179
|
+
worktreeTree: checkpoint.worktreeTree,
|
|
19180
|
+
timestamp: checkpoint.timestamp,
|
|
19181
|
+
upstreamRemote: checkpoint.upstreamRemote ?? null,
|
|
19182
|
+
upstreamMergeRef: checkpoint.upstreamMergeRef ?? null,
|
|
19183
|
+
remoteUrl: checkpoint.remoteUrl ?? null
|
|
19184
|
+
};
|
|
19185
|
+
}
|
|
19186
|
+
async uploadArtifactFile(filePath, name2, contentType) {
|
|
19187
|
+
if (!this.apiClient) {
|
|
19188
|
+
return { rawBytes: 0, wireBytes: 0 };
|
|
19189
|
+
}
|
|
19190
|
+
const content = await readFile4(filePath);
|
|
19191
|
+
const base64Content = content.toString("base64");
|
|
19192
|
+
const artifacts = await this.apiClient.uploadTaskArtifacts(
|
|
19193
|
+
this.taskId,
|
|
19194
|
+
this.runId,
|
|
19195
|
+
[
|
|
19328
19196
|
{
|
|
19329
|
-
|
|
19330
|
-
|
|
19197
|
+
name: name2,
|
|
19198
|
+
type: "artifact",
|
|
19199
|
+
content: base64Content,
|
|
19200
|
+
content_type: contentType
|
|
19331
19201
|
}
|
|
19332
|
-
|
|
19333
|
-
}
|
|
19334
|
-
const conversation = await this.readOnlyStep(
|
|
19335
|
-
"rebuild_conversation",
|
|
19336
|
-
() => Promise.resolve(this.rebuildConversation(entries))
|
|
19337
|
-
);
|
|
19338
|
-
const lastDevice = await this.readOnlyStep(
|
|
19339
|
-
"find_device",
|
|
19340
|
-
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
19202
|
+
]
|
|
19341
19203
|
);
|
|
19342
|
-
this.log.info("Resume state rebuilt", {
|
|
19343
|
-
turns: conversation.length,
|
|
19344
|
-
hasSnapshot: !!latestSnapshot,
|
|
19345
|
-
snapshotApplied,
|
|
19346
|
-
interrupted: latestSnapshot?.interrupted ?? false
|
|
19347
|
-
});
|
|
19348
19204
|
return {
|
|
19349
|
-
|
|
19350
|
-
|
|
19351
|
-
|
|
19352
|
-
interrupted: latestSnapshot?.interrupted ?? false,
|
|
19353
|
-
lastDevice,
|
|
19354
|
-
logEntryCount: entries.length
|
|
19205
|
+
storagePath: artifacts.at(-1)?.storage_path,
|
|
19206
|
+
rawBytes: content.byteLength,
|
|
19207
|
+
wireBytes: Buffer.byteLength(base64Content, "utf-8")
|
|
19355
19208
|
};
|
|
19356
19209
|
}
|
|
19357
|
-
|
|
19210
|
+
async uploadArtifacts(specs) {
|
|
19211
|
+
const results = [];
|
|
19212
|
+
for (const spec of specs) {
|
|
19213
|
+
if (!spec.filePath) {
|
|
19214
|
+
results.push([spec.key, void 0]);
|
|
19215
|
+
continue;
|
|
19216
|
+
}
|
|
19217
|
+
results.push([
|
|
19218
|
+
spec.key,
|
|
19219
|
+
await this.uploadArtifactFile(
|
|
19220
|
+
spec.filePath,
|
|
19221
|
+
spec.name,
|
|
19222
|
+
spec.contentType
|
|
19223
|
+
)
|
|
19224
|
+
]);
|
|
19225
|
+
}
|
|
19226
|
+
return Object.fromEntries(results);
|
|
19227
|
+
}
|
|
19228
|
+
async downloadArtifactToFile(artifactPath, filePath, label) {
|
|
19229
|
+
if (!this.apiClient) {
|
|
19230
|
+
throw new Error(`Cannot download ${label}: API client not configured`);
|
|
19231
|
+
}
|
|
19232
|
+
const arrayBuffer = await this.apiClient.downloadArtifact(
|
|
19233
|
+
this.taskId,
|
|
19234
|
+
this.runId,
|
|
19235
|
+
artifactPath
|
|
19236
|
+
);
|
|
19237
|
+
if (!arrayBuffer) {
|
|
19238
|
+
throw new Error(`Failed to download ${label} from ${artifactPath}`);
|
|
19239
|
+
}
|
|
19240
|
+
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19241
|
+
const binaryContent = Buffer.from(base64Content, "base64");
|
|
19242
|
+
await writeFile2(filePath, binaryContent);
|
|
19358
19243
|
return {
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
interrupted: false,
|
|
19363
|
-
logEntryCount: 0
|
|
19244
|
+
filePath,
|
|
19245
|
+
rawBytes: binaryContent.byteLength,
|
|
19246
|
+
wireBytes: arrayBuffer.byteLength
|
|
19364
19247
|
};
|
|
19365
19248
|
}
|
|
19366
|
-
|
|
19367
|
-
|
|
19368
|
-
|
|
19369
|
-
|
|
19370
|
-
|
|
19371
|
-
POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
19372
|
-
)) {
|
|
19373
|
-
const params = entry.notification.params;
|
|
19374
|
-
if (params?.treeHash) {
|
|
19375
|
-
return params;
|
|
19249
|
+
async downloadArtifacts(specs) {
|
|
19250
|
+
const downloads = await Promise.all(
|
|
19251
|
+
specs.map(async (spec) => {
|
|
19252
|
+
if (!spec.storagePath) {
|
|
19253
|
+
return [spec.key, void 0];
|
|
19376
19254
|
}
|
|
19377
|
-
|
|
19255
|
+
return [
|
|
19256
|
+
spec.key,
|
|
19257
|
+
await this.downloadArtifactToFile(
|
|
19258
|
+
spec.storagePath,
|
|
19259
|
+
spec.filePath,
|
|
19260
|
+
spec.label
|
|
19261
|
+
)
|
|
19262
|
+
];
|
|
19263
|
+
})
|
|
19264
|
+
);
|
|
19265
|
+
return Object.fromEntries(downloads);
|
|
19266
|
+
}
|
|
19267
|
+
createGitTracker() {
|
|
19268
|
+
return new GitHandoffTracker({
|
|
19269
|
+
repositoryPath: this.repositoryPath,
|
|
19270
|
+
logger: this.logger
|
|
19271
|
+
});
|
|
19272
|
+
}
|
|
19273
|
+
logCaptureMetrics(checkpoint, uploads) {
|
|
19274
|
+
this.logger.info("Captured handoff checkpoint", {
|
|
19275
|
+
checkpointId: checkpoint.checkpointId,
|
|
19276
|
+
branch: checkpoint.branch,
|
|
19277
|
+
head: checkpoint.head,
|
|
19278
|
+
artifactPath: uploads.pack?.storagePath,
|
|
19279
|
+
indexArtifactPath: uploads.index?.storagePath,
|
|
19280
|
+
...this.buildMetricPayload(uploads)
|
|
19281
|
+
});
|
|
19282
|
+
}
|
|
19283
|
+
logApplyMetrics(checkpoint, downloads, totalBytes) {
|
|
19284
|
+
this.logger.info("Applied handoff checkpoint", {
|
|
19285
|
+
checkpointId: checkpoint.checkpointId,
|
|
19286
|
+
commit: checkpoint.commit,
|
|
19287
|
+
branch: checkpoint.branch,
|
|
19288
|
+
head: checkpoint.head,
|
|
19289
|
+
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
19290
|
+
packWireBytes: downloads.pack?.wireBytes ?? 0,
|
|
19291
|
+
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
19292
|
+
indexWireBytes: downloads.index?.wireBytes ?? 0,
|
|
19293
|
+
totalBytes,
|
|
19294
|
+
totalWireBytes: this.sumWireBytes(downloads.pack, downloads.index)
|
|
19295
|
+
});
|
|
19296
|
+
}
|
|
19297
|
+
buildMetricPayload(metrics) {
|
|
19298
|
+
return {
|
|
19299
|
+
packBytes: metrics.pack?.rawBytes ?? 0,
|
|
19300
|
+
packWireBytes: metrics.pack?.wireBytes ?? 0,
|
|
19301
|
+
indexBytes: metrics.index?.rawBytes ?? 0,
|
|
19302
|
+
indexWireBytes: metrics.index?.wireBytes ?? 0,
|
|
19303
|
+
totalBytes: this.sumRawBytes(metrics.pack, metrics.index),
|
|
19304
|
+
totalWireBytes: this.sumWireBytes(metrics.pack, metrics.index)
|
|
19305
|
+
};
|
|
19306
|
+
}
|
|
19307
|
+
sumRawBytes(...artifacts) {
|
|
19308
|
+
return artifacts.reduce(
|
|
19309
|
+
(total, artifact) => total + (artifact?.rawBytes ?? 0),
|
|
19310
|
+
0
|
|
19311
|
+
);
|
|
19312
|
+
}
|
|
19313
|
+
sumWireBytes(...artifacts) {
|
|
19314
|
+
return artifacts.reduce(
|
|
19315
|
+
(total, artifact) => total + (artifact?.wireBytes ?? 0),
|
|
19316
|
+
0
|
|
19317
|
+
);
|
|
19318
|
+
}
|
|
19319
|
+
async removeIfPresent(filePath) {
|
|
19320
|
+
if (!filePath) {
|
|
19321
|
+
return;
|
|
19378
19322
|
}
|
|
19379
|
-
|
|
19323
|
+
await rm4(filePath, { force: true }).catch(() => {
|
|
19324
|
+
});
|
|
19380
19325
|
}
|
|
19381
|
-
|
|
19382
|
-
|
|
19383
|
-
|
|
19384
|
-
|
|
19385
|
-
|
|
19386
|
-
|
|
19326
|
+
};
|
|
19327
|
+
|
|
19328
|
+
// src/utils/gateway.ts
|
|
19329
|
+
function getGatewayBaseUrl(posthogHost) {
|
|
19330
|
+
const url = new URL(posthogHost);
|
|
19331
|
+
const hostname = url.hostname;
|
|
19332
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
19333
|
+
return `${url.protocol}//localhost:3308`;
|
|
19334
|
+
}
|
|
19335
|
+
if (hostname === "host.docker.internal") {
|
|
19336
|
+
return `${url.protocol}//host.docker.internal:3308`;
|
|
19337
|
+
}
|
|
19338
|
+
const region = hostname.match(/^(us|eu)\.posthog\.com$/)?.[1] ?? "us";
|
|
19339
|
+
return `https://gateway.${region}.posthog.com`;
|
|
19340
|
+
}
|
|
19341
|
+
function getLlmGatewayUrl(posthogHost, product = "posthog_code") {
|
|
19342
|
+
return `${getGatewayBaseUrl(posthogHost)}/${product}`;
|
|
19343
|
+
}
|
|
19344
|
+
|
|
19345
|
+
// src/posthog-api.ts
|
|
19346
|
+
var DEFAULT_USER_AGENT = `posthog/agent.hog.dev; version: ${package_default.version}`;
|
|
19347
|
+
var PostHogAPIClient = class {
|
|
19348
|
+
config;
|
|
19349
|
+
constructor(config) {
|
|
19350
|
+
this.config = config;
|
|
19351
|
+
}
|
|
19352
|
+
get baseUrl() {
|
|
19353
|
+
const host = this.config.apiUrl.endsWith("/") ? this.config.apiUrl.slice(0, -1) : this.config.apiUrl;
|
|
19354
|
+
return host;
|
|
19355
|
+
}
|
|
19356
|
+
isAuthFailure(status) {
|
|
19357
|
+
return status === 401 || status === 403;
|
|
19358
|
+
}
|
|
19359
|
+
async resolveApiKey(forceRefresh = false) {
|
|
19360
|
+
if (forceRefresh && this.config.refreshApiKey) {
|
|
19361
|
+
return this.config.refreshApiKey();
|
|
19362
|
+
}
|
|
19363
|
+
return this.config.getApiKey();
|
|
19364
|
+
}
|
|
19365
|
+
async buildHeaders(options, forceRefresh = false) {
|
|
19366
|
+
const headers = new Headers(options.headers);
|
|
19367
|
+
headers.set(
|
|
19368
|
+
"Authorization",
|
|
19369
|
+
`Bearer ${await this.resolveApiKey(forceRefresh)}`
|
|
19370
|
+
);
|
|
19371
|
+
headers.set("Content-Type", "application/json");
|
|
19372
|
+
headers.set("User-Agent", this.config.userAgent ?? DEFAULT_USER_AGENT);
|
|
19373
|
+
return headers;
|
|
19374
|
+
}
|
|
19375
|
+
async performRequest(endpoint, options, forceRefresh = false) {
|
|
19376
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
19377
|
+
return fetch(url, {
|
|
19378
|
+
...options,
|
|
19379
|
+
headers: await this.buildHeaders(options, forceRefresh)
|
|
19380
|
+
});
|
|
19381
|
+
}
|
|
19382
|
+
async performRequestWithRetry(endpoint, options = {}) {
|
|
19383
|
+
let response = await this.performRequest(endpoint, options);
|
|
19384
|
+
if (!response.ok && this.isAuthFailure(response.status)) {
|
|
19385
|
+
response = await this.performRequest(endpoint, options, true);
|
|
19386
|
+
}
|
|
19387
|
+
return response;
|
|
19388
|
+
}
|
|
19389
|
+
async apiRequest(endpoint, options = {}) {
|
|
19390
|
+
const response = await this.performRequestWithRetry(endpoint, options);
|
|
19391
|
+
if (!response.ok) {
|
|
19392
|
+
let errorMessage;
|
|
19393
|
+
try {
|
|
19394
|
+
const errorResponse = await response.json();
|
|
19395
|
+
errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
|
|
19396
|
+
} catch {
|
|
19397
|
+
errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
|
|
19387
19398
|
}
|
|
19399
|
+
throw new Error(errorMessage);
|
|
19388
19400
|
}
|
|
19389
|
-
return
|
|
19401
|
+
return response.json();
|
|
19402
|
+
}
|
|
19403
|
+
getTeamId() {
|
|
19404
|
+
return this.config.projectId;
|
|
19405
|
+
}
|
|
19406
|
+
async getApiKey(forceRefresh = false) {
|
|
19407
|
+
return this.resolveApiKey(forceRefresh);
|
|
19408
|
+
}
|
|
19409
|
+
getLlmGatewayUrl() {
|
|
19410
|
+
return getLlmGatewayUrl(this.baseUrl);
|
|
19411
|
+
}
|
|
19412
|
+
async getTask(taskId) {
|
|
19413
|
+
const teamId = this.getTeamId();
|
|
19414
|
+
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
|
|
19415
|
+
}
|
|
19416
|
+
async getTaskRun(taskId, runId) {
|
|
19417
|
+
const teamId = this.getTeamId();
|
|
19418
|
+
return this.apiRequest(
|
|
19419
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
19420
|
+
);
|
|
19421
|
+
}
|
|
19422
|
+
async resumeRunInCloud(taskId, runId) {
|
|
19423
|
+
const teamId = this.getTeamId();
|
|
19424
|
+
return this.apiRequest(
|
|
19425
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/resume_in_cloud/`,
|
|
19426
|
+
{ method: "POST" }
|
|
19427
|
+
);
|
|
19428
|
+
}
|
|
19429
|
+
async updateTaskRun(taskId, runId, payload) {
|
|
19430
|
+
const teamId = this.getTeamId();
|
|
19431
|
+
return this.apiRequest(
|
|
19432
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
|
|
19433
|
+
{
|
|
19434
|
+
method: "PATCH",
|
|
19435
|
+
body: JSON.stringify(payload)
|
|
19436
|
+
}
|
|
19437
|
+
);
|
|
19438
|
+
}
|
|
19439
|
+
async setTaskRunOutput(taskId, runId, output) {
|
|
19440
|
+
return this.apiRequest(
|
|
19441
|
+
`/api/projects/${this.getTeamId()}/tasks/${taskId}/runs/${runId}/set_output/`,
|
|
19442
|
+
{
|
|
19443
|
+
method: "PATCH",
|
|
19444
|
+
body: JSON.stringify(output)
|
|
19445
|
+
}
|
|
19446
|
+
);
|
|
19447
|
+
}
|
|
19448
|
+
async appendTaskRunLog(taskId, runId, entries) {
|
|
19449
|
+
const teamId = this.getTeamId();
|
|
19450
|
+
return this.apiRequest(
|
|
19451
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
|
|
19452
|
+
{
|
|
19453
|
+
method: "POST",
|
|
19454
|
+
body: JSON.stringify({ entries })
|
|
19455
|
+
}
|
|
19456
|
+
);
|
|
19457
|
+
}
|
|
19458
|
+
async relayMessage(taskId, runId, text2) {
|
|
19459
|
+
const teamId = this.getTeamId();
|
|
19460
|
+
await this.apiRequest(
|
|
19461
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
|
|
19462
|
+
{
|
|
19463
|
+
method: "POST",
|
|
19464
|
+
body: JSON.stringify({ text: text2 })
|
|
19465
|
+
}
|
|
19466
|
+
);
|
|
19467
|
+
}
|
|
19468
|
+
async uploadTaskArtifacts(taskId, runId, artifacts) {
|
|
19469
|
+
if (!artifacts.length) {
|
|
19470
|
+
return [];
|
|
19471
|
+
}
|
|
19472
|
+
const teamId = this.getTeamId();
|
|
19473
|
+
const response = await this.apiRequest(
|
|
19474
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
|
|
19475
|
+
{
|
|
19476
|
+
method: "POST",
|
|
19477
|
+
body: JSON.stringify({ artifacts })
|
|
19478
|
+
}
|
|
19479
|
+
);
|
|
19480
|
+
const manifest = response.artifacts ?? [];
|
|
19481
|
+
return manifest.slice(-artifacts.length);
|
|
19482
|
+
}
|
|
19483
|
+
/**
|
|
19484
|
+
* Download artifact content by storage path
|
|
19485
|
+
* Streams the file through the PostHog backend so the sandbox does not need
|
|
19486
|
+
* direct access to object storage.
|
|
19487
|
+
*/
|
|
19488
|
+
async downloadArtifact(taskId, runId, storagePath) {
|
|
19489
|
+
const teamId = this.getTeamId();
|
|
19490
|
+
try {
|
|
19491
|
+
const response = await this.performRequestWithRetry(
|
|
19492
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
|
|
19493
|
+
{
|
|
19494
|
+
method: "POST",
|
|
19495
|
+
body: JSON.stringify({ storage_path: storagePath })
|
|
19496
|
+
}
|
|
19497
|
+
);
|
|
19498
|
+
if (!response.ok) {
|
|
19499
|
+
throw new Error(`Failed to download artifact: ${response.status}`);
|
|
19500
|
+
}
|
|
19501
|
+
return response.arrayBuffer();
|
|
19502
|
+
} catch {
|
|
19503
|
+
return null;
|
|
19504
|
+
}
|
|
19505
|
+
}
|
|
19506
|
+
/**
|
|
19507
|
+
* Fetch logs for a task run via the logs API endpoint
|
|
19508
|
+
* @param taskRun - The task run to fetch logs for
|
|
19509
|
+
* @returns Array of stored entries, or empty array if no logs available
|
|
19510
|
+
*/
|
|
19511
|
+
async fetchTaskRunLogs(taskRun) {
|
|
19512
|
+
const teamId = this.getTeamId();
|
|
19513
|
+
const endpoint = `/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`;
|
|
19514
|
+
try {
|
|
19515
|
+
const response = await this.performRequestWithRetry(endpoint);
|
|
19516
|
+
if (!response.ok) {
|
|
19517
|
+
if (response.status === 404) {
|
|
19518
|
+
return [];
|
|
19519
|
+
}
|
|
19520
|
+
throw new Error(
|
|
19521
|
+
`Failed to fetch logs: ${response.status} ${response.statusText}`
|
|
19522
|
+
);
|
|
19523
|
+
}
|
|
19524
|
+
const content = await response.text();
|
|
19525
|
+
if (!content.trim()) {
|
|
19526
|
+
return [];
|
|
19527
|
+
}
|
|
19528
|
+
return content.trim().split("\n").map((line) => JSON.parse(line));
|
|
19529
|
+
} catch (error) {
|
|
19530
|
+
throw new Error(
|
|
19531
|
+
`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`
|
|
19532
|
+
);
|
|
19533
|
+
}
|
|
19534
|
+
}
|
|
19535
|
+
};
|
|
19536
|
+
|
|
19537
|
+
// src/adapters/claude/session/jsonl-hydration.ts
|
|
19538
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
19539
|
+
import * as fs11 from "fs/promises";
|
|
19540
|
+
import * as os6 from "os";
|
|
19541
|
+
import * as path14 from "path";
|
|
19542
|
+
var CHARS_PER_TOKEN = 4;
|
|
19543
|
+
var DEFAULT_MAX_TOKENS = 15e4;
|
|
19544
|
+
function estimateTurnTokens(turn) {
|
|
19545
|
+
let chars = 0;
|
|
19546
|
+
for (const block of turn.content) {
|
|
19547
|
+
if ("text" in block && typeof block.text === "string") {
|
|
19548
|
+
chars += block.text.length;
|
|
19549
|
+
}
|
|
19550
|
+
}
|
|
19551
|
+
if (turn.toolCalls) {
|
|
19552
|
+
for (const tc of turn.toolCalls) {
|
|
19553
|
+
chars += JSON.stringify(tc.input ?? "").length;
|
|
19554
|
+
if (tc.result !== void 0) {
|
|
19555
|
+
chars += typeof tc.result === "string" ? tc.result.length : JSON.stringify(tc.result).length;
|
|
19556
|
+
}
|
|
19557
|
+
}
|
|
19558
|
+
}
|
|
19559
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
19560
|
+
}
|
|
19561
|
+
function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
19562
|
+
let budget = maxTokens;
|
|
19563
|
+
let startIndex = turns.length;
|
|
19564
|
+
for (let i2 = turns.length - 1; i2 >= 0; i2--) {
|
|
19565
|
+
const cost = estimateTurnTokens(turns[i2]);
|
|
19566
|
+
if (cost > budget) break;
|
|
19567
|
+
budget -= cost;
|
|
19568
|
+
startIndex = i2;
|
|
19569
|
+
}
|
|
19570
|
+
while (startIndex < turns.length && turns[startIndex].role !== "user") {
|
|
19571
|
+
startIndex++;
|
|
19572
|
+
}
|
|
19573
|
+
return turns.slice(startIndex);
|
|
19574
|
+
}
|
|
19575
|
+
|
|
19576
|
+
// src/sagas/resume-saga.ts
|
|
19577
|
+
var ResumeSaga = class extends Saga {
|
|
19578
|
+
sagaName = "ResumeSaga";
|
|
19579
|
+
async execute(input) {
|
|
19580
|
+
const { taskId, runId, apiClient } = input;
|
|
19581
|
+
const taskRun = await this.readOnlyStep(
|
|
19582
|
+
"fetch_task_run",
|
|
19583
|
+
() => apiClient.getTaskRun(taskId, runId)
|
|
19584
|
+
);
|
|
19585
|
+
if (!taskRun.log_url) {
|
|
19586
|
+
this.log.info("No log URL found, starting fresh");
|
|
19587
|
+
return this.emptyResult();
|
|
19588
|
+
}
|
|
19589
|
+
const entries = await this.readOnlyStep(
|
|
19590
|
+
"fetch_logs",
|
|
19591
|
+
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
19592
|
+
);
|
|
19593
|
+
if (entries.length === 0) {
|
|
19594
|
+
this.log.info("No log entries found, starting fresh");
|
|
19595
|
+
return this.emptyResult();
|
|
19596
|
+
}
|
|
19597
|
+
this.log.info("Fetched log entries", { count: entries.length });
|
|
19598
|
+
const latestSnapshot = await this.readOnlyStep(
|
|
19599
|
+
"find_snapshot",
|
|
19600
|
+
() => Promise.resolve(this.findLatestTreeSnapshot(entries))
|
|
19601
|
+
);
|
|
19602
|
+
const latestGitCheckpoint = await this.readOnlyStep(
|
|
19603
|
+
"find_git_checkpoint",
|
|
19604
|
+
() => Promise.resolve(this.findLatestGitCheckpoint(entries))
|
|
19605
|
+
);
|
|
19606
|
+
if (latestSnapshot) {
|
|
19607
|
+
this.log.info("Found tree snapshot", {
|
|
19608
|
+
treeHash: latestSnapshot.treeHash,
|
|
19609
|
+
hasArchiveUrl: !!latestSnapshot.archiveUrl,
|
|
19610
|
+
changes: latestSnapshot.changes?.length ?? 0
|
|
19611
|
+
});
|
|
19612
|
+
}
|
|
19613
|
+
if (latestGitCheckpoint) {
|
|
19614
|
+
this.log.info("Found git checkpoint", {
|
|
19615
|
+
checkpointId: latestGitCheckpoint.checkpointId,
|
|
19616
|
+
branch: latestGitCheckpoint.branch
|
|
19617
|
+
});
|
|
19618
|
+
}
|
|
19619
|
+
const conversation = await this.readOnlyStep(
|
|
19620
|
+
"rebuild_conversation",
|
|
19621
|
+
() => Promise.resolve(this.rebuildConversation(entries))
|
|
19622
|
+
);
|
|
19623
|
+
const lastDevice = await this.readOnlyStep(
|
|
19624
|
+
"find_device",
|
|
19625
|
+
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
19626
|
+
);
|
|
19627
|
+
this.log.info("Resume state rebuilt", {
|
|
19628
|
+
turns: conversation.length,
|
|
19629
|
+
hasSnapshot: !!latestSnapshot,
|
|
19630
|
+
hasGitCheckpoint: !!latestGitCheckpoint,
|
|
19631
|
+
interrupted: latestSnapshot?.interrupted ?? false
|
|
19632
|
+
});
|
|
19633
|
+
return {
|
|
19634
|
+
conversation,
|
|
19635
|
+
latestSnapshot,
|
|
19636
|
+
latestGitCheckpoint,
|
|
19637
|
+
interrupted: latestSnapshot?.interrupted ?? false,
|
|
19638
|
+
lastDevice,
|
|
19639
|
+
logEntryCount: entries.length
|
|
19640
|
+
};
|
|
19641
|
+
}
|
|
19642
|
+
emptyResult() {
|
|
19643
|
+
return {
|
|
19644
|
+
conversation: [],
|
|
19645
|
+
latestSnapshot: null,
|
|
19646
|
+
latestGitCheckpoint: null,
|
|
19647
|
+
interrupted: false,
|
|
19648
|
+
logEntryCount: 0
|
|
19649
|
+
};
|
|
19650
|
+
}
|
|
19651
|
+
findLatestTreeSnapshot(entries) {
|
|
19652
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19653
|
+
const entry = entries[i2];
|
|
19654
|
+
if (isNotification(
|
|
19655
|
+
entry.notification?.method,
|
|
19656
|
+
POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
19657
|
+
)) {
|
|
19658
|
+
const params = entry.notification.params;
|
|
19659
|
+
if (params?.treeHash) {
|
|
19660
|
+
return params;
|
|
19661
|
+
}
|
|
19662
|
+
}
|
|
19663
|
+
}
|
|
19664
|
+
return null;
|
|
19665
|
+
}
|
|
19666
|
+
findLatestGitCheckpoint(entries) {
|
|
19667
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
|
|
19668
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19669
|
+
const entry = entries[i2];
|
|
19670
|
+
const method = entry.notification?.method;
|
|
19671
|
+
if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
|
|
19672
|
+
const params = entry.notification?.params;
|
|
19673
|
+
if (params?.checkpointId && params?.checkpointRef) {
|
|
19674
|
+
return params;
|
|
19675
|
+
}
|
|
19676
|
+
}
|
|
19677
|
+
}
|
|
19678
|
+
return null;
|
|
19679
|
+
}
|
|
19680
|
+
findLastDeviceInfo(entries) {
|
|
19681
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19682
|
+
const entry = entries[i2];
|
|
19683
|
+
const params = entry.notification?.params;
|
|
19684
|
+
if (params?.device) {
|
|
19685
|
+
return params.device;
|
|
19686
|
+
}
|
|
19687
|
+
}
|
|
19688
|
+
return void 0;
|
|
19390
19689
|
}
|
|
19391
19690
|
rebuildConversation(entries) {
|
|
19392
19691
|
const turns = [];
|
|
@@ -19442,463 +19741,1012 @@ var ResumeSaga = class extends Saga {
|
|
|
19442
19741
|
}
|
|
19443
19742
|
break;
|
|
19444
19743
|
}
|
|
19445
|
-
case "tool_call":
|
|
19446
|
-
case "tool_call_update": {
|
|
19447
|
-
const meta = update._meta?.claudeCode;
|
|
19448
|
-
if (meta) {
|
|
19449
|
-
const toolCallId = meta.toolCallId;
|
|
19450
|
-
const toolName = meta.toolName;
|
|
19451
|
-
const toolInput = meta.toolInput;
|
|
19452
|
-
const toolResponse = meta.toolResponse;
|
|
19453
|
-
if (toolCallId && toolName) {
|
|
19454
|
-
let toolCall = currentToolCalls.find(
|
|
19455
|
-
(tc) => tc.toolCallId === toolCallId
|
|
19456
|
-
);
|
|
19457
|
-
if (!toolCall) {
|
|
19458
|
-
toolCall = {
|
|
19459
|
-
toolCallId,
|
|
19460
|
-
toolName,
|
|
19461
|
-
input: toolInput
|
|
19462
|
-
};
|
|
19463
|
-
currentToolCalls.push(toolCall);
|
|
19464
|
-
}
|
|
19465
|
-
if (toolResponse !== void 0) {
|
|
19466
|
-
toolCall.result = toolResponse;
|
|
19467
|
-
}
|
|
19468
|
-
}
|
|
19469
|
-
}
|
|
19470
|
-
break;
|
|
19744
|
+
case "tool_call":
|
|
19745
|
+
case "tool_call_update": {
|
|
19746
|
+
const meta = update._meta?.claudeCode;
|
|
19747
|
+
if (meta) {
|
|
19748
|
+
const toolCallId = meta.toolCallId;
|
|
19749
|
+
const toolName = meta.toolName;
|
|
19750
|
+
const toolInput = meta.toolInput;
|
|
19751
|
+
const toolResponse = meta.toolResponse;
|
|
19752
|
+
if (toolCallId && toolName) {
|
|
19753
|
+
let toolCall = currentToolCalls.find(
|
|
19754
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19755
|
+
);
|
|
19756
|
+
if (!toolCall) {
|
|
19757
|
+
toolCall = {
|
|
19758
|
+
toolCallId,
|
|
19759
|
+
toolName,
|
|
19760
|
+
input: toolInput
|
|
19761
|
+
};
|
|
19762
|
+
currentToolCalls.push(toolCall);
|
|
19763
|
+
}
|
|
19764
|
+
if (toolResponse !== void 0) {
|
|
19765
|
+
toolCall.result = toolResponse;
|
|
19766
|
+
}
|
|
19767
|
+
}
|
|
19768
|
+
}
|
|
19769
|
+
break;
|
|
19770
|
+
}
|
|
19771
|
+
case "tool_result": {
|
|
19772
|
+
const meta = update._meta?.claudeCode;
|
|
19773
|
+
if (meta) {
|
|
19774
|
+
const toolCallId = meta.toolCallId;
|
|
19775
|
+
const toolResponse = meta.toolResponse;
|
|
19776
|
+
if (toolCallId) {
|
|
19777
|
+
const toolCall = currentToolCalls.find(
|
|
19778
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19779
|
+
);
|
|
19780
|
+
if (toolCall && toolResponse !== void 0) {
|
|
19781
|
+
toolCall.result = toolResponse;
|
|
19782
|
+
}
|
|
19783
|
+
}
|
|
19784
|
+
}
|
|
19785
|
+
break;
|
|
19786
|
+
}
|
|
19787
|
+
}
|
|
19788
|
+
}
|
|
19789
|
+
}
|
|
19790
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19791
|
+
turns.push({
|
|
19792
|
+
role: "assistant",
|
|
19793
|
+
content: currentAssistantContent,
|
|
19794
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19795
|
+
});
|
|
19796
|
+
}
|
|
19797
|
+
return turns;
|
|
19798
|
+
}
|
|
19799
|
+
};
|
|
19800
|
+
|
|
19801
|
+
// src/resume.ts
|
|
19802
|
+
async function resumeFromLog(config) {
|
|
19803
|
+
const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
19804
|
+
logger.info("Resuming from log", {
|
|
19805
|
+
taskId: config.taskId,
|
|
19806
|
+
runId: config.runId
|
|
19807
|
+
});
|
|
19808
|
+
const saga = new ResumeSaga(logger);
|
|
19809
|
+
const result = await saga.run({
|
|
19810
|
+
taskId: config.taskId,
|
|
19811
|
+
runId: config.runId,
|
|
19812
|
+
repositoryPath: config.repositoryPath,
|
|
19813
|
+
apiClient: config.apiClient,
|
|
19814
|
+
logger
|
|
19815
|
+
});
|
|
19816
|
+
if (!result.success) {
|
|
19817
|
+
logger.error("Failed to resume from log", {
|
|
19818
|
+
error: result.error,
|
|
19819
|
+
failedStep: result.failedStep
|
|
19820
|
+
});
|
|
19821
|
+
throw new Error(
|
|
19822
|
+
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
19823
|
+
);
|
|
19824
|
+
}
|
|
19825
|
+
return {
|
|
19826
|
+
conversation: result.data.conversation,
|
|
19827
|
+
latestSnapshot: result.data.latestSnapshot,
|
|
19828
|
+
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
19829
|
+
interrupted: result.data.interrupted,
|
|
19830
|
+
lastDevice: result.data.lastDevice,
|
|
19831
|
+
logEntryCount: result.data.logEntryCount
|
|
19832
|
+
};
|
|
19833
|
+
}
|
|
19834
|
+
var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
|
|
19835
|
+
var TOOL_RESULT_MAX_CHARS = 2e3;
|
|
19836
|
+
var RESUME_CONTEXT_MARKERS = [
|
|
19837
|
+
"You are resuming a previous conversation",
|
|
19838
|
+
"Here is the conversation history from the",
|
|
19839
|
+
"Continue from where you left off"
|
|
19840
|
+
];
|
|
19841
|
+
function isResumeContextTurn(turn) {
|
|
19842
|
+
if (turn.role !== "user") return false;
|
|
19843
|
+
const text2 = turn.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
19844
|
+
return RESUME_CONTEXT_MARKERS.some((marker) => text2.includes(marker));
|
|
19845
|
+
}
|
|
19846
|
+
function formatConversationForResume(conversation) {
|
|
19847
|
+
const filtered = conversation.filter((turn) => !isResumeContextTurn(turn));
|
|
19848
|
+
const selected = selectRecentTurns(filtered, RESUME_HISTORY_TOKEN_BUDGET);
|
|
19849
|
+
const parts2 = [];
|
|
19850
|
+
if (selected.length < filtered.length) {
|
|
19851
|
+
parts2.push(
|
|
19852
|
+
`*(${filtered.length - selected.length} earlier turns omitted)*`
|
|
19853
|
+
);
|
|
19854
|
+
}
|
|
19855
|
+
for (const turn of selected) {
|
|
19856
|
+
const role = turn.role === "user" ? "User" : "Assistant";
|
|
19857
|
+
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
19858
|
+
if (textParts.length > 0) {
|
|
19859
|
+
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
19860
|
+
}
|
|
19861
|
+
if (turn.toolCalls?.length) {
|
|
19862
|
+
const toolSummary = turn.toolCalls.map((tc) => {
|
|
19863
|
+
let resultStr = "";
|
|
19864
|
+
if (tc.result !== void 0) {
|
|
19865
|
+
const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
|
|
19866
|
+
resultStr = raw.length > TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
|
|
19867
|
+
}
|
|
19868
|
+
return ` - ${tc.toolName}${resultStr}`;
|
|
19869
|
+
}).join("\n");
|
|
19870
|
+
parts2.push(`**${role} (tools)**:
|
|
19871
|
+
${toolSummary}`);
|
|
19872
|
+
}
|
|
19873
|
+
}
|
|
19874
|
+
return parts2.join("\n\n");
|
|
19875
|
+
}
|
|
19876
|
+
|
|
19877
|
+
// src/session-log-writer.ts
|
|
19878
|
+
import fs12 from "fs";
|
|
19879
|
+
import fsp from "fs/promises";
|
|
19880
|
+
import path15 from "path";
|
|
19881
|
+
var SessionLogWriter = class _SessionLogWriter {
|
|
19882
|
+
static FLUSH_DEBOUNCE_MS = 500;
|
|
19883
|
+
static FLUSH_MAX_INTERVAL_MS = 5e3;
|
|
19884
|
+
static MAX_FLUSH_RETRIES = 10;
|
|
19885
|
+
static MAX_RETRY_DELAY_MS = 3e4;
|
|
19886
|
+
static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
19887
|
+
posthogAPI;
|
|
19888
|
+
pendingEntries = /* @__PURE__ */ new Map();
|
|
19889
|
+
flushTimeouts = /* @__PURE__ */ new Map();
|
|
19890
|
+
lastFlushAttemptTime = /* @__PURE__ */ new Map();
|
|
19891
|
+
retryCounts = /* @__PURE__ */ new Map();
|
|
19892
|
+
sessions = /* @__PURE__ */ new Map();
|
|
19893
|
+
flushQueues = /* @__PURE__ */ new Map();
|
|
19894
|
+
logger;
|
|
19895
|
+
localCachePath;
|
|
19896
|
+
constructor(options = {}) {
|
|
19897
|
+
this.posthogAPI = options.posthogAPI;
|
|
19898
|
+
this.localCachePath = options.localCachePath;
|
|
19899
|
+
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
19900
|
+
}
|
|
19901
|
+
async flushAll() {
|
|
19902
|
+
const flushPromises = [];
|
|
19903
|
+
for (const [sessionId, session] of this.sessions) {
|
|
19904
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19905
|
+
flushPromises.push(this.flush(sessionId));
|
|
19906
|
+
}
|
|
19907
|
+
await Promise.all(flushPromises);
|
|
19908
|
+
}
|
|
19909
|
+
register(sessionId, context) {
|
|
19910
|
+
if (this.sessions.has(sessionId)) {
|
|
19911
|
+
return;
|
|
19912
|
+
}
|
|
19913
|
+
this.sessions.set(sessionId, { context, currentTurnMessages: [] });
|
|
19914
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
19915
|
+
if (this.localCachePath) {
|
|
19916
|
+
const sessionDir = path15.join(
|
|
19917
|
+
this.localCachePath,
|
|
19918
|
+
"sessions",
|
|
19919
|
+
context.runId
|
|
19920
|
+
);
|
|
19921
|
+
try {
|
|
19922
|
+
fs12.mkdirSync(sessionDir, { recursive: true });
|
|
19923
|
+
} catch (error) {
|
|
19924
|
+
this.logger.warn("Failed to create local cache directory", {
|
|
19925
|
+
sessionDir,
|
|
19926
|
+
error
|
|
19927
|
+
});
|
|
19928
|
+
}
|
|
19929
|
+
}
|
|
19930
|
+
}
|
|
19931
|
+
isRegistered(sessionId) {
|
|
19932
|
+
return this.sessions.has(sessionId);
|
|
19933
|
+
}
|
|
19934
|
+
appendRawLine(sessionId, line) {
|
|
19935
|
+
const session = this.sessions.get(sessionId);
|
|
19936
|
+
if (!session) {
|
|
19937
|
+
this.logger.warn("appendRawLine called for unregistered session", {
|
|
19938
|
+
sessionId
|
|
19939
|
+
});
|
|
19940
|
+
return;
|
|
19941
|
+
}
|
|
19942
|
+
try {
|
|
19943
|
+
const message = JSON.parse(line);
|
|
19944
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19945
|
+
if (this.isAgentMessageChunk(message)) {
|
|
19946
|
+
const text2 = this.extractChunkText(message);
|
|
19947
|
+
if (text2) {
|
|
19948
|
+
if (!session.chunkBuffer) {
|
|
19949
|
+
session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
|
|
19950
|
+
} else {
|
|
19951
|
+
session.chunkBuffer.text += text2;
|
|
19952
|
+
}
|
|
19953
|
+
}
|
|
19954
|
+
return;
|
|
19955
|
+
}
|
|
19956
|
+
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
19957
|
+
session.chunkBuffer = void 0;
|
|
19958
|
+
} else {
|
|
19959
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19960
|
+
}
|
|
19961
|
+
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
19962
|
+
if (nonChunkAgentText) {
|
|
19963
|
+
session.lastAgentMessage = nonChunkAgentText;
|
|
19964
|
+
session.currentTurnMessages.push(nonChunkAgentText);
|
|
19965
|
+
}
|
|
19966
|
+
const entry = {
|
|
19967
|
+
type: "notification",
|
|
19968
|
+
timestamp,
|
|
19969
|
+
notification: message
|
|
19970
|
+
};
|
|
19971
|
+
this.writeToLocalCache(sessionId, entry);
|
|
19972
|
+
if (this.posthogAPI) {
|
|
19973
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
19974
|
+
pending.push(entry);
|
|
19975
|
+
this.pendingEntries.set(sessionId, pending);
|
|
19976
|
+
this.scheduleFlush(sessionId);
|
|
19977
|
+
}
|
|
19978
|
+
} catch {
|
|
19979
|
+
this.logger.warn("Failed to parse raw line for persistence", {
|
|
19980
|
+
taskId: session.context.taskId,
|
|
19981
|
+
runId: session.context.runId,
|
|
19982
|
+
lineLength: line.length
|
|
19983
|
+
});
|
|
19984
|
+
}
|
|
19985
|
+
}
|
|
19986
|
+
async flush(sessionId, { coalesce = false } = {}) {
|
|
19987
|
+
if (coalesce) {
|
|
19988
|
+
const session = this.sessions.get(sessionId);
|
|
19989
|
+
if (session) {
|
|
19990
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19991
|
+
}
|
|
19992
|
+
}
|
|
19993
|
+
const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
|
|
19994
|
+
const next = prev.catch(() => {
|
|
19995
|
+
}).then(() => this._doFlush(sessionId));
|
|
19996
|
+
this.flushQueues.set(sessionId, next);
|
|
19997
|
+
next.finally(() => {
|
|
19998
|
+
if (this.flushQueues.get(sessionId) === next) {
|
|
19999
|
+
this.flushQueues.delete(sessionId);
|
|
20000
|
+
}
|
|
20001
|
+
});
|
|
20002
|
+
return next;
|
|
20003
|
+
}
|
|
20004
|
+
async _doFlush(sessionId) {
|
|
20005
|
+
const session = this.sessions.get(sessionId);
|
|
20006
|
+
if (!session) {
|
|
20007
|
+
this.logger.warn("flush: no session found", { sessionId });
|
|
20008
|
+
return;
|
|
20009
|
+
}
|
|
20010
|
+
const pending = this.pendingEntries.get(sessionId);
|
|
20011
|
+
if (!this.posthogAPI || !pending?.length) {
|
|
20012
|
+
return;
|
|
20013
|
+
}
|
|
20014
|
+
this.pendingEntries.delete(sessionId);
|
|
20015
|
+
const timeout = this.flushTimeouts.get(sessionId);
|
|
20016
|
+
if (timeout) {
|
|
20017
|
+
clearTimeout(timeout);
|
|
20018
|
+
this.flushTimeouts.delete(sessionId);
|
|
20019
|
+
}
|
|
20020
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20021
|
+
try {
|
|
20022
|
+
await this.posthogAPI.appendTaskRunLog(
|
|
20023
|
+
session.context.taskId,
|
|
20024
|
+
session.context.runId,
|
|
20025
|
+
pending
|
|
20026
|
+
);
|
|
20027
|
+
this.retryCounts.set(sessionId, 0);
|
|
20028
|
+
} catch (error) {
|
|
20029
|
+
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
20030
|
+
this.retryCounts.set(sessionId, retryCount);
|
|
20031
|
+
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
20032
|
+
this.logger.error(
|
|
20033
|
+
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
20034
|
+
{
|
|
20035
|
+
taskId: session.context.taskId,
|
|
20036
|
+
runId: session.context.runId,
|
|
20037
|
+
error
|
|
19471
20038
|
}
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
19475
|
-
|
|
19476
|
-
|
|
19477
|
-
|
|
19478
|
-
|
|
19479
|
-
|
|
19480
|
-
|
|
19481
|
-
|
|
19482
|
-
toolCall.result = toolResponse;
|
|
19483
|
-
}
|
|
19484
|
-
}
|
|
20039
|
+
);
|
|
20040
|
+
this.retryCounts.set(sessionId, 0);
|
|
20041
|
+
} else {
|
|
20042
|
+
if (retryCount === 1) {
|
|
20043
|
+
this.logger.warn(
|
|
20044
|
+
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
20045
|
+
{
|
|
20046
|
+
taskId: session.context.taskId,
|
|
20047
|
+
runId: session.context.runId,
|
|
20048
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19485
20049
|
}
|
|
19486
|
-
|
|
20050
|
+
);
|
|
20051
|
+
}
|
|
20052
|
+
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
20053
|
+
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
20054
|
+
this.scheduleFlush(sessionId);
|
|
20055
|
+
}
|
|
20056
|
+
}
|
|
20057
|
+
}
|
|
20058
|
+
getSessionUpdateType(message) {
|
|
20059
|
+
if (message.method !== "session/update") return void 0;
|
|
20060
|
+
const params = message.params;
|
|
20061
|
+
const update = params?.update;
|
|
20062
|
+
return update?.sessionUpdate;
|
|
20063
|
+
}
|
|
20064
|
+
isDirectAgentMessage(message) {
|
|
20065
|
+
return this.getSessionUpdateType(message) === "agent_message";
|
|
20066
|
+
}
|
|
20067
|
+
isAgentMessageChunk(message) {
|
|
20068
|
+
return this.getSessionUpdateType(message) === "agent_message_chunk";
|
|
20069
|
+
}
|
|
20070
|
+
extractChunkText(message) {
|
|
20071
|
+
const params = message.params;
|
|
20072
|
+
const update = params?.update;
|
|
20073
|
+
const content = update?.content;
|
|
20074
|
+
if (content?.type === "text" && content.text) {
|
|
20075
|
+
return content.text;
|
|
20076
|
+
}
|
|
20077
|
+
return "";
|
|
20078
|
+
}
|
|
20079
|
+
emitCoalescedMessage(sessionId, session) {
|
|
20080
|
+
if (!session.chunkBuffer) return;
|
|
20081
|
+
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
20082
|
+
session.chunkBuffer = void 0;
|
|
20083
|
+
session.lastAgentMessage = text2;
|
|
20084
|
+
session.currentTurnMessages.push(text2);
|
|
20085
|
+
const entry = {
|
|
20086
|
+
type: "notification",
|
|
20087
|
+
timestamp: firstTimestamp,
|
|
20088
|
+
notification: {
|
|
20089
|
+
jsonrpc: "2.0",
|
|
20090
|
+
method: "session/update",
|
|
20091
|
+
params: {
|
|
20092
|
+
update: {
|
|
20093
|
+
sessionUpdate: "agent_message",
|
|
20094
|
+
content: { type: "text", text: text2 }
|
|
19487
20095
|
}
|
|
19488
20096
|
}
|
|
19489
20097
|
}
|
|
20098
|
+
};
|
|
20099
|
+
this.writeToLocalCache(sessionId, entry);
|
|
20100
|
+
if (this.posthogAPI) {
|
|
20101
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20102
|
+
pending.push(entry);
|
|
20103
|
+
this.pendingEntries.set(sessionId, pending);
|
|
20104
|
+
this.scheduleFlush(sessionId);
|
|
19490
20105
|
}
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19496
|
-
|
|
20106
|
+
}
|
|
20107
|
+
getLastAgentMessage(sessionId) {
|
|
20108
|
+
return this.sessions.get(sessionId)?.lastAgentMessage;
|
|
20109
|
+
}
|
|
20110
|
+
getFullAgentResponse(sessionId) {
|
|
20111
|
+
const session = this.sessions.get(sessionId);
|
|
20112
|
+
if (!session || session.currentTurnMessages.length === 0) return void 0;
|
|
20113
|
+
if (session.chunkBuffer) {
|
|
20114
|
+
this.logger.warn(
|
|
20115
|
+
"getFullAgentResponse called with non-empty chunk buffer",
|
|
20116
|
+
{
|
|
20117
|
+
sessionId,
|
|
20118
|
+
bufferedLength: session.chunkBuffer.text.length
|
|
20119
|
+
}
|
|
20120
|
+
);
|
|
19497
20121
|
}
|
|
19498
|
-
return
|
|
20122
|
+
return session.currentTurnMessages.join("\n\n");
|
|
19499
20123
|
}
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
19505
|
-
logger.info("Resuming from log", {
|
|
19506
|
-
taskId: config.taskId,
|
|
19507
|
-
runId: config.runId
|
|
19508
|
-
});
|
|
19509
|
-
const saga = new ResumeSaga(logger);
|
|
19510
|
-
const result = await saga.run({
|
|
19511
|
-
taskId: config.taskId,
|
|
19512
|
-
runId: config.runId,
|
|
19513
|
-
repositoryPath: config.repositoryPath,
|
|
19514
|
-
apiClient: config.apiClient,
|
|
19515
|
-
logger
|
|
19516
|
-
});
|
|
19517
|
-
if (!result.success) {
|
|
19518
|
-
logger.error("Failed to resume from log", {
|
|
19519
|
-
error: result.error,
|
|
19520
|
-
failedStep: result.failedStep
|
|
19521
|
-
});
|
|
19522
|
-
throw new Error(
|
|
19523
|
-
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
19524
|
-
);
|
|
20124
|
+
resetTurnMessages(sessionId) {
|
|
20125
|
+
const session = this.sessions.get(sessionId);
|
|
20126
|
+
if (session) {
|
|
20127
|
+
session.currentTurnMessages = [];
|
|
20128
|
+
}
|
|
19525
20129
|
}
|
|
19526
|
-
|
|
19527
|
-
|
|
19528
|
-
|
|
19529
|
-
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
|
|
19534
|
-
}
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
20130
|
+
extractAgentMessageText(message) {
|
|
20131
|
+
if (message.method !== "session/update") {
|
|
20132
|
+
return null;
|
|
20133
|
+
}
|
|
20134
|
+
const params = message.params;
|
|
20135
|
+
const update = params?.update;
|
|
20136
|
+
if (update?.sessionUpdate !== "agent_message") {
|
|
20137
|
+
return null;
|
|
20138
|
+
}
|
|
20139
|
+
const content = update.content;
|
|
20140
|
+
if (content?.type === "text" && typeof content.text === "string") {
|
|
20141
|
+
const trimmed2 = content.text.trim();
|
|
20142
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20143
|
+
}
|
|
20144
|
+
if (typeof update.message === "string") {
|
|
20145
|
+
const trimmed2 = update.message.trim();
|
|
20146
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20147
|
+
}
|
|
20148
|
+
return null;
|
|
19544
20149
|
}
|
|
19545
|
-
|
|
19546
|
-
const
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
20150
|
+
scheduleFlush(sessionId) {
|
|
20151
|
+
const existing = this.flushTimeouts.get(sessionId);
|
|
20152
|
+
if (existing) clearTimeout(existing);
|
|
20153
|
+
const retryCount = this.retryCounts.get(sessionId) ?? 0;
|
|
20154
|
+
const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
|
|
20155
|
+
const elapsed = Date.now() - lastAttempt;
|
|
20156
|
+
let delay3;
|
|
20157
|
+
if (retryCount > 0) {
|
|
20158
|
+
delay3 = Math.min(
|
|
20159
|
+
_SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
|
|
20160
|
+
_SessionLogWriter.MAX_RETRY_DELAY_MS
|
|
20161
|
+
);
|
|
20162
|
+
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20163
|
+
delay3 = 0;
|
|
20164
|
+
} else {
|
|
20165
|
+
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
19550
20166
|
}
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
20167
|
+
const timeout = setTimeout(() => this.flush(sessionId), delay3);
|
|
20168
|
+
this.flushTimeouts.set(sessionId, timeout);
|
|
20169
|
+
}
|
|
20170
|
+
writeToLocalCache(sessionId, entry) {
|
|
20171
|
+
if (!this.localCachePath) return;
|
|
20172
|
+
const session = this.sessions.get(sessionId);
|
|
20173
|
+
if (!session) return;
|
|
20174
|
+
const logPath = path15.join(
|
|
20175
|
+
this.localCachePath,
|
|
20176
|
+
"sessions",
|
|
20177
|
+
session.context.runId,
|
|
20178
|
+
"logs.ndjson"
|
|
20179
|
+
);
|
|
20180
|
+
try {
|
|
20181
|
+
fs12.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
20182
|
+
`);
|
|
20183
|
+
} catch (error) {
|
|
20184
|
+
this.logger.warn("Failed to write to local cache", {
|
|
20185
|
+
taskId: session.context.taskId,
|
|
20186
|
+
runId: session.context.runId,
|
|
20187
|
+
logPath,
|
|
20188
|
+
error
|
|
20189
|
+
});
|
|
20190
|
+
}
|
|
20191
|
+
}
|
|
20192
|
+
static async cleanupOldSessions(localCachePath) {
|
|
20193
|
+
const sessionsDir = path15.join(localCachePath, "sessions");
|
|
20194
|
+
let deleted = 0;
|
|
20195
|
+
try {
|
|
20196
|
+
const entries = await fsp.readdir(sessionsDir);
|
|
20197
|
+
const now = Date.now();
|
|
20198
|
+
for (const entry of entries) {
|
|
20199
|
+
const entryPath = path15.join(sessionsDir, entry);
|
|
20200
|
+
try {
|
|
20201
|
+
const stats = await fsp.stat(entryPath);
|
|
20202
|
+
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
20203
|
+
await fsp.rm(entryPath, { recursive: true, force: true });
|
|
20204
|
+
deleted++;
|
|
20205
|
+
}
|
|
20206
|
+
} catch {
|
|
19557
20207
|
}
|
|
19558
|
-
|
|
19559
|
-
|
|
19560
|
-
parts2.push(`**${role} (tools)**:
|
|
19561
|
-
${toolSummary}`);
|
|
20208
|
+
}
|
|
20209
|
+
} catch {
|
|
19562
20210
|
}
|
|
20211
|
+
return deleted;
|
|
19563
20212
|
}
|
|
19564
|
-
|
|
19565
|
-
}
|
|
20213
|
+
};
|
|
19566
20214
|
|
|
19567
|
-
// src/
|
|
19568
|
-
import
|
|
19569
|
-
import
|
|
19570
|
-
|
|
19571
|
-
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19585
|
-
|
|
19586
|
-
|
|
19587
|
-
|
|
19588
|
-
this.
|
|
19589
|
-
|
|
19590
|
-
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
20215
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20216
|
+
import { mkdir as mkdir7, rm as rm6, writeFile as writeFile5 } from "fs/promises";
|
|
20217
|
+
import { join as join12 } from "path";
|
|
20218
|
+
|
|
20219
|
+
// ../git/dist/sagas/tree.js
|
|
20220
|
+
import { existsSync as existsSync5 } from "fs";
|
|
20221
|
+
import * as fs13 from "fs/promises";
|
|
20222
|
+
import * as path16 from "path";
|
|
20223
|
+
import * as tar from "tar";
|
|
20224
|
+
var CaptureTreeSaga = class extends GitSaga {
|
|
20225
|
+
sagaName = "CaptureTreeSaga";
|
|
20226
|
+
tempIndexPath = null;
|
|
20227
|
+
async executeGitOperations(input) {
|
|
20228
|
+
const { baseDir, lastTreeHash, archivePath, signal } = input;
|
|
20229
|
+
const tmpDir = path16.join(baseDir, ".git", "posthog-code-tmp");
|
|
20230
|
+
await this.step({
|
|
20231
|
+
name: "create_tmp_dir",
|
|
20232
|
+
execute: () => fs13.mkdir(tmpDir, { recursive: true }),
|
|
20233
|
+
rollback: async () => {
|
|
20234
|
+
}
|
|
20235
|
+
});
|
|
20236
|
+
this.tempIndexPath = path16.join(tmpDir, `index-${Date.now()}`);
|
|
20237
|
+
const tempIndexGit = this.git.env({
|
|
20238
|
+
...process.env,
|
|
20239
|
+
GIT_INDEX_FILE: this.tempIndexPath
|
|
20240
|
+
});
|
|
20241
|
+
await this.step({
|
|
20242
|
+
name: "init_temp_index",
|
|
20243
|
+
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
20244
|
+
rollback: async () => {
|
|
20245
|
+
if (this.tempIndexPath) {
|
|
20246
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20247
|
+
});
|
|
20248
|
+
}
|
|
20249
|
+
}
|
|
20250
|
+
});
|
|
20251
|
+
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
20252
|
+
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
20253
|
+
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
20254
|
+
this.log.debug("No changes since last capture", { treeHash });
|
|
20255
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20256
|
+
});
|
|
20257
|
+
return { snapshot: null, changed: false };
|
|
20258
|
+
}
|
|
20259
|
+
const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
|
|
20260
|
+
try {
|
|
20261
|
+
return await getHeadSha(baseDir, { abortSignal: signal });
|
|
20262
|
+
} catch {
|
|
20263
|
+
return null;
|
|
20264
|
+
}
|
|
20265
|
+
});
|
|
20266
|
+
const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
|
|
20267
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20268
|
+
});
|
|
20269
|
+
const snapshot = {
|
|
20270
|
+
treeHash,
|
|
20271
|
+
baseCommit,
|
|
20272
|
+
changes,
|
|
20273
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20274
|
+
};
|
|
20275
|
+
let createdArchivePath;
|
|
20276
|
+
if (archivePath) {
|
|
20277
|
+
createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
|
|
19596
20278
|
}
|
|
19597
|
-
|
|
20279
|
+
this.log.info("Tree captured", {
|
|
20280
|
+
treeHash,
|
|
20281
|
+
changes: changes.length,
|
|
20282
|
+
archived: !!createdArchivePath
|
|
20283
|
+
});
|
|
20284
|
+
return { snapshot, archivePath: createdArchivePath, changed: true };
|
|
19598
20285
|
}
|
|
19599
|
-
|
|
19600
|
-
|
|
19601
|
-
|
|
20286
|
+
async createArchive(baseDir, archivePath, changes) {
|
|
20287
|
+
const filesToArchive = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20288
|
+
if (filesToArchive.length === 0) {
|
|
20289
|
+
return void 0;
|
|
19602
20290
|
}
|
|
19603
|
-
|
|
19604
|
-
|
|
19605
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
19608
|
-
|
|
19609
|
-
|
|
19610
|
-
|
|
19611
|
-
|
|
19612
|
-
|
|
19613
|
-
|
|
19614
|
-
|
|
19615
|
-
|
|
19616
|
-
|
|
20291
|
+
const existingFiles = filesToArchive.filter((f) => existsSync5(path16.join(baseDir, f)));
|
|
20292
|
+
if (existingFiles.length === 0) {
|
|
20293
|
+
return void 0;
|
|
20294
|
+
}
|
|
20295
|
+
await this.step({
|
|
20296
|
+
name: "create_archive",
|
|
20297
|
+
execute: async () => {
|
|
20298
|
+
const archiveDir = path16.dirname(archivePath);
|
|
20299
|
+
await fs13.mkdir(archiveDir, { recursive: true });
|
|
20300
|
+
await tar.create({
|
|
20301
|
+
gzip: true,
|
|
20302
|
+
file: archivePath,
|
|
20303
|
+
cwd: baseDir
|
|
20304
|
+
}, existingFiles);
|
|
20305
|
+
},
|
|
20306
|
+
rollback: async () => {
|
|
20307
|
+
await fs13.rm(archivePath, { force: true }).catch(() => {
|
|
19617
20308
|
});
|
|
19618
20309
|
}
|
|
19619
|
-
}
|
|
19620
|
-
|
|
19621
|
-
isRegistered(sessionId) {
|
|
19622
|
-
return this.sessions.has(sessionId);
|
|
20310
|
+
});
|
|
20311
|
+
return archivePath;
|
|
19623
20312
|
}
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
sessionId
|
|
19629
|
-
});
|
|
19630
|
-
return;
|
|
20313
|
+
async getChanges(git, fromRef, toRef) {
|
|
20314
|
+
if (!fromRef) {
|
|
20315
|
+
const stdout2 = await git.raw(["ls-tree", "-r", "--name-only", toRef]);
|
|
20316
|
+
return stdout2.split("\n").filter((p) => p.trim()).map((p) => ({ path: p, status: "A" }));
|
|
19631
20317
|
}
|
|
19632
|
-
|
|
19633
|
-
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
19647
|
-
|
|
20318
|
+
const stdout = await git.raw([
|
|
20319
|
+
"diff-tree",
|
|
20320
|
+
"-r",
|
|
20321
|
+
"--name-status",
|
|
20322
|
+
fromRef,
|
|
20323
|
+
toRef
|
|
20324
|
+
]);
|
|
20325
|
+
const changes = [];
|
|
20326
|
+
for (const line of stdout.split("\n")) {
|
|
20327
|
+
if (!line.trim())
|
|
20328
|
+
continue;
|
|
20329
|
+
const [status, filePath] = line.split(" ");
|
|
20330
|
+
if (!filePath)
|
|
20331
|
+
continue;
|
|
20332
|
+
let normalizedStatus;
|
|
20333
|
+
if (status === "D") {
|
|
20334
|
+
normalizedStatus = "D";
|
|
20335
|
+
} else if (status === "A") {
|
|
20336
|
+
normalizedStatus = "A";
|
|
19648
20337
|
} else {
|
|
19649
|
-
|
|
20338
|
+
normalizedStatus = "M";
|
|
19650
20339
|
}
|
|
19651
|
-
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
20340
|
+
changes.push({ path: filePath, status: normalizedStatus });
|
|
20341
|
+
}
|
|
20342
|
+
return changes;
|
|
20343
|
+
}
|
|
20344
|
+
};
|
|
20345
|
+
var ApplyTreeSaga = class extends GitSaga {
|
|
20346
|
+
sagaName = "ApplyTreeSaga";
|
|
20347
|
+
originalHead = null;
|
|
20348
|
+
originalBranch = null;
|
|
20349
|
+
extractedFiles = [];
|
|
20350
|
+
fileBackups = /* @__PURE__ */ new Map();
|
|
20351
|
+
async executeGitOperations(input) {
|
|
20352
|
+
const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
|
|
20353
|
+
const headInfo = await this.readOnlyStep("get_current_head", async () => {
|
|
20354
|
+
let head = null;
|
|
20355
|
+
let branch = null;
|
|
20356
|
+
try {
|
|
20357
|
+
head = await this.git.revparse(["HEAD"]);
|
|
20358
|
+
} catch {
|
|
20359
|
+
head = null;
|
|
19655
20360
|
}
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
};
|
|
19661
|
-
this.writeToLocalCache(sessionId, entry);
|
|
19662
|
-
if (this.posthogAPI) {
|
|
19663
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
19664
|
-
pending.push(entry);
|
|
19665
|
-
this.pendingEntries.set(sessionId, pending);
|
|
19666
|
-
this.scheduleFlush(sessionId);
|
|
20361
|
+
try {
|
|
20362
|
+
branch = await this.git.raw(["symbolic-ref", "--short", "HEAD"]);
|
|
20363
|
+
} catch {
|
|
20364
|
+
branch = null;
|
|
19667
20365
|
}
|
|
19668
|
-
|
|
19669
|
-
|
|
19670
|
-
|
|
19671
|
-
|
|
19672
|
-
|
|
20366
|
+
return { head, branch };
|
|
20367
|
+
});
|
|
20368
|
+
this.originalHead = headInfo.head;
|
|
20369
|
+
this.originalBranch = headInfo.branch;
|
|
20370
|
+
let checkoutPerformed = false;
|
|
20371
|
+
if (baseCommit && baseCommit !== this.originalHead) {
|
|
20372
|
+
await this.readOnlyStep("check_working_tree", async () => {
|
|
20373
|
+
const status = await this.git.status();
|
|
20374
|
+
if (!status.isClean()) {
|
|
20375
|
+
const changedFiles = status.modified.length + status.staged.length + status.deleted.length;
|
|
20376
|
+
throw new Error(`Cannot apply tree: ${changedFiles} uncommitted change(s) exist. Commit or stash your changes first.`);
|
|
20377
|
+
}
|
|
20378
|
+
});
|
|
20379
|
+
await this.step({
|
|
20380
|
+
name: "checkout_base",
|
|
20381
|
+
execute: async () => {
|
|
20382
|
+
await this.git.checkout(baseCommit);
|
|
20383
|
+
checkoutPerformed = true;
|
|
20384
|
+
this.log.warn("Applied tree from different commit - now in detached HEAD state", {
|
|
20385
|
+
originalHead: this.originalHead,
|
|
20386
|
+
originalBranch: this.originalBranch,
|
|
20387
|
+
baseCommit
|
|
20388
|
+
});
|
|
20389
|
+
},
|
|
20390
|
+
rollback: async () => {
|
|
20391
|
+
try {
|
|
20392
|
+
if (this.originalBranch) {
|
|
20393
|
+
await this.git.checkout(this.originalBranch);
|
|
20394
|
+
} else if (this.originalHead) {
|
|
20395
|
+
await this.git.checkout(this.originalHead);
|
|
20396
|
+
}
|
|
20397
|
+
} catch (error) {
|
|
20398
|
+
this.log.warn("Failed to rollback checkout", { error });
|
|
20399
|
+
}
|
|
20400
|
+
}
|
|
20401
|
+
});
|
|
20402
|
+
}
|
|
20403
|
+
if (archivePath) {
|
|
20404
|
+
const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20405
|
+
await this.readOnlyStep("backup_existing_files", async () => {
|
|
20406
|
+
for (const filePath of filesToExtract) {
|
|
20407
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20408
|
+
try {
|
|
20409
|
+
const content = await fs13.readFile(fullPath);
|
|
20410
|
+
this.fileBackups.set(filePath, content);
|
|
20411
|
+
} catch {
|
|
20412
|
+
}
|
|
20413
|
+
}
|
|
20414
|
+
});
|
|
20415
|
+
await this.step({
|
|
20416
|
+
name: "extract_archive",
|
|
20417
|
+
execute: async () => {
|
|
20418
|
+
await tar.extract({
|
|
20419
|
+
file: archivePath,
|
|
20420
|
+
cwd: baseDir
|
|
20421
|
+
});
|
|
20422
|
+
this.extractedFiles = filesToExtract;
|
|
20423
|
+
},
|
|
20424
|
+
rollback: async () => {
|
|
20425
|
+
for (const filePath of this.extractedFiles) {
|
|
20426
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20427
|
+
const backup = this.fileBackups.get(filePath);
|
|
20428
|
+
if (backup) {
|
|
20429
|
+
const dir = path16.dirname(fullPath);
|
|
20430
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20431
|
+
});
|
|
20432
|
+
await fs13.writeFile(fullPath, backup).catch(() => {
|
|
20433
|
+
});
|
|
20434
|
+
} else {
|
|
20435
|
+
await fs13.rm(fullPath, { force: true }).catch(() => {
|
|
20436
|
+
});
|
|
20437
|
+
}
|
|
20438
|
+
}
|
|
20439
|
+
}
|
|
20440
|
+
});
|
|
20441
|
+
}
|
|
20442
|
+
for (const change of changes.filter((c) => c.status === "D")) {
|
|
20443
|
+
const fullPath = path16.join(baseDir, change.path);
|
|
20444
|
+
const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
|
|
20445
|
+
try {
|
|
20446
|
+
return await fs13.readFile(fullPath);
|
|
20447
|
+
} catch {
|
|
20448
|
+
return null;
|
|
20449
|
+
}
|
|
20450
|
+
});
|
|
20451
|
+
await this.step({
|
|
20452
|
+
name: `delete_${change.path}`,
|
|
20453
|
+
execute: async () => {
|
|
20454
|
+
await fs13.rm(fullPath, { force: true });
|
|
20455
|
+
this.log.debug(`Deleted file: ${change.path}`);
|
|
20456
|
+
},
|
|
20457
|
+
rollback: async () => {
|
|
20458
|
+
if (backupContent) {
|
|
20459
|
+
const dir = path16.dirname(fullPath);
|
|
20460
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20461
|
+
});
|
|
20462
|
+
await fs13.writeFile(fullPath, backupContent).catch(() => {
|
|
20463
|
+
});
|
|
20464
|
+
}
|
|
20465
|
+
}
|
|
19673
20466
|
});
|
|
19674
20467
|
}
|
|
20468
|
+
const deletedCount = changes.filter((c) => c.status === "D").length;
|
|
20469
|
+
this.log.info("Tree applied", {
|
|
20470
|
+
treeHash,
|
|
20471
|
+
totalChanges: changes.length,
|
|
20472
|
+
deletedFiles: deletedCount,
|
|
20473
|
+
checkoutPerformed
|
|
20474
|
+
});
|
|
20475
|
+
return { treeHash, checkoutPerformed };
|
|
19675
20476
|
}
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
|
|
19681
|
-
|
|
20477
|
+
};
|
|
20478
|
+
|
|
20479
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20480
|
+
var ApplySnapshotSaga = class extends Saga {
|
|
20481
|
+
sagaName = "ApplySnapshotSaga";
|
|
20482
|
+
archivePath = null;
|
|
20483
|
+
async execute(input) {
|
|
20484
|
+
const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
|
|
20485
|
+
const tmpDir = join12(repositoryPath, ".posthog", "tmp");
|
|
20486
|
+
if (!snapshot.archiveUrl) {
|
|
20487
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19682
20488
|
}
|
|
19683
|
-
const
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
if (this.flushQueues.get(sessionId) === next) {
|
|
19689
|
-
this.flushQueues.delete(sessionId);
|
|
20489
|
+
const archiveUrl = snapshot.archiveUrl;
|
|
20490
|
+
await this.step({
|
|
20491
|
+
name: "create_tmp_dir",
|
|
20492
|
+
execute: () => mkdir7(tmpDir, { recursive: true }),
|
|
20493
|
+
rollback: async () => {
|
|
19690
20494
|
}
|
|
19691
20495
|
});
|
|
19692
|
-
|
|
20496
|
+
const archivePath = join12(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
20497
|
+
this.archivePath = archivePath;
|
|
20498
|
+
await this.step({
|
|
20499
|
+
name: "download_archive",
|
|
20500
|
+
execute: async () => {
|
|
20501
|
+
const arrayBuffer = await apiClient.downloadArtifact(
|
|
20502
|
+
taskId,
|
|
20503
|
+
runId,
|
|
20504
|
+
archiveUrl
|
|
20505
|
+
);
|
|
20506
|
+
if (!arrayBuffer) {
|
|
20507
|
+
throw new Error("Failed to download archive");
|
|
20508
|
+
}
|
|
20509
|
+
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
20510
|
+
const binaryContent = Buffer.from(base64Content, "base64");
|
|
20511
|
+
await writeFile5(archivePath, binaryContent);
|
|
20512
|
+
this.log.info("Tree archive downloaded", {
|
|
20513
|
+
treeHash: snapshot.treeHash,
|
|
20514
|
+
snapshotBytes: binaryContent.byteLength,
|
|
20515
|
+
snapshotWireBytes: arrayBuffer.byteLength,
|
|
20516
|
+
totalBytes: binaryContent.byteLength,
|
|
20517
|
+
totalWireBytes: arrayBuffer.byteLength
|
|
20518
|
+
});
|
|
20519
|
+
},
|
|
20520
|
+
rollback: async () => {
|
|
20521
|
+
if (this.archivePath) {
|
|
20522
|
+
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
20523
|
+
});
|
|
20524
|
+
}
|
|
20525
|
+
}
|
|
20526
|
+
});
|
|
20527
|
+
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
20528
|
+
const applyResult = await gitApplySaga.run({
|
|
20529
|
+
baseDir: repositoryPath,
|
|
20530
|
+
treeHash: snapshot.treeHash,
|
|
20531
|
+
baseCommit: snapshot.baseCommit,
|
|
20532
|
+
changes: snapshot.changes,
|
|
20533
|
+
archivePath: this.archivePath
|
|
20534
|
+
});
|
|
20535
|
+
if (!applyResult.success) {
|
|
20536
|
+
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
20537
|
+
}
|
|
20538
|
+
await rm6(this.archivePath, { force: true }).catch(() => {
|
|
20539
|
+
});
|
|
20540
|
+
this.log.info("Tree snapshot applied", {
|
|
20541
|
+
treeHash: snapshot.treeHash,
|
|
20542
|
+
totalChanges: snapshot.changes.length,
|
|
20543
|
+
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
20544
|
+
});
|
|
20545
|
+
return { treeHash: snapshot.treeHash };
|
|
19693
20546
|
}
|
|
19694
|
-
|
|
19695
|
-
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
|
|
20547
|
+
};
|
|
20548
|
+
|
|
20549
|
+
// src/sagas/capture-tree-saga.ts
|
|
20550
|
+
import { existsSync as existsSync6 } from "fs";
|
|
20551
|
+
import { readFile as readFile6, rm as rm7 } from "fs/promises";
|
|
20552
|
+
import { join as join13 } from "path";
|
|
20553
|
+
var CaptureTreeSaga2 = class extends Saga {
|
|
20554
|
+
sagaName = "CaptureTreeSaga";
|
|
20555
|
+
async execute(input) {
|
|
20556
|
+
const {
|
|
20557
|
+
repositoryPath,
|
|
20558
|
+
lastTreeHash,
|
|
20559
|
+
interrupted,
|
|
20560
|
+
apiClient,
|
|
20561
|
+
taskId,
|
|
20562
|
+
runId
|
|
20563
|
+
} = input;
|
|
20564
|
+
const tmpDir = join13(repositoryPath, ".posthog", "tmp");
|
|
20565
|
+
if (existsSync6(join13(repositoryPath, ".gitmodules"))) {
|
|
20566
|
+
this.log.warn(
|
|
20567
|
+
"Repository has submodules - snapshot may not capture submodule state"
|
|
20568
|
+
);
|
|
19699
20569
|
}
|
|
19700
|
-
const
|
|
19701
|
-
|
|
19702
|
-
|
|
20570
|
+
const shouldArchive = !!apiClient;
|
|
20571
|
+
const archivePath = shouldArchive ? join13(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
20572
|
+
const gitCaptureSaga = new CaptureTreeSaga(this.log);
|
|
20573
|
+
const captureResult = await gitCaptureSaga.run({
|
|
20574
|
+
baseDir: repositoryPath,
|
|
20575
|
+
lastTreeHash,
|
|
20576
|
+
archivePath
|
|
20577
|
+
});
|
|
20578
|
+
if (!captureResult.success) {
|
|
20579
|
+
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
19703
20580
|
}
|
|
19704
|
-
|
|
19705
|
-
|
|
19706
|
-
|
|
19707
|
-
|
|
19708
|
-
|
|
20581
|
+
const {
|
|
20582
|
+
snapshot: gitSnapshot,
|
|
20583
|
+
archivePath: createdArchivePath,
|
|
20584
|
+
changed
|
|
20585
|
+
} = captureResult.data;
|
|
20586
|
+
if (!changed || !gitSnapshot) {
|
|
20587
|
+
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
20588
|
+
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
19709
20589
|
}
|
|
19710
|
-
|
|
19711
|
-
|
|
19712
|
-
|
|
19713
|
-
|
|
19714
|
-
|
|
19715
|
-
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
19720
|
-
this.retryCounts.set(sessionId, retryCount);
|
|
19721
|
-
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
19722
|
-
this.logger.error(
|
|
19723
|
-
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
19724
|
-
{
|
|
19725
|
-
taskId: session.context.taskId,
|
|
19726
|
-
runId: session.context.runId,
|
|
19727
|
-
error
|
|
19728
|
-
}
|
|
20590
|
+
let archiveUrl;
|
|
20591
|
+
if (apiClient && createdArchivePath) {
|
|
20592
|
+
try {
|
|
20593
|
+
archiveUrl = await this.uploadArchive(
|
|
20594
|
+
createdArchivePath,
|
|
20595
|
+
gitSnapshot.treeHash,
|
|
20596
|
+
apiClient,
|
|
20597
|
+
taskId,
|
|
20598
|
+
runId
|
|
19729
20599
|
);
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
this.logger.warn(
|
|
19734
|
-
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
19735
|
-
{
|
|
19736
|
-
taskId: session.context.taskId,
|
|
19737
|
-
runId: session.context.runId,
|
|
19738
|
-
error: error instanceof Error ? error.message : String(error)
|
|
19739
|
-
}
|
|
19740
|
-
);
|
|
19741
|
-
}
|
|
19742
|
-
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
19743
|
-
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
19744
|
-
this.scheduleFlush(sessionId);
|
|
20600
|
+
} finally {
|
|
20601
|
+
await rm7(createdArchivePath, { force: true }).catch(() => {
|
|
20602
|
+
});
|
|
19745
20603
|
}
|
|
19746
20604
|
}
|
|
20605
|
+
const snapshot = {
|
|
20606
|
+
treeHash: gitSnapshot.treeHash,
|
|
20607
|
+
baseCommit: gitSnapshot.baseCommit,
|
|
20608
|
+
changes: gitSnapshot.changes,
|
|
20609
|
+
timestamp: gitSnapshot.timestamp,
|
|
20610
|
+
interrupted,
|
|
20611
|
+
archiveUrl
|
|
20612
|
+
};
|
|
20613
|
+
this.log.info("Tree captured", {
|
|
20614
|
+
treeHash: snapshot.treeHash,
|
|
20615
|
+
changes: snapshot.changes.length,
|
|
20616
|
+
interrupted,
|
|
20617
|
+
archiveUrl
|
|
20618
|
+
});
|
|
20619
|
+
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
19747
20620
|
}
|
|
19748
|
-
|
|
19749
|
-
|
|
19750
|
-
|
|
19751
|
-
|
|
19752
|
-
|
|
19753
|
-
|
|
19754
|
-
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
|
|
19761
|
-
|
|
19762
|
-
const update = params?.update;
|
|
19763
|
-
const content = update?.content;
|
|
19764
|
-
if (content?.type === "text" && content.text) {
|
|
19765
|
-
return content.text;
|
|
19766
|
-
}
|
|
19767
|
-
return "";
|
|
19768
|
-
}
|
|
19769
|
-
emitCoalescedMessage(sessionId, session) {
|
|
19770
|
-
if (!session.chunkBuffer) return;
|
|
19771
|
-
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
19772
|
-
session.chunkBuffer = void 0;
|
|
19773
|
-
session.lastAgentMessage = text2;
|
|
19774
|
-
session.currentTurnMessages.push(text2);
|
|
19775
|
-
const entry = {
|
|
19776
|
-
type: "notification",
|
|
19777
|
-
timestamp: firstTimestamp,
|
|
19778
|
-
notification: {
|
|
19779
|
-
jsonrpc: "2.0",
|
|
19780
|
-
method: "session/update",
|
|
19781
|
-
params: {
|
|
19782
|
-
update: {
|
|
19783
|
-
sessionUpdate: "agent_message",
|
|
19784
|
-
content: { type: "text", text: text2 }
|
|
20621
|
+
async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
|
|
20622
|
+
const archiveUrl = await this.step({
|
|
20623
|
+
name: "upload_archive",
|
|
20624
|
+
execute: async () => {
|
|
20625
|
+
const archiveContent = await readFile6(archivePath);
|
|
20626
|
+
const base64Content = archiveContent.toString("base64");
|
|
20627
|
+
const snapshotBytes = archiveContent.byteLength;
|
|
20628
|
+
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
20629
|
+
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
20630
|
+
{
|
|
20631
|
+
name: `trees/${treeHash}.tar.gz`,
|
|
20632
|
+
type: "tree_snapshot",
|
|
20633
|
+
content: base64Content,
|
|
20634
|
+
content_type: "application/gzip"
|
|
19785
20635
|
}
|
|
20636
|
+
]);
|
|
20637
|
+
const uploadedArtifact = artifacts[0];
|
|
20638
|
+
if (uploadedArtifact?.storage_path) {
|
|
20639
|
+
this.log.info("Tree archive uploaded", {
|
|
20640
|
+
storagePath: uploadedArtifact.storage_path,
|
|
20641
|
+
treeHash,
|
|
20642
|
+
snapshotBytes,
|
|
20643
|
+
snapshotWireBytes,
|
|
20644
|
+
totalBytes: snapshotBytes,
|
|
20645
|
+
totalWireBytes: snapshotWireBytes
|
|
20646
|
+
});
|
|
20647
|
+
return uploadedArtifact.storage_path;
|
|
19786
20648
|
}
|
|
20649
|
+
return void 0;
|
|
20650
|
+
},
|
|
20651
|
+
rollback: async () => {
|
|
20652
|
+
await rm7(archivePath, { force: true }).catch(() => {
|
|
20653
|
+
});
|
|
19787
20654
|
}
|
|
19788
|
-
};
|
|
19789
|
-
|
|
19790
|
-
if (this.posthogAPI) {
|
|
19791
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
19792
|
-
pending.push(entry);
|
|
19793
|
-
this.pendingEntries.set(sessionId, pending);
|
|
19794
|
-
this.scheduleFlush(sessionId);
|
|
19795
|
-
}
|
|
20655
|
+
});
|
|
20656
|
+
return archiveUrl;
|
|
19796
20657
|
}
|
|
19797
|
-
|
|
19798
|
-
|
|
20658
|
+
};
|
|
20659
|
+
|
|
20660
|
+
// src/tree-tracker.ts
|
|
20661
|
+
var TreeTracker = class {
|
|
20662
|
+
repositoryPath;
|
|
20663
|
+
taskId;
|
|
20664
|
+
runId;
|
|
20665
|
+
apiClient;
|
|
20666
|
+
logger;
|
|
20667
|
+
lastTreeHash = null;
|
|
20668
|
+
constructor(config) {
|
|
20669
|
+
this.repositoryPath = config.repositoryPath;
|
|
20670
|
+
this.taskId = config.taskId;
|
|
20671
|
+
this.runId = config.runId;
|
|
20672
|
+
this.apiClient = config.apiClient;
|
|
20673
|
+
this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
19799
20674
|
}
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
20675
|
+
/**
|
|
20676
|
+
* Capture current working tree state as a snapshot.
|
|
20677
|
+
* Uses a temporary index to avoid modifying user's staging area.
|
|
20678
|
+
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
20679
|
+
*/
|
|
20680
|
+
async captureTree(options) {
|
|
20681
|
+
const saga = new CaptureTreeSaga2(this.logger);
|
|
20682
|
+
const result = await saga.run({
|
|
20683
|
+
repositoryPath: this.repositoryPath,
|
|
20684
|
+
taskId: this.taskId,
|
|
20685
|
+
runId: this.runId,
|
|
20686
|
+
apiClient: this.apiClient,
|
|
20687
|
+
lastTreeHash: this.lastTreeHash,
|
|
20688
|
+
interrupted: options?.interrupted
|
|
20689
|
+
});
|
|
20690
|
+
if (!result.success) {
|
|
20691
|
+
this.logger.error("Failed to capture tree", {
|
|
20692
|
+
error: result.error,
|
|
20693
|
+
failedStep: result.failedStep
|
|
20694
|
+
});
|
|
20695
|
+
throw new Error(
|
|
20696
|
+
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
19810
20697
|
);
|
|
19811
20698
|
}
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
resetTurnMessages(sessionId) {
|
|
19815
|
-
const session = this.sessions.get(sessionId);
|
|
19816
|
-
if (session) {
|
|
19817
|
-
session.currentTurnMessages = [];
|
|
20699
|
+
if (result.data.newTreeHash !== null) {
|
|
20700
|
+
this.lastTreeHash = result.data.newTreeHash;
|
|
19818
20701
|
}
|
|
20702
|
+
return result.data.snapshot;
|
|
19819
20703
|
}
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
|
|
19825
|
-
|
|
19826
|
-
|
|
19827
|
-
return null;
|
|
19828
|
-
}
|
|
19829
|
-
const content = update.content;
|
|
19830
|
-
if (content?.type === "text" && typeof content.text === "string") {
|
|
19831
|
-
const trimmed2 = content.text.trim();
|
|
19832
|
-
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20704
|
+
/**
|
|
20705
|
+
* Download and apply a tree snapshot.
|
|
20706
|
+
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
20707
|
+
*/
|
|
20708
|
+
async applyTreeSnapshot(snapshot) {
|
|
20709
|
+
if (!this.apiClient) {
|
|
20710
|
+
throw new Error("Cannot apply snapshot: API client not configured");
|
|
19833
20711
|
}
|
|
19834
|
-
if (
|
|
19835
|
-
|
|
19836
|
-
|
|
20712
|
+
if (!snapshot.archiveUrl) {
|
|
20713
|
+
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
20714
|
+
treeHash: snapshot.treeHash,
|
|
20715
|
+
changes: snapshot.changes.length
|
|
20716
|
+
});
|
|
20717
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19837
20718
|
}
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
|
|
19849
|
-
|
|
19850
|
-
|
|
20719
|
+
const saga = new ApplySnapshotSaga(this.logger);
|
|
20720
|
+
const result = await saga.run({
|
|
20721
|
+
snapshot,
|
|
20722
|
+
repositoryPath: this.repositoryPath,
|
|
20723
|
+
apiClient: this.apiClient,
|
|
20724
|
+
taskId: this.taskId,
|
|
20725
|
+
runId: this.runId
|
|
20726
|
+
});
|
|
20727
|
+
if (!result.success) {
|
|
20728
|
+
this.logger.error("Failed to apply tree snapshot", {
|
|
20729
|
+
error: result.error,
|
|
20730
|
+
failedStep: result.failedStep,
|
|
20731
|
+
treeHash: snapshot.treeHash
|
|
20732
|
+
});
|
|
20733
|
+
throw new Error(
|
|
20734
|
+
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
19851
20735
|
);
|
|
19852
|
-
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
19853
|
-
delay3 = 0;
|
|
19854
|
-
} else {
|
|
19855
|
-
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
19856
20736
|
}
|
|
19857
|
-
|
|
19858
|
-
this.flushTimeouts.set(sessionId, timeout);
|
|
20737
|
+
this.lastTreeHash = result.data.treeHash;
|
|
19859
20738
|
}
|
|
19860
|
-
|
|
19861
|
-
|
|
19862
|
-
|
|
19863
|
-
|
|
19864
|
-
|
|
19865
|
-
this.localCachePath,
|
|
19866
|
-
"sessions",
|
|
19867
|
-
session.context.runId,
|
|
19868
|
-
"logs.ndjson"
|
|
19869
|
-
);
|
|
19870
|
-
try {
|
|
19871
|
-
fs12.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
19872
|
-
`);
|
|
19873
|
-
} catch (error) {
|
|
19874
|
-
this.logger.warn("Failed to write to local cache", {
|
|
19875
|
-
taskId: session.context.taskId,
|
|
19876
|
-
runId: session.context.runId,
|
|
19877
|
-
logPath,
|
|
19878
|
-
error
|
|
19879
|
-
});
|
|
19880
|
-
}
|
|
20739
|
+
/**
|
|
20740
|
+
* Get the last captured tree hash.
|
|
20741
|
+
*/
|
|
20742
|
+
getLastTreeHash() {
|
|
20743
|
+
return this.lastTreeHash;
|
|
19881
20744
|
}
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
const now = Date.now();
|
|
19888
|
-
for (const entry of entries) {
|
|
19889
|
-
const entryPath = path14.join(sessionsDir, entry);
|
|
19890
|
-
try {
|
|
19891
|
-
const stats = await fsp.stat(entryPath);
|
|
19892
|
-
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
19893
|
-
await fsp.rm(entryPath, { recursive: true, force: true });
|
|
19894
|
-
deleted++;
|
|
19895
|
-
}
|
|
19896
|
-
} catch {
|
|
19897
|
-
}
|
|
19898
|
-
}
|
|
19899
|
-
} catch {
|
|
19900
|
-
}
|
|
19901
|
-
return deleted;
|
|
20745
|
+
/**
|
|
20746
|
+
* Set the last tree hash (used when resuming).
|
|
20747
|
+
*/
|
|
20748
|
+
setLastTreeHash(hash) {
|
|
20749
|
+
this.lastTreeHash = hash;
|
|
19902
20750
|
}
|
|
19903
20751
|
};
|
|
19904
20752
|
|
|
@@ -19968,6 +20816,14 @@ var httpHeaderSchema = z3.object({
|
|
|
19968
20816
|
name: z3.string(),
|
|
19969
20817
|
value: z3.string()
|
|
19970
20818
|
});
|
|
20819
|
+
var nullishString = z3.string().nullish().transform((value) => value ?? null);
|
|
20820
|
+
var handoffLocalGitStateSchema = z3.object({
|
|
20821
|
+
head: nullishString,
|
|
20822
|
+
branch: nullishString,
|
|
20823
|
+
upstreamHead: nullishString,
|
|
20824
|
+
upstreamRemote: nullishString,
|
|
20825
|
+
upstreamMergeRef: nullishString
|
|
20826
|
+
});
|
|
19971
20827
|
var remoteMcpServerSchema = z3.object({
|
|
19972
20828
|
type: z3.enum(["http", "sse"]),
|
|
19973
20829
|
name: z3.string().min(1, "MCP server name is required"),
|
|
@@ -20019,13 +20875,16 @@ var setConfigOptionParamsSchema = z3.object({
|
|
|
20019
20875
|
var refreshSessionParamsSchema = z3.object({
|
|
20020
20876
|
mcpServers: mcpServersSchema
|
|
20021
20877
|
});
|
|
20878
|
+
var closeParamsSchema = z3.object({
|
|
20879
|
+
localGitState: handoffLocalGitStateSchema.optional()
|
|
20880
|
+
}).optional();
|
|
20022
20881
|
var commandParamsSchemas = {
|
|
20023
20882
|
user_message: userMessageParamsSchema,
|
|
20024
20883
|
"posthog/user_message": userMessageParamsSchema,
|
|
20025
20884
|
cancel: z3.object({}).optional(),
|
|
20026
20885
|
"posthog/cancel": z3.object({}).optional(),
|
|
20027
|
-
close:
|
|
20028
|
-
"posthog/close":
|
|
20886
|
+
close: closeParamsSchema,
|
|
20887
|
+
"posthog/close": closeParamsSchema,
|
|
20029
20888
|
permission_response: permissionResponseParamsSchema,
|
|
20030
20889
|
"posthog/permission_response": permissionResponseParamsSchema,
|
|
20031
20890
|
set_config_option: setConfigOptionParamsSchema,
|
|
@@ -20347,7 +21206,7 @@ var AgentServer = class {
|
|
|
20347
21206
|
return app;
|
|
20348
21207
|
}
|
|
20349
21208
|
async start() {
|
|
20350
|
-
await new Promise((
|
|
21209
|
+
await new Promise((resolve7) => {
|
|
20351
21210
|
this.server = serve(
|
|
20352
21211
|
{
|
|
20353
21212
|
fetch: this.app.fetch,
|
|
@@ -20357,51 +21216,35 @@ var AgentServer = class {
|
|
|
20357
21216
|
this.logger.debug(
|
|
20358
21217
|
`HTTP server listening on port ${this.config.port}`
|
|
20359
21218
|
);
|
|
20360
|
-
|
|
21219
|
+
resolve7();
|
|
20361
21220
|
}
|
|
20362
21221
|
);
|
|
20363
21222
|
});
|
|
20364
21223
|
await this.autoInitializeSession();
|
|
20365
21224
|
}
|
|
20366
|
-
async
|
|
20367
|
-
|
|
20368
|
-
|
|
20369
|
-
|
|
20370
|
-
|
|
20371
|
-
|
|
20372
|
-
|
|
20373
|
-
|
|
21225
|
+
async loadResumeState(taskId, resumeRunId, currentRunId) {
|
|
21226
|
+
this.logger.debug("Loading resume state", { resumeRunId, currentRunId });
|
|
21227
|
+
try {
|
|
21228
|
+
this.resumeState = await resumeFromLog({
|
|
21229
|
+
taskId,
|
|
21230
|
+
runId: resumeRunId,
|
|
21231
|
+
repositoryPath: this.config.repositoryPath,
|
|
21232
|
+
apiClient: this.posthogAPI,
|
|
21233
|
+
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
20374
21234
|
});
|
|
20375
|
-
|
|
20376
|
-
this.resumeState
|
|
20377
|
-
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
|
|
20381
|
-
|
|
20382
|
-
|
|
20383
|
-
|
|
20384
|
-
|
|
20385
|
-
|
|
20386
|
-
|
|
20387
|
-
});
|
|
20388
|
-
} catch (error) {
|
|
20389
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
20390
|
-
error
|
|
20391
|
-
});
|
|
20392
|
-
this.resumeState = null;
|
|
20393
|
-
}
|
|
21235
|
+
this.logger.debug("Resume state loaded", {
|
|
21236
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
21237
|
+
hasSnapshot: !!this.resumeState.latestSnapshot,
|
|
21238
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21239
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null,
|
|
21240
|
+
logEntries: this.resumeState.logEntryCount
|
|
21241
|
+
});
|
|
21242
|
+
} catch (error) {
|
|
21243
|
+
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21244
|
+
error
|
|
21245
|
+
});
|
|
21246
|
+
this.resumeState = null;
|
|
20394
21247
|
}
|
|
20395
|
-
const payload = {
|
|
20396
|
-
task_id: taskId,
|
|
20397
|
-
run_id: runId,
|
|
20398
|
-
team_id: projectId,
|
|
20399
|
-
user_id: 0,
|
|
20400
|
-
// System-initiated
|
|
20401
|
-
distinct_id: "agent-server",
|
|
20402
|
-
mode
|
|
20403
|
-
};
|
|
20404
|
-
await this.initializeSession(payload, null);
|
|
20405
21248
|
}
|
|
20406
21249
|
async stop() {
|
|
20407
21250
|
this.logger.debug("Stopping agent server...");
|
|
@@ -20511,6 +21354,10 @@ var AgentServer = class {
|
|
|
20511
21354
|
case POSTHOG_NOTIFICATIONS.CLOSE:
|
|
20512
21355
|
case "close": {
|
|
20513
21356
|
this.logger.debug("Close requested");
|
|
21357
|
+
const localGitState = this.extractHandoffLocalGitState(params);
|
|
21358
|
+
if (localGitState && this.session) {
|
|
21359
|
+
this.session.pendingHandoffGitState = localGitState;
|
|
21360
|
+
}
|
|
20514
21361
|
await this.cleanupSession();
|
|
20515
21362
|
return { closed: true };
|
|
20516
21363
|
}
|
|
@@ -20737,7 +21584,8 @@ var AgentServer = class {
|
|
|
20737
21584
|
deviceInfo,
|
|
20738
21585
|
logWriter,
|
|
20739
21586
|
permissionMode: initialPermissionMode,
|
|
20740
|
-
hasDesktopConnected: sseController !== null
|
|
21587
|
+
hasDesktopConnected: sseController !== null,
|
|
21588
|
+
pendingHandoffGitState: void 0
|
|
20741
21589
|
};
|
|
20742
21590
|
this.logger = new Logger({
|
|
20743
21591
|
debug: true,
|
|
@@ -20795,29 +21643,11 @@ var AgentServer = class {
|
|
|
20795
21643
|
if (!this.resumeState) {
|
|
20796
21644
|
const resumeRunId = this.getResumeRunId(taskRun);
|
|
20797
21645
|
if (resumeRunId) {
|
|
20798
|
-
this.
|
|
21646
|
+
await this.loadResumeState(
|
|
21647
|
+
payload.task_id,
|
|
20799
21648
|
resumeRunId,
|
|
20800
|
-
|
|
20801
|
-
|
|
20802
|
-
try {
|
|
20803
|
-
this.resumeState = await resumeFromLog({
|
|
20804
|
-
taskId: payload.task_id,
|
|
20805
|
-
runId: resumeRunId,
|
|
20806
|
-
repositoryPath: this.config.repositoryPath,
|
|
20807
|
-
apiClient: this.posthogAPI,
|
|
20808
|
-
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
20809
|
-
});
|
|
20810
|
-
this.logger.debug("Resume state loaded (via TaskRun state)", {
|
|
20811
|
-
conversationTurns: this.resumeState.conversation.length,
|
|
20812
|
-
snapshotApplied: this.resumeState.snapshotApplied,
|
|
20813
|
-
logEntries: this.resumeState.logEntryCount
|
|
20814
|
-
});
|
|
20815
|
-
} catch (error) {
|
|
20816
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
20817
|
-
error
|
|
20818
|
-
});
|
|
20819
|
-
this.resumeState = null;
|
|
20820
|
-
}
|
|
21649
|
+
payload.run_id
|
|
21650
|
+
);
|
|
20821
21651
|
}
|
|
20822
21652
|
}
|
|
20823
21653
|
if (this.resumeState && this.resumeState.conversation.length > 0) {
|
|
@@ -20876,8 +21706,59 @@ var AgentServer = class {
|
|
|
20876
21706
|
const conversationSummary = formatConversationForResume(
|
|
20877
21707
|
this.resumeState.conversation
|
|
20878
21708
|
);
|
|
21709
|
+
let snapshotApplied = false;
|
|
21710
|
+
if (this.resumeState.latestSnapshot?.archiveUrl && this.config.repositoryPath && this.posthogAPI) {
|
|
21711
|
+
try {
|
|
21712
|
+
const treeTracker = new TreeTracker({
|
|
21713
|
+
repositoryPath: this.config.repositoryPath,
|
|
21714
|
+
taskId: payload.task_id,
|
|
21715
|
+
runId: payload.run_id,
|
|
21716
|
+
apiClient: this.posthogAPI,
|
|
21717
|
+
logger: this.logger.child("TreeTracker")
|
|
21718
|
+
});
|
|
21719
|
+
await treeTracker.applyTreeSnapshot(this.resumeState.latestSnapshot);
|
|
21720
|
+
treeTracker.setLastTreeHash(this.resumeState.latestSnapshot.treeHash);
|
|
21721
|
+
snapshotApplied = true;
|
|
21722
|
+
this.logger.info("Tree snapshot applied", {
|
|
21723
|
+
treeHash: this.resumeState.latestSnapshot.treeHash,
|
|
21724
|
+
changes: this.resumeState.latestSnapshot.changes?.length ?? 0,
|
|
21725
|
+
hasArchiveUrl: !!this.resumeState.latestSnapshot.archiveUrl
|
|
21726
|
+
});
|
|
21727
|
+
} catch (error) {
|
|
21728
|
+
this.logger.warn("Failed to apply tree snapshot", {
|
|
21729
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21730
|
+
treeHash: this.resumeState.latestSnapshot.treeHash
|
|
21731
|
+
});
|
|
21732
|
+
}
|
|
21733
|
+
}
|
|
21734
|
+
if (this.resumeState.latestGitCheckpoint && this.config.repositoryPath && this.posthogAPI) {
|
|
21735
|
+
try {
|
|
21736
|
+
const checkpointTracker = new HandoffCheckpointTracker({
|
|
21737
|
+
repositoryPath: this.config.repositoryPath,
|
|
21738
|
+
taskId: payload.task_id,
|
|
21739
|
+
runId: payload.run_id,
|
|
21740
|
+
apiClient: this.posthogAPI,
|
|
21741
|
+
logger: this.logger.child("HandoffCheckpoint")
|
|
21742
|
+
});
|
|
21743
|
+
const metrics = await checkpointTracker.applyFromHandoff(
|
|
21744
|
+
this.resumeState.latestGitCheckpoint
|
|
21745
|
+
);
|
|
21746
|
+
this.logger.info("Git checkpoint applied", {
|
|
21747
|
+
branch: this.resumeState.latestGitCheckpoint.branch,
|
|
21748
|
+
head: this.resumeState.latestGitCheckpoint.head,
|
|
21749
|
+
packBytes: metrics.packBytes,
|
|
21750
|
+
indexBytes: metrics.indexBytes,
|
|
21751
|
+
totalBytes: metrics.totalBytes
|
|
21752
|
+
});
|
|
21753
|
+
} catch (error) {
|
|
21754
|
+
this.logger.warn("Failed to apply git checkpoint", {
|
|
21755
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21756
|
+
branch: this.resumeState.latestGitCheckpoint.branch
|
|
21757
|
+
});
|
|
21758
|
+
}
|
|
21759
|
+
}
|
|
20879
21760
|
const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
|
|
20880
|
-
const sandboxContext =
|
|
21761
|
+
const sandboxContext = snapshotApplied ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.` : `The workspace files from the previous session were not restored (the file snapshot may have expired), so you are starting with a fresh environment. Your conversation history is fully preserved below.`;
|
|
20881
21762
|
let resumePromptBlocks;
|
|
20882
21763
|
if (pendingUserPrompt?.length) {
|
|
20883
21764
|
resumePromptBlocks = [
|
|
@@ -20918,7 +21799,9 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
20918
21799
|
conversationTurns: this.resumeState.conversation.length,
|
|
20919
21800
|
promptLength: promptBlocksToText(resumePromptBlocks).length,
|
|
20920
21801
|
hasPendingUserMessage: !!pendingUserPrompt?.length,
|
|
20921
|
-
snapshotApplied
|
|
21802
|
+
snapshotApplied,
|
|
21803
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21804
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null
|
|
20922
21805
|
});
|
|
20923
21806
|
this.resumeState = null;
|
|
20924
21807
|
this.session.logWriter.resetTurnMessages(payload.run_id);
|
|
@@ -21062,16 +21945,16 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21062
21945
|
throw new Error(`Failed to download artifact ${artifact.name}`);
|
|
21063
21946
|
}
|
|
21064
21947
|
const safeName = this.getSafeArtifactName(artifact.name);
|
|
21065
|
-
const artifactDir =
|
|
21948
|
+
const artifactDir = join14(
|
|
21066
21949
|
this.config.repositoryPath ?? "/tmp/workspace",
|
|
21067
21950
|
".posthog",
|
|
21068
21951
|
"attachments",
|
|
21069
21952
|
runId,
|
|
21070
21953
|
artifact.id ?? safeName
|
|
21071
21954
|
);
|
|
21072
|
-
await
|
|
21073
|
-
const artifactPath =
|
|
21074
|
-
await
|
|
21955
|
+
await mkdir8(artifactDir, { recursive: true });
|
|
21956
|
+
const artifactPath = join14(artifactDir, safeName);
|
|
21957
|
+
await writeFile6(artifactPath, Buffer.from(data));
|
|
21075
21958
|
return resourceLink(pathToFileURL(artifactPath).toString(), artifact.name, {
|
|
21076
21959
|
...artifact.content_type ? { mimeType: artifact.content_type } : {},
|
|
21077
21960
|
...typeof artifact.size === "number" ? { size: artifact.size } : {}
|
|
@@ -21082,6 +21965,24 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21082
21965
|
const normalizedName = baseName.replace(/[^\w.-]/g, "_");
|
|
21083
21966
|
return normalizedName.length > 0 ? normalizedName : "attachment";
|
|
21084
21967
|
}
|
|
21968
|
+
async autoInitializeSession() {
|
|
21969
|
+
const { taskId, runId, mode, projectId } = this.config;
|
|
21970
|
+
this.logger.debug("Auto-initializing session", { taskId, runId, mode });
|
|
21971
|
+
const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21972
|
+
if (resumeRunId) {
|
|
21973
|
+
await this.loadResumeState(taskId, resumeRunId, runId);
|
|
21974
|
+
}
|
|
21975
|
+
const payload = {
|
|
21976
|
+
task_id: taskId,
|
|
21977
|
+
run_id: runId,
|
|
21978
|
+
team_id: projectId,
|
|
21979
|
+
user_id: 0,
|
|
21980
|
+
// System-initiated
|
|
21981
|
+
distinct_id: "agent-server",
|
|
21982
|
+
mode
|
|
21983
|
+
};
|
|
21984
|
+
await this.initializeSession(payload, null);
|
|
21985
|
+
}
|
|
21085
21986
|
getResumeRunId(taskRun) {
|
|
21086
21987
|
const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21087
21988
|
if (envRunId) return envRunId;
|
|
@@ -21570,6 +22471,11 @@ ${attributionInstructions}
|
|
|
21570
22471
|
async cleanupSession() {
|
|
21571
22472
|
if (!this.session) return;
|
|
21572
22473
|
this.logger.debug("Cleaning up session");
|
|
22474
|
+
try {
|
|
22475
|
+
await this.captureHandoffCheckpoint();
|
|
22476
|
+
} catch (error) {
|
|
22477
|
+
this.logger.error("Failed to capture handoff checkpoint", error);
|
|
22478
|
+
}
|
|
21573
22479
|
try {
|
|
21574
22480
|
await this.captureTreeState();
|
|
21575
22481
|
} catch (error) {
|
|
@@ -21629,6 +22535,50 @@ ${attributionInstructions}
|
|
|
21629
22535
|
this.logger.error("Failed to capture tree state", error);
|
|
21630
22536
|
}
|
|
21631
22537
|
}
|
|
22538
|
+
async captureHandoffCheckpoint() {
|
|
22539
|
+
if (!this.session?.treeTracker || !this.session.pendingHandoffGitState) {
|
|
22540
|
+
return;
|
|
22541
|
+
}
|
|
22542
|
+
if (!this.posthogAPI) {
|
|
22543
|
+
this.logger.warn(
|
|
22544
|
+
"Skipping handoff checkpoint capture: PostHog API client is not configured"
|
|
22545
|
+
);
|
|
22546
|
+
return;
|
|
22547
|
+
}
|
|
22548
|
+
const tracker = new HandoffCheckpointTracker({
|
|
22549
|
+
repositoryPath: this.config.repositoryPath ?? "/tmp/workspace",
|
|
22550
|
+
taskId: this.session.payload.task_id,
|
|
22551
|
+
runId: this.session.payload.run_id,
|
|
22552
|
+
apiClient: this.posthogAPI,
|
|
22553
|
+
logger: this.logger.child("HandoffCheckpoint")
|
|
22554
|
+
});
|
|
22555
|
+
const checkpoint = await tracker.captureForHandoff(
|
|
22556
|
+
this.session.pendingHandoffGitState
|
|
22557
|
+
);
|
|
22558
|
+
if (!checkpoint) return;
|
|
22559
|
+
const checkpointWithDevice = {
|
|
22560
|
+
...checkpoint,
|
|
22561
|
+
device: this.session.deviceInfo
|
|
22562
|
+
};
|
|
22563
|
+
const notification = {
|
|
22564
|
+
jsonrpc: "2.0",
|
|
22565
|
+
method: POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT,
|
|
22566
|
+
params: checkpointWithDevice
|
|
22567
|
+
};
|
|
22568
|
+
this.broadcastEvent({
|
|
22569
|
+
type: "notification",
|
|
22570
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22571
|
+
notification
|
|
22572
|
+
});
|
|
22573
|
+
this.session.logWriter.appendRawLine(
|
|
22574
|
+
this.session.payload.run_id,
|
|
22575
|
+
JSON.stringify(notification)
|
|
22576
|
+
);
|
|
22577
|
+
}
|
|
22578
|
+
extractHandoffLocalGitState(params) {
|
|
22579
|
+
const result = handoffLocalGitStateSchema.safeParse(params.localGitState);
|
|
22580
|
+
return result.success ? result.data : null;
|
|
22581
|
+
}
|
|
21632
22582
|
broadcastTurnComplete(stopReason) {
|
|
21633
22583
|
if (!this.session) return;
|
|
21634
22584
|
this.broadcastEvent({
|
|
@@ -21682,8 +22632,8 @@ ${attributionInstructions}
|
|
|
21682
22632
|
options: params.options,
|
|
21683
22633
|
toolCall: params.toolCall
|
|
21684
22634
|
});
|
|
21685
|
-
return new Promise((
|
|
21686
|
-
this.pendingPermissions.set(requestId, { resolve:
|
|
22635
|
+
return new Promise((resolve7) => {
|
|
22636
|
+
this.pendingPermissions.set(requestId, { resolve: resolve7 });
|
|
21687
22637
|
});
|
|
21688
22638
|
}
|
|
21689
22639
|
resolvePermission(requestId, optionId, customInput, answers) {
|