@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
package/dist/server/bin.cjs
CHANGED
|
@@ -805,15 +805,15 @@ var require_src2 = __commonJS({
|
|
|
805
805
|
var fs_1 = require("fs");
|
|
806
806
|
var debug_1 = __importDefault(require_src());
|
|
807
807
|
var log = debug_1.default("@kwsites/file-exists");
|
|
808
|
-
function check(
|
|
809
|
-
log(`checking %s`,
|
|
808
|
+
function check(path17, isFile2, isDirectory) {
|
|
809
|
+
log(`checking %s`, path17);
|
|
810
810
|
try {
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
811
|
+
const stat4 = fs_1.statSync(path17);
|
|
812
|
+
if (stat4.isFile() && isFile2) {
|
|
813
813
|
log(`[OK] path represents a file`);
|
|
814
814
|
return true;
|
|
815
815
|
}
|
|
816
|
-
if (
|
|
816
|
+
if (stat4.isDirectory() && isDirectory) {
|
|
817
817
|
log(`[OK] path represents a directory`);
|
|
818
818
|
return true;
|
|
819
819
|
}
|
|
@@ -828,8 +828,8 @@ var require_src2 = __commonJS({
|
|
|
828
828
|
throw e;
|
|
829
829
|
}
|
|
830
830
|
}
|
|
831
|
-
function exists2(
|
|
832
|
-
return check(
|
|
831
|
+
function exists2(path17, type = exports2.READABLE) {
|
|
832
|
+
return check(path17, (type & exports2.FILE) > 0, (type & exports2.FOLDER) > 0);
|
|
833
833
|
}
|
|
834
834
|
exports2.exists = exists2;
|
|
835
835
|
exports2.FILE = 1;
|
|
@@ -925,11 +925,11 @@ var require_tree_sitter = __commonJS({
|
|
|
925
925
|
throw toThrow;
|
|
926
926
|
};
|
|
927
927
|
var scriptDirectory = "";
|
|
928
|
-
function locateFile(
|
|
928
|
+
function locateFile(path17) {
|
|
929
929
|
if (Module["locateFile"]) {
|
|
930
|
-
return Module["locateFile"](
|
|
930
|
+
return Module["locateFile"](path17, scriptDirectory);
|
|
931
931
|
}
|
|
932
|
-
return scriptDirectory +
|
|
932
|
+
return scriptDirectory + path17;
|
|
933
933
|
}
|
|
934
934
|
var readAsync, readBinary;
|
|
935
935
|
if (ENVIRONMENT_IS_NODE) {
|
|
@@ -943,10 +943,10 @@ var require_tree_sitter = __commonJS({
|
|
|
943
943
|
};
|
|
944
944
|
readAsync = (filename, binary2 = true) => {
|
|
945
945
|
filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename);
|
|
946
|
-
return new Promise((
|
|
946
|
+
return new Promise((resolve7, reject) => {
|
|
947
947
|
fs.readFile(filename, binary2 ? void 0 : "utf8", (err2, data) => {
|
|
948
948
|
if (err2) reject(err2);
|
|
949
|
-
else
|
|
949
|
+
else resolve7(binary2 ? data.buffer : data);
|
|
950
950
|
});
|
|
951
951
|
});
|
|
952
952
|
};
|
|
@@ -987,13 +987,13 @@ var require_tree_sitter = __commonJS({
|
|
|
987
987
|
}
|
|
988
988
|
readAsync = (url) => {
|
|
989
989
|
if (isFileURI(url)) {
|
|
990
|
-
return new Promise((reject,
|
|
990
|
+
return new Promise((reject, resolve7) => {
|
|
991
991
|
var xhr = new XMLHttpRequest();
|
|
992
992
|
xhr.open("GET", url, true);
|
|
993
993
|
xhr.responseType = "arraybuffer";
|
|
994
994
|
xhr.onload = () => {
|
|
995
995
|
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
|
|
996
|
-
|
|
996
|
+
resolve7(xhr.response);
|
|
997
997
|
}
|
|
998
998
|
reject(xhr.status);
|
|
999
999
|
};
|
|
@@ -1953,8 +1953,8 @@ var require_tree_sitter = __commonJS({
|
|
|
1953
1953
|
}
|
|
1954
1954
|
var libFile = locateFile(libName2);
|
|
1955
1955
|
if (flags2.loadAsync) {
|
|
1956
|
-
return new Promise(function(
|
|
1957
|
-
asyncLoad(libFile,
|
|
1956
|
+
return new Promise(function(resolve7, reject) {
|
|
1957
|
+
asyncLoad(libFile, resolve7, reject);
|
|
1958
1958
|
});
|
|
1959
1959
|
}
|
|
1960
1960
|
if (!readBinary) {
|
|
@@ -3450,8 +3450,8 @@ var require_tree_sitter = __commonJS({
|
|
|
3450
3450
|
} else {
|
|
3451
3451
|
const url = input;
|
|
3452
3452
|
if (typeof process !== "undefined" && process.versions && process.versions.node) {
|
|
3453
|
-
const
|
|
3454
|
-
bytes = Promise.resolve(
|
|
3453
|
+
const fs14 = require("fs");
|
|
3454
|
+
bytes = Promise.resolve(fs14.readFileSync(url));
|
|
3455
3455
|
} else {
|
|
3456
3456
|
bytes = fetch(url).then((response) => response.arrayBuffer().then((buffer) => {
|
|
3457
3457
|
if (response.ok) {
|
|
@@ -3912,8 +3912,8 @@ function isSupportedReasoningEffort(adapter, modelId, value) {
|
|
|
3912
3912
|
}
|
|
3913
3913
|
|
|
3914
3914
|
// src/server/agent-server.ts
|
|
3915
|
-
var
|
|
3916
|
-
var
|
|
3915
|
+
var import_promises7 = require("fs/promises");
|
|
3916
|
+
var import_node_path10 = require("path");
|
|
3917
3917
|
var import_node_url2 = require("url");
|
|
3918
3918
|
var import_sdk5 = require("@agentclientprotocol/sdk");
|
|
3919
3919
|
var import_node_server = require("@hono/node-server");
|
|
@@ -3959,8 +3959,8 @@ function pathspec(...paths) {
|
|
|
3959
3959
|
cache.set(key, paths);
|
|
3960
3960
|
return key;
|
|
3961
3961
|
}
|
|
3962
|
-
function isPathSpec(
|
|
3963
|
-
return
|
|
3962
|
+
function isPathSpec(path17) {
|
|
3963
|
+
return path17 instanceof String && cache.has(path17);
|
|
3964
3964
|
}
|
|
3965
3965
|
function toPaths(pathSpec) {
|
|
3966
3966
|
return cache.get(pathSpec) || [];
|
|
@@ -4049,8 +4049,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
|
|
|
4049
4049
|
function forEachLineWithContent(input, callback) {
|
|
4050
4050
|
return toLinesWithContent(input, true).map((line) => callback(line));
|
|
4051
4051
|
}
|
|
4052
|
-
function folderExists(
|
|
4053
|
-
return (0, import_file_exists.exists)(
|
|
4052
|
+
function folderExists(path17) {
|
|
4053
|
+
return (0, import_file_exists.exists)(path17, import_file_exists.FOLDER);
|
|
4054
4054
|
}
|
|
4055
4055
|
function append(target, item) {
|
|
4056
4056
|
if (Array.isArray(target)) {
|
|
@@ -4454,8 +4454,8 @@ function checkIsRepoRootTask() {
|
|
|
4454
4454
|
commands,
|
|
4455
4455
|
format: "utf-8",
|
|
4456
4456
|
onError,
|
|
4457
|
-
parser(
|
|
4458
|
-
return /^\.(git)?$/.test(
|
|
4457
|
+
parser(path17) {
|
|
4458
|
+
return /^\.(git)?$/.test(path17.trim());
|
|
4459
4459
|
}
|
|
4460
4460
|
};
|
|
4461
4461
|
}
|
|
@@ -4889,11 +4889,11 @@ function parseGrep(grep) {
|
|
|
4889
4889
|
const paths = /* @__PURE__ */ new Set();
|
|
4890
4890
|
const results = {};
|
|
4891
4891
|
forEachLineWithContent(grep, (input) => {
|
|
4892
|
-
const [
|
|
4893
|
-
paths.add(
|
|
4894
|
-
(results[
|
|
4892
|
+
const [path17, line, preview] = input.split(NULL);
|
|
4893
|
+
paths.add(path17);
|
|
4894
|
+
(results[path17] = results[path17] || []).push({
|
|
4895
4895
|
line: asNumber(line),
|
|
4896
|
-
path:
|
|
4896
|
+
path: path17,
|
|
4897
4897
|
preview
|
|
4898
4898
|
});
|
|
4899
4899
|
});
|
|
@@ -5658,14 +5658,14 @@ var init_hash_object = __esm({
|
|
|
5658
5658
|
init_task();
|
|
5659
5659
|
}
|
|
5660
5660
|
});
|
|
5661
|
-
function parseInit(bare,
|
|
5661
|
+
function parseInit(bare, path17, text2) {
|
|
5662
5662
|
const response = String(text2).trim();
|
|
5663
5663
|
let result;
|
|
5664
5664
|
if (result = initResponseRegex.exec(response)) {
|
|
5665
|
-
return new InitSummary(bare,
|
|
5665
|
+
return new InitSummary(bare, path17, false, result[1]);
|
|
5666
5666
|
}
|
|
5667
5667
|
if (result = reInitResponseRegex.exec(response)) {
|
|
5668
|
-
return new InitSummary(bare,
|
|
5668
|
+
return new InitSummary(bare, path17, true, result[1]);
|
|
5669
5669
|
}
|
|
5670
5670
|
let gitDir = "";
|
|
5671
5671
|
const tokens = response.split(" ");
|
|
@@ -5676,7 +5676,7 @@ function parseInit(bare, path15, text2) {
|
|
|
5676
5676
|
break;
|
|
5677
5677
|
}
|
|
5678
5678
|
}
|
|
5679
|
-
return new InitSummary(bare,
|
|
5679
|
+
return new InitSummary(bare, path17, /^re/i.test(response), gitDir);
|
|
5680
5680
|
}
|
|
5681
5681
|
var InitSummary;
|
|
5682
5682
|
var initResponseRegex;
|
|
@@ -5685,9 +5685,9 @@ var init_InitSummary = __esm({
|
|
|
5685
5685
|
"src/lib/responses/InitSummary.ts"() {
|
|
5686
5686
|
"use strict";
|
|
5687
5687
|
InitSummary = class {
|
|
5688
|
-
constructor(bare,
|
|
5688
|
+
constructor(bare, path17, existing, gitDir) {
|
|
5689
5689
|
this.bare = bare;
|
|
5690
|
-
this.path =
|
|
5690
|
+
this.path = path17;
|
|
5691
5691
|
this.existing = existing;
|
|
5692
5692
|
this.gitDir = gitDir;
|
|
5693
5693
|
}
|
|
@@ -5699,7 +5699,7 @@ var init_InitSummary = __esm({
|
|
|
5699
5699
|
function hasBareCommand(command) {
|
|
5700
5700
|
return command.includes(bareCommand);
|
|
5701
5701
|
}
|
|
5702
|
-
function initTask(bare = false,
|
|
5702
|
+
function initTask(bare = false, path17, customArgs) {
|
|
5703
5703
|
const commands = ["init", ...customArgs];
|
|
5704
5704
|
if (bare && !hasBareCommand(commands)) {
|
|
5705
5705
|
commands.splice(1, 0, bareCommand);
|
|
@@ -5708,7 +5708,7 @@ function initTask(bare = false, path15, customArgs) {
|
|
|
5708
5708
|
commands,
|
|
5709
5709
|
format: "utf-8",
|
|
5710
5710
|
parser(text2) {
|
|
5711
|
-
return parseInit(commands.includes("--bare"),
|
|
5711
|
+
return parseInit(commands.includes("--bare"), path17, text2);
|
|
5712
5712
|
}
|
|
5713
5713
|
};
|
|
5714
5714
|
}
|
|
@@ -6524,12 +6524,12 @@ var init_FileStatusSummary = __esm({
|
|
|
6524
6524
|
"use strict";
|
|
6525
6525
|
fromPathRegex = /^(.+)\0(.+)$/;
|
|
6526
6526
|
FileStatusSummary = class {
|
|
6527
|
-
constructor(
|
|
6528
|
-
this.path =
|
|
6527
|
+
constructor(path17, index, working_dir) {
|
|
6528
|
+
this.path = path17;
|
|
6529
6529
|
this.index = index;
|
|
6530
6530
|
this.working_dir = working_dir;
|
|
6531
6531
|
if (index === "R" || working_dir === "R") {
|
|
6532
|
-
const detail = fromPathRegex.exec(
|
|
6532
|
+
const detail = fromPathRegex.exec(path17) || [null, path17, path17];
|
|
6533
6533
|
this.from = detail[2] || "";
|
|
6534
6534
|
this.path = detail[1] || "";
|
|
6535
6535
|
}
|
|
@@ -6560,14 +6560,14 @@ function splitLine(result, lineStr) {
|
|
|
6560
6560
|
default:
|
|
6561
6561
|
return;
|
|
6562
6562
|
}
|
|
6563
|
-
function data(index, workingDir,
|
|
6563
|
+
function data(index, workingDir, path17) {
|
|
6564
6564
|
const raw = `${index}${workingDir}`;
|
|
6565
6565
|
const handler = parsers6.get(raw);
|
|
6566
6566
|
if (handler) {
|
|
6567
|
-
handler(result,
|
|
6567
|
+
handler(result, path17);
|
|
6568
6568
|
}
|
|
6569
6569
|
if (raw !== "##" && raw !== "!!") {
|
|
6570
|
-
result.files.push(new FileStatusSummary(
|
|
6570
|
+
result.files.push(new FileStatusSummary(path17, index, workingDir));
|
|
6571
6571
|
}
|
|
6572
6572
|
}
|
|
6573
6573
|
}
|
|
@@ -6880,9 +6880,9 @@ var init_simple_git_api = __esm({
|
|
|
6880
6880
|
next
|
|
6881
6881
|
);
|
|
6882
6882
|
}
|
|
6883
|
-
hashObject(
|
|
6883
|
+
hashObject(path17, write) {
|
|
6884
6884
|
return this._runTask(
|
|
6885
|
-
hashObjectTask(
|
|
6885
|
+
hashObjectTask(path17, write === true),
|
|
6886
6886
|
trailingFunctionArgument(arguments)
|
|
6887
6887
|
);
|
|
6888
6888
|
}
|
|
@@ -7235,8 +7235,8 @@ var init_branch = __esm({
|
|
|
7235
7235
|
}
|
|
7236
7236
|
});
|
|
7237
7237
|
function toPath(input) {
|
|
7238
|
-
const
|
|
7239
|
-
return
|
|
7238
|
+
const path17 = input.trim().replace(/^["']|["']$/g, "");
|
|
7239
|
+
return path17 && (0, import_node_path.normalize)(path17);
|
|
7240
7240
|
}
|
|
7241
7241
|
var parseCheckIgnore;
|
|
7242
7242
|
var init_CheckIgnore = __esm({
|
|
@@ -7550,8 +7550,8 @@ __export(sub_module_exports, {
|
|
|
7550
7550
|
subModuleTask: () => subModuleTask,
|
|
7551
7551
|
updateSubModuleTask: () => updateSubModuleTask
|
|
7552
7552
|
});
|
|
7553
|
-
function addSubModuleTask(repo,
|
|
7554
|
-
return subModuleTask(["add", repo,
|
|
7553
|
+
function addSubModuleTask(repo, path17) {
|
|
7554
|
+
return subModuleTask(["add", repo, path17]);
|
|
7555
7555
|
}
|
|
7556
7556
|
function initSubModuleTask(customArgs) {
|
|
7557
7557
|
return subModuleTask(["init", ...customArgs]);
|
|
@@ -7881,8 +7881,8 @@ var require_git = __commonJS2({
|
|
|
7881
7881
|
}
|
|
7882
7882
|
return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
|
|
7883
7883
|
};
|
|
7884
|
-
Git2.prototype.submoduleAdd = function(repo,
|
|
7885
|
-
return this._runTask(addSubModuleTask2(repo,
|
|
7884
|
+
Git2.prototype.submoduleAdd = function(repo, path17, then) {
|
|
7885
|
+
return this._runTask(addSubModuleTask2(repo, path17), trailingFunctionArgument2(arguments));
|
|
7886
7886
|
};
|
|
7887
7887
|
Git2.prototype.submoduleUpdate = function(args2, then) {
|
|
7888
7888
|
return this._runTask(
|
|
@@ -8498,10 +8498,10 @@ async function getIndexLockPath(repoPath) {
|
|
|
8498
8498
|
async function getLockInfo(repoPath) {
|
|
8499
8499
|
const lockPath = await getIndexLockPath(repoPath);
|
|
8500
8500
|
try {
|
|
8501
|
-
const
|
|
8501
|
+
const stat4 = await import_promises.default.stat(lockPath);
|
|
8502
8502
|
return {
|
|
8503
8503
|
path: lockPath,
|
|
8504
|
-
ageMs: Date.now() -
|
|
8504
|
+
ageMs: Date.now() - stat4.mtimeMs
|
|
8505
8505
|
};
|
|
8506
8506
|
} catch {
|
|
8507
8507
|
return null;
|
|
@@ -8536,10 +8536,10 @@ var AsyncReaderWriterLock = class {
|
|
|
8536
8536
|
this.readers++;
|
|
8537
8537
|
return;
|
|
8538
8538
|
}
|
|
8539
|
-
return new Promise((
|
|
8539
|
+
return new Promise((resolve7) => {
|
|
8540
8540
|
this.readQueue.push(() => {
|
|
8541
8541
|
this.readers++;
|
|
8542
|
-
|
|
8542
|
+
resolve7();
|
|
8543
8543
|
});
|
|
8544
8544
|
});
|
|
8545
8545
|
}
|
|
@@ -8553,11 +8553,11 @@ var AsyncReaderWriterLock = class {
|
|
|
8553
8553
|
return;
|
|
8554
8554
|
}
|
|
8555
8555
|
this.writerWaiting = true;
|
|
8556
|
-
return new Promise((
|
|
8556
|
+
return new Promise((resolve7) => {
|
|
8557
8557
|
this.writeQueue.push(() => {
|
|
8558
8558
|
this.writerWaiting = this.writeQueue.length > 0;
|
|
8559
8559
|
this.writer = true;
|
|
8560
|
-
|
|
8560
|
+
resolve7();
|
|
8561
8561
|
});
|
|
8562
8562
|
});
|
|
8563
8563
|
}
|
|
@@ -8729,7 +8729,7 @@ var import_zod3 = require("zod");
|
|
|
8729
8729
|
// package.json
|
|
8730
8730
|
var package_default = {
|
|
8731
8731
|
name: "@posthog/agent",
|
|
8732
|
-
version: "2.3.
|
|
8732
|
+
version: "2.3.388",
|
|
8733
8733
|
repository: "https://github.com/PostHog/code",
|
|
8734
8734
|
description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
8735
8735
|
exports: {
|
|
@@ -8789,6 +8789,10 @@ var package_default = {
|
|
|
8789
8789
|
types: "./dist/resume.d.ts",
|
|
8790
8790
|
import: "./dist/resume.js"
|
|
8791
8791
|
},
|
|
8792
|
+
"./handoff-checkpoint": {
|
|
8793
|
+
types: "./dist/handoff-checkpoint.d.ts",
|
|
8794
|
+
import: "./dist/handoff-checkpoint.js"
|
|
8795
|
+
},
|
|
8792
8796
|
"./tree-tracker": {
|
|
8793
8797
|
types: "./dist/tree-tracker.d.ts",
|
|
8794
8798
|
import: "./dist/tree-tracker.js"
|
|
@@ -8796,6 +8800,10 @@ var package_default = {
|
|
|
8796
8800
|
"./server": {
|
|
8797
8801
|
types: "./dist/server/agent-server.d.ts",
|
|
8798
8802
|
import: "./dist/server/agent-server.js"
|
|
8803
|
+
},
|
|
8804
|
+
"./server/schemas": {
|
|
8805
|
+
types: "./dist/server/schemas.d.ts",
|
|
8806
|
+
import: "./dist/server/schemas.js"
|
|
8799
8807
|
}
|
|
8800
8808
|
},
|
|
8801
8809
|
bin: {
|
|
@@ -8886,6 +8894,8 @@ var POSTHOG_NOTIFICATIONS = {
|
|
|
8886
8894
|
SDK_SESSION: "_posthog/sdk_session",
|
|
8887
8895
|
/** Tree state snapshot captured (git tree hash + file archive) */
|
|
8888
8896
|
TREE_SNAPSHOT: "_posthog/tree_snapshot",
|
|
8897
|
+
/** Git checkpoint captured for handoff */
|
|
8898
|
+
GIT_CHECKPOINT: "_posthog/git_checkpoint",
|
|
8889
8899
|
/** Agent mode changed (interactive/background) */
|
|
8890
8900
|
MODE_CHANGE: "_posthog/mode_change",
|
|
8891
8901
|
/** Request to resume a session from previous state */
|
|
@@ -8992,17 +9002,17 @@ var Pushable = class {
|
|
|
8992
9002
|
resolvers = [];
|
|
8993
9003
|
done = false;
|
|
8994
9004
|
push(item) {
|
|
8995
|
-
const
|
|
8996
|
-
if (
|
|
8997
|
-
|
|
9005
|
+
const resolve7 = this.resolvers.shift();
|
|
9006
|
+
if (resolve7) {
|
|
9007
|
+
resolve7({ value: item, done: false });
|
|
8998
9008
|
} else {
|
|
8999
9009
|
this.queue.push(item);
|
|
9000
9010
|
}
|
|
9001
9011
|
}
|
|
9002
9012
|
end() {
|
|
9003
9013
|
this.done = true;
|
|
9004
|
-
for (const
|
|
9005
|
-
|
|
9014
|
+
for (const resolve7 of this.resolvers) {
|
|
9015
|
+
resolve7({ value: void 0, done: true });
|
|
9006
9016
|
}
|
|
9007
9017
|
this.resolvers = [];
|
|
9008
9018
|
}
|
|
@@ -9019,8 +9029,8 @@ var Pushable = class {
|
|
|
9019
9029
|
done: true
|
|
9020
9030
|
});
|
|
9021
9031
|
}
|
|
9022
|
-
return new Promise((
|
|
9023
|
-
this.resolvers.push(
|
|
9032
|
+
return new Promise((resolve7) => {
|
|
9033
|
+
this.resolvers.push(resolve7);
|
|
9024
9034
|
});
|
|
9025
9035
|
}
|
|
9026
9036
|
};
|
|
@@ -9134,20 +9144,20 @@ function nodeReadableToWebReadable(nodeStream) {
|
|
|
9134
9144
|
function nodeWritableToWebWritable(nodeStream) {
|
|
9135
9145
|
return new import_web.WritableStream({
|
|
9136
9146
|
write(chunk) {
|
|
9137
|
-
return new Promise((
|
|
9147
|
+
return new Promise((resolve7, reject) => {
|
|
9138
9148
|
const ok = nodeStream.write(Buffer.from(chunk), (err2) => {
|
|
9139
9149
|
if (err2) reject(err2);
|
|
9140
9150
|
});
|
|
9141
9151
|
if (ok) {
|
|
9142
|
-
|
|
9152
|
+
resolve7();
|
|
9143
9153
|
} else {
|
|
9144
|
-
nodeStream.once("drain",
|
|
9154
|
+
nodeStream.once("drain", resolve7);
|
|
9145
9155
|
}
|
|
9146
9156
|
});
|
|
9147
9157
|
},
|
|
9148
9158
|
close() {
|
|
9149
|
-
return new Promise((
|
|
9150
|
-
nodeStream.end(
|
|
9159
|
+
return new Promise((resolve7) => {
|
|
9160
|
+
nodeStream.end(resolve7);
|
|
9151
9161
|
});
|
|
9152
9162
|
},
|
|
9153
9163
|
abort(reason) {
|
|
@@ -13089,9 +13099,9 @@ var PostHogEnricher = class {
|
|
|
13089
13099
|
}
|
|
13090
13100
|
let mtimeMs = 0;
|
|
13091
13101
|
try {
|
|
13092
|
-
const
|
|
13093
|
-
mtimeMs =
|
|
13094
|
-
if (
|
|
13102
|
+
const stat22 = await fs4.stat(absPath);
|
|
13103
|
+
mtimeMs = stat22.mtimeMs;
|
|
13104
|
+
if (stat22.size > MAX_WRAPPER_SOURCE_BYTES) {
|
|
13095
13105
|
return this.setWrapperCache(absPath, mtimeMs, []);
|
|
13096
13106
|
}
|
|
13097
13107
|
} catch {
|
|
@@ -13257,7 +13267,7 @@ async function buildWrapperContext(deps, content, langId, absPath) {
|
|
|
13257
13267
|
// src/utils/common.ts
|
|
13258
13268
|
async function withTimeout(operation, timeoutMs) {
|
|
13259
13269
|
const timeoutPromise = new Promise(
|
|
13260
|
-
(
|
|
13270
|
+
(resolve7) => setTimeout(() => resolve7({ result: "timeout" }), timeoutMs)
|
|
13261
13271
|
);
|
|
13262
13272
|
const operationPromise = operation.then((value) => ({
|
|
13263
13273
|
result: "success",
|
|
@@ -13555,8 +13565,8 @@ var ToolContentBuilder = class {
|
|
|
13555
13565
|
this.items.push({ type: "content", content: image(data, mimeType, uri) });
|
|
13556
13566
|
return this;
|
|
13557
13567
|
}
|
|
13558
|
-
diff(
|
|
13559
|
-
this.items.push({ type: "diff", path:
|
|
13568
|
+
diff(path17, oldText, newText) {
|
|
13569
|
+
this.items.push({ type: "diff", path: path17, oldText, newText });
|
|
13560
13570
|
return this;
|
|
13561
13571
|
}
|
|
13562
13572
|
build() {
|
|
@@ -13743,7 +13753,7 @@ function buildToolKey(serverName, toolName) {
|
|
|
13743
13753
|
return `mcp__${serverName}__${toolName}`;
|
|
13744
13754
|
}
|
|
13745
13755
|
function delay2(ms) {
|
|
13746
|
-
return new Promise((
|
|
13756
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
13747
13757
|
}
|
|
13748
13758
|
async function fetchMcpToolMetadata(q, logger = new Logger({ debug: false, prefix: "[McpToolMetadata]" })) {
|
|
13749
13759
|
let retries = 0;
|
|
@@ -15957,8 +15967,8 @@ var AsyncMutex = class {
|
|
|
15957
15967
|
this.locked = true;
|
|
15958
15968
|
return;
|
|
15959
15969
|
}
|
|
15960
|
-
return new Promise((
|
|
15961
|
-
this.queue.push(
|
|
15970
|
+
return new Promise((resolve7) => {
|
|
15971
|
+
this.queue.push(resolve7);
|
|
15962
15972
|
});
|
|
15963
15973
|
}
|
|
15964
15974
|
release() {
|
|
@@ -16485,8 +16495,8 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
|
|
|
16485
16495
|
if (this.session.promptRunning) {
|
|
16486
16496
|
this.session.input.push(userMessage);
|
|
16487
16497
|
const order = this.session.nextPendingOrder++;
|
|
16488
|
-
const cancelled = await new Promise((
|
|
16489
|
-
this.session.pendingMessages.set(promptUuid, { resolve:
|
|
16498
|
+
const cancelled = await new Promise((resolve7) => {
|
|
16499
|
+
this.session.pendingMessages.set(promptUuid, { resolve: resolve7, order });
|
|
16490
16500
|
});
|
|
16491
16501
|
if (cancelled) {
|
|
16492
16502
|
return { stopReason: "cancelled" };
|
|
@@ -17367,7 +17377,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
|
|
|
17367
17377
|
*/
|
|
17368
17378
|
deferBackgroundFetches(q) {
|
|
17369
17379
|
Promise.all([
|
|
17370
|
-
new Promise((
|
|
17380
|
+
new Promise((resolve7) => setTimeout(resolve7, 10)).then(
|
|
17371
17381
|
() => this.sendAvailableCommandsUpdate()
|
|
17372
17382
|
),
|
|
17373
17383
|
fetchMcpToolMetadata(q, this.logger).then(() => {
|
|
@@ -18331,245 +18341,19 @@ function createCodexConnection(config) {
|
|
|
18331
18341
|
};
|
|
18332
18342
|
}
|
|
18333
18343
|
|
|
18334
|
-
// src/
|
|
18335
|
-
|
|
18336
|
-
|
|
18337
|
-
const hostname = url.hostname;
|
|
18338
|
-
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
18339
|
-
return `${url.protocol}//localhost:3308`;
|
|
18340
|
-
}
|
|
18341
|
-
if (hostname === "host.docker.internal") {
|
|
18342
|
-
return `${url.protocol}//host.docker.internal:3308`;
|
|
18343
|
-
}
|
|
18344
|
-
const region = hostname.match(/^(us|eu)\.posthog\.com$/)?.[1] ?? "us";
|
|
18345
|
-
return `https://gateway.${region}.posthog.com`;
|
|
18346
|
-
}
|
|
18347
|
-
function getLlmGatewayUrl(posthogHost, product = "posthog_code") {
|
|
18348
|
-
return `${getGatewayBaseUrl(posthogHost)}/${product}`;
|
|
18349
|
-
}
|
|
18344
|
+
// src/handoff-checkpoint.ts
|
|
18345
|
+
var import_promises3 = require("fs/promises");
|
|
18346
|
+
var import_node_path6 = require("path");
|
|
18350
18347
|
|
|
18351
|
-
//
|
|
18352
|
-
var
|
|
18353
|
-
var
|
|
18354
|
-
|
|
18355
|
-
constructor(config) {
|
|
18356
|
-
this.config = config;
|
|
18357
|
-
}
|
|
18358
|
-
get baseUrl() {
|
|
18359
|
-
const host = this.config.apiUrl.endsWith("/") ? this.config.apiUrl.slice(0, -1) : this.config.apiUrl;
|
|
18360
|
-
return host;
|
|
18361
|
-
}
|
|
18362
|
-
isAuthFailure(status) {
|
|
18363
|
-
return status === 401 || status === 403;
|
|
18364
|
-
}
|
|
18365
|
-
async resolveApiKey(forceRefresh = false) {
|
|
18366
|
-
if (forceRefresh && this.config.refreshApiKey) {
|
|
18367
|
-
return this.config.refreshApiKey();
|
|
18368
|
-
}
|
|
18369
|
-
return this.config.getApiKey();
|
|
18370
|
-
}
|
|
18371
|
-
async buildHeaders(options, forceRefresh = false) {
|
|
18372
|
-
const headers = new Headers(options.headers);
|
|
18373
|
-
headers.set(
|
|
18374
|
-
"Authorization",
|
|
18375
|
-
`Bearer ${await this.resolveApiKey(forceRefresh)}`
|
|
18376
|
-
);
|
|
18377
|
-
headers.set("Content-Type", "application/json");
|
|
18378
|
-
headers.set("User-Agent", this.config.userAgent ?? DEFAULT_USER_AGENT);
|
|
18379
|
-
return headers;
|
|
18380
|
-
}
|
|
18381
|
-
async performRequest(endpoint, options, forceRefresh = false) {
|
|
18382
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
18383
|
-
return fetch(url, {
|
|
18384
|
-
...options,
|
|
18385
|
-
headers: await this.buildHeaders(options, forceRefresh)
|
|
18386
|
-
});
|
|
18387
|
-
}
|
|
18388
|
-
async performRequestWithRetry(endpoint, options = {}) {
|
|
18389
|
-
let response = await this.performRequest(endpoint, options);
|
|
18390
|
-
if (!response.ok && this.isAuthFailure(response.status)) {
|
|
18391
|
-
response = await this.performRequest(endpoint, options, true);
|
|
18392
|
-
}
|
|
18393
|
-
return response;
|
|
18394
|
-
}
|
|
18395
|
-
async apiRequest(endpoint, options = {}) {
|
|
18396
|
-
const response = await this.performRequestWithRetry(endpoint, options);
|
|
18397
|
-
if (!response.ok) {
|
|
18398
|
-
let errorMessage;
|
|
18399
|
-
try {
|
|
18400
|
-
const errorResponse = await response.json();
|
|
18401
|
-
errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
|
|
18402
|
-
} catch {
|
|
18403
|
-
errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
|
|
18404
|
-
}
|
|
18405
|
-
throw new Error(errorMessage);
|
|
18406
|
-
}
|
|
18407
|
-
return response.json();
|
|
18408
|
-
}
|
|
18409
|
-
getTeamId() {
|
|
18410
|
-
return this.config.projectId;
|
|
18411
|
-
}
|
|
18412
|
-
async getApiKey(forceRefresh = false) {
|
|
18413
|
-
return this.resolveApiKey(forceRefresh);
|
|
18414
|
-
}
|
|
18415
|
-
getLlmGatewayUrl() {
|
|
18416
|
-
return getLlmGatewayUrl(this.baseUrl);
|
|
18417
|
-
}
|
|
18418
|
-
async getTask(taskId) {
|
|
18419
|
-
const teamId = this.getTeamId();
|
|
18420
|
-
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
|
|
18421
|
-
}
|
|
18422
|
-
async getTaskRun(taskId, runId) {
|
|
18423
|
-
const teamId = this.getTeamId();
|
|
18424
|
-
return this.apiRequest(
|
|
18425
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
18426
|
-
);
|
|
18427
|
-
}
|
|
18428
|
-
async updateTaskRun(taskId, runId, payload) {
|
|
18429
|
-
const teamId = this.getTeamId();
|
|
18430
|
-
return this.apiRequest(
|
|
18431
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
|
|
18432
|
-
{
|
|
18433
|
-
method: "PATCH",
|
|
18434
|
-
body: JSON.stringify(payload)
|
|
18435
|
-
}
|
|
18436
|
-
);
|
|
18437
|
-
}
|
|
18438
|
-
async setTaskRunOutput(taskId, runId, output) {
|
|
18439
|
-
return this.apiRequest(
|
|
18440
|
-
`/api/projects/${this.getTeamId()}/tasks/${taskId}/runs/${runId}/set_output/`,
|
|
18441
|
-
{
|
|
18442
|
-
method: "PATCH",
|
|
18443
|
-
body: JSON.stringify(output)
|
|
18444
|
-
}
|
|
18445
|
-
);
|
|
18446
|
-
}
|
|
18447
|
-
async appendTaskRunLog(taskId, runId, entries) {
|
|
18448
|
-
const teamId = this.getTeamId();
|
|
18449
|
-
return this.apiRequest(
|
|
18450
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
|
|
18451
|
-
{
|
|
18452
|
-
method: "POST",
|
|
18453
|
-
body: JSON.stringify({ entries })
|
|
18454
|
-
}
|
|
18455
|
-
);
|
|
18456
|
-
}
|
|
18457
|
-
async relayMessage(taskId, runId, text2) {
|
|
18458
|
-
const teamId = this.getTeamId();
|
|
18459
|
-
await this.apiRequest(
|
|
18460
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
|
|
18461
|
-
{
|
|
18462
|
-
method: "POST",
|
|
18463
|
-
body: JSON.stringify({ text: text2 })
|
|
18464
|
-
}
|
|
18465
|
-
);
|
|
18466
|
-
}
|
|
18467
|
-
async uploadTaskArtifacts(taskId, runId, artifacts) {
|
|
18468
|
-
if (!artifacts.length) {
|
|
18469
|
-
return [];
|
|
18470
|
-
}
|
|
18471
|
-
const teamId = this.getTeamId();
|
|
18472
|
-
const response = await this.apiRequest(
|
|
18473
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
|
|
18474
|
-
{
|
|
18475
|
-
method: "POST",
|
|
18476
|
-
body: JSON.stringify({ artifacts })
|
|
18477
|
-
}
|
|
18478
|
-
);
|
|
18479
|
-
return response.artifacts ?? [];
|
|
18480
|
-
}
|
|
18481
|
-
/**
|
|
18482
|
-
* Download artifact content by storage path
|
|
18483
|
-
* Streams the file through the PostHog backend so the sandbox does not need
|
|
18484
|
-
* direct access to object storage.
|
|
18485
|
-
*/
|
|
18486
|
-
async downloadArtifact(taskId, runId, storagePath) {
|
|
18487
|
-
const teamId = this.getTeamId();
|
|
18488
|
-
try {
|
|
18489
|
-
const response = await this.performRequestWithRetry(
|
|
18490
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
|
|
18491
|
-
{
|
|
18492
|
-
method: "POST",
|
|
18493
|
-
body: JSON.stringify({ storage_path: storagePath })
|
|
18494
|
-
}
|
|
18495
|
-
);
|
|
18496
|
-
if (!response.ok) {
|
|
18497
|
-
throw new Error(`Failed to download artifact: ${response.status}`);
|
|
18498
|
-
}
|
|
18499
|
-
return response.arrayBuffer();
|
|
18500
|
-
} catch {
|
|
18501
|
-
return null;
|
|
18502
|
-
}
|
|
18503
|
-
}
|
|
18504
|
-
/**
|
|
18505
|
-
* Fetch logs for a task run via the logs API endpoint
|
|
18506
|
-
* @param taskRun - The task run to fetch logs for
|
|
18507
|
-
* @returns Array of stored entries, or empty array if no logs available
|
|
18508
|
-
*/
|
|
18509
|
-
async fetchTaskRunLogs(taskRun) {
|
|
18510
|
-
const teamId = this.getTeamId();
|
|
18511
|
-
const endpoint = `/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`;
|
|
18512
|
-
try {
|
|
18513
|
-
const response = await this.performRequestWithRetry(endpoint);
|
|
18514
|
-
if (!response.ok) {
|
|
18515
|
-
if (response.status === 404) {
|
|
18516
|
-
return [];
|
|
18517
|
-
}
|
|
18518
|
-
throw new Error(
|
|
18519
|
-
`Failed to fetch logs: ${response.status} ${response.statusText}`
|
|
18520
|
-
);
|
|
18521
|
-
}
|
|
18522
|
-
const content = await response.text();
|
|
18523
|
-
if (!content.trim()) {
|
|
18524
|
-
return [];
|
|
18525
|
-
}
|
|
18526
|
-
return content.trim().split("\n").map((line) => JSON.parse(line));
|
|
18527
|
-
} catch (error) {
|
|
18528
|
-
throw new Error(
|
|
18529
|
-
`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`
|
|
18530
|
-
);
|
|
18531
|
-
}
|
|
18532
|
-
}
|
|
18533
|
-
};
|
|
18348
|
+
// ../git/dist/handoff.js
|
|
18349
|
+
var import_node_child_process4 = require("child_process");
|
|
18350
|
+
var import_promises2 = require("fs/promises");
|
|
18351
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
18534
18352
|
|
|
18535
|
-
//
|
|
18353
|
+
// ../git/dist/sagas/checkpoint.js
|
|
18536
18354
|
var import_node_crypto2 = require("crypto");
|
|
18537
18355
|
var fs10 = __toESM(require("fs/promises"), 1);
|
|
18538
|
-
var os6 = __toESM(require("os"), 1);
|
|
18539
18356
|
var path12 = __toESM(require("path"), 1);
|
|
18540
|
-
var CHARS_PER_TOKEN = 4;
|
|
18541
|
-
var DEFAULT_MAX_TOKENS = 15e4;
|
|
18542
|
-
function estimateTurnTokens(turn) {
|
|
18543
|
-
let chars = 0;
|
|
18544
|
-
for (const block of turn.content) {
|
|
18545
|
-
if ("text" in block && typeof block.text === "string") {
|
|
18546
|
-
chars += block.text.length;
|
|
18547
|
-
}
|
|
18548
|
-
}
|
|
18549
|
-
if (turn.toolCalls) {
|
|
18550
|
-
for (const tc of turn.toolCalls) {
|
|
18551
|
-
chars += JSON.stringify(tc.input ?? "").length;
|
|
18552
|
-
if (tc.result !== void 0) {
|
|
18553
|
-
chars += typeof tc.result === "string" ? tc.result.length : JSON.stringify(tc.result).length;
|
|
18554
|
-
}
|
|
18555
|
-
}
|
|
18556
|
-
}
|
|
18557
|
-
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
18558
|
-
}
|
|
18559
|
-
function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
18560
|
-
let budget = maxTokens;
|
|
18561
|
-
let startIndex = turns.length;
|
|
18562
|
-
for (let i2 = turns.length - 1; i2 >= 0; i2--) {
|
|
18563
|
-
const cost = estimateTurnTokens(turns[i2]);
|
|
18564
|
-
if (cost > budget) break;
|
|
18565
|
-
budget -= cost;
|
|
18566
|
-
startIndex = i2;
|
|
18567
|
-
}
|
|
18568
|
-
while (startIndex < turns.length && turns[startIndex].role !== "user") {
|
|
18569
|
-
startIndex++;
|
|
18570
|
-
}
|
|
18571
|
-
return turns.slice(startIndex);
|
|
18572
|
-
}
|
|
18573
18357
|
|
|
18574
18358
|
// ../shared/dist/index.js
|
|
18575
18359
|
var CLOUD_PROMPT_PREFIX = "__twig_cloud_prompt_v1__:";
|
|
@@ -18713,16 +18497,6 @@ var Saga = class {
|
|
|
18713
18497
|
}
|
|
18714
18498
|
};
|
|
18715
18499
|
|
|
18716
|
-
// src/sagas/apply-snapshot-saga.ts
|
|
18717
|
-
var import_promises2 = require("fs/promises");
|
|
18718
|
-
var import_node_path5 = require("path");
|
|
18719
|
-
|
|
18720
|
-
// ../git/dist/sagas/tree.js
|
|
18721
|
-
var import_node_fs3 = require("fs");
|
|
18722
|
-
var fs11 = __toESM(require("fs/promises"), 1);
|
|
18723
|
-
var path13 = __toESM(require("path"), 1);
|
|
18724
|
-
var tar = __toESM(require("tar"), 1);
|
|
18725
|
-
|
|
18726
18500
|
// ../git/dist/git-saga.js
|
|
18727
18501
|
var GitSaga = class extends Saga {
|
|
18728
18502
|
_git = null;
|
|
@@ -18741,664 +18515,1189 @@ var GitSaga = class extends Saga {
|
|
|
18741
18515
|
}
|
|
18742
18516
|
};
|
|
18743
18517
|
|
|
18744
|
-
// ../git/dist/sagas/
|
|
18745
|
-
var
|
|
18746
|
-
|
|
18747
|
-
|
|
18518
|
+
// ../git/dist/sagas/checkpoint.js
|
|
18519
|
+
var CHECKPOINT_REF_PREFIX = "refs/posthog-code-checkpoint/";
|
|
18520
|
+
var CHECKPOINT_VERSION = "v1";
|
|
18521
|
+
var UNMERGED_INDEX_ERROR = "Cannot capture checkpoint with unresolved merge conflicts in the index";
|
|
18522
|
+
var GIT_BUSY_ERROR = "Cannot capture checkpoint while git operation is in progress";
|
|
18523
|
+
var CHECKPOINT_AUTHOR = {
|
|
18524
|
+
name: "PostHog Code",
|
|
18525
|
+
email: "posthog-code@local"
|
|
18526
|
+
};
|
|
18527
|
+
var CaptureCheckpointSaga = class extends GitSaga {
|
|
18528
|
+
sagaName = "CaptureCheckpointSaga";
|
|
18748
18529
|
async executeGitOperations(input) {
|
|
18749
|
-
const { baseDir
|
|
18750
|
-
const
|
|
18751
|
-
await this.
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18530
|
+
const { baseDir } = input;
|
|
18531
|
+
const headInfo = await this.readOnlyStep("get_head_info", () => getHeadInfo(this.git));
|
|
18532
|
+
const busyState = await this.readOnlyStep("check_git_busy", () => getGitBusyState(this.git));
|
|
18533
|
+
if (busyState.busy) {
|
|
18534
|
+
throw new Error(`${GIT_BUSY_ERROR}: ${busyState.operation}`);
|
|
18535
|
+
}
|
|
18536
|
+
const hasUnmerged = await this.readOnlyStep("check_unmerged_index", () => hasUnmergedEntries(this.git));
|
|
18537
|
+
if (hasUnmerged) {
|
|
18538
|
+
throw new Error(UNMERGED_INDEX_ERROR);
|
|
18539
|
+
}
|
|
18540
|
+
const indexTree = await this.readOnlyStep("write_index_tree", () => this.git.raw(["write-tree"]));
|
|
18541
|
+
const worktreeTree = await this.readOnlyStep("write_worktree_tree", () => createWorktreeTree(this.git, baseDir, headInfo.head));
|
|
18542
|
+
const metaTree = await this.readOnlyStep("write_meta_tree", () => createMetaTree(this.git, baseDir, indexTree.trim(), worktreeTree.trim()));
|
|
18543
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
18544
|
+
const message = formatCheckpointMessage({
|
|
18545
|
+
head: headInfo.head,
|
|
18546
|
+
branch: headInfo.branch,
|
|
18547
|
+
indexTree: indexTree.trim(),
|
|
18548
|
+
worktreeTree: worktreeTree.trim(),
|
|
18549
|
+
timestamp
|
|
18761
18550
|
});
|
|
18762
|
-
await this.step({
|
|
18763
|
-
name: "
|
|
18764
|
-
execute: () =>
|
|
18551
|
+
const commitHash = await this.step({
|
|
18552
|
+
name: "create_checkpoint_commit",
|
|
18553
|
+
execute: async () => {
|
|
18554
|
+
const commitGit = this.git.env({
|
|
18555
|
+
...process.env,
|
|
18556
|
+
GIT_AUTHOR_NAME: CHECKPOINT_AUTHOR.name,
|
|
18557
|
+
GIT_AUTHOR_EMAIL: CHECKPOINT_AUTHOR.email,
|
|
18558
|
+
GIT_COMMITTER_NAME: CHECKPOINT_AUTHOR.name,
|
|
18559
|
+
GIT_COMMITTER_EMAIL: CHECKPOINT_AUTHOR.email
|
|
18560
|
+
});
|
|
18561
|
+
const rawCommit = await commitGit.raw([
|
|
18562
|
+
"commit-tree",
|
|
18563
|
+
metaTree.trim(),
|
|
18564
|
+
...headInfo.head ? ["-p", headInfo.head] : [],
|
|
18565
|
+
"-m",
|
|
18566
|
+
message
|
|
18567
|
+
]);
|
|
18568
|
+
return rawCommit.trim();
|
|
18569
|
+
},
|
|
18765
18570
|
rollback: async () => {
|
|
18766
|
-
if (this.tempIndexPath) {
|
|
18767
|
-
await fs11.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
18768
|
-
});
|
|
18769
|
-
}
|
|
18770
18571
|
}
|
|
18771
18572
|
});
|
|
18772
|
-
|
|
18773
|
-
const
|
|
18774
|
-
|
|
18775
|
-
this.log.debug("No changes since last capture", { treeHash });
|
|
18776
|
-
await fs11.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
18777
|
-
});
|
|
18778
|
-
return { snapshot: null, changed: false };
|
|
18779
|
-
}
|
|
18780
|
-
const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
|
|
18573
|
+
const checkpointId = input.checkpointId ?? (0, import_node_crypto2.randomUUID)();
|
|
18574
|
+
const refName = `${CHECKPOINT_REF_PREFIX}${checkpointId}`;
|
|
18575
|
+
const existingRef = await this.readOnlyStep("check_existing_ref", async () => {
|
|
18781
18576
|
try {
|
|
18782
|
-
|
|
18577
|
+
await this.git.revparse(["--verify", refName]);
|
|
18578
|
+
return true;
|
|
18783
18579
|
} catch {
|
|
18784
|
-
return
|
|
18580
|
+
return false;
|
|
18785
18581
|
}
|
|
18786
18582
|
});
|
|
18787
|
-
|
|
18788
|
-
|
|
18583
|
+
if (existingRef) {
|
|
18584
|
+
throw new Error(`Checkpoint ref already exists: ${refName}`);
|
|
18585
|
+
}
|
|
18586
|
+
await this.step({
|
|
18587
|
+
name: "update_checkpoint_ref",
|
|
18588
|
+
execute: () => this.git.raw(["update-ref", refName, commitHash]),
|
|
18589
|
+
rollback: async () => {
|
|
18590
|
+
await this.git.raw(["update-ref", "-d", refName]).catch(() => {
|
|
18591
|
+
});
|
|
18592
|
+
}
|
|
18789
18593
|
});
|
|
18790
|
-
|
|
18791
|
-
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18594
|
+
return {
|
|
18595
|
+
checkpointId,
|
|
18596
|
+
commit: commitHash,
|
|
18597
|
+
head: headInfo.head,
|
|
18598
|
+
branch: headInfo.branch,
|
|
18599
|
+
indexTree: indexTree.trim(),
|
|
18600
|
+
worktreeTree: worktreeTree.trim(),
|
|
18601
|
+
timestamp
|
|
18795
18602
|
};
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18603
|
+
}
|
|
18604
|
+
};
|
|
18605
|
+
async function getHeadInfo(git) {
|
|
18606
|
+
let head = null;
|
|
18607
|
+
let branch = null;
|
|
18608
|
+
try {
|
|
18609
|
+
head = (await git.revparse(["HEAD"]))?.trim() || null;
|
|
18610
|
+
} catch {
|
|
18611
|
+
head = null;
|
|
18612
|
+
}
|
|
18613
|
+
try {
|
|
18614
|
+
const rawBranch = await git.raw(["symbolic-ref", "--short", "HEAD"]);
|
|
18615
|
+
branch = rawBranch.trim() || null;
|
|
18616
|
+
} catch {
|
|
18617
|
+
branch = null;
|
|
18618
|
+
}
|
|
18619
|
+
return { head, branch };
|
|
18620
|
+
}
|
|
18621
|
+
async function hasUnmergedEntries(git) {
|
|
18622
|
+
const output = await git.raw(["ls-files", "--unmerged"]);
|
|
18623
|
+
return output.trim().length > 0;
|
|
18624
|
+
}
|
|
18625
|
+
async function getGitBusyState(git) {
|
|
18626
|
+
const toplevel = (await git.raw(["rev-parse", "--show-toplevel"])).trim();
|
|
18627
|
+
const resolveGitPath = async (gitPath) => {
|
|
18628
|
+
const relative = (await git.raw(["rev-parse", "--git-path", gitPath])).trim();
|
|
18629
|
+
return path12.isAbsolute(relative) ? relative : path12.resolve(toplevel, relative);
|
|
18630
|
+
};
|
|
18631
|
+
const pathExists = async (gitPath) => {
|
|
18632
|
+
const resolved = await resolveGitPath(gitPath);
|
|
18633
|
+
try {
|
|
18634
|
+
await fs10.access(resolved);
|
|
18635
|
+
return true;
|
|
18636
|
+
} catch {
|
|
18637
|
+
return false;
|
|
18799
18638
|
}
|
|
18800
|
-
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18639
|
+
};
|
|
18640
|
+
const dirExists = async (gitPath) => {
|
|
18641
|
+
const resolved = await resolveGitPath(gitPath);
|
|
18642
|
+
try {
|
|
18643
|
+
const stat4 = await fs10.stat(resolved);
|
|
18644
|
+
return stat4.isDirectory();
|
|
18645
|
+
} catch {
|
|
18646
|
+
return false;
|
|
18647
|
+
}
|
|
18648
|
+
};
|
|
18649
|
+
if (await dirExists("rebase-merge") || await dirExists("rebase-apply")) {
|
|
18650
|
+
return { busy: true, operation: "rebase" };
|
|
18651
|
+
}
|
|
18652
|
+
if (await pathExists("MERGE_HEAD")) {
|
|
18653
|
+
return { busy: true, operation: "merge" };
|
|
18654
|
+
}
|
|
18655
|
+
if (await pathExists("CHERRY_PICK_HEAD")) {
|
|
18656
|
+
return { busy: true, operation: "cherry-pick" };
|
|
18657
|
+
}
|
|
18658
|
+
if (await pathExists("REVERT_HEAD")) {
|
|
18659
|
+
return { busy: true, operation: "revert" };
|
|
18660
|
+
}
|
|
18661
|
+
return { busy: false };
|
|
18662
|
+
}
|
|
18663
|
+
async function createWorktreeTree(git, baseDir, head) {
|
|
18664
|
+
const { tempGit, tempIndexPath } = await createTempIndexGit(git, baseDir, "checkpoint-worktree");
|
|
18665
|
+
try {
|
|
18666
|
+
if (head) {
|
|
18667
|
+
await tempGit.raw(["read-tree", head]);
|
|
18668
|
+
} else {
|
|
18669
|
+
await tempGit.raw(["read-tree", "--empty"]);
|
|
18670
|
+
}
|
|
18671
|
+
await tempGit.raw(["add", "-A", "--", "."]);
|
|
18672
|
+
const treeHash = await tempGit.raw(["write-tree"]);
|
|
18673
|
+
return treeHash.trim();
|
|
18674
|
+
} finally {
|
|
18675
|
+
await fs10.rm(tempIndexPath, { force: true }).catch(() => {
|
|
18804
18676
|
});
|
|
18805
|
-
return { snapshot, archivePath: createdArchivePath, changed: true };
|
|
18806
18677
|
}
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18678
|
+
}
|
|
18679
|
+
async function createMetaTree(git, baseDir, indexTree, worktreeTree) {
|
|
18680
|
+
const { tempGit, tempIndexPath } = await createTempIndexGit(git, baseDir, "checkpoint-meta");
|
|
18681
|
+
try {
|
|
18682
|
+
await tempGit.raw(["read-tree", "--empty"]);
|
|
18683
|
+
await tempGit.raw([
|
|
18684
|
+
"update-index",
|
|
18685
|
+
"--add",
|
|
18686
|
+
"--cacheinfo",
|
|
18687
|
+
"040000",
|
|
18688
|
+
indexTree,
|
|
18689
|
+
"index"
|
|
18690
|
+
]);
|
|
18691
|
+
await tempGit.raw([
|
|
18692
|
+
"update-index",
|
|
18693
|
+
"--add",
|
|
18694
|
+
"--cacheinfo",
|
|
18695
|
+
"040000",
|
|
18696
|
+
worktreeTree,
|
|
18697
|
+
"worktree"
|
|
18698
|
+
]);
|
|
18699
|
+
const metaTree = await tempGit.raw(["write-tree"]);
|
|
18700
|
+
return metaTree.trim();
|
|
18701
|
+
} finally {
|
|
18702
|
+
await fs10.rm(tempIndexPath, { force: true }).catch(() => {
|
|
18703
|
+
});
|
|
18704
|
+
}
|
|
18705
|
+
}
|
|
18706
|
+
function formatCheckpointMessage(meta) {
|
|
18707
|
+
return [
|
|
18708
|
+
`POSTHOG-CODE-CHECKPOINT ${CHECKPOINT_VERSION}`,
|
|
18709
|
+
`head=${meta.head ?? "null"}`,
|
|
18710
|
+
`branch=${meta.branch ?? "null"}`,
|
|
18711
|
+
`index=${meta.indexTree}`,
|
|
18712
|
+
`worktree=${meta.worktreeTree}`,
|
|
18713
|
+
`timestamp=${meta.timestamp}`
|
|
18714
|
+
].join("\n");
|
|
18715
|
+
}
|
|
18716
|
+
async function getGitCommonDir(git, baseDir) {
|
|
18717
|
+
const raw = await git.raw(["rev-parse", "--git-common-dir"]);
|
|
18718
|
+
const dir = raw.trim() || ".git";
|
|
18719
|
+
return path12.isAbsolute(dir) ? dir : path12.resolve(baseDir, dir);
|
|
18720
|
+
}
|
|
18721
|
+
async function createTempIndexGit(git, baseDir, label) {
|
|
18722
|
+
const tmpDir = path12.join(await getGitCommonDir(git, baseDir), "posthog-code-tmp");
|
|
18723
|
+
await fs10.mkdir(tmpDir, { recursive: true });
|
|
18724
|
+
const tempIndexPath = path12.join(tmpDir, `${label}-${Date.now()}-${(0, import_node_crypto2.randomUUID)()}`);
|
|
18725
|
+
const tempGit = createGitClient(baseDir).env({
|
|
18726
|
+
...process.env,
|
|
18727
|
+
GIT_INDEX_FILE: tempIndexPath
|
|
18728
|
+
});
|
|
18729
|
+
return { tempGit, tempIndexPath };
|
|
18730
|
+
}
|
|
18731
|
+
async function refExists(git, refName) {
|
|
18732
|
+
try {
|
|
18733
|
+
await git.revparse(["--verify", refName]);
|
|
18734
|
+
return true;
|
|
18735
|
+
} catch {
|
|
18736
|
+
return false;
|
|
18737
|
+
}
|
|
18738
|
+
}
|
|
18739
|
+
async function deleteCheckpoint(git, checkpointId) {
|
|
18740
|
+
const refName = `${CHECKPOINT_REF_PREFIX}${checkpointId}`;
|
|
18741
|
+
const exists2 = await refExists(git, refName);
|
|
18742
|
+
if (!exists2) {
|
|
18743
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
18744
|
+
}
|
|
18745
|
+
await git.raw(["update-ref", "-d", refName]);
|
|
18746
|
+
}
|
|
18747
|
+
|
|
18748
|
+
// ../git/dist/handoff.js
|
|
18749
|
+
var HANDOFF_HEAD_REF_PREFIX = "refs/posthog-code-handoff/head/";
|
|
18750
|
+
var CHECKPOINT_REF_PREFIX2 = "refs/posthog-code-checkpoint/";
|
|
18751
|
+
var GitHandoffTracker = class {
|
|
18752
|
+
repositoryPath;
|
|
18753
|
+
logger;
|
|
18754
|
+
constructor(config) {
|
|
18755
|
+
this.repositoryPath = config.repositoryPath;
|
|
18756
|
+
this.logger = config.logger;
|
|
18757
|
+
}
|
|
18758
|
+
async captureForHandoff(localGitState) {
|
|
18759
|
+
const captureSaga = new CaptureCheckpointSaga(this.logger);
|
|
18760
|
+
const result = await captureSaga.run({ baseDir: this.repositoryPath });
|
|
18761
|
+
if (!result.success) {
|
|
18762
|
+
throw new Error(`Failed to capture checkpoint at step '${result.failedStep}': ${result.error}`);
|
|
18763
|
+
}
|
|
18764
|
+
const checkpoint = result.data;
|
|
18765
|
+
const git = createGitClient(this.repositoryPath);
|
|
18766
|
+
const tempDir = await this.getTempDir(git);
|
|
18767
|
+
const checkpointRef = `${CHECKPOINT_REF_PREFIX2}${checkpoint.checkpointId}`;
|
|
18768
|
+
const shouldIncludeHead = !!checkpoint.head && checkpoint.head !== localGitState?.head;
|
|
18769
|
+
const headRef = shouldIncludeHead ? `${HANDOFF_HEAD_REF_PREFIX}${checkpoint.checkpointId}` : void 0;
|
|
18770
|
+
const packPrefix = import_node_path5.default.join(tempDir, checkpoint.checkpointId);
|
|
18771
|
+
try {
|
|
18772
|
+
const [headPack, indexFile, tracking] = await Promise.all([
|
|
18773
|
+
shouldIncludeHead && checkpoint.head ? this.captureHeadPack(packPrefix, checkpoint.head) : Promise.resolve(void 0),
|
|
18774
|
+
this.copyIndexFile(git, checkpoint.checkpointId),
|
|
18775
|
+
getTrackingMetadata(git, checkpoint.branch)
|
|
18776
|
+
]);
|
|
18777
|
+
return {
|
|
18778
|
+
checkpoint: {
|
|
18779
|
+
checkpointId: checkpoint.checkpointId,
|
|
18780
|
+
commit: checkpoint.commit,
|
|
18781
|
+
checkpointRef,
|
|
18782
|
+
headRef,
|
|
18783
|
+
head: checkpoint.head,
|
|
18784
|
+
branch: checkpoint.branch,
|
|
18785
|
+
indexTree: checkpoint.indexTree,
|
|
18786
|
+
worktreeTree: checkpoint.worktreeTree,
|
|
18787
|
+
timestamp: checkpoint.timestamp,
|
|
18788
|
+
upstreamRemote: tracking.upstreamRemote,
|
|
18789
|
+
upstreamMergeRef: tracking.upstreamMergeRef,
|
|
18790
|
+
remoteUrl: tracking.remoteUrl
|
|
18791
|
+
},
|
|
18792
|
+
headPack,
|
|
18793
|
+
indexFile,
|
|
18794
|
+
totalBytes: (headPack?.rawBytes ?? 0) + indexFile.rawBytes
|
|
18795
|
+
};
|
|
18796
|
+
} finally {
|
|
18797
|
+
await deleteCheckpoint(git, checkpoint.checkpointId).catch(() => {
|
|
18798
|
+
});
|
|
18811
18799
|
}
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18800
|
+
}
|
|
18801
|
+
async applyFromHandoff(input) {
|
|
18802
|
+
const { checkpoint, headPackPath, indexPath, localGitState, onDivergedBranch } = input;
|
|
18803
|
+
const git = createGitClient(this.repositoryPath);
|
|
18804
|
+
if (headPackPath) {
|
|
18805
|
+
await this.unpackPackFile(headPackPath);
|
|
18815
18806
|
}
|
|
18816
|
-
|
|
18817
|
-
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
|
|
18822
|
-
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
},
|
|
18827
|
-
rollback: async () => {
|
|
18828
|
-
await fs11.rm(archivePath, { force: true }).catch(() => {
|
|
18829
|
-
});
|
|
18807
|
+
if (checkpoint.branch && checkpoint.head) {
|
|
18808
|
+
const branchStatus2 = await this.resolveBranchRestoreStatus(git, checkpoint.branch, checkpoint.head, localGitState);
|
|
18809
|
+
const tracking = this.getPreferredTracking(localGitState, checkpoint);
|
|
18810
|
+
if (branchStatus2.kind === "diverged" && !await onDivergedBranch?.(branchStatus2.divergence)) {
|
|
18811
|
+
throw new Error(`Handoff aborted: local branch '${checkpoint.branch}' has diverged`);
|
|
18812
|
+
}
|
|
18813
|
+
await this.checkoutBranchAtHead(git, checkpoint.branch, checkpoint.head);
|
|
18814
|
+
if (this.shouldRestoreTracking(branchStatus2, localGitState, tracking)) {
|
|
18815
|
+
await this.ensureRemoteForTracking(git, tracking);
|
|
18816
|
+
await this.configureUpstream(git, checkpoint.branch, tracking);
|
|
18830
18817
|
}
|
|
18818
|
+
} else if (checkpoint.head) {
|
|
18819
|
+
await git.checkout(checkpoint.head);
|
|
18820
|
+
}
|
|
18821
|
+
if (indexPath) {
|
|
18822
|
+
await this.restoreIndexFile(git, indexPath);
|
|
18823
|
+
}
|
|
18824
|
+
const packBytes = headPackPath ? await this.getFileSize(headPackPath) : 0;
|
|
18825
|
+
const indexBytes = indexPath ? await this.getFileSize(indexPath) : 0;
|
|
18826
|
+
return {
|
|
18827
|
+
packBytes,
|
|
18828
|
+
indexBytes,
|
|
18829
|
+
totalBytes: packBytes + indexBytes
|
|
18830
|
+
};
|
|
18831
|
+
}
|
|
18832
|
+
async captureHeadPack(packPrefix, headCommit) {
|
|
18833
|
+
const hash = await this.runGitWithInput(["pack-objects", packPrefix, "--revs"], `${headCommit}
|
|
18834
|
+
`);
|
|
18835
|
+
const packPath = `${packPrefix}-${hash.trim()}.pack`;
|
|
18836
|
+
const rawBytes = await this.getFileSize(packPath);
|
|
18837
|
+
await (0, import_promises2.rm)(`${packPath}.idx`, { force: true }).catch(() => {
|
|
18831
18838
|
});
|
|
18832
|
-
return
|
|
18839
|
+
return { path: packPath, rawBytes };
|
|
18833
18840
|
}
|
|
18834
|
-
async
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18841
|
+
async copyIndexFile(git, checkpointId) {
|
|
18842
|
+
const indexPath = await this.getGitPath(git, "index");
|
|
18843
|
+
const tempDir = await this.getTempDir(git);
|
|
18844
|
+
const copiedIndexPath = import_node_path5.default.join(tempDir, `${checkpointId}.index`);
|
|
18845
|
+
await (0, import_promises2.copyFile)(indexPath, copiedIndexPath);
|
|
18846
|
+
return {
|
|
18847
|
+
path: copiedIndexPath,
|
|
18848
|
+
rawBytes: await this.getFileSize(copiedIndexPath)
|
|
18849
|
+
};
|
|
18850
|
+
}
|
|
18851
|
+
async restoreIndexFile(git, indexPath) {
|
|
18852
|
+
const gitIndexPath = await this.getGitPath(git, "index");
|
|
18853
|
+
await (0, import_promises2.copyFile)(indexPath, gitIndexPath);
|
|
18854
|
+
}
|
|
18855
|
+
async unpackPackFile(packPath) {
|
|
18856
|
+
const content = await (0, import_promises2.readFile)(packPath);
|
|
18857
|
+
await this.runGitWithBuffer(["unpack-objects", "-r"], content);
|
|
18858
|
+
}
|
|
18859
|
+
getPreferredTracking(localGitState, checkpoint) {
|
|
18860
|
+
const state = localGitState;
|
|
18861
|
+
if (state && hasTrackingConfig(state)) {
|
|
18862
|
+
return {
|
|
18863
|
+
upstreamRemote: state.upstreamRemote ?? null,
|
|
18864
|
+
upstreamMergeRef: state.upstreamMergeRef ?? null,
|
|
18865
|
+
remoteUrl: state.upstreamRemote && state.upstreamRemote === checkpoint.upstreamRemote ? checkpoint.remoteUrl : null
|
|
18866
|
+
};
|
|
18838
18867
|
}
|
|
18839
|
-
|
|
18840
|
-
|
|
18841
|
-
|
|
18842
|
-
|
|
18843
|
-
|
|
18844
|
-
|
|
18845
|
-
|
|
18846
|
-
|
|
18847
|
-
|
|
18848
|
-
|
|
18849
|
-
|
|
18850
|
-
|
|
18851
|
-
|
|
18852
|
-
|
|
18853
|
-
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
18857
|
-
|
|
18858
|
-
|
|
18859
|
-
|
|
18860
|
-
|
|
18861
|
-
|
|
18868
|
+
return {
|
|
18869
|
+
upstreamRemote: checkpoint.upstreamRemote,
|
|
18870
|
+
upstreamMergeRef: checkpoint.upstreamMergeRef,
|
|
18871
|
+
remoteUrl: checkpoint.remoteUrl
|
|
18872
|
+
};
|
|
18873
|
+
}
|
|
18874
|
+
shouldRestoreTracking(branchStatus2, localGitState, tracking) {
|
|
18875
|
+
return branchStatus2.kind === "missing" || !hasTrackingConfig(localGitState) && (tracking.upstreamRemote !== null || tracking.upstreamMergeRef !== null);
|
|
18876
|
+
}
|
|
18877
|
+
async ensureRemoteForTracking(git, tracking) {
|
|
18878
|
+
if (!tracking.upstreamRemote || !tracking.remoteUrl)
|
|
18879
|
+
return;
|
|
18880
|
+
const remotes = await git.getRemotes(true);
|
|
18881
|
+
const existing = remotes.find((remote) => remote.name === tracking.upstreamRemote);
|
|
18882
|
+
if (!existing) {
|
|
18883
|
+
await git.addRemote(tracking.upstreamRemote, tracking.remoteUrl);
|
|
18884
|
+
}
|
|
18885
|
+
}
|
|
18886
|
+
async configureUpstream(git, branch, tracking) {
|
|
18887
|
+
if (tracking.upstreamRemote) {
|
|
18888
|
+
await git.raw([
|
|
18889
|
+
"config",
|
|
18890
|
+
`branch.${branch}.remote`,
|
|
18891
|
+
tracking.upstreamRemote
|
|
18892
|
+
]);
|
|
18893
|
+
}
|
|
18894
|
+
if (tracking.upstreamMergeRef) {
|
|
18895
|
+
await git.raw([
|
|
18896
|
+
"config",
|
|
18897
|
+
`branch.${branch}.merge`,
|
|
18898
|
+
tracking.upstreamMergeRef
|
|
18899
|
+
]);
|
|
18900
|
+
}
|
|
18901
|
+
}
|
|
18902
|
+
async resolveBranchRestoreStatus(git, branch, cloudHead, localGitState) {
|
|
18903
|
+
const branchRef = `refs/heads/${branch}`;
|
|
18904
|
+
const branchExists = await this.refExists(git, branchRef);
|
|
18905
|
+
if (!branchExists) {
|
|
18906
|
+
return { kind: "missing" };
|
|
18907
|
+
}
|
|
18908
|
+
const currentBranchHead = (await git.revparse([branchRef])).trim();
|
|
18909
|
+
const candidateHeads = [
|
|
18910
|
+
currentBranchHead,
|
|
18911
|
+
...localGitState?.branch === branch && localGitState.head ? [localGitState.head] : []
|
|
18912
|
+
].filter((value, index, array) => array.indexOf(value) === index);
|
|
18913
|
+
if (candidateHeads.every((head) => head === cloudHead)) {
|
|
18914
|
+
return { kind: "match" };
|
|
18915
|
+
}
|
|
18916
|
+
const nonAncestorHead = await this.findNonAncestorHead(git, candidateHeads, cloudHead);
|
|
18917
|
+
if (!nonAncestorHead) {
|
|
18918
|
+
return { kind: "fast_forward" };
|
|
18862
18919
|
}
|
|
18863
|
-
return
|
|
18920
|
+
return {
|
|
18921
|
+
kind: "diverged",
|
|
18922
|
+
divergence: {
|
|
18923
|
+
branch,
|
|
18924
|
+
localHead: nonAncestorHead,
|
|
18925
|
+
cloudHead
|
|
18926
|
+
}
|
|
18927
|
+
};
|
|
18864
18928
|
}
|
|
18865
|
-
|
|
18866
|
-
|
|
18867
|
-
|
|
18868
|
-
|
|
18869
|
-
originalBranch = null;
|
|
18870
|
-
extractedFiles = [];
|
|
18871
|
-
fileBackups = /* @__PURE__ */ new Map();
|
|
18872
|
-
async executeGitOperations(input) {
|
|
18873
|
-
const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
|
|
18874
|
-
const headInfo = await this.readOnlyStep("get_current_head", async () => {
|
|
18875
|
-
let head = null;
|
|
18876
|
-
let branch = null;
|
|
18877
|
-
try {
|
|
18878
|
-
head = await this.git.revparse(["HEAD"]);
|
|
18879
|
-
} catch {
|
|
18880
|
-
head = null;
|
|
18929
|
+
async findNonAncestorHead(_git, heads, cloudHead) {
|
|
18930
|
+
for (const head of heads) {
|
|
18931
|
+
if (head === cloudHead) {
|
|
18932
|
+
continue;
|
|
18881
18933
|
}
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
} catch {
|
|
18885
|
-
branch = null;
|
|
18934
|
+
if (!await this.isAncestor(head, cloudHead)) {
|
|
18935
|
+
return head;
|
|
18886
18936
|
}
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
if (
|
|
18893
|
-
await
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18937
|
+
}
|
|
18938
|
+
return null;
|
|
18939
|
+
}
|
|
18940
|
+
async checkoutBranchAtHead(git, branch, head) {
|
|
18941
|
+
const currentBranch = await getCurrentBranchName(git);
|
|
18942
|
+
if (currentBranch === branch) {
|
|
18943
|
+
await git.reset(["--hard", head]);
|
|
18944
|
+
return;
|
|
18945
|
+
}
|
|
18946
|
+
const branchRef = `refs/heads/${branch}`;
|
|
18947
|
+
if (await this.refExists(git, branchRef)) {
|
|
18948
|
+
await git.branch(["-f", branch, head]);
|
|
18949
|
+
await git.checkout(branch);
|
|
18950
|
+
return;
|
|
18951
|
+
}
|
|
18952
|
+
await git.checkout(["-b", branch, head]);
|
|
18953
|
+
}
|
|
18954
|
+
async refExists(git, ref) {
|
|
18955
|
+
try {
|
|
18956
|
+
await git.revparse(["--verify", ref]);
|
|
18957
|
+
return true;
|
|
18958
|
+
} catch {
|
|
18959
|
+
return false;
|
|
18960
|
+
}
|
|
18961
|
+
}
|
|
18962
|
+
async isAncestor(ancestor, descendant) {
|
|
18963
|
+
const exitCode = await this.runGitProcessAllowingFailure([
|
|
18964
|
+
"merge-base",
|
|
18965
|
+
"--is-ancestor",
|
|
18966
|
+
ancestor,
|
|
18967
|
+
descendant
|
|
18968
|
+
]);
|
|
18969
|
+
return exitCode === 0;
|
|
18970
|
+
}
|
|
18971
|
+
async getTempDir(git) {
|
|
18972
|
+
const raw = await git.raw(["rev-parse", "--git-common-dir"]);
|
|
18973
|
+
const commonDir = raw.trim() || ".git";
|
|
18974
|
+
const resolved = import_node_path5.default.isAbsolute(commonDir) ? commonDir : import_node_path5.default.resolve(this.repositoryPath, commonDir);
|
|
18975
|
+
const tempDir = import_node_path5.default.join(resolved, "posthog-code-tmp");
|
|
18976
|
+
await (0, import_promises2.mkdir)(tempDir, { recursive: true });
|
|
18977
|
+
return tempDir;
|
|
18978
|
+
}
|
|
18979
|
+
async getGitPath(git, gitPath) {
|
|
18980
|
+
const raw = await git.raw(["rev-parse", "--git-path", gitPath]);
|
|
18981
|
+
const resolved = raw.trim();
|
|
18982
|
+
return import_node_path5.default.isAbsolute(resolved) ? resolved : import_node_path5.default.resolve(this.repositoryPath, resolved);
|
|
18983
|
+
}
|
|
18984
|
+
async getFileSize(filePath) {
|
|
18985
|
+
return (await (0, import_promises2.stat)(filePath)).size;
|
|
18986
|
+
}
|
|
18987
|
+
async runGitWithInput(args2, input) {
|
|
18988
|
+
const { stdout } = await this.runGitProcess(args2, input);
|
|
18989
|
+
return stdout;
|
|
18990
|
+
}
|
|
18991
|
+
async runGitWithBuffer(args2, input) {
|
|
18992
|
+
await this.runGitProcess(args2, input);
|
|
18993
|
+
}
|
|
18994
|
+
async runGitProcessAllowingFailure(args2) {
|
|
18995
|
+
return new Promise((resolve7, reject) => {
|
|
18996
|
+
const child = (0, import_node_child_process4.spawn)("git", args2, {
|
|
18997
|
+
cwd: this.repositoryPath,
|
|
18998
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
18899
18999
|
});
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
if (this.originalBranch) {
|
|
18914
|
-
await this.git.checkout(this.originalBranch);
|
|
18915
|
-
} else if (this.originalHead) {
|
|
18916
|
-
await this.git.checkout(this.originalHead);
|
|
18917
|
-
}
|
|
18918
|
-
} catch (error) {
|
|
18919
|
-
this.log.warn("Failed to rollback checkout", { error });
|
|
18920
|
-
}
|
|
19000
|
+
let stderr = "";
|
|
19001
|
+
child.stderr.on("data", (chunk) => {
|
|
19002
|
+
stderr += chunk.toString();
|
|
19003
|
+
});
|
|
19004
|
+
child.on("error", reject);
|
|
19005
|
+
child.on("close", (code) => {
|
|
19006
|
+
if (code === null) {
|
|
19007
|
+
reject(new Error(`git ${args2.join(" ")} exited unexpectedly`));
|
|
19008
|
+
return;
|
|
19009
|
+
}
|
|
19010
|
+
if (code > 1) {
|
|
19011
|
+
reject(new Error(stderr || `git ${args2.join(" ")} failed with code ${code}`));
|
|
19012
|
+
return;
|
|
18921
19013
|
}
|
|
19014
|
+
resolve7(code);
|
|
18922
19015
|
});
|
|
18923
|
-
}
|
|
18924
|
-
|
|
18925
|
-
|
|
18926
|
-
|
|
18927
|
-
|
|
18928
|
-
|
|
18929
|
-
|
|
18930
|
-
const content = await fs11.readFile(fullPath);
|
|
18931
|
-
this.fileBackups.set(filePath, content);
|
|
18932
|
-
} catch {
|
|
18933
|
-
}
|
|
18934
|
-
}
|
|
19016
|
+
});
|
|
19017
|
+
}
|
|
19018
|
+
runGitProcess(args2, input) {
|
|
19019
|
+
return new Promise((resolve7, reject) => {
|
|
19020
|
+
const child = (0, import_node_child_process4.spawn)("git", args2, {
|
|
19021
|
+
cwd: this.repositoryPath,
|
|
19022
|
+
stdio: "pipe"
|
|
18935
19023
|
});
|
|
18936
|
-
|
|
18937
|
-
|
|
18938
|
-
|
|
18939
|
-
|
|
18940
|
-
file: archivePath,
|
|
18941
|
-
cwd: baseDir
|
|
18942
|
-
});
|
|
18943
|
-
this.extractedFiles = filesToExtract;
|
|
18944
|
-
},
|
|
18945
|
-
rollback: async () => {
|
|
18946
|
-
for (const filePath of this.extractedFiles) {
|
|
18947
|
-
const fullPath = path13.join(baseDir, filePath);
|
|
18948
|
-
const backup = this.fileBackups.get(filePath);
|
|
18949
|
-
if (backup) {
|
|
18950
|
-
const dir = path13.dirname(fullPath);
|
|
18951
|
-
await fs11.mkdir(dir, { recursive: true }).catch(() => {
|
|
18952
|
-
});
|
|
18953
|
-
await fs11.writeFile(fullPath, backup).catch(() => {
|
|
18954
|
-
});
|
|
18955
|
-
} else {
|
|
18956
|
-
await fs11.rm(fullPath, { force: true }).catch(() => {
|
|
18957
|
-
});
|
|
18958
|
-
}
|
|
18959
|
-
}
|
|
18960
|
-
}
|
|
19024
|
+
let stdout = "";
|
|
19025
|
+
let stderr = "";
|
|
19026
|
+
child.stdout.on("data", (chunk) => {
|
|
19027
|
+
stdout += chunk.toString();
|
|
18961
19028
|
});
|
|
18962
|
-
|
|
18963
|
-
|
|
18964
|
-
const fullPath = path13.join(baseDir, change.path);
|
|
18965
|
-
const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
|
|
18966
|
-
try {
|
|
18967
|
-
return await fs11.readFile(fullPath);
|
|
18968
|
-
} catch {
|
|
18969
|
-
return null;
|
|
18970
|
-
}
|
|
19029
|
+
child.stderr.on("data", (chunk) => {
|
|
19030
|
+
stderr += chunk.toString();
|
|
18971
19031
|
});
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
|
|
18976
|
-
|
|
18977
|
-
},
|
|
18978
|
-
rollback: async () => {
|
|
18979
|
-
if (backupContent) {
|
|
18980
|
-
const dir = path13.dirname(fullPath);
|
|
18981
|
-
await fs11.mkdir(dir, { recursive: true }).catch(() => {
|
|
18982
|
-
});
|
|
18983
|
-
await fs11.writeFile(fullPath, backupContent).catch(() => {
|
|
18984
|
-
});
|
|
18985
|
-
}
|
|
19032
|
+
child.on("error", reject);
|
|
19033
|
+
child.on("close", (code) => {
|
|
19034
|
+
if (code === 0) {
|
|
19035
|
+
resolve7({ stdout, stderr });
|
|
19036
|
+
return;
|
|
18986
19037
|
}
|
|
19038
|
+
reject(new Error(stderr || `git ${args2.join(" ")} failed with code ${code}`));
|
|
18987
19039
|
});
|
|
18988
|
-
|
|
18989
|
-
const deletedCount = changes.filter((c) => c.status === "D").length;
|
|
18990
|
-
this.log.info("Tree applied", {
|
|
18991
|
-
treeHash,
|
|
18992
|
-
totalChanges: changes.length,
|
|
18993
|
-
deletedFiles: deletedCount,
|
|
18994
|
-
checkoutPerformed
|
|
19040
|
+
child.stdin.end(input);
|
|
18995
19041
|
});
|
|
18996
|
-
return { treeHash, checkoutPerformed };
|
|
18997
19042
|
}
|
|
18998
19043
|
};
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
const tmpDir = (0, import_node_path5.join)(repositoryPath, ".posthog", "tmp");
|
|
19007
|
-
if (!snapshot.archiveUrl) {
|
|
19008
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19009
|
-
}
|
|
19010
|
-
const archiveUrl = snapshot.archiveUrl;
|
|
19011
|
-
await this.step({
|
|
19012
|
-
name: "create_tmp_dir",
|
|
19013
|
-
execute: () => (0, import_promises2.mkdir)(tmpDir, { recursive: true }),
|
|
19014
|
-
rollback: async () => {
|
|
19015
|
-
}
|
|
19016
|
-
});
|
|
19017
|
-
const archivePath = (0, import_node_path5.join)(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
19018
|
-
this.archivePath = archivePath;
|
|
19019
|
-
await this.step({
|
|
19020
|
-
name: "download_archive",
|
|
19021
|
-
execute: async () => {
|
|
19022
|
-
const arrayBuffer = await apiClient.downloadArtifact(
|
|
19023
|
-
taskId,
|
|
19024
|
-
runId,
|
|
19025
|
-
archiveUrl
|
|
19026
|
-
);
|
|
19027
|
-
if (!arrayBuffer) {
|
|
19028
|
-
throw new Error("Failed to download archive");
|
|
19029
|
-
}
|
|
19030
|
-
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19031
|
-
const binaryContent = Buffer.from(base64Content, "base64");
|
|
19032
|
-
await (0, import_promises2.writeFile)(archivePath, binaryContent);
|
|
19033
|
-
},
|
|
19034
|
-
rollback: async () => {
|
|
19035
|
-
if (this.archivePath) {
|
|
19036
|
-
await (0, import_promises2.rm)(this.archivePath, { force: true }).catch(() => {
|
|
19037
|
-
});
|
|
19038
|
-
}
|
|
19039
|
-
}
|
|
19040
|
-
});
|
|
19041
|
-
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
19042
|
-
const applyResult = await gitApplySaga.run({
|
|
19043
|
-
baseDir: repositoryPath,
|
|
19044
|
-
treeHash: snapshot.treeHash,
|
|
19045
|
-
baseCommit: snapshot.baseCommit,
|
|
19046
|
-
changes: snapshot.changes,
|
|
19047
|
-
archivePath: this.archivePath
|
|
19048
|
-
});
|
|
19049
|
-
if (!applyResult.success) {
|
|
19050
|
-
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
19051
|
-
}
|
|
19052
|
-
await (0, import_promises2.rm)(this.archivePath, { force: true }).catch(() => {
|
|
19053
|
-
});
|
|
19054
|
-
this.log.info("Tree snapshot applied", {
|
|
19055
|
-
treeHash: snapshot.treeHash,
|
|
19056
|
-
totalChanges: snapshot.changes.length,
|
|
19057
|
-
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
19058
|
-
});
|
|
19059
|
-
return { treeHash: snapshot.treeHash };
|
|
19044
|
+
async function getCurrentBranchName(git) {
|
|
19045
|
+
try {
|
|
19046
|
+
const raw = await git.revparse(["--abbrev-ref", "HEAD"]);
|
|
19047
|
+
const branch = raw.trim();
|
|
19048
|
+
return branch === "HEAD" ? null : branch;
|
|
19049
|
+
} catch {
|
|
19050
|
+
return null;
|
|
19060
19051
|
}
|
|
19061
|
-
}
|
|
19062
|
-
|
|
19063
|
-
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
sagaName = "CaptureTreeSaga";
|
|
19069
|
-
async execute(input) {
|
|
19070
|
-
const {
|
|
19071
|
-
repositoryPath,
|
|
19072
|
-
lastTreeHash,
|
|
19073
|
-
interrupted,
|
|
19074
|
-
apiClient,
|
|
19075
|
-
taskId,
|
|
19076
|
-
runId
|
|
19077
|
-
} = input;
|
|
19078
|
-
const tmpDir = (0, import_node_path6.join)(repositoryPath, ".posthog", "tmp");
|
|
19079
|
-
if ((0, import_node_fs4.existsSync)((0, import_node_path6.join)(repositoryPath, ".gitmodules"))) {
|
|
19080
|
-
this.log.warn(
|
|
19081
|
-
"Repository has submodules - snapshot may not capture submodule state"
|
|
19082
|
-
);
|
|
19083
|
-
}
|
|
19084
|
-
const shouldArchive = !!apiClient;
|
|
19085
|
-
const archivePath = shouldArchive ? (0, import_node_path6.join)(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
19086
|
-
const gitCaptureSaga = new CaptureTreeSaga(this.log);
|
|
19087
|
-
const captureResult = await gitCaptureSaga.run({
|
|
19088
|
-
baseDir: repositoryPath,
|
|
19089
|
-
lastTreeHash,
|
|
19090
|
-
archivePath
|
|
19091
|
-
});
|
|
19092
|
-
if (!captureResult.success) {
|
|
19093
|
-
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
19094
|
-
}
|
|
19095
|
-
const {
|
|
19096
|
-
snapshot: gitSnapshot,
|
|
19097
|
-
archivePath: createdArchivePath,
|
|
19098
|
-
changed
|
|
19099
|
-
} = captureResult.data;
|
|
19100
|
-
if (!changed || !gitSnapshot) {
|
|
19101
|
-
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
19102
|
-
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
19103
|
-
}
|
|
19104
|
-
let archiveUrl;
|
|
19105
|
-
if (apiClient && createdArchivePath) {
|
|
19106
|
-
try {
|
|
19107
|
-
archiveUrl = await this.uploadArchive(
|
|
19108
|
-
createdArchivePath,
|
|
19109
|
-
gitSnapshot.treeHash,
|
|
19110
|
-
apiClient,
|
|
19111
|
-
taskId,
|
|
19112
|
-
runId
|
|
19113
|
-
);
|
|
19114
|
-
} finally {
|
|
19115
|
-
await (0, import_promises3.rm)(createdArchivePath, { force: true }).catch(() => {
|
|
19116
|
-
});
|
|
19117
|
-
}
|
|
19118
|
-
}
|
|
19119
|
-
const snapshot = {
|
|
19120
|
-
treeHash: gitSnapshot.treeHash,
|
|
19121
|
-
baseCommit: gitSnapshot.baseCommit,
|
|
19122
|
-
changes: gitSnapshot.changes,
|
|
19123
|
-
timestamp: gitSnapshot.timestamp,
|
|
19124
|
-
interrupted,
|
|
19125
|
-
archiveUrl
|
|
19052
|
+
}
|
|
19053
|
+
async function getTrackingMetadata(git, branch) {
|
|
19054
|
+
if (!branch) {
|
|
19055
|
+
return {
|
|
19056
|
+
upstreamRemote: null,
|
|
19057
|
+
upstreamMergeRef: null,
|
|
19058
|
+
remoteUrl: null
|
|
19126
19059
|
};
|
|
19127
|
-
this.log.info("Tree captured", {
|
|
19128
|
-
treeHash: snapshot.treeHash,
|
|
19129
|
-
changes: snapshot.changes.length,
|
|
19130
|
-
interrupted,
|
|
19131
|
-
archiveUrl
|
|
19132
|
-
});
|
|
19133
|
-
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
19134
19060
|
}
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19146
|
-
content_type: "application/gzip"
|
|
19147
|
-
}
|
|
19148
|
-
]);
|
|
19149
|
-
if (artifacts.length > 0 && artifacts[0].storage_path) {
|
|
19150
|
-
this.log.info("Tree archive uploaded", {
|
|
19151
|
-
storagePath: artifacts[0].storage_path,
|
|
19152
|
-
treeHash
|
|
19153
|
-
});
|
|
19154
|
-
return artifacts[0].storage_path;
|
|
19155
|
-
}
|
|
19156
|
-
return void 0;
|
|
19157
|
-
},
|
|
19158
|
-
rollback: async () => {
|
|
19159
|
-
await (0, import_promises3.rm)(archivePath, { force: true }).catch(() => {
|
|
19160
|
-
});
|
|
19161
|
-
}
|
|
19162
|
-
});
|
|
19163
|
-
return archiveUrl;
|
|
19061
|
+
const upstreamRemote = await getGitConfigValue(git, `branch.${branch}.remote`);
|
|
19062
|
+
const upstreamMergeRef = await getGitConfigValue(git, `branch.${branch}.merge`);
|
|
19063
|
+
const remoteUrl = upstreamRemote ? await getRemoteUrl(git, upstreamRemote) : null;
|
|
19064
|
+
return { upstreamRemote, upstreamMergeRef, remoteUrl };
|
|
19065
|
+
}
|
|
19066
|
+
async function getGitConfigValue(git, key) {
|
|
19067
|
+
try {
|
|
19068
|
+
const value = await git.raw(["config", "--get", key]);
|
|
19069
|
+
return value.trim() || null;
|
|
19070
|
+
} catch {
|
|
19071
|
+
return null;
|
|
19164
19072
|
}
|
|
19165
|
-
}
|
|
19073
|
+
}
|
|
19074
|
+
async function getRemoteUrl(git, remote) {
|
|
19075
|
+
try {
|
|
19076
|
+
const value = await git.remote(["get-url", remote]);
|
|
19077
|
+
return typeof value === "string" ? value.trim() || null : null;
|
|
19078
|
+
} catch {
|
|
19079
|
+
return null;
|
|
19080
|
+
}
|
|
19081
|
+
}
|
|
19082
|
+
function hasTrackingConfig(localGitState) {
|
|
19083
|
+
return !!(localGitState?.upstreamRemote || localGitState?.upstreamMergeRef);
|
|
19084
|
+
}
|
|
19166
19085
|
|
|
19167
|
-
// src/
|
|
19168
|
-
var
|
|
19086
|
+
// src/handoff-checkpoint.ts
|
|
19087
|
+
var HandoffCheckpointTracker = class {
|
|
19169
19088
|
repositoryPath;
|
|
19170
19089
|
taskId;
|
|
19171
19090
|
runId;
|
|
19172
19091
|
apiClient;
|
|
19173
19092
|
logger;
|
|
19174
|
-
lastTreeHash = null;
|
|
19175
19093
|
constructor(config) {
|
|
19176
19094
|
this.repositoryPath = config.repositoryPath;
|
|
19177
19095
|
this.taskId = config.taskId;
|
|
19178
19096
|
this.runId = config.runId;
|
|
19179
19097
|
this.apiClient = config.apiClient;
|
|
19180
|
-
this.logger = config.logger || new Logger({ debug: false, prefix: "[
|
|
19098
|
+
this.logger = config.logger || new Logger({ debug: false, prefix: "[HandoffCheckpointTracker]" });
|
|
19181
19099
|
}
|
|
19182
|
-
|
|
19183
|
-
|
|
19184
|
-
* Uses a temporary index to avoid modifying user's staging area.
|
|
19185
|
-
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
19186
|
-
*/
|
|
19187
|
-
async captureTree(options) {
|
|
19188
|
-
const saga = new CaptureTreeSaga2(this.logger);
|
|
19189
|
-
const result = await saga.run({
|
|
19190
|
-
repositoryPath: this.repositoryPath,
|
|
19191
|
-
taskId: this.taskId,
|
|
19192
|
-
runId: this.runId,
|
|
19193
|
-
apiClient: this.apiClient,
|
|
19194
|
-
lastTreeHash: this.lastTreeHash,
|
|
19195
|
-
interrupted: options?.interrupted
|
|
19196
|
-
});
|
|
19197
|
-
if (!result.success) {
|
|
19198
|
-
this.logger.error("Failed to capture tree", {
|
|
19199
|
-
error: result.error,
|
|
19200
|
-
failedStep: result.failedStep
|
|
19201
|
-
});
|
|
19100
|
+
async captureForHandoff(localGitState) {
|
|
19101
|
+
if (!this.apiClient) {
|
|
19202
19102
|
throw new Error(
|
|
19203
|
-
|
|
19103
|
+
"Cannot capture handoff checkpoint: API client not configured"
|
|
19204
19104
|
);
|
|
19205
19105
|
}
|
|
19206
|
-
|
|
19207
|
-
|
|
19106
|
+
const gitTracker = this.createGitTracker();
|
|
19107
|
+
const capture = await gitTracker.captureForHandoff(localGitState);
|
|
19108
|
+
try {
|
|
19109
|
+
const uploads = await this.uploadArtifacts([
|
|
19110
|
+
{
|
|
19111
|
+
key: "pack",
|
|
19112
|
+
filePath: capture.headPack?.path,
|
|
19113
|
+
name: `handoff/${capture.checkpoint.checkpointId}.pack`,
|
|
19114
|
+
contentType: "application/x-git-packed-objects"
|
|
19115
|
+
},
|
|
19116
|
+
{
|
|
19117
|
+
key: "index",
|
|
19118
|
+
filePath: capture.indexFile.path,
|
|
19119
|
+
name: `handoff/${capture.checkpoint.checkpointId}.index`,
|
|
19120
|
+
contentType: "application/octet-stream"
|
|
19121
|
+
}
|
|
19122
|
+
]);
|
|
19123
|
+
this.logCaptureMetrics(capture.checkpoint, uploads);
|
|
19124
|
+
return {
|
|
19125
|
+
...capture.checkpoint,
|
|
19126
|
+
artifactPath: uploads.pack?.storagePath,
|
|
19127
|
+
indexArtifactPath: uploads.index?.storagePath
|
|
19128
|
+
};
|
|
19129
|
+
} finally {
|
|
19130
|
+
await this.removeIfPresent(capture.headPack?.path);
|
|
19131
|
+
await this.removeIfPresent(capture.indexFile.path);
|
|
19208
19132
|
}
|
|
19209
|
-
return result.data.snapshot;
|
|
19210
19133
|
}
|
|
19211
|
-
|
|
19212
|
-
* Download and apply a tree snapshot.
|
|
19213
|
-
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
19214
|
-
*/
|
|
19215
|
-
async applyTreeSnapshot(snapshot) {
|
|
19134
|
+
async applyFromHandoff(checkpoint, options) {
|
|
19216
19135
|
if (!this.apiClient) {
|
|
19217
|
-
throw new Error("Cannot apply snapshot: API client not configured");
|
|
19218
|
-
}
|
|
19219
|
-
if (!snapshot.archiveUrl) {
|
|
19220
|
-
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
19221
|
-
treeHash: snapshot.treeHash,
|
|
19222
|
-
changes: snapshot.changes.length
|
|
19223
|
-
});
|
|
19224
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19225
|
-
}
|
|
19226
|
-
const saga = new ApplySnapshotSaga(this.logger);
|
|
19227
|
-
const result = await saga.run({
|
|
19228
|
-
snapshot,
|
|
19229
|
-
repositoryPath: this.repositoryPath,
|
|
19230
|
-
apiClient: this.apiClient,
|
|
19231
|
-
taskId: this.taskId,
|
|
19232
|
-
runId: this.runId
|
|
19233
|
-
});
|
|
19234
|
-
if (!result.success) {
|
|
19235
|
-
this.logger.error("Failed to apply tree snapshot", {
|
|
19236
|
-
error: result.error,
|
|
19237
|
-
failedStep: result.failedStep,
|
|
19238
|
-
treeHash: snapshot.treeHash
|
|
19239
|
-
});
|
|
19240
19136
|
throw new Error(
|
|
19241
|
-
|
|
19137
|
+
"Cannot apply handoff checkpoint: API client not configured"
|
|
19242
19138
|
);
|
|
19243
19139
|
}
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
|
|
19248
|
-
|
|
19249
|
-
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
this.lastTreeHash = hash;
|
|
19257
|
-
}
|
|
19258
|
-
};
|
|
19259
|
-
|
|
19260
|
-
// src/sagas/resume-saga.ts
|
|
19261
|
-
var ResumeSaga = class extends Saga {
|
|
19262
|
-
sagaName = "ResumeSaga";
|
|
19263
|
-
async execute(input) {
|
|
19264
|
-
const { taskId, runId, repositoryPath, apiClient } = input;
|
|
19265
|
-
const logger = input.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
19266
|
-
const taskRun = await this.readOnlyStep(
|
|
19267
|
-
"fetch_task_run",
|
|
19268
|
-
() => apiClient.getTaskRun(taskId, runId)
|
|
19269
|
-
);
|
|
19270
|
-
if (!taskRun.log_url) {
|
|
19271
|
-
this.log.info("No log URL found, starting fresh");
|
|
19272
|
-
return this.emptyResult();
|
|
19273
|
-
}
|
|
19274
|
-
const entries = await this.readOnlyStep(
|
|
19275
|
-
"fetch_logs",
|
|
19276
|
-
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
19277
|
-
);
|
|
19278
|
-
if (entries.length === 0) {
|
|
19279
|
-
this.log.info("No log entries found, starting fresh");
|
|
19280
|
-
return this.emptyResult();
|
|
19281
|
-
}
|
|
19282
|
-
this.log.info("Fetched log entries", { count: entries.length });
|
|
19283
|
-
const latestSnapshot = await this.readOnlyStep(
|
|
19284
|
-
"find_snapshot",
|
|
19285
|
-
() => Promise.resolve(this.findLatestTreeSnapshot(entries))
|
|
19286
|
-
);
|
|
19287
|
-
let snapshotApplied = false;
|
|
19288
|
-
if (latestSnapshot?.archiveUrl && repositoryPath) {
|
|
19289
|
-
this.log.info("Found tree snapshot", {
|
|
19290
|
-
treeHash: latestSnapshot.treeHash,
|
|
19291
|
-
hasArchiveUrl: true,
|
|
19292
|
-
changes: latestSnapshot.changes?.length ?? 0,
|
|
19293
|
-
interrupted: latestSnapshot.interrupted
|
|
19294
|
-
});
|
|
19295
|
-
await this.step({
|
|
19296
|
-
name: "apply_snapshot",
|
|
19297
|
-
execute: async () => {
|
|
19298
|
-
const treeTracker = new TreeTracker({
|
|
19299
|
-
repositoryPath,
|
|
19300
|
-
taskId,
|
|
19301
|
-
runId,
|
|
19302
|
-
apiClient,
|
|
19303
|
-
logger: logger.child("TreeTracker")
|
|
19304
|
-
});
|
|
19305
|
-
try {
|
|
19306
|
-
await treeTracker.applyTreeSnapshot(latestSnapshot);
|
|
19307
|
-
treeTracker.setLastTreeHash(latestSnapshot.treeHash);
|
|
19308
|
-
snapshotApplied = true;
|
|
19309
|
-
this.log.info("Tree snapshot applied successfully", {
|
|
19310
|
-
treeHash: latestSnapshot.treeHash
|
|
19311
|
-
});
|
|
19312
|
-
} catch (error) {
|
|
19313
|
-
this.log.warn(
|
|
19314
|
-
"Failed to apply tree snapshot, continuing without it",
|
|
19315
|
-
{
|
|
19316
|
-
error: error instanceof Error ? error.message : String(error),
|
|
19317
|
-
treeHash: latestSnapshot.treeHash
|
|
19318
|
-
}
|
|
19319
|
-
);
|
|
19320
|
-
}
|
|
19140
|
+
const gitTracker = this.createGitTracker();
|
|
19141
|
+
const tmpDir = (0, import_node_path6.join)(this.repositoryPath, ".posthog", "tmp");
|
|
19142
|
+
await (0, import_promises3.mkdir)(tmpDir, { recursive: true });
|
|
19143
|
+
const packPath = (0, import_node_path6.join)(tmpDir, `${checkpoint.checkpointId}.pack`);
|
|
19144
|
+
const indexPath = (0, import_node_path6.join)(tmpDir, `${checkpoint.checkpointId}.index`);
|
|
19145
|
+
try {
|
|
19146
|
+
const downloads = await this.downloadArtifacts([
|
|
19147
|
+
{
|
|
19148
|
+
key: "pack",
|
|
19149
|
+
storagePath: checkpoint.artifactPath,
|
|
19150
|
+
filePath: packPath,
|
|
19151
|
+
label: "handoff pack"
|
|
19321
19152
|
},
|
|
19322
|
-
rollback: async () => {
|
|
19323
|
-
}
|
|
19324
|
-
});
|
|
19325
|
-
} else if (latestSnapshot?.archiveUrl && !repositoryPath) {
|
|
19326
|
-
this.log.warn(
|
|
19327
|
-
"Snapshot found but no repositoryPath configured - files cannot be restored",
|
|
19328
19153
|
{
|
|
19329
|
-
|
|
19330
|
-
|
|
19154
|
+
key: "index",
|
|
19155
|
+
storagePath: checkpoint.indexArtifactPath,
|
|
19156
|
+
filePath: indexPath,
|
|
19157
|
+
label: "handoff index"
|
|
19331
19158
|
}
|
|
19332
|
-
);
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19159
|
+
]);
|
|
19160
|
+
const applyResult = await gitTracker.applyFromHandoff({
|
|
19161
|
+
checkpoint: this.toGitCheckpoint(checkpoint),
|
|
19162
|
+
headPackPath: downloads.pack?.filePath,
|
|
19163
|
+
indexPath: downloads.index?.filePath,
|
|
19164
|
+
localGitState: options?.localGitState,
|
|
19165
|
+
onDivergedBranch: options?.onDivergedBranch
|
|
19166
|
+
});
|
|
19167
|
+
this.logApplyMetrics(checkpoint, downloads, applyResult.totalBytes);
|
|
19168
|
+
return {
|
|
19169
|
+
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
19170
|
+
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
19171
|
+
totalBytes: applyResult.totalBytes
|
|
19172
|
+
};
|
|
19173
|
+
} finally {
|
|
19174
|
+
await this.removeIfPresent(packPath);
|
|
19175
|
+
await this.removeIfPresent(indexPath);
|
|
19176
|
+
}
|
|
19177
|
+
}
|
|
19178
|
+
toGitCheckpoint(checkpoint) {
|
|
19179
|
+
return {
|
|
19180
|
+
checkpointId: checkpoint.checkpointId,
|
|
19181
|
+
commit: checkpoint.commit,
|
|
19182
|
+
checkpointRef: checkpoint.checkpointRef,
|
|
19183
|
+
headRef: checkpoint.headRef,
|
|
19184
|
+
head: checkpoint.head,
|
|
19185
|
+
branch: checkpoint.branch,
|
|
19186
|
+
indexTree: checkpoint.indexTree,
|
|
19187
|
+
worktreeTree: checkpoint.worktreeTree,
|
|
19188
|
+
timestamp: checkpoint.timestamp,
|
|
19189
|
+
upstreamRemote: checkpoint.upstreamRemote ?? null,
|
|
19190
|
+
upstreamMergeRef: checkpoint.upstreamMergeRef ?? null,
|
|
19191
|
+
remoteUrl: checkpoint.remoteUrl ?? null
|
|
19192
|
+
};
|
|
19193
|
+
}
|
|
19194
|
+
async uploadArtifactFile(filePath, name2, contentType) {
|
|
19195
|
+
if (!this.apiClient) {
|
|
19196
|
+
return { rawBytes: 0, wireBytes: 0 };
|
|
19197
|
+
}
|
|
19198
|
+
const content = await (0, import_promises3.readFile)(filePath);
|
|
19199
|
+
const base64Content = content.toString("base64");
|
|
19200
|
+
const artifacts = await this.apiClient.uploadTaskArtifacts(
|
|
19201
|
+
this.taskId,
|
|
19202
|
+
this.runId,
|
|
19203
|
+
[
|
|
19336
19204
|
{
|
|
19337
|
-
|
|
19338
|
-
|
|
19205
|
+
name: name2,
|
|
19206
|
+
type: "artifact",
|
|
19207
|
+
content: base64Content,
|
|
19208
|
+
content_type: contentType
|
|
19339
19209
|
}
|
|
19340
|
-
|
|
19341
|
-
}
|
|
19342
|
-
const conversation = await this.readOnlyStep(
|
|
19343
|
-
"rebuild_conversation",
|
|
19344
|
-
() => Promise.resolve(this.rebuildConversation(entries))
|
|
19345
|
-
);
|
|
19346
|
-
const lastDevice = await this.readOnlyStep(
|
|
19347
|
-
"find_device",
|
|
19348
|
-
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
19210
|
+
]
|
|
19349
19211
|
);
|
|
19350
|
-
this.log.info("Resume state rebuilt", {
|
|
19351
|
-
turns: conversation.length,
|
|
19352
|
-
hasSnapshot: !!latestSnapshot,
|
|
19353
|
-
snapshotApplied,
|
|
19354
|
-
interrupted: latestSnapshot?.interrupted ?? false
|
|
19355
|
-
});
|
|
19356
19212
|
return {
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
interrupted: latestSnapshot?.interrupted ?? false,
|
|
19361
|
-
lastDevice,
|
|
19362
|
-
logEntryCount: entries.length
|
|
19213
|
+
storagePath: artifacts.at(-1)?.storage_path,
|
|
19214
|
+
rawBytes: content.byteLength,
|
|
19215
|
+
wireBytes: Buffer.byteLength(base64Content, "utf-8")
|
|
19363
19216
|
};
|
|
19364
19217
|
}
|
|
19365
|
-
|
|
19218
|
+
async uploadArtifacts(specs) {
|
|
19219
|
+
const results = [];
|
|
19220
|
+
for (const spec of specs) {
|
|
19221
|
+
if (!spec.filePath) {
|
|
19222
|
+
results.push([spec.key, void 0]);
|
|
19223
|
+
continue;
|
|
19224
|
+
}
|
|
19225
|
+
results.push([
|
|
19226
|
+
spec.key,
|
|
19227
|
+
await this.uploadArtifactFile(
|
|
19228
|
+
spec.filePath,
|
|
19229
|
+
spec.name,
|
|
19230
|
+
spec.contentType
|
|
19231
|
+
)
|
|
19232
|
+
]);
|
|
19233
|
+
}
|
|
19234
|
+
return Object.fromEntries(results);
|
|
19235
|
+
}
|
|
19236
|
+
async downloadArtifactToFile(artifactPath, filePath, label) {
|
|
19237
|
+
if (!this.apiClient) {
|
|
19238
|
+
throw new Error(`Cannot download ${label}: API client not configured`);
|
|
19239
|
+
}
|
|
19240
|
+
const arrayBuffer = await this.apiClient.downloadArtifact(
|
|
19241
|
+
this.taskId,
|
|
19242
|
+
this.runId,
|
|
19243
|
+
artifactPath
|
|
19244
|
+
);
|
|
19245
|
+
if (!arrayBuffer) {
|
|
19246
|
+
throw new Error(`Failed to download ${label} from ${artifactPath}`);
|
|
19247
|
+
}
|
|
19248
|
+
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
19249
|
+
const binaryContent = Buffer.from(base64Content, "base64");
|
|
19250
|
+
await (0, import_promises3.writeFile)(filePath, binaryContent);
|
|
19366
19251
|
return {
|
|
19367
|
-
|
|
19368
|
-
|
|
19369
|
-
|
|
19370
|
-
interrupted: false,
|
|
19371
|
-
logEntryCount: 0
|
|
19252
|
+
filePath,
|
|
19253
|
+
rawBytes: binaryContent.byteLength,
|
|
19254
|
+
wireBytes: arrayBuffer.byteLength
|
|
19372
19255
|
};
|
|
19373
19256
|
}
|
|
19374
|
-
|
|
19375
|
-
|
|
19376
|
-
|
|
19377
|
-
|
|
19378
|
-
|
|
19379
|
-
POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
19380
|
-
)) {
|
|
19381
|
-
const params = entry.notification.params;
|
|
19382
|
-
if (params?.treeHash) {
|
|
19383
|
-
return params;
|
|
19257
|
+
async downloadArtifacts(specs) {
|
|
19258
|
+
const downloads = await Promise.all(
|
|
19259
|
+
specs.map(async (spec) => {
|
|
19260
|
+
if (!spec.storagePath) {
|
|
19261
|
+
return [spec.key, void 0];
|
|
19384
19262
|
}
|
|
19385
|
-
|
|
19263
|
+
return [
|
|
19264
|
+
spec.key,
|
|
19265
|
+
await this.downloadArtifactToFile(
|
|
19266
|
+
spec.storagePath,
|
|
19267
|
+
spec.filePath,
|
|
19268
|
+
spec.label
|
|
19269
|
+
)
|
|
19270
|
+
];
|
|
19271
|
+
})
|
|
19272
|
+
);
|
|
19273
|
+
return Object.fromEntries(downloads);
|
|
19274
|
+
}
|
|
19275
|
+
createGitTracker() {
|
|
19276
|
+
return new GitHandoffTracker({
|
|
19277
|
+
repositoryPath: this.repositoryPath,
|
|
19278
|
+
logger: this.logger
|
|
19279
|
+
});
|
|
19280
|
+
}
|
|
19281
|
+
logCaptureMetrics(checkpoint, uploads) {
|
|
19282
|
+
this.logger.info("Captured handoff checkpoint", {
|
|
19283
|
+
checkpointId: checkpoint.checkpointId,
|
|
19284
|
+
branch: checkpoint.branch,
|
|
19285
|
+
head: checkpoint.head,
|
|
19286
|
+
artifactPath: uploads.pack?.storagePath,
|
|
19287
|
+
indexArtifactPath: uploads.index?.storagePath,
|
|
19288
|
+
...this.buildMetricPayload(uploads)
|
|
19289
|
+
});
|
|
19290
|
+
}
|
|
19291
|
+
logApplyMetrics(checkpoint, downloads, totalBytes) {
|
|
19292
|
+
this.logger.info("Applied handoff checkpoint", {
|
|
19293
|
+
checkpointId: checkpoint.checkpointId,
|
|
19294
|
+
commit: checkpoint.commit,
|
|
19295
|
+
branch: checkpoint.branch,
|
|
19296
|
+
head: checkpoint.head,
|
|
19297
|
+
packBytes: downloads.pack?.rawBytes ?? 0,
|
|
19298
|
+
packWireBytes: downloads.pack?.wireBytes ?? 0,
|
|
19299
|
+
indexBytes: downloads.index?.rawBytes ?? 0,
|
|
19300
|
+
indexWireBytes: downloads.index?.wireBytes ?? 0,
|
|
19301
|
+
totalBytes,
|
|
19302
|
+
totalWireBytes: this.sumWireBytes(downloads.pack, downloads.index)
|
|
19303
|
+
});
|
|
19304
|
+
}
|
|
19305
|
+
buildMetricPayload(metrics) {
|
|
19306
|
+
return {
|
|
19307
|
+
packBytes: metrics.pack?.rawBytes ?? 0,
|
|
19308
|
+
packWireBytes: metrics.pack?.wireBytes ?? 0,
|
|
19309
|
+
indexBytes: metrics.index?.rawBytes ?? 0,
|
|
19310
|
+
indexWireBytes: metrics.index?.wireBytes ?? 0,
|
|
19311
|
+
totalBytes: this.sumRawBytes(metrics.pack, metrics.index),
|
|
19312
|
+
totalWireBytes: this.sumWireBytes(metrics.pack, metrics.index)
|
|
19313
|
+
};
|
|
19314
|
+
}
|
|
19315
|
+
sumRawBytes(...artifacts) {
|
|
19316
|
+
return artifacts.reduce(
|
|
19317
|
+
(total, artifact) => total + (artifact?.rawBytes ?? 0),
|
|
19318
|
+
0
|
|
19319
|
+
);
|
|
19320
|
+
}
|
|
19321
|
+
sumWireBytes(...artifacts) {
|
|
19322
|
+
return artifacts.reduce(
|
|
19323
|
+
(total, artifact) => total + (artifact?.wireBytes ?? 0),
|
|
19324
|
+
0
|
|
19325
|
+
);
|
|
19326
|
+
}
|
|
19327
|
+
async removeIfPresent(filePath) {
|
|
19328
|
+
if (!filePath) {
|
|
19329
|
+
return;
|
|
19386
19330
|
}
|
|
19387
|
-
|
|
19331
|
+
await (0, import_promises3.rm)(filePath, { force: true }).catch(() => {
|
|
19332
|
+
});
|
|
19388
19333
|
}
|
|
19389
|
-
|
|
19390
|
-
|
|
19391
|
-
|
|
19392
|
-
|
|
19393
|
-
|
|
19394
|
-
|
|
19334
|
+
};
|
|
19335
|
+
|
|
19336
|
+
// src/utils/gateway.ts
|
|
19337
|
+
function getGatewayBaseUrl(posthogHost) {
|
|
19338
|
+
const url = new URL(posthogHost);
|
|
19339
|
+
const hostname = url.hostname;
|
|
19340
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
19341
|
+
return `${url.protocol}//localhost:3308`;
|
|
19342
|
+
}
|
|
19343
|
+
if (hostname === "host.docker.internal") {
|
|
19344
|
+
return `${url.protocol}//host.docker.internal:3308`;
|
|
19345
|
+
}
|
|
19346
|
+
const region = hostname.match(/^(us|eu)\.posthog\.com$/)?.[1] ?? "us";
|
|
19347
|
+
return `https://gateway.${region}.posthog.com`;
|
|
19348
|
+
}
|
|
19349
|
+
function getLlmGatewayUrl(posthogHost, product = "posthog_code") {
|
|
19350
|
+
return `${getGatewayBaseUrl(posthogHost)}/${product}`;
|
|
19351
|
+
}
|
|
19352
|
+
|
|
19353
|
+
// src/posthog-api.ts
|
|
19354
|
+
var DEFAULT_USER_AGENT = `posthog/agent.hog.dev; version: ${package_default.version}`;
|
|
19355
|
+
var PostHogAPIClient = class {
|
|
19356
|
+
config;
|
|
19357
|
+
constructor(config) {
|
|
19358
|
+
this.config = config;
|
|
19359
|
+
}
|
|
19360
|
+
get baseUrl() {
|
|
19361
|
+
const host = this.config.apiUrl.endsWith("/") ? this.config.apiUrl.slice(0, -1) : this.config.apiUrl;
|
|
19362
|
+
return host;
|
|
19363
|
+
}
|
|
19364
|
+
isAuthFailure(status) {
|
|
19365
|
+
return status === 401 || status === 403;
|
|
19366
|
+
}
|
|
19367
|
+
async resolveApiKey(forceRefresh = false) {
|
|
19368
|
+
if (forceRefresh && this.config.refreshApiKey) {
|
|
19369
|
+
return this.config.refreshApiKey();
|
|
19370
|
+
}
|
|
19371
|
+
return this.config.getApiKey();
|
|
19372
|
+
}
|
|
19373
|
+
async buildHeaders(options, forceRefresh = false) {
|
|
19374
|
+
const headers = new Headers(options.headers);
|
|
19375
|
+
headers.set(
|
|
19376
|
+
"Authorization",
|
|
19377
|
+
`Bearer ${await this.resolveApiKey(forceRefresh)}`
|
|
19378
|
+
);
|
|
19379
|
+
headers.set("Content-Type", "application/json");
|
|
19380
|
+
headers.set("User-Agent", this.config.userAgent ?? DEFAULT_USER_AGENT);
|
|
19381
|
+
return headers;
|
|
19382
|
+
}
|
|
19383
|
+
async performRequest(endpoint, options, forceRefresh = false) {
|
|
19384
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
19385
|
+
return fetch(url, {
|
|
19386
|
+
...options,
|
|
19387
|
+
headers: await this.buildHeaders(options, forceRefresh)
|
|
19388
|
+
});
|
|
19389
|
+
}
|
|
19390
|
+
async performRequestWithRetry(endpoint, options = {}) {
|
|
19391
|
+
let response = await this.performRequest(endpoint, options);
|
|
19392
|
+
if (!response.ok && this.isAuthFailure(response.status)) {
|
|
19393
|
+
response = await this.performRequest(endpoint, options, true);
|
|
19394
|
+
}
|
|
19395
|
+
return response;
|
|
19396
|
+
}
|
|
19397
|
+
async apiRequest(endpoint, options = {}) {
|
|
19398
|
+
const response = await this.performRequestWithRetry(endpoint, options);
|
|
19399
|
+
if (!response.ok) {
|
|
19400
|
+
let errorMessage;
|
|
19401
|
+
try {
|
|
19402
|
+
const errorResponse = await response.json();
|
|
19403
|
+
errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
|
|
19404
|
+
} catch {
|
|
19405
|
+
errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
|
|
19395
19406
|
}
|
|
19407
|
+
throw new Error(errorMessage);
|
|
19396
19408
|
}
|
|
19397
|
-
return
|
|
19409
|
+
return response.json();
|
|
19398
19410
|
}
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19411
|
+
getTeamId() {
|
|
19412
|
+
return this.config.projectId;
|
|
19413
|
+
}
|
|
19414
|
+
async getApiKey(forceRefresh = false) {
|
|
19415
|
+
return this.resolveApiKey(forceRefresh);
|
|
19416
|
+
}
|
|
19417
|
+
getLlmGatewayUrl() {
|
|
19418
|
+
return getLlmGatewayUrl(this.baseUrl);
|
|
19419
|
+
}
|
|
19420
|
+
async getTask(taskId) {
|
|
19421
|
+
const teamId = this.getTeamId();
|
|
19422
|
+
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
|
|
19423
|
+
}
|
|
19424
|
+
async getTaskRun(taskId, runId) {
|
|
19425
|
+
const teamId = this.getTeamId();
|
|
19426
|
+
return this.apiRequest(
|
|
19427
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
19428
|
+
);
|
|
19429
|
+
}
|
|
19430
|
+
async resumeRunInCloud(taskId, runId) {
|
|
19431
|
+
const teamId = this.getTeamId();
|
|
19432
|
+
return this.apiRequest(
|
|
19433
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/resume_in_cloud/`,
|
|
19434
|
+
{ method: "POST" }
|
|
19435
|
+
);
|
|
19436
|
+
}
|
|
19437
|
+
async updateTaskRun(taskId, runId, payload) {
|
|
19438
|
+
const teamId = this.getTeamId();
|
|
19439
|
+
return this.apiRequest(
|
|
19440
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
|
|
19441
|
+
{
|
|
19442
|
+
method: "PATCH",
|
|
19443
|
+
body: JSON.stringify(payload)
|
|
19444
|
+
}
|
|
19445
|
+
);
|
|
19446
|
+
}
|
|
19447
|
+
async setTaskRunOutput(taskId, runId, output) {
|
|
19448
|
+
return this.apiRequest(
|
|
19449
|
+
`/api/projects/${this.getTeamId()}/tasks/${taskId}/runs/${runId}/set_output/`,
|
|
19450
|
+
{
|
|
19451
|
+
method: "PATCH",
|
|
19452
|
+
body: JSON.stringify(output)
|
|
19453
|
+
}
|
|
19454
|
+
);
|
|
19455
|
+
}
|
|
19456
|
+
async appendTaskRunLog(taskId, runId, entries) {
|
|
19457
|
+
const teamId = this.getTeamId();
|
|
19458
|
+
return this.apiRequest(
|
|
19459
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
|
|
19460
|
+
{
|
|
19461
|
+
method: "POST",
|
|
19462
|
+
body: JSON.stringify({ entries })
|
|
19463
|
+
}
|
|
19464
|
+
);
|
|
19465
|
+
}
|
|
19466
|
+
async relayMessage(taskId, runId, text2) {
|
|
19467
|
+
const teamId = this.getTeamId();
|
|
19468
|
+
await this.apiRequest(
|
|
19469
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
|
|
19470
|
+
{
|
|
19471
|
+
method: "POST",
|
|
19472
|
+
body: JSON.stringify({ text: text2 })
|
|
19473
|
+
}
|
|
19474
|
+
);
|
|
19475
|
+
}
|
|
19476
|
+
async uploadTaskArtifacts(taskId, runId, artifacts) {
|
|
19477
|
+
if (!artifacts.length) {
|
|
19478
|
+
return [];
|
|
19479
|
+
}
|
|
19480
|
+
const teamId = this.getTeamId();
|
|
19481
|
+
const response = await this.apiRequest(
|
|
19482
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
|
|
19483
|
+
{
|
|
19484
|
+
method: "POST",
|
|
19485
|
+
body: JSON.stringify({ artifacts })
|
|
19486
|
+
}
|
|
19487
|
+
);
|
|
19488
|
+
const manifest = response.artifacts ?? [];
|
|
19489
|
+
return manifest.slice(-artifacts.length);
|
|
19490
|
+
}
|
|
19491
|
+
/**
|
|
19492
|
+
* Download artifact content by storage path
|
|
19493
|
+
* Streams the file through the PostHog backend so the sandbox does not need
|
|
19494
|
+
* direct access to object storage.
|
|
19495
|
+
*/
|
|
19496
|
+
async downloadArtifact(taskId, runId, storagePath) {
|
|
19497
|
+
const teamId = this.getTeamId();
|
|
19498
|
+
try {
|
|
19499
|
+
const response = await this.performRequestWithRetry(
|
|
19500
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
|
|
19501
|
+
{
|
|
19502
|
+
method: "POST",
|
|
19503
|
+
body: JSON.stringify({ storage_path: storagePath })
|
|
19504
|
+
}
|
|
19505
|
+
);
|
|
19506
|
+
if (!response.ok) {
|
|
19507
|
+
throw new Error(`Failed to download artifact: ${response.status}`);
|
|
19508
|
+
}
|
|
19509
|
+
return response.arrayBuffer();
|
|
19510
|
+
} catch {
|
|
19511
|
+
return null;
|
|
19512
|
+
}
|
|
19513
|
+
}
|
|
19514
|
+
/**
|
|
19515
|
+
* Fetch logs for a task run via the logs API endpoint
|
|
19516
|
+
* @param taskRun - The task run to fetch logs for
|
|
19517
|
+
* @returns Array of stored entries, or empty array if no logs available
|
|
19518
|
+
*/
|
|
19519
|
+
async fetchTaskRunLogs(taskRun) {
|
|
19520
|
+
const teamId = this.getTeamId();
|
|
19521
|
+
const endpoint = `/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`;
|
|
19522
|
+
try {
|
|
19523
|
+
const response = await this.performRequestWithRetry(endpoint);
|
|
19524
|
+
if (!response.ok) {
|
|
19525
|
+
if (response.status === 404) {
|
|
19526
|
+
return [];
|
|
19527
|
+
}
|
|
19528
|
+
throw new Error(
|
|
19529
|
+
`Failed to fetch logs: ${response.status} ${response.statusText}`
|
|
19530
|
+
);
|
|
19531
|
+
}
|
|
19532
|
+
const content = await response.text();
|
|
19533
|
+
if (!content.trim()) {
|
|
19534
|
+
return [];
|
|
19535
|
+
}
|
|
19536
|
+
return content.trim().split("\n").map((line) => JSON.parse(line));
|
|
19537
|
+
} catch (error) {
|
|
19538
|
+
throw new Error(
|
|
19539
|
+
`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`
|
|
19540
|
+
);
|
|
19541
|
+
}
|
|
19542
|
+
}
|
|
19543
|
+
};
|
|
19544
|
+
|
|
19545
|
+
// src/adapters/claude/session/jsonl-hydration.ts
|
|
19546
|
+
var import_node_crypto3 = require("crypto");
|
|
19547
|
+
var fs11 = __toESM(require("fs/promises"), 1);
|
|
19548
|
+
var os6 = __toESM(require("os"), 1);
|
|
19549
|
+
var path14 = __toESM(require("path"), 1);
|
|
19550
|
+
var CHARS_PER_TOKEN = 4;
|
|
19551
|
+
var DEFAULT_MAX_TOKENS = 15e4;
|
|
19552
|
+
function estimateTurnTokens(turn) {
|
|
19553
|
+
let chars = 0;
|
|
19554
|
+
for (const block of turn.content) {
|
|
19555
|
+
if ("text" in block && typeof block.text === "string") {
|
|
19556
|
+
chars += block.text.length;
|
|
19557
|
+
}
|
|
19558
|
+
}
|
|
19559
|
+
if (turn.toolCalls) {
|
|
19560
|
+
for (const tc of turn.toolCalls) {
|
|
19561
|
+
chars += JSON.stringify(tc.input ?? "").length;
|
|
19562
|
+
if (tc.result !== void 0) {
|
|
19563
|
+
chars += typeof tc.result === "string" ? tc.result.length : JSON.stringify(tc.result).length;
|
|
19564
|
+
}
|
|
19565
|
+
}
|
|
19566
|
+
}
|
|
19567
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
19568
|
+
}
|
|
19569
|
+
function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
|
|
19570
|
+
let budget = maxTokens;
|
|
19571
|
+
let startIndex = turns.length;
|
|
19572
|
+
for (let i2 = turns.length - 1; i2 >= 0; i2--) {
|
|
19573
|
+
const cost = estimateTurnTokens(turns[i2]);
|
|
19574
|
+
if (cost > budget) break;
|
|
19575
|
+
budget -= cost;
|
|
19576
|
+
startIndex = i2;
|
|
19577
|
+
}
|
|
19578
|
+
while (startIndex < turns.length && turns[startIndex].role !== "user") {
|
|
19579
|
+
startIndex++;
|
|
19580
|
+
}
|
|
19581
|
+
return turns.slice(startIndex);
|
|
19582
|
+
}
|
|
19583
|
+
|
|
19584
|
+
// src/sagas/resume-saga.ts
|
|
19585
|
+
var ResumeSaga = class extends Saga {
|
|
19586
|
+
sagaName = "ResumeSaga";
|
|
19587
|
+
async execute(input) {
|
|
19588
|
+
const { taskId, runId, apiClient } = input;
|
|
19589
|
+
const taskRun = await this.readOnlyStep(
|
|
19590
|
+
"fetch_task_run",
|
|
19591
|
+
() => apiClient.getTaskRun(taskId, runId)
|
|
19592
|
+
);
|
|
19593
|
+
if (!taskRun.log_url) {
|
|
19594
|
+
this.log.info("No log URL found, starting fresh");
|
|
19595
|
+
return this.emptyResult();
|
|
19596
|
+
}
|
|
19597
|
+
const entries = await this.readOnlyStep(
|
|
19598
|
+
"fetch_logs",
|
|
19599
|
+
() => apiClient.fetchTaskRunLogs(taskRun)
|
|
19600
|
+
);
|
|
19601
|
+
if (entries.length === 0) {
|
|
19602
|
+
this.log.info("No log entries found, starting fresh");
|
|
19603
|
+
return this.emptyResult();
|
|
19604
|
+
}
|
|
19605
|
+
this.log.info("Fetched log entries", { count: entries.length });
|
|
19606
|
+
const latestSnapshot = await this.readOnlyStep(
|
|
19607
|
+
"find_snapshot",
|
|
19608
|
+
() => Promise.resolve(this.findLatestTreeSnapshot(entries))
|
|
19609
|
+
);
|
|
19610
|
+
const latestGitCheckpoint = await this.readOnlyStep(
|
|
19611
|
+
"find_git_checkpoint",
|
|
19612
|
+
() => Promise.resolve(this.findLatestGitCheckpoint(entries))
|
|
19613
|
+
);
|
|
19614
|
+
if (latestSnapshot) {
|
|
19615
|
+
this.log.info("Found tree snapshot", {
|
|
19616
|
+
treeHash: latestSnapshot.treeHash,
|
|
19617
|
+
hasArchiveUrl: !!latestSnapshot.archiveUrl,
|
|
19618
|
+
changes: latestSnapshot.changes?.length ?? 0
|
|
19619
|
+
});
|
|
19620
|
+
}
|
|
19621
|
+
if (latestGitCheckpoint) {
|
|
19622
|
+
this.log.info("Found git checkpoint", {
|
|
19623
|
+
checkpointId: latestGitCheckpoint.checkpointId,
|
|
19624
|
+
branch: latestGitCheckpoint.branch
|
|
19625
|
+
});
|
|
19626
|
+
}
|
|
19627
|
+
const conversation = await this.readOnlyStep(
|
|
19628
|
+
"rebuild_conversation",
|
|
19629
|
+
() => Promise.resolve(this.rebuildConversation(entries))
|
|
19630
|
+
);
|
|
19631
|
+
const lastDevice = await this.readOnlyStep(
|
|
19632
|
+
"find_device",
|
|
19633
|
+
() => Promise.resolve(this.findLastDeviceInfo(entries))
|
|
19634
|
+
);
|
|
19635
|
+
this.log.info("Resume state rebuilt", {
|
|
19636
|
+
turns: conversation.length,
|
|
19637
|
+
hasSnapshot: !!latestSnapshot,
|
|
19638
|
+
hasGitCheckpoint: !!latestGitCheckpoint,
|
|
19639
|
+
interrupted: latestSnapshot?.interrupted ?? false
|
|
19640
|
+
});
|
|
19641
|
+
return {
|
|
19642
|
+
conversation,
|
|
19643
|
+
latestSnapshot,
|
|
19644
|
+
latestGitCheckpoint,
|
|
19645
|
+
interrupted: latestSnapshot?.interrupted ?? false,
|
|
19646
|
+
lastDevice,
|
|
19647
|
+
logEntryCount: entries.length
|
|
19648
|
+
};
|
|
19649
|
+
}
|
|
19650
|
+
emptyResult() {
|
|
19651
|
+
return {
|
|
19652
|
+
conversation: [],
|
|
19653
|
+
latestSnapshot: null,
|
|
19654
|
+
latestGitCheckpoint: null,
|
|
19655
|
+
interrupted: false,
|
|
19656
|
+
logEntryCount: 0
|
|
19657
|
+
};
|
|
19658
|
+
}
|
|
19659
|
+
findLatestTreeSnapshot(entries) {
|
|
19660
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19661
|
+
const entry = entries[i2];
|
|
19662
|
+
if (isNotification(
|
|
19663
|
+
entry.notification?.method,
|
|
19664
|
+
POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT
|
|
19665
|
+
)) {
|
|
19666
|
+
const params = entry.notification.params;
|
|
19667
|
+
if (params?.treeHash) {
|
|
19668
|
+
return params;
|
|
19669
|
+
}
|
|
19670
|
+
}
|
|
19671
|
+
}
|
|
19672
|
+
return null;
|
|
19673
|
+
}
|
|
19674
|
+
findLatestGitCheckpoint(entries) {
|
|
19675
|
+
const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
|
|
19676
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19677
|
+
const entry = entries[i2];
|
|
19678
|
+
const method = entry.notification?.method;
|
|
19679
|
+
if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
|
|
19680
|
+
const params = entry.notification?.params;
|
|
19681
|
+
if (params?.checkpointId && params?.checkpointRef) {
|
|
19682
|
+
return params;
|
|
19683
|
+
}
|
|
19684
|
+
}
|
|
19685
|
+
}
|
|
19686
|
+
return null;
|
|
19687
|
+
}
|
|
19688
|
+
findLastDeviceInfo(entries) {
|
|
19689
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
19690
|
+
const entry = entries[i2];
|
|
19691
|
+
const params = entry.notification?.params;
|
|
19692
|
+
if (params?.device) {
|
|
19693
|
+
return params.device;
|
|
19694
|
+
}
|
|
19695
|
+
}
|
|
19696
|
+
return void 0;
|
|
19697
|
+
}
|
|
19698
|
+
rebuildConversation(entries) {
|
|
19699
|
+
const turns = [];
|
|
19700
|
+
let currentAssistantContent = [];
|
|
19402
19701
|
let currentToolCalls = [];
|
|
19403
19702
|
for (const entry of entries) {
|
|
19404
19703
|
const method = entry.notification?.method;
|
|
@@ -19450,463 +19749,1012 @@ var ResumeSaga = class extends Saga {
|
|
|
19450
19749
|
}
|
|
19451
19750
|
break;
|
|
19452
19751
|
}
|
|
19453
|
-
case "tool_call":
|
|
19454
|
-
case "tool_call_update": {
|
|
19455
|
-
const meta = update._meta?.claudeCode;
|
|
19456
|
-
if (meta) {
|
|
19457
|
-
const toolCallId = meta.toolCallId;
|
|
19458
|
-
const toolName = meta.toolName;
|
|
19459
|
-
const toolInput = meta.toolInput;
|
|
19460
|
-
const toolResponse = meta.toolResponse;
|
|
19461
|
-
if (toolCallId && toolName) {
|
|
19462
|
-
let toolCall = currentToolCalls.find(
|
|
19463
|
-
(tc) => tc.toolCallId === toolCallId
|
|
19464
|
-
);
|
|
19465
|
-
if (!toolCall) {
|
|
19466
|
-
toolCall = {
|
|
19467
|
-
toolCallId,
|
|
19468
|
-
toolName,
|
|
19469
|
-
input: toolInput
|
|
19470
|
-
};
|
|
19471
|
-
currentToolCalls.push(toolCall);
|
|
19472
|
-
}
|
|
19473
|
-
if (toolResponse !== void 0) {
|
|
19474
|
-
toolCall.result = toolResponse;
|
|
19475
|
-
}
|
|
19476
|
-
}
|
|
19477
|
-
}
|
|
19478
|
-
break;
|
|
19752
|
+
case "tool_call":
|
|
19753
|
+
case "tool_call_update": {
|
|
19754
|
+
const meta = update._meta?.claudeCode;
|
|
19755
|
+
if (meta) {
|
|
19756
|
+
const toolCallId = meta.toolCallId;
|
|
19757
|
+
const toolName = meta.toolName;
|
|
19758
|
+
const toolInput = meta.toolInput;
|
|
19759
|
+
const toolResponse = meta.toolResponse;
|
|
19760
|
+
if (toolCallId && toolName) {
|
|
19761
|
+
let toolCall = currentToolCalls.find(
|
|
19762
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19763
|
+
);
|
|
19764
|
+
if (!toolCall) {
|
|
19765
|
+
toolCall = {
|
|
19766
|
+
toolCallId,
|
|
19767
|
+
toolName,
|
|
19768
|
+
input: toolInput
|
|
19769
|
+
};
|
|
19770
|
+
currentToolCalls.push(toolCall);
|
|
19771
|
+
}
|
|
19772
|
+
if (toolResponse !== void 0) {
|
|
19773
|
+
toolCall.result = toolResponse;
|
|
19774
|
+
}
|
|
19775
|
+
}
|
|
19776
|
+
}
|
|
19777
|
+
break;
|
|
19778
|
+
}
|
|
19779
|
+
case "tool_result": {
|
|
19780
|
+
const meta = update._meta?.claudeCode;
|
|
19781
|
+
if (meta) {
|
|
19782
|
+
const toolCallId = meta.toolCallId;
|
|
19783
|
+
const toolResponse = meta.toolResponse;
|
|
19784
|
+
if (toolCallId) {
|
|
19785
|
+
const toolCall = currentToolCalls.find(
|
|
19786
|
+
(tc) => tc.toolCallId === toolCallId
|
|
19787
|
+
);
|
|
19788
|
+
if (toolCall && toolResponse !== void 0) {
|
|
19789
|
+
toolCall.result = toolResponse;
|
|
19790
|
+
}
|
|
19791
|
+
}
|
|
19792
|
+
}
|
|
19793
|
+
break;
|
|
19794
|
+
}
|
|
19795
|
+
}
|
|
19796
|
+
}
|
|
19797
|
+
}
|
|
19798
|
+
if (currentAssistantContent.length > 0 || currentToolCalls.length > 0) {
|
|
19799
|
+
turns.push({
|
|
19800
|
+
role: "assistant",
|
|
19801
|
+
content: currentAssistantContent,
|
|
19802
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0
|
|
19803
|
+
});
|
|
19804
|
+
}
|
|
19805
|
+
return turns;
|
|
19806
|
+
}
|
|
19807
|
+
};
|
|
19808
|
+
|
|
19809
|
+
// src/resume.ts
|
|
19810
|
+
async function resumeFromLog(config) {
|
|
19811
|
+
const logger = config.logger || new Logger({ debug: false, prefix: "[Resume]" });
|
|
19812
|
+
logger.info("Resuming from log", {
|
|
19813
|
+
taskId: config.taskId,
|
|
19814
|
+
runId: config.runId
|
|
19815
|
+
});
|
|
19816
|
+
const saga = new ResumeSaga(logger);
|
|
19817
|
+
const result = await saga.run({
|
|
19818
|
+
taskId: config.taskId,
|
|
19819
|
+
runId: config.runId,
|
|
19820
|
+
repositoryPath: config.repositoryPath,
|
|
19821
|
+
apiClient: config.apiClient,
|
|
19822
|
+
logger
|
|
19823
|
+
});
|
|
19824
|
+
if (!result.success) {
|
|
19825
|
+
logger.error("Failed to resume from log", {
|
|
19826
|
+
error: result.error,
|
|
19827
|
+
failedStep: result.failedStep
|
|
19828
|
+
});
|
|
19829
|
+
throw new Error(
|
|
19830
|
+
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
19831
|
+
);
|
|
19832
|
+
}
|
|
19833
|
+
return {
|
|
19834
|
+
conversation: result.data.conversation,
|
|
19835
|
+
latestSnapshot: result.data.latestSnapshot,
|
|
19836
|
+
latestGitCheckpoint: result.data.latestGitCheckpoint,
|
|
19837
|
+
interrupted: result.data.interrupted,
|
|
19838
|
+
lastDevice: result.data.lastDevice,
|
|
19839
|
+
logEntryCount: result.data.logEntryCount
|
|
19840
|
+
};
|
|
19841
|
+
}
|
|
19842
|
+
var RESUME_HISTORY_TOKEN_BUDGET = 5e4;
|
|
19843
|
+
var TOOL_RESULT_MAX_CHARS = 2e3;
|
|
19844
|
+
var RESUME_CONTEXT_MARKERS = [
|
|
19845
|
+
"You are resuming a previous conversation",
|
|
19846
|
+
"Here is the conversation history from the",
|
|
19847
|
+
"Continue from where you left off"
|
|
19848
|
+
];
|
|
19849
|
+
function isResumeContextTurn(turn) {
|
|
19850
|
+
if (turn.role !== "user") return false;
|
|
19851
|
+
const text2 = turn.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
19852
|
+
return RESUME_CONTEXT_MARKERS.some((marker) => text2.includes(marker));
|
|
19853
|
+
}
|
|
19854
|
+
function formatConversationForResume(conversation) {
|
|
19855
|
+
const filtered = conversation.filter((turn) => !isResumeContextTurn(turn));
|
|
19856
|
+
const selected = selectRecentTurns(filtered, RESUME_HISTORY_TOKEN_BUDGET);
|
|
19857
|
+
const parts2 = [];
|
|
19858
|
+
if (selected.length < filtered.length) {
|
|
19859
|
+
parts2.push(
|
|
19860
|
+
`*(${filtered.length - selected.length} earlier turns omitted)*`
|
|
19861
|
+
);
|
|
19862
|
+
}
|
|
19863
|
+
for (const turn of selected) {
|
|
19864
|
+
const role = turn.role === "user" ? "User" : "Assistant";
|
|
19865
|
+
const textParts = turn.content.filter((block) => block.type === "text").map((block) => block.text);
|
|
19866
|
+
if (textParts.length > 0) {
|
|
19867
|
+
parts2.push(`**${role}**: ${textParts.join("\n")}`);
|
|
19868
|
+
}
|
|
19869
|
+
if (turn.toolCalls?.length) {
|
|
19870
|
+
const toolSummary = turn.toolCalls.map((tc) => {
|
|
19871
|
+
let resultStr = "";
|
|
19872
|
+
if (tc.result !== void 0) {
|
|
19873
|
+
const raw = typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result);
|
|
19874
|
+
resultStr = raw.length > TOOL_RESULT_MAX_CHARS ? ` \u2192 ${raw.substring(0, TOOL_RESULT_MAX_CHARS)}...(truncated)` : ` \u2192 ${raw}`;
|
|
19875
|
+
}
|
|
19876
|
+
return ` - ${tc.toolName}${resultStr}`;
|
|
19877
|
+
}).join("\n");
|
|
19878
|
+
parts2.push(`**${role} (tools)**:
|
|
19879
|
+
${toolSummary}`);
|
|
19880
|
+
}
|
|
19881
|
+
}
|
|
19882
|
+
return parts2.join("\n\n");
|
|
19883
|
+
}
|
|
19884
|
+
|
|
19885
|
+
// src/session-log-writer.ts
|
|
19886
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
19887
|
+
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
19888
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
19889
|
+
var SessionLogWriter = class _SessionLogWriter {
|
|
19890
|
+
static FLUSH_DEBOUNCE_MS = 500;
|
|
19891
|
+
static FLUSH_MAX_INTERVAL_MS = 5e3;
|
|
19892
|
+
static MAX_FLUSH_RETRIES = 10;
|
|
19893
|
+
static MAX_RETRY_DELAY_MS = 3e4;
|
|
19894
|
+
static SESSIONS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
19895
|
+
posthogAPI;
|
|
19896
|
+
pendingEntries = /* @__PURE__ */ new Map();
|
|
19897
|
+
flushTimeouts = /* @__PURE__ */ new Map();
|
|
19898
|
+
lastFlushAttemptTime = /* @__PURE__ */ new Map();
|
|
19899
|
+
retryCounts = /* @__PURE__ */ new Map();
|
|
19900
|
+
sessions = /* @__PURE__ */ new Map();
|
|
19901
|
+
flushQueues = /* @__PURE__ */ new Map();
|
|
19902
|
+
logger;
|
|
19903
|
+
localCachePath;
|
|
19904
|
+
constructor(options = {}) {
|
|
19905
|
+
this.posthogAPI = options.posthogAPI;
|
|
19906
|
+
this.localCachePath = options.localCachePath;
|
|
19907
|
+
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
19908
|
+
}
|
|
19909
|
+
async flushAll() {
|
|
19910
|
+
const flushPromises = [];
|
|
19911
|
+
for (const [sessionId, session] of this.sessions) {
|
|
19912
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19913
|
+
flushPromises.push(this.flush(sessionId));
|
|
19914
|
+
}
|
|
19915
|
+
await Promise.all(flushPromises);
|
|
19916
|
+
}
|
|
19917
|
+
register(sessionId, context) {
|
|
19918
|
+
if (this.sessions.has(sessionId)) {
|
|
19919
|
+
return;
|
|
19920
|
+
}
|
|
19921
|
+
this.sessions.set(sessionId, { context, currentTurnMessages: [] });
|
|
19922
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
19923
|
+
if (this.localCachePath) {
|
|
19924
|
+
const sessionDir = import_node_path7.default.join(
|
|
19925
|
+
this.localCachePath,
|
|
19926
|
+
"sessions",
|
|
19927
|
+
context.runId
|
|
19928
|
+
);
|
|
19929
|
+
try {
|
|
19930
|
+
import_node_fs3.default.mkdirSync(sessionDir, { recursive: true });
|
|
19931
|
+
} catch (error) {
|
|
19932
|
+
this.logger.warn("Failed to create local cache directory", {
|
|
19933
|
+
sessionDir,
|
|
19934
|
+
error
|
|
19935
|
+
});
|
|
19936
|
+
}
|
|
19937
|
+
}
|
|
19938
|
+
}
|
|
19939
|
+
isRegistered(sessionId) {
|
|
19940
|
+
return this.sessions.has(sessionId);
|
|
19941
|
+
}
|
|
19942
|
+
appendRawLine(sessionId, line) {
|
|
19943
|
+
const session = this.sessions.get(sessionId);
|
|
19944
|
+
if (!session) {
|
|
19945
|
+
this.logger.warn("appendRawLine called for unregistered session", {
|
|
19946
|
+
sessionId
|
|
19947
|
+
});
|
|
19948
|
+
return;
|
|
19949
|
+
}
|
|
19950
|
+
try {
|
|
19951
|
+
const message = JSON.parse(line);
|
|
19952
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19953
|
+
if (this.isAgentMessageChunk(message)) {
|
|
19954
|
+
const text2 = this.extractChunkText(message);
|
|
19955
|
+
if (text2) {
|
|
19956
|
+
if (!session.chunkBuffer) {
|
|
19957
|
+
session.chunkBuffer = { text: text2, firstTimestamp: timestamp };
|
|
19958
|
+
} else {
|
|
19959
|
+
session.chunkBuffer.text += text2;
|
|
19960
|
+
}
|
|
19961
|
+
}
|
|
19962
|
+
return;
|
|
19963
|
+
}
|
|
19964
|
+
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
19965
|
+
session.chunkBuffer = void 0;
|
|
19966
|
+
} else {
|
|
19967
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19968
|
+
}
|
|
19969
|
+
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
19970
|
+
if (nonChunkAgentText) {
|
|
19971
|
+
session.lastAgentMessage = nonChunkAgentText;
|
|
19972
|
+
session.currentTurnMessages.push(nonChunkAgentText);
|
|
19973
|
+
}
|
|
19974
|
+
const entry = {
|
|
19975
|
+
type: "notification",
|
|
19976
|
+
timestamp,
|
|
19977
|
+
notification: message
|
|
19978
|
+
};
|
|
19979
|
+
this.writeToLocalCache(sessionId, entry);
|
|
19980
|
+
if (this.posthogAPI) {
|
|
19981
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
19982
|
+
pending.push(entry);
|
|
19983
|
+
this.pendingEntries.set(sessionId, pending);
|
|
19984
|
+
this.scheduleFlush(sessionId);
|
|
19985
|
+
}
|
|
19986
|
+
} catch {
|
|
19987
|
+
this.logger.warn("Failed to parse raw line for persistence", {
|
|
19988
|
+
taskId: session.context.taskId,
|
|
19989
|
+
runId: session.context.runId,
|
|
19990
|
+
lineLength: line.length
|
|
19991
|
+
});
|
|
19992
|
+
}
|
|
19993
|
+
}
|
|
19994
|
+
async flush(sessionId, { coalesce = false } = {}) {
|
|
19995
|
+
if (coalesce) {
|
|
19996
|
+
const session = this.sessions.get(sessionId);
|
|
19997
|
+
if (session) {
|
|
19998
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
19999
|
+
}
|
|
20000
|
+
}
|
|
20001
|
+
const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
|
|
20002
|
+
const next = prev.catch(() => {
|
|
20003
|
+
}).then(() => this._doFlush(sessionId));
|
|
20004
|
+
this.flushQueues.set(sessionId, next);
|
|
20005
|
+
next.finally(() => {
|
|
20006
|
+
if (this.flushQueues.get(sessionId) === next) {
|
|
20007
|
+
this.flushQueues.delete(sessionId);
|
|
20008
|
+
}
|
|
20009
|
+
});
|
|
20010
|
+
return next;
|
|
20011
|
+
}
|
|
20012
|
+
async _doFlush(sessionId) {
|
|
20013
|
+
const session = this.sessions.get(sessionId);
|
|
20014
|
+
if (!session) {
|
|
20015
|
+
this.logger.warn("flush: no session found", { sessionId });
|
|
20016
|
+
return;
|
|
20017
|
+
}
|
|
20018
|
+
const pending = this.pendingEntries.get(sessionId);
|
|
20019
|
+
if (!this.posthogAPI || !pending?.length) {
|
|
20020
|
+
return;
|
|
20021
|
+
}
|
|
20022
|
+
this.pendingEntries.delete(sessionId);
|
|
20023
|
+
const timeout = this.flushTimeouts.get(sessionId);
|
|
20024
|
+
if (timeout) {
|
|
20025
|
+
clearTimeout(timeout);
|
|
20026
|
+
this.flushTimeouts.delete(sessionId);
|
|
20027
|
+
}
|
|
20028
|
+
this.lastFlushAttemptTime.set(sessionId, Date.now());
|
|
20029
|
+
try {
|
|
20030
|
+
await this.posthogAPI.appendTaskRunLog(
|
|
20031
|
+
session.context.taskId,
|
|
20032
|
+
session.context.runId,
|
|
20033
|
+
pending
|
|
20034
|
+
);
|
|
20035
|
+
this.retryCounts.set(sessionId, 0);
|
|
20036
|
+
} catch (error) {
|
|
20037
|
+
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
20038
|
+
this.retryCounts.set(sessionId, retryCount);
|
|
20039
|
+
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
20040
|
+
this.logger.error(
|
|
20041
|
+
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
20042
|
+
{
|
|
20043
|
+
taskId: session.context.taskId,
|
|
20044
|
+
runId: session.context.runId,
|
|
20045
|
+
error
|
|
19479
20046
|
}
|
|
19480
|
-
|
|
19481
|
-
|
|
19482
|
-
|
|
19483
|
-
|
|
19484
|
-
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
toolCall.result = toolResponse;
|
|
19491
|
-
}
|
|
19492
|
-
}
|
|
20047
|
+
);
|
|
20048
|
+
this.retryCounts.set(sessionId, 0);
|
|
20049
|
+
} else {
|
|
20050
|
+
if (retryCount === 1) {
|
|
20051
|
+
this.logger.warn(
|
|
20052
|
+
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
20053
|
+
{
|
|
20054
|
+
taskId: session.context.taskId,
|
|
20055
|
+
runId: session.context.runId,
|
|
20056
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19493
20057
|
}
|
|
19494
|
-
|
|
20058
|
+
);
|
|
20059
|
+
}
|
|
20060
|
+
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
20061
|
+
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
20062
|
+
this.scheduleFlush(sessionId);
|
|
20063
|
+
}
|
|
20064
|
+
}
|
|
20065
|
+
}
|
|
20066
|
+
getSessionUpdateType(message) {
|
|
20067
|
+
if (message.method !== "session/update") return void 0;
|
|
20068
|
+
const params = message.params;
|
|
20069
|
+
const update = params?.update;
|
|
20070
|
+
return update?.sessionUpdate;
|
|
20071
|
+
}
|
|
20072
|
+
isDirectAgentMessage(message) {
|
|
20073
|
+
return this.getSessionUpdateType(message) === "agent_message";
|
|
20074
|
+
}
|
|
20075
|
+
isAgentMessageChunk(message) {
|
|
20076
|
+
return this.getSessionUpdateType(message) === "agent_message_chunk";
|
|
20077
|
+
}
|
|
20078
|
+
extractChunkText(message) {
|
|
20079
|
+
const params = message.params;
|
|
20080
|
+
const update = params?.update;
|
|
20081
|
+
const content = update?.content;
|
|
20082
|
+
if (content?.type === "text" && content.text) {
|
|
20083
|
+
return content.text;
|
|
20084
|
+
}
|
|
20085
|
+
return "";
|
|
20086
|
+
}
|
|
20087
|
+
emitCoalescedMessage(sessionId, session) {
|
|
20088
|
+
if (!session.chunkBuffer) return;
|
|
20089
|
+
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
20090
|
+
session.chunkBuffer = void 0;
|
|
20091
|
+
session.lastAgentMessage = text2;
|
|
20092
|
+
session.currentTurnMessages.push(text2);
|
|
20093
|
+
const entry = {
|
|
20094
|
+
type: "notification",
|
|
20095
|
+
timestamp: firstTimestamp,
|
|
20096
|
+
notification: {
|
|
20097
|
+
jsonrpc: "2.0",
|
|
20098
|
+
method: "session/update",
|
|
20099
|
+
params: {
|
|
20100
|
+
update: {
|
|
20101
|
+
sessionUpdate: "agent_message",
|
|
20102
|
+
content: { type: "text", text: text2 }
|
|
19495
20103
|
}
|
|
19496
20104
|
}
|
|
19497
20105
|
}
|
|
20106
|
+
};
|
|
20107
|
+
this.writeToLocalCache(sessionId, entry);
|
|
20108
|
+
if (this.posthogAPI) {
|
|
20109
|
+
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
20110
|
+
pending.push(entry);
|
|
20111
|
+
this.pendingEntries.set(sessionId, pending);
|
|
20112
|
+
this.scheduleFlush(sessionId);
|
|
19498
20113
|
}
|
|
19499
|
-
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
19503
|
-
|
|
19504
|
-
|
|
20114
|
+
}
|
|
20115
|
+
getLastAgentMessage(sessionId) {
|
|
20116
|
+
return this.sessions.get(sessionId)?.lastAgentMessage;
|
|
20117
|
+
}
|
|
20118
|
+
getFullAgentResponse(sessionId) {
|
|
20119
|
+
const session = this.sessions.get(sessionId);
|
|
20120
|
+
if (!session || session.currentTurnMessages.length === 0) return void 0;
|
|
20121
|
+
if (session.chunkBuffer) {
|
|
20122
|
+
this.logger.warn(
|
|
20123
|
+
"getFullAgentResponse called with non-empty chunk buffer",
|
|
20124
|
+
{
|
|
20125
|
+
sessionId,
|
|
20126
|
+
bufferedLength: session.chunkBuffer.text.length
|
|
20127
|
+
}
|
|
20128
|
+
);
|
|
19505
20129
|
}
|
|
19506
|
-
return
|
|
20130
|
+
return session.currentTurnMessages.join("\n\n");
|
|
19507
20131
|
}
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
|
|
19512
|
-
|
|
19513
|
-
logger.info("Resuming from log", {
|
|
19514
|
-
taskId: config.taskId,
|
|
19515
|
-
runId: config.runId
|
|
19516
|
-
});
|
|
19517
|
-
const saga = new ResumeSaga(logger);
|
|
19518
|
-
const result = await saga.run({
|
|
19519
|
-
taskId: config.taskId,
|
|
19520
|
-
runId: config.runId,
|
|
19521
|
-
repositoryPath: config.repositoryPath,
|
|
19522
|
-
apiClient: config.apiClient,
|
|
19523
|
-
logger
|
|
19524
|
-
});
|
|
19525
|
-
if (!result.success) {
|
|
19526
|
-
logger.error("Failed to resume from log", {
|
|
19527
|
-
error: result.error,
|
|
19528
|
-
failedStep: result.failedStep
|
|
19529
|
-
});
|
|
19530
|
-
throw new Error(
|
|
19531
|
-
`Failed to resume at step '${result.failedStep}': ${result.error}`
|
|
19532
|
-
);
|
|
20132
|
+
resetTurnMessages(sessionId) {
|
|
20133
|
+
const session = this.sessions.get(sessionId);
|
|
20134
|
+
if (session) {
|
|
20135
|
+
session.currentTurnMessages = [];
|
|
20136
|
+
}
|
|
19533
20137
|
}
|
|
19534
|
-
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
|
|
19542
|
-
}
|
|
19543
|
-
|
|
19544
|
-
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
20138
|
+
extractAgentMessageText(message) {
|
|
20139
|
+
if (message.method !== "session/update") {
|
|
20140
|
+
return null;
|
|
20141
|
+
}
|
|
20142
|
+
const params = message.params;
|
|
20143
|
+
const update = params?.update;
|
|
20144
|
+
if (update?.sessionUpdate !== "agent_message") {
|
|
20145
|
+
return null;
|
|
20146
|
+
}
|
|
20147
|
+
const content = update.content;
|
|
20148
|
+
if (content?.type === "text" && typeof content.text === "string") {
|
|
20149
|
+
const trimmed2 = content.text.trim();
|
|
20150
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20151
|
+
}
|
|
20152
|
+
if (typeof update.message === "string") {
|
|
20153
|
+
const trimmed2 = update.message.trim();
|
|
20154
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20155
|
+
}
|
|
20156
|
+
return null;
|
|
19552
20157
|
}
|
|
19553
|
-
|
|
19554
|
-
const
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
20158
|
+
scheduleFlush(sessionId) {
|
|
20159
|
+
const existing = this.flushTimeouts.get(sessionId);
|
|
20160
|
+
if (existing) clearTimeout(existing);
|
|
20161
|
+
const retryCount = this.retryCounts.get(sessionId) ?? 0;
|
|
20162
|
+
const lastAttempt = this.lastFlushAttemptTime.get(sessionId) ?? 0;
|
|
20163
|
+
const elapsed = Date.now() - lastAttempt;
|
|
20164
|
+
let delay3;
|
|
20165
|
+
if (retryCount > 0) {
|
|
20166
|
+
delay3 = Math.min(
|
|
20167
|
+
_SessionLogWriter.FLUSH_DEBOUNCE_MS * 2 ** retryCount,
|
|
20168
|
+
_SessionLogWriter.MAX_RETRY_DELAY_MS
|
|
20169
|
+
);
|
|
20170
|
+
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
20171
|
+
delay3 = 0;
|
|
20172
|
+
} else {
|
|
20173
|
+
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
19558
20174
|
}
|
|
19559
|
-
|
|
19560
|
-
|
|
19561
|
-
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
20175
|
+
const timeout = setTimeout(() => this.flush(sessionId), delay3);
|
|
20176
|
+
this.flushTimeouts.set(sessionId, timeout);
|
|
20177
|
+
}
|
|
20178
|
+
writeToLocalCache(sessionId, entry) {
|
|
20179
|
+
if (!this.localCachePath) return;
|
|
20180
|
+
const session = this.sessions.get(sessionId);
|
|
20181
|
+
if (!session) return;
|
|
20182
|
+
const logPath = import_node_path7.default.join(
|
|
20183
|
+
this.localCachePath,
|
|
20184
|
+
"sessions",
|
|
20185
|
+
session.context.runId,
|
|
20186
|
+
"logs.ndjson"
|
|
20187
|
+
);
|
|
20188
|
+
try {
|
|
20189
|
+
import_node_fs3.default.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
20190
|
+
`);
|
|
20191
|
+
} catch (error) {
|
|
20192
|
+
this.logger.warn("Failed to write to local cache", {
|
|
20193
|
+
taskId: session.context.taskId,
|
|
20194
|
+
runId: session.context.runId,
|
|
20195
|
+
logPath,
|
|
20196
|
+
error
|
|
20197
|
+
});
|
|
20198
|
+
}
|
|
20199
|
+
}
|
|
20200
|
+
static async cleanupOldSessions(localCachePath) {
|
|
20201
|
+
const sessionsDir = import_node_path7.default.join(localCachePath, "sessions");
|
|
20202
|
+
let deleted = 0;
|
|
20203
|
+
try {
|
|
20204
|
+
const entries = await import_promises4.default.readdir(sessionsDir);
|
|
20205
|
+
const now = Date.now();
|
|
20206
|
+
for (const entry of entries) {
|
|
20207
|
+
const entryPath = import_node_path7.default.join(sessionsDir, entry);
|
|
20208
|
+
try {
|
|
20209
|
+
const stats = await import_promises4.default.stat(entryPath);
|
|
20210
|
+
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
20211
|
+
await import_promises4.default.rm(entryPath, { recursive: true, force: true });
|
|
20212
|
+
deleted++;
|
|
20213
|
+
}
|
|
20214
|
+
} catch {
|
|
19565
20215
|
}
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
parts2.push(`**${role} (tools)**:
|
|
19569
|
-
${toolSummary}`);
|
|
20216
|
+
}
|
|
20217
|
+
} catch {
|
|
19570
20218
|
}
|
|
20219
|
+
return deleted;
|
|
19571
20220
|
}
|
|
19572
|
-
|
|
19573
|
-
}
|
|
20221
|
+
};
|
|
19574
20222
|
|
|
19575
|
-
// src/
|
|
19576
|
-
var
|
|
19577
|
-
var
|
|
19578
|
-
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19585
|
-
|
|
19586
|
-
|
|
19587
|
-
|
|
19588
|
-
|
|
19589
|
-
|
|
19590
|
-
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
19596
|
-
this.
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
|
|
19600
|
-
|
|
19601
|
-
|
|
19602
|
-
|
|
19603
|
-
|
|
20223
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20224
|
+
var import_promises5 = require("fs/promises");
|
|
20225
|
+
var import_node_path8 = require("path");
|
|
20226
|
+
|
|
20227
|
+
// ../git/dist/sagas/tree.js
|
|
20228
|
+
var import_node_fs4 = require("fs");
|
|
20229
|
+
var fs13 = __toESM(require("fs/promises"), 1);
|
|
20230
|
+
var path16 = __toESM(require("path"), 1);
|
|
20231
|
+
var tar = __toESM(require("tar"), 1);
|
|
20232
|
+
var CaptureTreeSaga = class extends GitSaga {
|
|
20233
|
+
sagaName = "CaptureTreeSaga";
|
|
20234
|
+
tempIndexPath = null;
|
|
20235
|
+
async executeGitOperations(input) {
|
|
20236
|
+
const { baseDir, lastTreeHash, archivePath, signal } = input;
|
|
20237
|
+
const tmpDir = path16.join(baseDir, ".git", "posthog-code-tmp");
|
|
20238
|
+
await this.step({
|
|
20239
|
+
name: "create_tmp_dir",
|
|
20240
|
+
execute: () => fs13.mkdir(tmpDir, { recursive: true }),
|
|
20241
|
+
rollback: async () => {
|
|
20242
|
+
}
|
|
20243
|
+
});
|
|
20244
|
+
this.tempIndexPath = path16.join(tmpDir, `index-${Date.now()}`);
|
|
20245
|
+
const tempIndexGit = this.git.env({
|
|
20246
|
+
...process.env,
|
|
20247
|
+
GIT_INDEX_FILE: this.tempIndexPath
|
|
20248
|
+
});
|
|
20249
|
+
await this.step({
|
|
20250
|
+
name: "init_temp_index",
|
|
20251
|
+
execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
|
|
20252
|
+
rollback: async () => {
|
|
20253
|
+
if (this.tempIndexPath) {
|
|
20254
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20255
|
+
});
|
|
20256
|
+
}
|
|
20257
|
+
}
|
|
20258
|
+
});
|
|
20259
|
+
await this.readOnlyStep("stage_files", () => tempIndexGit.raw(["add", "-A"]));
|
|
20260
|
+
const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
|
|
20261
|
+
if (lastTreeHash && treeHash === lastTreeHash) {
|
|
20262
|
+
this.log.debug("No changes since last capture", { treeHash });
|
|
20263
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20264
|
+
});
|
|
20265
|
+
return { snapshot: null, changed: false };
|
|
20266
|
+
}
|
|
20267
|
+
const baseCommit = await this.readOnlyStep("get_base_commit", async () => {
|
|
20268
|
+
try {
|
|
20269
|
+
return await getHeadSha(baseDir, { abortSignal: signal });
|
|
20270
|
+
} catch {
|
|
20271
|
+
return null;
|
|
20272
|
+
}
|
|
20273
|
+
});
|
|
20274
|
+
const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
|
|
20275
|
+
await fs13.rm(this.tempIndexPath, { force: true }).catch(() => {
|
|
20276
|
+
});
|
|
20277
|
+
const snapshot = {
|
|
20278
|
+
treeHash,
|
|
20279
|
+
baseCommit,
|
|
20280
|
+
changes,
|
|
20281
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20282
|
+
};
|
|
20283
|
+
let createdArchivePath;
|
|
20284
|
+
if (archivePath) {
|
|
20285
|
+
createdArchivePath = await this.createArchive(baseDir, archivePath, changes);
|
|
19604
20286
|
}
|
|
19605
|
-
|
|
20287
|
+
this.log.info("Tree captured", {
|
|
20288
|
+
treeHash,
|
|
20289
|
+
changes: changes.length,
|
|
20290
|
+
archived: !!createdArchivePath
|
|
20291
|
+
});
|
|
20292
|
+
return { snapshot, archivePath: createdArchivePath, changed: true };
|
|
19606
20293
|
}
|
|
19607
|
-
|
|
19608
|
-
|
|
19609
|
-
|
|
20294
|
+
async createArchive(baseDir, archivePath, changes) {
|
|
20295
|
+
const filesToArchive = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20296
|
+
if (filesToArchive.length === 0) {
|
|
20297
|
+
return void 0;
|
|
19610
20298
|
}
|
|
19611
|
-
|
|
19612
|
-
|
|
19613
|
-
|
|
19614
|
-
|
|
19615
|
-
|
|
19616
|
-
|
|
19617
|
-
|
|
19618
|
-
|
|
19619
|
-
|
|
19620
|
-
|
|
19621
|
-
|
|
19622
|
-
|
|
19623
|
-
|
|
19624
|
-
|
|
20299
|
+
const existingFiles = filesToArchive.filter((f) => (0, import_node_fs4.existsSync)(path16.join(baseDir, f)));
|
|
20300
|
+
if (existingFiles.length === 0) {
|
|
20301
|
+
return void 0;
|
|
20302
|
+
}
|
|
20303
|
+
await this.step({
|
|
20304
|
+
name: "create_archive",
|
|
20305
|
+
execute: async () => {
|
|
20306
|
+
const archiveDir = path16.dirname(archivePath);
|
|
20307
|
+
await fs13.mkdir(archiveDir, { recursive: true });
|
|
20308
|
+
await tar.create({
|
|
20309
|
+
gzip: true,
|
|
20310
|
+
file: archivePath,
|
|
20311
|
+
cwd: baseDir
|
|
20312
|
+
}, existingFiles);
|
|
20313
|
+
},
|
|
20314
|
+
rollback: async () => {
|
|
20315
|
+
await fs13.rm(archivePath, { force: true }).catch(() => {
|
|
19625
20316
|
});
|
|
19626
20317
|
}
|
|
19627
|
-
}
|
|
19628
|
-
|
|
19629
|
-
isRegistered(sessionId) {
|
|
19630
|
-
return this.sessions.has(sessionId);
|
|
20318
|
+
});
|
|
20319
|
+
return archivePath;
|
|
19631
20320
|
}
|
|
19632
|
-
|
|
19633
|
-
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
sessionId
|
|
19637
|
-
});
|
|
19638
|
-
return;
|
|
20321
|
+
async getChanges(git, fromRef, toRef) {
|
|
20322
|
+
if (!fromRef) {
|
|
20323
|
+
const stdout2 = await git.raw(["ls-tree", "-r", "--name-only", toRef]);
|
|
20324
|
+
return stdout2.split("\n").filter((p) => p.trim()).map((p) => ({ path: p, status: "A" }));
|
|
19639
20325
|
}
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
|
|
19651
|
-
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
20326
|
+
const stdout = await git.raw([
|
|
20327
|
+
"diff-tree",
|
|
20328
|
+
"-r",
|
|
20329
|
+
"--name-status",
|
|
20330
|
+
fromRef,
|
|
20331
|
+
toRef
|
|
20332
|
+
]);
|
|
20333
|
+
const changes = [];
|
|
20334
|
+
for (const line of stdout.split("\n")) {
|
|
20335
|
+
if (!line.trim())
|
|
20336
|
+
continue;
|
|
20337
|
+
const [status, filePath] = line.split(" ");
|
|
20338
|
+
if (!filePath)
|
|
20339
|
+
continue;
|
|
20340
|
+
let normalizedStatus;
|
|
20341
|
+
if (status === "D") {
|
|
20342
|
+
normalizedStatus = "D";
|
|
20343
|
+
} else if (status === "A") {
|
|
20344
|
+
normalizedStatus = "A";
|
|
19656
20345
|
} else {
|
|
19657
|
-
|
|
20346
|
+
normalizedStatus = "M";
|
|
19658
20347
|
}
|
|
19659
|
-
|
|
19660
|
-
|
|
19661
|
-
|
|
19662
|
-
|
|
20348
|
+
changes.push({ path: filePath, status: normalizedStatus });
|
|
20349
|
+
}
|
|
20350
|
+
return changes;
|
|
20351
|
+
}
|
|
20352
|
+
};
|
|
20353
|
+
var ApplyTreeSaga = class extends GitSaga {
|
|
20354
|
+
sagaName = "ApplyTreeSaga";
|
|
20355
|
+
originalHead = null;
|
|
20356
|
+
originalBranch = null;
|
|
20357
|
+
extractedFiles = [];
|
|
20358
|
+
fileBackups = /* @__PURE__ */ new Map();
|
|
20359
|
+
async executeGitOperations(input) {
|
|
20360
|
+
const { baseDir, treeHash, baseCommit, changes, archivePath } = input;
|
|
20361
|
+
const headInfo = await this.readOnlyStep("get_current_head", async () => {
|
|
20362
|
+
let head = null;
|
|
20363
|
+
let branch = null;
|
|
20364
|
+
try {
|
|
20365
|
+
head = await this.git.revparse(["HEAD"]);
|
|
20366
|
+
} catch {
|
|
20367
|
+
head = null;
|
|
19663
20368
|
}
|
|
19664
|
-
|
|
19665
|
-
|
|
19666
|
-
|
|
19667
|
-
|
|
19668
|
-
};
|
|
19669
|
-
this.writeToLocalCache(sessionId, entry);
|
|
19670
|
-
if (this.posthogAPI) {
|
|
19671
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
19672
|
-
pending.push(entry);
|
|
19673
|
-
this.pendingEntries.set(sessionId, pending);
|
|
19674
|
-
this.scheduleFlush(sessionId);
|
|
20369
|
+
try {
|
|
20370
|
+
branch = await this.git.raw(["symbolic-ref", "--short", "HEAD"]);
|
|
20371
|
+
} catch {
|
|
20372
|
+
branch = null;
|
|
19675
20373
|
}
|
|
19676
|
-
|
|
19677
|
-
|
|
19678
|
-
|
|
19679
|
-
|
|
19680
|
-
|
|
20374
|
+
return { head, branch };
|
|
20375
|
+
});
|
|
20376
|
+
this.originalHead = headInfo.head;
|
|
20377
|
+
this.originalBranch = headInfo.branch;
|
|
20378
|
+
let checkoutPerformed = false;
|
|
20379
|
+
if (baseCommit && baseCommit !== this.originalHead) {
|
|
20380
|
+
await this.readOnlyStep("check_working_tree", async () => {
|
|
20381
|
+
const status = await this.git.status();
|
|
20382
|
+
if (!status.isClean()) {
|
|
20383
|
+
const changedFiles = status.modified.length + status.staged.length + status.deleted.length;
|
|
20384
|
+
throw new Error(`Cannot apply tree: ${changedFiles} uncommitted change(s) exist. Commit or stash your changes first.`);
|
|
20385
|
+
}
|
|
20386
|
+
});
|
|
20387
|
+
await this.step({
|
|
20388
|
+
name: "checkout_base",
|
|
20389
|
+
execute: async () => {
|
|
20390
|
+
await this.git.checkout(baseCommit);
|
|
20391
|
+
checkoutPerformed = true;
|
|
20392
|
+
this.log.warn("Applied tree from different commit - now in detached HEAD state", {
|
|
20393
|
+
originalHead: this.originalHead,
|
|
20394
|
+
originalBranch: this.originalBranch,
|
|
20395
|
+
baseCommit
|
|
20396
|
+
});
|
|
20397
|
+
},
|
|
20398
|
+
rollback: async () => {
|
|
20399
|
+
try {
|
|
20400
|
+
if (this.originalBranch) {
|
|
20401
|
+
await this.git.checkout(this.originalBranch);
|
|
20402
|
+
} else if (this.originalHead) {
|
|
20403
|
+
await this.git.checkout(this.originalHead);
|
|
20404
|
+
}
|
|
20405
|
+
} catch (error) {
|
|
20406
|
+
this.log.warn("Failed to rollback checkout", { error });
|
|
20407
|
+
}
|
|
20408
|
+
}
|
|
20409
|
+
});
|
|
20410
|
+
}
|
|
20411
|
+
if (archivePath) {
|
|
20412
|
+
const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
|
|
20413
|
+
await this.readOnlyStep("backup_existing_files", async () => {
|
|
20414
|
+
for (const filePath of filesToExtract) {
|
|
20415
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20416
|
+
try {
|
|
20417
|
+
const content = await fs13.readFile(fullPath);
|
|
20418
|
+
this.fileBackups.set(filePath, content);
|
|
20419
|
+
} catch {
|
|
20420
|
+
}
|
|
20421
|
+
}
|
|
20422
|
+
});
|
|
20423
|
+
await this.step({
|
|
20424
|
+
name: "extract_archive",
|
|
20425
|
+
execute: async () => {
|
|
20426
|
+
await tar.extract({
|
|
20427
|
+
file: archivePath,
|
|
20428
|
+
cwd: baseDir
|
|
20429
|
+
});
|
|
20430
|
+
this.extractedFiles = filesToExtract;
|
|
20431
|
+
},
|
|
20432
|
+
rollback: async () => {
|
|
20433
|
+
for (const filePath of this.extractedFiles) {
|
|
20434
|
+
const fullPath = path16.join(baseDir, filePath);
|
|
20435
|
+
const backup = this.fileBackups.get(filePath);
|
|
20436
|
+
if (backup) {
|
|
20437
|
+
const dir = path16.dirname(fullPath);
|
|
20438
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20439
|
+
});
|
|
20440
|
+
await fs13.writeFile(fullPath, backup).catch(() => {
|
|
20441
|
+
});
|
|
20442
|
+
} else {
|
|
20443
|
+
await fs13.rm(fullPath, { force: true }).catch(() => {
|
|
20444
|
+
});
|
|
20445
|
+
}
|
|
20446
|
+
}
|
|
20447
|
+
}
|
|
20448
|
+
});
|
|
20449
|
+
}
|
|
20450
|
+
for (const change of changes.filter((c) => c.status === "D")) {
|
|
20451
|
+
const fullPath = path16.join(baseDir, change.path);
|
|
20452
|
+
const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
|
|
20453
|
+
try {
|
|
20454
|
+
return await fs13.readFile(fullPath);
|
|
20455
|
+
} catch {
|
|
20456
|
+
return null;
|
|
20457
|
+
}
|
|
20458
|
+
});
|
|
20459
|
+
await this.step({
|
|
20460
|
+
name: `delete_${change.path}`,
|
|
20461
|
+
execute: async () => {
|
|
20462
|
+
await fs13.rm(fullPath, { force: true });
|
|
20463
|
+
this.log.debug(`Deleted file: ${change.path}`);
|
|
20464
|
+
},
|
|
20465
|
+
rollback: async () => {
|
|
20466
|
+
if (backupContent) {
|
|
20467
|
+
const dir = path16.dirname(fullPath);
|
|
20468
|
+
await fs13.mkdir(dir, { recursive: true }).catch(() => {
|
|
20469
|
+
});
|
|
20470
|
+
await fs13.writeFile(fullPath, backupContent).catch(() => {
|
|
20471
|
+
});
|
|
20472
|
+
}
|
|
20473
|
+
}
|
|
19681
20474
|
});
|
|
19682
20475
|
}
|
|
20476
|
+
const deletedCount = changes.filter((c) => c.status === "D").length;
|
|
20477
|
+
this.log.info("Tree applied", {
|
|
20478
|
+
treeHash,
|
|
20479
|
+
totalChanges: changes.length,
|
|
20480
|
+
deletedFiles: deletedCount,
|
|
20481
|
+
checkoutPerformed
|
|
20482
|
+
});
|
|
20483
|
+
return { treeHash, checkoutPerformed };
|
|
19683
20484
|
}
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
|
|
19689
|
-
|
|
20485
|
+
};
|
|
20486
|
+
|
|
20487
|
+
// src/sagas/apply-snapshot-saga.ts
|
|
20488
|
+
var ApplySnapshotSaga = class extends Saga {
|
|
20489
|
+
sagaName = "ApplySnapshotSaga";
|
|
20490
|
+
archivePath = null;
|
|
20491
|
+
async execute(input) {
|
|
20492
|
+
const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
|
|
20493
|
+
const tmpDir = (0, import_node_path8.join)(repositoryPath, ".posthog", "tmp");
|
|
20494
|
+
if (!snapshot.archiveUrl) {
|
|
20495
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19690
20496
|
}
|
|
19691
|
-
const
|
|
19692
|
-
|
|
19693
|
-
|
|
19694
|
-
|
|
19695
|
-
|
|
19696
|
-
if (this.flushQueues.get(sessionId) === next) {
|
|
19697
|
-
this.flushQueues.delete(sessionId);
|
|
20497
|
+
const archiveUrl = snapshot.archiveUrl;
|
|
20498
|
+
await this.step({
|
|
20499
|
+
name: "create_tmp_dir",
|
|
20500
|
+
execute: () => (0, import_promises5.mkdir)(tmpDir, { recursive: true }),
|
|
20501
|
+
rollback: async () => {
|
|
19698
20502
|
}
|
|
19699
20503
|
});
|
|
19700
|
-
|
|
20504
|
+
const archivePath = (0, import_node_path8.join)(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
20505
|
+
this.archivePath = archivePath;
|
|
20506
|
+
await this.step({
|
|
20507
|
+
name: "download_archive",
|
|
20508
|
+
execute: async () => {
|
|
20509
|
+
const arrayBuffer = await apiClient.downloadArtifact(
|
|
20510
|
+
taskId,
|
|
20511
|
+
runId,
|
|
20512
|
+
archiveUrl
|
|
20513
|
+
);
|
|
20514
|
+
if (!arrayBuffer) {
|
|
20515
|
+
throw new Error("Failed to download archive");
|
|
20516
|
+
}
|
|
20517
|
+
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
20518
|
+
const binaryContent = Buffer.from(base64Content, "base64");
|
|
20519
|
+
await (0, import_promises5.writeFile)(archivePath, binaryContent);
|
|
20520
|
+
this.log.info("Tree archive downloaded", {
|
|
20521
|
+
treeHash: snapshot.treeHash,
|
|
20522
|
+
snapshotBytes: binaryContent.byteLength,
|
|
20523
|
+
snapshotWireBytes: arrayBuffer.byteLength,
|
|
20524
|
+
totalBytes: binaryContent.byteLength,
|
|
20525
|
+
totalWireBytes: arrayBuffer.byteLength
|
|
20526
|
+
});
|
|
20527
|
+
},
|
|
20528
|
+
rollback: async () => {
|
|
20529
|
+
if (this.archivePath) {
|
|
20530
|
+
await (0, import_promises5.rm)(this.archivePath, { force: true }).catch(() => {
|
|
20531
|
+
});
|
|
20532
|
+
}
|
|
20533
|
+
}
|
|
20534
|
+
});
|
|
20535
|
+
const gitApplySaga = new ApplyTreeSaga(this.log);
|
|
20536
|
+
const applyResult = await gitApplySaga.run({
|
|
20537
|
+
baseDir: repositoryPath,
|
|
20538
|
+
treeHash: snapshot.treeHash,
|
|
20539
|
+
baseCommit: snapshot.baseCommit,
|
|
20540
|
+
changes: snapshot.changes,
|
|
20541
|
+
archivePath: this.archivePath
|
|
20542
|
+
});
|
|
20543
|
+
if (!applyResult.success) {
|
|
20544
|
+
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
20545
|
+
}
|
|
20546
|
+
await (0, import_promises5.rm)(this.archivePath, { force: true }).catch(() => {
|
|
20547
|
+
});
|
|
20548
|
+
this.log.info("Tree snapshot applied", {
|
|
20549
|
+
treeHash: snapshot.treeHash,
|
|
20550
|
+
totalChanges: snapshot.changes.length,
|
|
20551
|
+
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
20552
|
+
});
|
|
20553
|
+
return { treeHash: snapshot.treeHash };
|
|
19701
20554
|
}
|
|
19702
|
-
|
|
19703
|
-
|
|
19704
|
-
|
|
19705
|
-
|
|
19706
|
-
|
|
20555
|
+
};
|
|
20556
|
+
|
|
20557
|
+
// src/sagas/capture-tree-saga.ts
|
|
20558
|
+
var import_node_fs5 = require("fs");
|
|
20559
|
+
var import_promises6 = require("fs/promises");
|
|
20560
|
+
var import_node_path9 = require("path");
|
|
20561
|
+
var CaptureTreeSaga2 = class extends Saga {
|
|
20562
|
+
sagaName = "CaptureTreeSaga";
|
|
20563
|
+
async execute(input) {
|
|
20564
|
+
const {
|
|
20565
|
+
repositoryPath,
|
|
20566
|
+
lastTreeHash,
|
|
20567
|
+
interrupted,
|
|
20568
|
+
apiClient,
|
|
20569
|
+
taskId,
|
|
20570
|
+
runId
|
|
20571
|
+
} = input;
|
|
20572
|
+
const tmpDir = (0, import_node_path9.join)(repositoryPath, ".posthog", "tmp");
|
|
20573
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path9.join)(repositoryPath, ".gitmodules"))) {
|
|
20574
|
+
this.log.warn(
|
|
20575
|
+
"Repository has submodules - snapshot may not capture submodule state"
|
|
20576
|
+
);
|
|
19707
20577
|
}
|
|
19708
|
-
const
|
|
19709
|
-
|
|
19710
|
-
|
|
20578
|
+
const shouldArchive = !!apiClient;
|
|
20579
|
+
const archivePath = shouldArchive ? (0, import_node_path9.join)(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
20580
|
+
const gitCaptureSaga = new CaptureTreeSaga(this.log);
|
|
20581
|
+
const captureResult = await gitCaptureSaga.run({
|
|
20582
|
+
baseDir: repositoryPath,
|
|
20583
|
+
lastTreeHash,
|
|
20584
|
+
archivePath
|
|
20585
|
+
});
|
|
20586
|
+
if (!captureResult.success) {
|
|
20587
|
+
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
19711
20588
|
}
|
|
19712
|
-
|
|
19713
|
-
|
|
19714
|
-
|
|
19715
|
-
|
|
19716
|
-
|
|
20589
|
+
const {
|
|
20590
|
+
snapshot: gitSnapshot,
|
|
20591
|
+
archivePath: createdArchivePath,
|
|
20592
|
+
changed
|
|
20593
|
+
} = captureResult.data;
|
|
20594
|
+
if (!changed || !gitSnapshot) {
|
|
20595
|
+
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
20596
|
+
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
19717
20597
|
}
|
|
19718
|
-
|
|
19719
|
-
|
|
19720
|
-
|
|
19721
|
-
|
|
19722
|
-
|
|
19723
|
-
|
|
19724
|
-
|
|
19725
|
-
|
|
19726
|
-
|
|
19727
|
-
const retryCount = (this.retryCounts.get(sessionId) ?? 0) + 1;
|
|
19728
|
-
this.retryCounts.set(sessionId, retryCount);
|
|
19729
|
-
if (retryCount >= _SessionLogWriter.MAX_FLUSH_RETRIES) {
|
|
19730
|
-
this.logger.error(
|
|
19731
|
-
`Dropping ${pending.length} session log entries after ${retryCount} failed flush attempts`,
|
|
19732
|
-
{
|
|
19733
|
-
taskId: session.context.taskId,
|
|
19734
|
-
runId: session.context.runId,
|
|
19735
|
-
error
|
|
19736
|
-
}
|
|
20598
|
+
let archiveUrl;
|
|
20599
|
+
if (apiClient && createdArchivePath) {
|
|
20600
|
+
try {
|
|
20601
|
+
archiveUrl = await this.uploadArchive(
|
|
20602
|
+
createdArchivePath,
|
|
20603
|
+
gitSnapshot.treeHash,
|
|
20604
|
+
apiClient,
|
|
20605
|
+
taskId,
|
|
20606
|
+
runId
|
|
19737
20607
|
);
|
|
19738
|
-
|
|
19739
|
-
|
|
19740
|
-
|
|
19741
|
-
this.logger.warn(
|
|
19742
|
-
`Failed to persist session logs, will retry (up to ${_SessionLogWriter.MAX_FLUSH_RETRIES} attempts)`,
|
|
19743
|
-
{
|
|
19744
|
-
taskId: session.context.taskId,
|
|
19745
|
-
runId: session.context.runId,
|
|
19746
|
-
error: error instanceof Error ? error.message : String(error)
|
|
19747
|
-
}
|
|
19748
|
-
);
|
|
19749
|
-
}
|
|
19750
|
-
const currentPending = this.pendingEntries.get(sessionId) ?? [];
|
|
19751
|
-
this.pendingEntries.set(sessionId, [...pending, ...currentPending]);
|
|
19752
|
-
this.scheduleFlush(sessionId);
|
|
20608
|
+
} finally {
|
|
20609
|
+
await (0, import_promises6.rm)(createdArchivePath, { force: true }).catch(() => {
|
|
20610
|
+
});
|
|
19753
20611
|
}
|
|
19754
20612
|
}
|
|
20613
|
+
const snapshot = {
|
|
20614
|
+
treeHash: gitSnapshot.treeHash,
|
|
20615
|
+
baseCommit: gitSnapshot.baseCommit,
|
|
20616
|
+
changes: gitSnapshot.changes,
|
|
20617
|
+
timestamp: gitSnapshot.timestamp,
|
|
20618
|
+
interrupted,
|
|
20619
|
+
archiveUrl
|
|
20620
|
+
};
|
|
20621
|
+
this.log.info("Tree captured", {
|
|
20622
|
+
treeHash: snapshot.treeHash,
|
|
20623
|
+
changes: snapshot.changes.length,
|
|
20624
|
+
interrupted,
|
|
20625
|
+
archiveUrl
|
|
20626
|
+
});
|
|
20627
|
+
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
19755
20628
|
}
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
|
|
19759
|
-
|
|
19760
|
-
|
|
19761
|
-
|
|
19762
|
-
|
|
19763
|
-
|
|
19764
|
-
|
|
19765
|
-
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
|
|
19769
|
-
|
|
19770
|
-
const update = params?.update;
|
|
19771
|
-
const content = update?.content;
|
|
19772
|
-
if (content?.type === "text" && content.text) {
|
|
19773
|
-
return content.text;
|
|
19774
|
-
}
|
|
19775
|
-
return "";
|
|
19776
|
-
}
|
|
19777
|
-
emitCoalescedMessage(sessionId, session) {
|
|
19778
|
-
if (!session.chunkBuffer) return;
|
|
19779
|
-
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
19780
|
-
session.chunkBuffer = void 0;
|
|
19781
|
-
session.lastAgentMessage = text2;
|
|
19782
|
-
session.currentTurnMessages.push(text2);
|
|
19783
|
-
const entry = {
|
|
19784
|
-
type: "notification",
|
|
19785
|
-
timestamp: firstTimestamp,
|
|
19786
|
-
notification: {
|
|
19787
|
-
jsonrpc: "2.0",
|
|
19788
|
-
method: "session/update",
|
|
19789
|
-
params: {
|
|
19790
|
-
update: {
|
|
19791
|
-
sessionUpdate: "agent_message",
|
|
19792
|
-
content: { type: "text", text: text2 }
|
|
20629
|
+
async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
|
|
20630
|
+
const archiveUrl = await this.step({
|
|
20631
|
+
name: "upload_archive",
|
|
20632
|
+
execute: async () => {
|
|
20633
|
+
const archiveContent = await (0, import_promises6.readFile)(archivePath);
|
|
20634
|
+
const base64Content = archiveContent.toString("base64");
|
|
20635
|
+
const snapshotBytes = archiveContent.byteLength;
|
|
20636
|
+
const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
|
|
20637
|
+
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
20638
|
+
{
|
|
20639
|
+
name: `trees/${treeHash}.tar.gz`,
|
|
20640
|
+
type: "tree_snapshot",
|
|
20641
|
+
content: base64Content,
|
|
20642
|
+
content_type: "application/gzip"
|
|
19793
20643
|
}
|
|
20644
|
+
]);
|
|
20645
|
+
const uploadedArtifact = artifacts[0];
|
|
20646
|
+
if (uploadedArtifact?.storage_path) {
|
|
20647
|
+
this.log.info("Tree archive uploaded", {
|
|
20648
|
+
storagePath: uploadedArtifact.storage_path,
|
|
20649
|
+
treeHash,
|
|
20650
|
+
snapshotBytes,
|
|
20651
|
+
snapshotWireBytes,
|
|
20652
|
+
totalBytes: snapshotBytes,
|
|
20653
|
+
totalWireBytes: snapshotWireBytes
|
|
20654
|
+
});
|
|
20655
|
+
return uploadedArtifact.storage_path;
|
|
19794
20656
|
}
|
|
20657
|
+
return void 0;
|
|
20658
|
+
},
|
|
20659
|
+
rollback: async () => {
|
|
20660
|
+
await (0, import_promises6.rm)(archivePath, { force: true }).catch(() => {
|
|
20661
|
+
});
|
|
19795
20662
|
}
|
|
19796
|
-
};
|
|
19797
|
-
|
|
19798
|
-
if (this.posthogAPI) {
|
|
19799
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
19800
|
-
pending.push(entry);
|
|
19801
|
-
this.pendingEntries.set(sessionId, pending);
|
|
19802
|
-
this.scheduleFlush(sessionId);
|
|
19803
|
-
}
|
|
20663
|
+
});
|
|
20664
|
+
return archiveUrl;
|
|
19804
20665
|
}
|
|
19805
|
-
|
|
19806
|
-
|
|
20666
|
+
};
|
|
20667
|
+
|
|
20668
|
+
// src/tree-tracker.ts
|
|
20669
|
+
var TreeTracker = class {
|
|
20670
|
+
repositoryPath;
|
|
20671
|
+
taskId;
|
|
20672
|
+
runId;
|
|
20673
|
+
apiClient;
|
|
20674
|
+
logger;
|
|
20675
|
+
lastTreeHash = null;
|
|
20676
|
+
constructor(config) {
|
|
20677
|
+
this.repositoryPath = config.repositoryPath;
|
|
20678
|
+
this.taskId = config.taskId;
|
|
20679
|
+
this.runId = config.runId;
|
|
20680
|
+
this.apiClient = config.apiClient;
|
|
20681
|
+
this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
19807
20682
|
}
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19817
|
-
|
|
20683
|
+
/**
|
|
20684
|
+
* Capture current working tree state as a snapshot.
|
|
20685
|
+
* Uses a temporary index to avoid modifying user's staging area.
|
|
20686
|
+
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
20687
|
+
*/
|
|
20688
|
+
async captureTree(options) {
|
|
20689
|
+
const saga = new CaptureTreeSaga2(this.logger);
|
|
20690
|
+
const result = await saga.run({
|
|
20691
|
+
repositoryPath: this.repositoryPath,
|
|
20692
|
+
taskId: this.taskId,
|
|
20693
|
+
runId: this.runId,
|
|
20694
|
+
apiClient: this.apiClient,
|
|
20695
|
+
lastTreeHash: this.lastTreeHash,
|
|
20696
|
+
interrupted: options?.interrupted
|
|
20697
|
+
});
|
|
20698
|
+
if (!result.success) {
|
|
20699
|
+
this.logger.error("Failed to capture tree", {
|
|
20700
|
+
error: result.error,
|
|
20701
|
+
failedStep: result.failedStep
|
|
20702
|
+
});
|
|
20703
|
+
throw new Error(
|
|
20704
|
+
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
19818
20705
|
);
|
|
19819
20706
|
}
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
resetTurnMessages(sessionId) {
|
|
19823
|
-
const session = this.sessions.get(sessionId);
|
|
19824
|
-
if (session) {
|
|
19825
|
-
session.currentTurnMessages = [];
|
|
20707
|
+
if (result.data.newTreeHash !== null) {
|
|
20708
|
+
this.lastTreeHash = result.data.newTreeHash;
|
|
19826
20709
|
}
|
|
20710
|
+
return result.data.snapshot;
|
|
19827
20711
|
}
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
return null;
|
|
19836
|
-
}
|
|
19837
|
-
const content = update.content;
|
|
19838
|
-
if (content?.type === "text" && typeof content.text === "string") {
|
|
19839
|
-
const trimmed2 = content.text.trim();
|
|
19840
|
-
return trimmed2.length > 0 ? trimmed2 : null;
|
|
20712
|
+
/**
|
|
20713
|
+
* Download and apply a tree snapshot.
|
|
20714
|
+
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
20715
|
+
*/
|
|
20716
|
+
async applyTreeSnapshot(snapshot) {
|
|
20717
|
+
if (!this.apiClient) {
|
|
20718
|
+
throw new Error("Cannot apply snapshot: API client not configured");
|
|
19841
20719
|
}
|
|
19842
|
-
if (
|
|
19843
|
-
|
|
19844
|
-
|
|
20720
|
+
if (!snapshot.archiveUrl) {
|
|
20721
|
+
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
20722
|
+
treeHash: snapshot.treeHash,
|
|
20723
|
+
changes: snapshot.changes.length
|
|
20724
|
+
});
|
|
20725
|
+
throw new Error("Cannot apply snapshot: no archive URL");
|
|
19845
20726
|
}
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
|
|
20727
|
+
const saga = new ApplySnapshotSaga(this.logger);
|
|
20728
|
+
const result = await saga.run({
|
|
20729
|
+
snapshot,
|
|
20730
|
+
repositoryPath: this.repositoryPath,
|
|
20731
|
+
apiClient: this.apiClient,
|
|
20732
|
+
taskId: this.taskId,
|
|
20733
|
+
runId: this.runId
|
|
20734
|
+
});
|
|
20735
|
+
if (!result.success) {
|
|
20736
|
+
this.logger.error("Failed to apply tree snapshot", {
|
|
20737
|
+
error: result.error,
|
|
20738
|
+
failedStep: result.failedStep,
|
|
20739
|
+
treeHash: snapshot.treeHash
|
|
20740
|
+
});
|
|
20741
|
+
throw new Error(
|
|
20742
|
+
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
19859
20743
|
);
|
|
19860
|
-
} else if (elapsed >= _SessionLogWriter.FLUSH_MAX_INTERVAL_MS) {
|
|
19861
|
-
delay3 = 0;
|
|
19862
|
-
} else {
|
|
19863
|
-
delay3 = _SessionLogWriter.FLUSH_DEBOUNCE_MS;
|
|
19864
20744
|
}
|
|
19865
|
-
|
|
19866
|
-
this.flushTimeouts.set(sessionId, timeout);
|
|
20745
|
+
this.lastTreeHash = result.data.treeHash;
|
|
19867
20746
|
}
|
|
19868
|
-
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
|
|
19873
|
-
this.localCachePath,
|
|
19874
|
-
"sessions",
|
|
19875
|
-
session.context.runId,
|
|
19876
|
-
"logs.ndjson"
|
|
19877
|
-
);
|
|
19878
|
-
try {
|
|
19879
|
-
import_node_fs5.default.appendFileSync(logPath, `${JSON.stringify(entry)}
|
|
19880
|
-
`);
|
|
19881
|
-
} catch (error) {
|
|
19882
|
-
this.logger.warn("Failed to write to local cache", {
|
|
19883
|
-
taskId: session.context.taskId,
|
|
19884
|
-
runId: session.context.runId,
|
|
19885
|
-
logPath,
|
|
19886
|
-
error
|
|
19887
|
-
});
|
|
19888
|
-
}
|
|
20747
|
+
/**
|
|
20748
|
+
* Get the last captured tree hash.
|
|
20749
|
+
*/
|
|
20750
|
+
getLastTreeHash() {
|
|
20751
|
+
return this.lastTreeHash;
|
|
19889
20752
|
}
|
|
19890
|
-
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
|
|
19894
|
-
|
|
19895
|
-
const now = Date.now();
|
|
19896
|
-
for (const entry of entries) {
|
|
19897
|
-
const entryPath = import_node_path7.default.join(sessionsDir, entry);
|
|
19898
|
-
try {
|
|
19899
|
-
const stats = await import_promises4.default.stat(entryPath);
|
|
19900
|
-
if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
|
|
19901
|
-
await import_promises4.default.rm(entryPath, { recursive: true, force: true });
|
|
19902
|
-
deleted++;
|
|
19903
|
-
}
|
|
19904
|
-
} catch {
|
|
19905
|
-
}
|
|
19906
|
-
}
|
|
19907
|
-
} catch {
|
|
19908
|
-
}
|
|
19909
|
-
return deleted;
|
|
20753
|
+
/**
|
|
20754
|
+
* Set the last tree hash (used when resuming).
|
|
20755
|
+
*/
|
|
20756
|
+
setLastTreeHash(hash) {
|
|
20757
|
+
this.lastTreeHash = hash;
|
|
19910
20758
|
}
|
|
19911
20759
|
};
|
|
19912
20760
|
|
|
@@ -19976,6 +20824,14 @@ var httpHeaderSchema = import_v4.z.object({
|
|
|
19976
20824
|
name: import_v4.z.string(),
|
|
19977
20825
|
value: import_v4.z.string()
|
|
19978
20826
|
});
|
|
20827
|
+
var nullishString = import_v4.z.string().nullish().transform((value) => value ?? null);
|
|
20828
|
+
var handoffLocalGitStateSchema = import_v4.z.object({
|
|
20829
|
+
head: nullishString,
|
|
20830
|
+
branch: nullishString,
|
|
20831
|
+
upstreamHead: nullishString,
|
|
20832
|
+
upstreamRemote: nullishString,
|
|
20833
|
+
upstreamMergeRef: nullishString
|
|
20834
|
+
});
|
|
19979
20835
|
var remoteMcpServerSchema = import_v4.z.object({
|
|
19980
20836
|
type: import_v4.z.enum(["http", "sse"]),
|
|
19981
20837
|
name: import_v4.z.string().min(1, "MCP server name is required"),
|
|
@@ -20027,13 +20883,16 @@ var setConfigOptionParamsSchema = import_v4.z.object({
|
|
|
20027
20883
|
var refreshSessionParamsSchema = import_v4.z.object({
|
|
20028
20884
|
mcpServers: mcpServersSchema
|
|
20029
20885
|
});
|
|
20886
|
+
var closeParamsSchema = import_v4.z.object({
|
|
20887
|
+
localGitState: handoffLocalGitStateSchema.optional()
|
|
20888
|
+
}).optional();
|
|
20030
20889
|
var commandParamsSchemas = {
|
|
20031
20890
|
user_message: userMessageParamsSchema,
|
|
20032
20891
|
"posthog/user_message": userMessageParamsSchema,
|
|
20033
20892
|
cancel: import_v4.z.object({}).optional(),
|
|
20034
20893
|
"posthog/cancel": import_v4.z.object({}).optional(),
|
|
20035
|
-
close:
|
|
20036
|
-
"posthog/close":
|
|
20894
|
+
close: closeParamsSchema,
|
|
20895
|
+
"posthog/close": closeParamsSchema,
|
|
20037
20896
|
permission_response: permissionResponseParamsSchema,
|
|
20038
20897
|
"posthog/permission_response": permissionResponseParamsSchema,
|
|
20039
20898
|
set_config_option: setConfigOptionParamsSchema,
|
|
@@ -20355,7 +21214,7 @@ var AgentServer = class {
|
|
|
20355
21214
|
return app;
|
|
20356
21215
|
}
|
|
20357
21216
|
async start() {
|
|
20358
|
-
await new Promise((
|
|
21217
|
+
await new Promise((resolve7) => {
|
|
20359
21218
|
this.server = (0, import_node_server.serve)(
|
|
20360
21219
|
{
|
|
20361
21220
|
fetch: this.app.fetch,
|
|
@@ -20365,51 +21224,35 @@ var AgentServer = class {
|
|
|
20365
21224
|
this.logger.debug(
|
|
20366
21225
|
`HTTP server listening on port ${this.config.port}`
|
|
20367
21226
|
);
|
|
20368
|
-
|
|
21227
|
+
resolve7();
|
|
20369
21228
|
}
|
|
20370
21229
|
);
|
|
20371
21230
|
});
|
|
20372
21231
|
await this.autoInitializeSession();
|
|
20373
21232
|
}
|
|
20374
|
-
async
|
|
20375
|
-
|
|
20376
|
-
|
|
20377
|
-
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
|
|
20381
|
-
|
|
21233
|
+
async loadResumeState(taskId, resumeRunId, currentRunId) {
|
|
21234
|
+
this.logger.debug("Loading resume state", { resumeRunId, currentRunId });
|
|
21235
|
+
try {
|
|
21236
|
+
this.resumeState = await resumeFromLog({
|
|
21237
|
+
taskId,
|
|
21238
|
+
runId: resumeRunId,
|
|
21239
|
+
repositoryPath: this.config.repositoryPath,
|
|
21240
|
+
apiClient: this.posthogAPI,
|
|
21241
|
+
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
20382
21242
|
});
|
|
20383
|
-
|
|
20384
|
-
this.resumeState
|
|
20385
|
-
|
|
20386
|
-
|
|
20387
|
-
|
|
20388
|
-
|
|
20389
|
-
|
|
20390
|
-
|
|
20391
|
-
|
|
20392
|
-
|
|
20393
|
-
|
|
20394
|
-
|
|
20395
|
-
});
|
|
20396
|
-
} catch (error) {
|
|
20397
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
20398
|
-
error
|
|
20399
|
-
});
|
|
20400
|
-
this.resumeState = null;
|
|
20401
|
-
}
|
|
21243
|
+
this.logger.debug("Resume state loaded", {
|
|
21244
|
+
conversationTurns: this.resumeState.conversation.length,
|
|
21245
|
+
hasSnapshot: !!this.resumeState.latestSnapshot,
|
|
21246
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21247
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null,
|
|
21248
|
+
logEntries: this.resumeState.logEntryCount
|
|
21249
|
+
});
|
|
21250
|
+
} catch (error) {
|
|
21251
|
+
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
21252
|
+
error
|
|
21253
|
+
});
|
|
21254
|
+
this.resumeState = null;
|
|
20402
21255
|
}
|
|
20403
|
-
const payload = {
|
|
20404
|
-
task_id: taskId,
|
|
20405
|
-
run_id: runId,
|
|
20406
|
-
team_id: projectId,
|
|
20407
|
-
user_id: 0,
|
|
20408
|
-
// System-initiated
|
|
20409
|
-
distinct_id: "agent-server",
|
|
20410
|
-
mode
|
|
20411
|
-
};
|
|
20412
|
-
await this.initializeSession(payload, null);
|
|
20413
21256
|
}
|
|
20414
21257
|
async stop() {
|
|
20415
21258
|
this.logger.debug("Stopping agent server...");
|
|
@@ -20519,6 +21362,10 @@ var AgentServer = class {
|
|
|
20519
21362
|
case POSTHOG_NOTIFICATIONS.CLOSE:
|
|
20520
21363
|
case "close": {
|
|
20521
21364
|
this.logger.debug("Close requested");
|
|
21365
|
+
const localGitState = this.extractHandoffLocalGitState(params);
|
|
21366
|
+
if (localGitState && this.session) {
|
|
21367
|
+
this.session.pendingHandoffGitState = localGitState;
|
|
21368
|
+
}
|
|
20522
21369
|
await this.cleanupSession();
|
|
20523
21370
|
return { closed: true };
|
|
20524
21371
|
}
|
|
@@ -20745,7 +21592,8 @@ var AgentServer = class {
|
|
|
20745
21592
|
deviceInfo,
|
|
20746
21593
|
logWriter,
|
|
20747
21594
|
permissionMode: initialPermissionMode,
|
|
20748
|
-
hasDesktopConnected: sseController !== null
|
|
21595
|
+
hasDesktopConnected: sseController !== null,
|
|
21596
|
+
pendingHandoffGitState: void 0
|
|
20749
21597
|
};
|
|
20750
21598
|
this.logger = new Logger({
|
|
20751
21599
|
debug: true,
|
|
@@ -20803,29 +21651,11 @@ var AgentServer = class {
|
|
|
20803
21651
|
if (!this.resumeState) {
|
|
20804
21652
|
const resumeRunId = this.getResumeRunId(taskRun);
|
|
20805
21653
|
if (resumeRunId) {
|
|
20806
|
-
this.
|
|
21654
|
+
await this.loadResumeState(
|
|
21655
|
+
payload.task_id,
|
|
20807
21656
|
resumeRunId,
|
|
20808
|
-
|
|
20809
|
-
|
|
20810
|
-
try {
|
|
20811
|
-
this.resumeState = await resumeFromLog({
|
|
20812
|
-
taskId: payload.task_id,
|
|
20813
|
-
runId: resumeRunId,
|
|
20814
|
-
repositoryPath: this.config.repositoryPath,
|
|
20815
|
-
apiClient: this.posthogAPI,
|
|
20816
|
-
logger: new Logger({ debug: true, prefix: "[Resume]" })
|
|
20817
|
-
});
|
|
20818
|
-
this.logger.debug("Resume state loaded (via TaskRun state)", {
|
|
20819
|
-
conversationTurns: this.resumeState.conversation.length,
|
|
20820
|
-
snapshotApplied: this.resumeState.snapshotApplied,
|
|
20821
|
-
logEntries: this.resumeState.logEntryCount
|
|
20822
|
-
});
|
|
20823
|
-
} catch (error) {
|
|
20824
|
-
this.logger.debug("Failed to load resume state, starting fresh", {
|
|
20825
|
-
error
|
|
20826
|
-
});
|
|
20827
|
-
this.resumeState = null;
|
|
20828
|
-
}
|
|
21657
|
+
payload.run_id
|
|
21658
|
+
);
|
|
20829
21659
|
}
|
|
20830
21660
|
}
|
|
20831
21661
|
if (this.resumeState && this.resumeState.conversation.length > 0) {
|
|
@@ -20884,8 +21714,59 @@ var AgentServer = class {
|
|
|
20884
21714
|
const conversationSummary = formatConversationForResume(
|
|
20885
21715
|
this.resumeState.conversation
|
|
20886
21716
|
);
|
|
21717
|
+
let snapshotApplied = false;
|
|
21718
|
+
if (this.resumeState.latestSnapshot?.archiveUrl && this.config.repositoryPath && this.posthogAPI) {
|
|
21719
|
+
try {
|
|
21720
|
+
const treeTracker = new TreeTracker({
|
|
21721
|
+
repositoryPath: this.config.repositoryPath,
|
|
21722
|
+
taskId: payload.task_id,
|
|
21723
|
+
runId: payload.run_id,
|
|
21724
|
+
apiClient: this.posthogAPI,
|
|
21725
|
+
logger: this.logger.child("TreeTracker")
|
|
21726
|
+
});
|
|
21727
|
+
await treeTracker.applyTreeSnapshot(this.resumeState.latestSnapshot);
|
|
21728
|
+
treeTracker.setLastTreeHash(this.resumeState.latestSnapshot.treeHash);
|
|
21729
|
+
snapshotApplied = true;
|
|
21730
|
+
this.logger.info("Tree snapshot applied", {
|
|
21731
|
+
treeHash: this.resumeState.latestSnapshot.treeHash,
|
|
21732
|
+
changes: this.resumeState.latestSnapshot.changes?.length ?? 0,
|
|
21733
|
+
hasArchiveUrl: !!this.resumeState.latestSnapshot.archiveUrl
|
|
21734
|
+
});
|
|
21735
|
+
} catch (error) {
|
|
21736
|
+
this.logger.warn("Failed to apply tree snapshot", {
|
|
21737
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21738
|
+
treeHash: this.resumeState.latestSnapshot.treeHash
|
|
21739
|
+
});
|
|
21740
|
+
}
|
|
21741
|
+
}
|
|
21742
|
+
if (this.resumeState.latestGitCheckpoint && this.config.repositoryPath && this.posthogAPI) {
|
|
21743
|
+
try {
|
|
21744
|
+
const checkpointTracker = new HandoffCheckpointTracker({
|
|
21745
|
+
repositoryPath: this.config.repositoryPath,
|
|
21746
|
+
taskId: payload.task_id,
|
|
21747
|
+
runId: payload.run_id,
|
|
21748
|
+
apiClient: this.posthogAPI,
|
|
21749
|
+
logger: this.logger.child("HandoffCheckpoint")
|
|
21750
|
+
});
|
|
21751
|
+
const metrics = await checkpointTracker.applyFromHandoff(
|
|
21752
|
+
this.resumeState.latestGitCheckpoint
|
|
21753
|
+
);
|
|
21754
|
+
this.logger.info("Git checkpoint applied", {
|
|
21755
|
+
branch: this.resumeState.latestGitCheckpoint.branch,
|
|
21756
|
+
head: this.resumeState.latestGitCheckpoint.head,
|
|
21757
|
+
packBytes: metrics.packBytes,
|
|
21758
|
+
indexBytes: metrics.indexBytes,
|
|
21759
|
+
totalBytes: metrics.totalBytes
|
|
21760
|
+
});
|
|
21761
|
+
} catch (error) {
|
|
21762
|
+
this.logger.warn("Failed to apply git checkpoint", {
|
|
21763
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21764
|
+
branch: this.resumeState.latestGitCheckpoint.branch
|
|
21765
|
+
});
|
|
21766
|
+
}
|
|
21767
|
+
}
|
|
20887
21768
|
const pendingUserPrompt = await this.getPendingUserPrompt(taskRun);
|
|
20888
|
-
const sandboxContext =
|
|
21769
|
+
const sandboxContext = snapshotApplied ? `The workspace environment (all files, packages, and code changes) has been fully restored from where you left off.` : `The workspace files from the previous session were not restored (the file snapshot may have expired), so you are starting with a fresh environment. Your conversation history is fully preserved below.`;
|
|
20889
21770
|
let resumePromptBlocks;
|
|
20890
21771
|
if (pendingUserPrompt?.length) {
|
|
20891
21772
|
resumePromptBlocks = [
|
|
@@ -20926,7 +21807,9 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
20926
21807
|
conversationTurns: this.resumeState.conversation.length,
|
|
20927
21808
|
promptLength: promptBlocksToText(resumePromptBlocks).length,
|
|
20928
21809
|
hasPendingUserMessage: !!pendingUserPrompt?.length,
|
|
20929
|
-
snapshotApplied
|
|
21810
|
+
snapshotApplied,
|
|
21811
|
+
hasGitCheckpoint: !!this.resumeState.latestGitCheckpoint,
|
|
21812
|
+
gitCheckpointBranch: this.resumeState.latestGitCheckpoint?.branch ?? null
|
|
20930
21813
|
});
|
|
20931
21814
|
this.resumeState = null;
|
|
20932
21815
|
this.session.logWriter.resetTurnMessages(payload.run_id);
|
|
@@ -21070,26 +21953,44 @@ Continue from where you left off. The user is waiting for your response.`
|
|
|
21070
21953
|
throw new Error(`Failed to download artifact ${artifact.name}`);
|
|
21071
21954
|
}
|
|
21072
21955
|
const safeName = this.getSafeArtifactName(artifact.name);
|
|
21073
|
-
const artifactDir = (0,
|
|
21956
|
+
const artifactDir = (0, import_node_path10.join)(
|
|
21074
21957
|
this.config.repositoryPath ?? "/tmp/workspace",
|
|
21075
21958
|
".posthog",
|
|
21076
21959
|
"attachments",
|
|
21077
21960
|
runId,
|
|
21078
21961
|
artifact.id ?? safeName
|
|
21079
21962
|
);
|
|
21080
|
-
await (0,
|
|
21081
|
-
const artifactPath = (0,
|
|
21082
|
-
await (0,
|
|
21963
|
+
await (0, import_promises7.mkdir)(artifactDir, { recursive: true });
|
|
21964
|
+
const artifactPath = (0, import_node_path10.join)(artifactDir, safeName);
|
|
21965
|
+
await (0, import_promises7.writeFile)(artifactPath, Buffer.from(data));
|
|
21083
21966
|
return resourceLink((0, import_node_url2.pathToFileURL)(artifactPath).toString(), artifact.name, {
|
|
21084
21967
|
...artifact.content_type ? { mimeType: artifact.content_type } : {},
|
|
21085
21968
|
...typeof artifact.size === "number" ? { size: artifact.size } : {}
|
|
21086
21969
|
});
|
|
21087
21970
|
}
|
|
21088
21971
|
getSafeArtifactName(name2) {
|
|
21089
|
-
const baseName = (0,
|
|
21972
|
+
const baseName = (0, import_node_path10.basename)(name2).trim();
|
|
21090
21973
|
const normalizedName = baseName.replace(/[^\w.-]/g, "_");
|
|
21091
21974
|
return normalizedName.length > 0 ? normalizedName : "attachment";
|
|
21092
21975
|
}
|
|
21976
|
+
async autoInitializeSession() {
|
|
21977
|
+
const { taskId, runId, mode, projectId } = this.config;
|
|
21978
|
+
this.logger.debug("Auto-initializing session", { taskId, runId, mode });
|
|
21979
|
+
const resumeRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21980
|
+
if (resumeRunId) {
|
|
21981
|
+
await this.loadResumeState(taskId, resumeRunId, runId);
|
|
21982
|
+
}
|
|
21983
|
+
const payload = {
|
|
21984
|
+
task_id: taskId,
|
|
21985
|
+
run_id: runId,
|
|
21986
|
+
team_id: projectId,
|
|
21987
|
+
user_id: 0,
|
|
21988
|
+
// System-initiated
|
|
21989
|
+
distinct_id: "agent-server",
|
|
21990
|
+
mode
|
|
21991
|
+
};
|
|
21992
|
+
await this.initializeSession(payload, null);
|
|
21993
|
+
}
|
|
21093
21994
|
getResumeRunId(taskRun) {
|
|
21094
21995
|
const envRunId = process.env.POSTHOG_RESUME_RUN_ID;
|
|
21095
21996
|
if (envRunId) return envRunId;
|
|
@@ -21578,6 +22479,11 @@ ${attributionInstructions}
|
|
|
21578
22479
|
async cleanupSession() {
|
|
21579
22480
|
if (!this.session) return;
|
|
21580
22481
|
this.logger.debug("Cleaning up session");
|
|
22482
|
+
try {
|
|
22483
|
+
await this.captureHandoffCheckpoint();
|
|
22484
|
+
} catch (error) {
|
|
22485
|
+
this.logger.error("Failed to capture handoff checkpoint", error);
|
|
22486
|
+
}
|
|
21581
22487
|
try {
|
|
21582
22488
|
await this.captureTreeState();
|
|
21583
22489
|
} catch (error) {
|
|
@@ -21637,6 +22543,50 @@ ${attributionInstructions}
|
|
|
21637
22543
|
this.logger.error("Failed to capture tree state", error);
|
|
21638
22544
|
}
|
|
21639
22545
|
}
|
|
22546
|
+
async captureHandoffCheckpoint() {
|
|
22547
|
+
if (!this.session?.treeTracker || !this.session.pendingHandoffGitState) {
|
|
22548
|
+
return;
|
|
22549
|
+
}
|
|
22550
|
+
if (!this.posthogAPI) {
|
|
22551
|
+
this.logger.warn(
|
|
22552
|
+
"Skipping handoff checkpoint capture: PostHog API client is not configured"
|
|
22553
|
+
);
|
|
22554
|
+
return;
|
|
22555
|
+
}
|
|
22556
|
+
const tracker = new HandoffCheckpointTracker({
|
|
22557
|
+
repositoryPath: this.config.repositoryPath ?? "/tmp/workspace",
|
|
22558
|
+
taskId: this.session.payload.task_id,
|
|
22559
|
+
runId: this.session.payload.run_id,
|
|
22560
|
+
apiClient: this.posthogAPI,
|
|
22561
|
+
logger: this.logger.child("HandoffCheckpoint")
|
|
22562
|
+
});
|
|
22563
|
+
const checkpoint = await tracker.captureForHandoff(
|
|
22564
|
+
this.session.pendingHandoffGitState
|
|
22565
|
+
);
|
|
22566
|
+
if (!checkpoint) return;
|
|
22567
|
+
const checkpointWithDevice = {
|
|
22568
|
+
...checkpoint,
|
|
22569
|
+
device: this.session.deviceInfo
|
|
22570
|
+
};
|
|
22571
|
+
const notification = {
|
|
22572
|
+
jsonrpc: "2.0",
|
|
22573
|
+
method: POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT,
|
|
22574
|
+
params: checkpointWithDevice
|
|
22575
|
+
};
|
|
22576
|
+
this.broadcastEvent({
|
|
22577
|
+
type: "notification",
|
|
22578
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22579
|
+
notification
|
|
22580
|
+
});
|
|
22581
|
+
this.session.logWriter.appendRawLine(
|
|
22582
|
+
this.session.payload.run_id,
|
|
22583
|
+
JSON.stringify(notification)
|
|
22584
|
+
);
|
|
22585
|
+
}
|
|
22586
|
+
extractHandoffLocalGitState(params) {
|
|
22587
|
+
const result = handoffLocalGitStateSchema.safeParse(params.localGitState);
|
|
22588
|
+
return result.success ? result.data : null;
|
|
22589
|
+
}
|
|
21640
22590
|
broadcastTurnComplete(stopReason) {
|
|
21641
22591
|
if (!this.session) return;
|
|
21642
22592
|
this.broadcastEvent({
|
|
@@ -21690,8 +22640,8 @@ ${attributionInstructions}
|
|
|
21690
22640
|
options: params.options,
|
|
21691
22641
|
toolCall: params.toolCall
|
|
21692
22642
|
});
|
|
21693
|
-
return new Promise((
|
|
21694
|
-
this.pendingPermissions.set(requestId, { resolve:
|
|
22643
|
+
return new Promise((resolve7) => {
|
|
22644
|
+
this.pendingPermissions.set(requestId, { resolve: resolve7 });
|
|
21695
22645
|
});
|
|
21696
22646
|
}
|
|
21697
22647
|
resolvePermission(requestId, optionId, customInput, answers) {
|