@posthog/agent 2.3.386 → 2.3.387

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.
Files changed (44) hide show
  1. package/dist/adapters/claude/session/jsonl-hydration.d.ts +1 -0
  2. package/dist/agent.d.ts +1 -0
  3. package/dist/agent.js +13 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/handoff-checkpoint.d.ts +39 -0
  6. package/dist/handoff-checkpoint.js +6679 -0
  7. package/dist/handoff-checkpoint.js.map +1 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/posthog-api.d.ts +1 -0
  12. package/dist/posthog-api.js +11 -2
  13. package/dist/posthog-api.js.map +1 -1
  14. package/dist/resume.d.ts +3 -1
  15. package/dist/resume.js +41 -4
  16. package/dist/resume.js.map +1 -1
  17. package/dist/server/agent-server.d.ts +5 -15
  18. package/dist/server/agent-server.js +1330 -394
  19. package/dist/server/agent-server.js.map +1 -1
  20. package/dist/server/bin.cjs +1332 -396
  21. package/dist/server/bin.cjs.map +1 -1
  22. package/dist/server/schemas.d.ts +191 -0
  23. package/dist/server/schemas.js +108 -0
  24. package/dist/server/schemas.js.map +1 -0
  25. package/dist/tree-tracker.d.ts +1 -0
  26. package/dist/tree-tracker.js +18 -4
  27. package/dist/tree-tracker.js.map +1 -1
  28. package/dist/types.d.ts +18 -1
  29. package/dist/types.js +5 -0
  30. package/dist/types.js.map +1 -1
  31. package/package.json +9 -1
  32. package/src/acp-extensions.ts +3 -0
  33. package/src/handoff-checkpoint.test.ts +183 -0
  34. package/src/handoff-checkpoint.ts +361 -0
  35. package/src/posthog-api.test.ts +29 -0
  36. package/src/posthog-api.ts +5 -1
  37. package/src/resume.ts +7 -1
  38. package/src/sagas/apply-snapshot-saga.ts +7 -0
  39. package/src/sagas/capture-tree-saga.ts +10 -3
  40. package/src/sagas/resume-saga.ts +32 -0
  41. package/src/sagas/test-fixtures.ts +46 -0
  42. package/src/server/agent-server.ts +74 -1
  43. package/src/server/schemas.ts +21 -2
  44. package/src/types.ts +24 -0
@@ -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(path15, isFile2, isDirectory) {
809
- log(`checking %s`, path15);
808
+ function check(path17, isFile2, isDirectory) {
809
+ log(`checking %s`, path17);
810
810
  try {
811
- const stat2 = fs_1.statSync(path15);
812
- if (stat2.isFile() && isFile2) {
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 (stat2.isDirectory() && isDirectory) {
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(path15, type = exports2.READABLE) {
832
- return check(path15, (type & exports2.FILE) > 0, (type & exports2.FOLDER) > 0);
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(path15) {
928
+ function locateFile(path17) {
929
929
  if (Module["locateFile"]) {
930
- return Module["locateFile"](path15, scriptDirectory);
930
+ return Module["locateFile"](path17, scriptDirectory);
931
931
  }
932
- return scriptDirectory + path15;
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((resolve6, reject) => {
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 resolve6(binary2 ? data.buffer : data);
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, resolve6) => {
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
- resolve6(xhr.response);
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(resolve6, reject) {
1957
- asyncLoad(libFile, resolve6, reject);
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 fs13 = require("fs");
3454
- bytes = Promise.resolve(fs13.readFileSync(url));
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 import_promises5 = require("fs/promises");
3916
- var import_node_path8 = require("path");
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(path15) {
3963
- return path15 instanceof String && cache.has(path15);
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(path15) {
4053
- return (0, import_file_exists.exists)(path15, import_file_exists.FOLDER);
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(path15) {
4458
- return /^\.(git)?$/.test(path15.trim());
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 [path15, line, preview] = input.split(NULL);
4893
- paths.add(path15);
4894
- (results[path15] = results[path15] || []).push({
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: path15,
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, path15, text2) {
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, path15, false, result[1]);
5665
+ return new InitSummary(bare, path17, false, result[1]);
5666
5666
  }
5667
5667
  if (result = reInitResponseRegex.exec(response)) {
5668
- return new InitSummary(bare, path15, true, result[1]);
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, path15, /^re/i.test(response), gitDir);
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, path15, existing, gitDir) {
5688
+ constructor(bare, path17, existing, gitDir) {
5689
5689
  this.bare = bare;
5690
- this.path = path15;
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, path15, customArgs) {
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"), path15, text2);
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(path15, index, working_dir) {
6528
- this.path = path15;
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(path15) || [null, path15, path15];
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, path15) {
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, path15);
6567
+ handler(result, path17);
6568
6568
  }
6569
6569
  if (raw !== "##" && raw !== "!!") {
6570
- result.files.push(new FileStatusSummary(path15, index, workingDir));
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(path15, write) {
6883
+ hashObject(path17, write) {
6884
6884
  return this._runTask(
6885
- hashObjectTask(path15, write === true),
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 path15 = input.trim().replace(/^["']|["']$/g, "");
7239
- return path15 && (0, import_node_path.normalize)(path15);
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, path15) {
7554
- return subModuleTask(["add", repo, path15]);
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, path15, then) {
7885
- return this._runTask(addSubModuleTask2(repo, path15), trailingFunctionArgument2(arguments));
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 stat2 = await import_promises.default.stat(lockPath);
8501
+ const stat4 = await import_promises.default.stat(lockPath);
8502
8502
  return {
8503
8503
  path: lockPath,
8504
- ageMs: Date.now() - stat2.mtimeMs
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((resolve6) => {
8539
+ return new Promise((resolve7) => {
8540
8540
  this.readQueue.push(() => {
8541
8541
  this.readers++;
8542
- resolve6();
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((resolve6) => {
8556
+ return new Promise((resolve7) => {
8557
8557
  this.writeQueue.push(() => {
8558
8558
  this.writerWaiting = this.writeQueue.length > 0;
8559
8559
  this.writer = true;
8560
- resolve6();
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.386",
8732
+ version: "2.3.387",
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 resolve6 = this.resolvers.shift();
8996
- if (resolve6) {
8997
- resolve6({ value: item, done: false });
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 resolve6 of this.resolvers) {
9005
- resolve6({ value: void 0, done: true });
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((resolve6) => {
9023
- this.resolvers.push(resolve6);
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((resolve6, reject) => {
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
- resolve6();
9152
+ resolve7();
9143
9153
  } else {
9144
- nodeStream.once("drain", resolve6);
9154
+ nodeStream.once("drain", resolve7);
9145
9155
  }
9146
9156
  });
9147
9157
  },
9148
9158
  close() {
9149
- return new Promise((resolve6) => {
9150
- nodeStream.end(resolve6);
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 stat2 = await fs4.stat(absPath);
13093
- mtimeMs = stat2.mtimeMs;
13094
- if (stat2.size > MAX_WRAPPER_SOURCE_BYTES) {
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
- (resolve6) => setTimeout(() => resolve6({ result: "timeout" }), timeoutMs)
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(path15, oldText, newText) {
13559
- this.items.push({ type: "diff", path: path15, oldText, newText });
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((resolve6) => setTimeout(resolve6, ms));
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((resolve6) => {
15961
- this.queue.push(resolve6);
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((resolve6) => {
16489
- this.session.pendingMessages.set(promptUuid, { resolve: resolve6, order });
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((resolve6) => setTimeout(resolve6, 10)).then(
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/utils/gateway.ts
18335
- function getGatewayBaseUrl(posthogHost) {
18336
- const url = new URL(posthogHost);
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
- // src/posthog-api.ts
18352
- var DEFAULT_USER_AGENT = `posthog/agent.hog.dev; version: ${package_default.version}`;
18353
- var PostHogAPIClient = class {
18354
- config;
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
- // src/adapters/claude/session/jsonl-hydration.ts
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,20 +18515,1082 @@ var GitSaga = class extends Saga {
18741
18515
  }
18742
18516
  };
18743
18517
 
18744
- // ../git/dist/sagas/tree.js
18745
- var CaptureTreeSaga = class extends GitSaga {
18746
- sagaName = "CaptureTreeSaga";
18747
- tempIndexPath = null;
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, lastTreeHash, archivePath, signal } = input;
18750
- const tmpDir = path13.join(baseDir, ".git", "posthog-code-tmp");
18751
- await this.step({
18752
- name: "create_tmp_dir",
18753
- execute: () => fs11.mkdir(tmpDir, { recursive: true }),
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
18550
+ });
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
+ },
18570
+ rollback: async () => {
18571
+ }
18572
+ });
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 () => {
18576
+ try {
18577
+ await this.git.revparse(["--verify", refName]);
18578
+ return true;
18579
+ } catch {
18580
+ return false;
18581
+ }
18582
+ });
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
+ }
18593
+ });
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
18602
+ };
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;
18638
+ }
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(() => {
18676
+ });
18677
+ }
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
+ });
18799
+ }
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);
18806
+ }
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);
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(() => {
18838
+ });
18839
+ return { path: packPath, rawBytes };
18840
+ }
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
+ };
18867
+ }
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" };
18919
+ }
18920
+ return {
18921
+ kind: "diverged",
18922
+ divergence: {
18923
+ branch,
18924
+ localHead: nonAncestorHead,
18925
+ cloudHead
18926
+ }
18927
+ };
18928
+ }
18929
+ async findNonAncestorHead(_git, heads, cloudHead) {
18930
+ for (const head of heads) {
18931
+ if (head === cloudHead) {
18932
+ continue;
18933
+ }
18934
+ if (!await this.isAncestor(head, cloudHead)) {
18935
+ return head;
18936
+ }
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"]
18999
+ });
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;
19013
+ }
19014
+ resolve7(code);
19015
+ });
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"
19023
+ });
19024
+ let stdout = "";
19025
+ let stderr = "";
19026
+ child.stdout.on("data", (chunk) => {
19027
+ stdout += chunk.toString();
19028
+ });
19029
+ child.stderr.on("data", (chunk) => {
19030
+ stderr += chunk.toString();
19031
+ });
19032
+ child.on("error", reject);
19033
+ child.on("close", (code) => {
19034
+ if (code === 0) {
19035
+ resolve7({ stdout, stderr });
19036
+ return;
19037
+ }
19038
+ reject(new Error(stderr || `git ${args2.join(" ")} failed with code ${code}`));
19039
+ });
19040
+ child.stdin.end(input);
19041
+ });
19042
+ }
19043
+ };
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;
19051
+ }
19052
+ }
19053
+ async function getTrackingMetadata(git, branch) {
19054
+ if (!branch) {
19055
+ return {
19056
+ upstreamRemote: null,
19057
+ upstreamMergeRef: null,
19058
+ remoteUrl: null
19059
+ };
19060
+ }
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;
19072
+ }
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
+ }
19085
+
19086
+ // src/handoff-checkpoint.ts
19087
+ var HandoffCheckpointTracker = class {
19088
+ repositoryPath;
19089
+ taskId;
19090
+ runId;
19091
+ apiClient;
19092
+ logger;
19093
+ constructor(config) {
19094
+ this.repositoryPath = config.repositoryPath;
19095
+ this.taskId = config.taskId;
19096
+ this.runId = config.runId;
19097
+ this.apiClient = config.apiClient;
19098
+ this.logger = config.logger || new Logger({ debug: false, prefix: "[HandoffCheckpointTracker]" });
19099
+ }
19100
+ async captureForHandoff(localGitState) {
19101
+ if (!this.apiClient) {
19102
+ throw new Error(
19103
+ "Cannot capture handoff checkpoint: API client not configured"
19104
+ );
19105
+ }
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);
19132
+ }
19133
+ }
19134
+ async applyFromHandoff(checkpoint, options) {
19135
+ if (!this.apiClient) {
19136
+ throw new Error(
19137
+ "Cannot apply handoff checkpoint: API client not configured"
19138
+ );
19139
+ }
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"
19152
+ },
19153
+ {
19154
+ key: "index",
19155
+ storagePath: checkpoint.indexArtifactPath,
19156
+ filePath: indexPath,
19157
+ label: "handoff index"
19158
+ }
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
+ } finally {
19169
+ await this.removeIfPresent(packPath);
19170
+ await this.removeIfPresent(indexPath);
19171
+ }
19172
+ }
19173
+ toGitCheckpoint(checkpoint) {
19174
+ return {
19175
+ checkpointId: checkpoint.checkpointId,
19176
+ commit: checkpoint.commit,
19177
+ checkpointRef: checkpoint.checkpointRef,
19178
+ headRef: checkpoint.headRef,
19179
+ head: checkpoint.head,
19180
+ branch: checkpoint.branch,
19181
+ indexTree: checkpoint.indexTree,
19182
+ worktreeTree: checkpoint.worktreeTree,
19183
+ timestamp: checkpoint.timestamp,
19184
+ upstreamRemote: checkpoint.upstreamRemote ?? null,
19185
+ upstreamMergeRef: checkpoint.upstreamMergeRef ?? null,
19186
+ remoteUrl: checkpoint.remoteUrl ?? null
19187
+ };
19188
+ }
19189
+ async uploadArtifactFile(filePath, name2, contentType) {
19190
+ if (!this.apiClient) {
19191
+ return { rawBytes: 0, wireBytes: 0 };
19192
+ }
19193
+ const content = await (0, import_promises3.readFile)(filePath);
19194
+ const base64Content = content.toString("base64");
19195
+ const artifacts = await this.apiClient.uploadTaskArtifacts(
19196
+ this.taskId,
19197
+ this.runId,
19198
+ [
19199
+ {
19200
+ name: name2,
19201
+ type: "artifact",
19202
+ content: base64Content,
19203
+ content_type: contentType
19204
+ }
19205
+ ]
19206
+ );
19207
+ return {
19208
+ storagePath: artifacts.at(-1)?.storage_path,
19209
+ rawBytes: content.byteLength,
19210
+ wireBytes: Buffer.byteLength(base64Content, "utf-8")
19211
+ };
19212
+ }
19213
+ async uploadArtifacts(specs) {
19214
+ const uploads = await Promise.all(
19215
+ specs.map(async (spec) => {
19216
+ if (!spec.filePath) {
19217
+ return [spec.key, void 0];
19218
+ }
19219
+ return [
19220
+ spec.key,
19221
+ await this.uploadArtifactFile(
19222
+ spec.filePath,
19223
+ spec.name,
19224
+ spec.contentType
19225
+ )
19226
+ ];
19227
+ })
19228
+ );
19229
+ return Object.fromEntries(uploads);
19230
+ }
19231
+ async downloadArtifactToFile(artifactPath, filePath, label) {
19232
+ if (!this.apiClient) {
19233
+ throw new Error(`Cannot download ${label}: API client not configured`);
19234
+ }
19235
+ const arrayBuffer = await this.apiClient.downloadArtifact(
19236
+ this.taskId,
19237
+ this.runId,
19238
+ artifactPath
19239
+ );
19240
+ if (!arrayBuffer) {
19241
+ throw new Error(`Failed to download ${label}`);
19242
+ }
19243
+ const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
19244
+ const binaryContent = Buffer.from(base64Content, "base64");
19245
+ await (0, import_promises3.writeFile)(filePath, binaryContent);
19246
+ return {
19247
+ filePath,
19248
+ rawBytes: binaryContent.byteLength,
19249
+ wireBytes: arrayBuffer.byteLength
19250
+ };
19251
+ }
19252
+ async downloadArtifacts(specs) {
19253
+ const downloads = await Promise.all(
19254
+ specs.map(async (spec) => {
19255
+ if (!spec.storagePath) {
19256
+ return [spec.key, void 0];
19257
+ }
19258
+ return [
19259
+ spec.key,
19260
+ await this.downloadArtifactToFile(
19261
+ spec.storagePath,
19262
+ spec.filePath,
19263
+ spec.label
19264
+ )
19265
+ ];
19266
+ })
19267
+ );
19268
+ return Object.fromEntries(downloads);
19269
+ }
19270
+ createGitTracker() {
19271
+ return new GitHandoffTracker({
19272
+ repositoryPath: this.repositoryPath,
19273
+ logger: this.logger
19274
+ });
19275
+ }
19276
+ logCaptureMetrics(checkpoint, uploads) {
19277
+ this.logger.info("Captured handoff checkpoint", {
19278
+ checkpointId: checkpoint.checkpointId,
19279
+ branch: checkpoint.branch,
19280
+ head: checkpoint.head,
19281
+ artifactPath: uploads.pack?.storagePath,
19282
+ indexArtifactPath: uploads.index?.storagePath,
19283
+ ...this.buildMetricPayload(uploads)
19284
+ });
19285
+ }
19286
+ logApplyMetrics(checkpoint, downloads, totalBytes) {
19287
+ this.logger.info("Applied handoff checkpoint", {
19288
+ checkpointId: checkpoint.checkpointId,
19289
+ commit: checkpoint.commit,
19290
+ branch: checkpoint.branch,
19291
+ head: checkpoint.head,
19292
+ packBytes: downloads.pack?.rawBytes ?? 0,
19293
+ packWireBytes: downloads.pack?.wireBytes ?? 0,
19294
+ indexBytes: downloads.index?.rawBytes ?? 0,
19295
+ indexWireBytes: downloads.index?.wireBytes ?? 0,
19296
+ totalBytes,
19297
+ totalWireBytes: this.sumWireBytes(downloads.pack, downloads.index)
19298
+ });
19299
+ }
19300
+ buildMetricPayload(metrics) {
19301
+ return {
19302
+ packBytes: metrics.pack?.rawBytes ?? 0,
19303
+ packWireBytes: metrics.pack?.wireBytes ?? 0,
19304
+ indexBytes: metrics.index?.rawBytes ?? 0,
19305
+ indexWireBytes: metrics.index?.wireBytes ?? 0,
19306
+ totalBytes: this.sumRawBytes(metrics.pack, metrics.index),
19307
+ totalWireBytes: this.sumWireBytes(metrics.pack, metrics.index)
19308
+ };
19309
+ }
19310
+ sumRawBytes(...artifacts) {
19311
+ return artifacts.reduce(
19312
+ (total, artifact) => total + (artifact?.rawBytes ?? 0),
19313
+ 0
19314
+ );
19315
+ }
19316
+ sumWireBytes(...artifacts) {
19317
+ return artifacts.reduce(
19318
+ (total, artifact) => total + (artifact?.wireBytes ?? 0),
19319
+ 0
19320
+ );
19321
+ }
19322
+ async removeIfPresent(filePath) {
19323
+ if (!filePath) {
19324
+ return;
19325
+ }
19326
+ await (0, import_promises3.rm)(filePath, { force: true }).catch(() => {
19327
+ });
19328
+ }
19329
+ };
19330
+
19331
+ // src/utils/gateway.ts
19332
+ function getGatewayBaseUrl(posthogHost) {
19333
+ const url = new URL(posthogHost);
19334
+ const hostname = url.hostname;
19335
+ if (hostname === "localhost" || hostname === "127.0.0.1") {
19336
+ return `${url.protocol}//localhost:3308`;
19337
+ }
19338
+ if (hostname === "host.docker.internal") {
19339
+ return `${url.protocol}//host.docker.internal:3308`;
19340
+ }
19341
+ const region = hostname.match(/^(us|eu)\.posthog\.com$/)?.[1] ?? "us";
19342
+ return `https://gateway.${region}.posthog.com`;
19343
+ }
19344
+ function getLlmGatewayUrl(posthogHost, product = "posthog_code") {
19345
+ return `${getGatewayBaseUrl(posthogHost)}/${product}`;
19346
+ }
19347
+
19348
+ // src/posthog-api.ts
19349
+ var DEFAULT_USER_AGENT = `posthog/agent.hog.dev; version: ${package_default.version}`;
19350
+ var PostHogAPIClient = class {
19351
+ config;
19352
+ constructor(config) {
19353
+ this.config = config;
19354
+ }
19355
+ get baseUrl() {
19356
+ const host = this.config.apiUrl.endsWith("/") ? this.config.apiUrl.slice(0, -1) : this.config.apiUrl;
19357
+ return host;
19358
+ }
19359
+ isAuthFailure(status) {
19360
+ return status === 401 || status === 403;
19361
+ }
19362
+ async resolveApiKey(forceRefresh = false) {
19363
+ if (forceRefresh && this.config.refreshApiKey) {
19364
+ return this.config.refreshApiKey();
19365
+ }
19366
+ return this.config.getApiKey();
19367
+ }
19368
+ async buildHeaders(options, forceRefresh = false) {
19369
+ const headers = new Headers(options.headers);
19370
+ headers.set(
19371
+ "Authorization",
19372
+ `Bearer ${await this.resolveApiKey(forceRefresh)}`
19373
+ );
19374
+ headers.set("Content-Type", "application/json");
19375
+ headers.set("User-Agent", this.config.userAgent ?? DEFAULT_USER_AGENT);
19376
+ return headers;
19377
+ }
19378
+ async performRequest(endpoint, options, forceRefresh = false) {
19379
+ const url = `${this.baseUrl}${endpoint}`;
19380
+ return fetch(url, {
19381
+ ...options,
19382
+ headers: await this.buildHeaders(options, forceRefresh)
19383
+ });
19384
+ }
19385
+ async performRequestWithRetry(endpoint, options = {}) {
19386
+ let response = await this.performRequest(endpoint, options);
19387
+ if (!response.ok && this.isAuthFailure(response.status)) {
19388
+ response = await this.performRequest(endpoint, options, true);
19389
+ }
19390
+ return response;
19391
+ }
19392
+ async apiRequest(endpoint, options = {}) {
19393
+ const response = await this.performRequestWithRetry(endpoint, options);
19394
+ if (!response.ok) {
19395
+ let errorMessage;
19396
+ try {
19397
+ const errorResponse = await response.json();
19398
+ errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
19399
+ } catch {
19400
+ errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
19401
+ }
19402
+ throw new Error(errorMessage);
19403
+ }
19404
+ return response.json();
19405
+ }
19406
+ getTeamId() {
19407
+ return this.config.projectId;
19408
+ }
19409
+ async getApiKey(forceRefresh = false) {
19410
+ return this.resolveApiKey(forceRefresh);
19411
+ }
19412
+ getLlmGatewayUrl() {
19413
+ return getLlmGatewayUrl(this.baseUrl);
19414
+ }
19415
+ async getTask(taskId) {
19416
+ const teamId = this.getTeamId();
19417
+ return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
19418
+ }
19419
+ async getTaskRun(taskId, runId) {
19420
+ const teamId = this.getTeamId();
19421
+ return this.apiRequest(
19422
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
19423
+ );
19424
+ }
19425
+ async updateTaskRun(taskId, runId, payload) {
19426
+ const teamId = this.getTeamId();
19427
+ return this.apiRequest(
19428
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
19429
+ {
19430
+ method: "PATCH",
19431
+ body: JSON.stringify(payload)
19432
+ }
19433
+ );
19434
+ }
19435
+ async setTaskRunOutput(taskId, runId, output) {
19436
+ return this.apiRequest(
19437
+ `/api/projects/${this.getTeamId()}/tasks/${taskId}/runs/${runId}/set_output/`,
19438
+ {
19439
+ method: "PATCH",
19440
+ body: JSON.stringify(output)
19441
+ }
19442
+ );
19443
+ }
19444
+ async appendTaskRunLog(taskId, runId, entries) {
19445
+ const teamId = this.getTeamId();
19446
+ return this.apiRequest(
19447
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
19448
+ {
19449
+ method: "POST",
19450
+ body: JSON.stringify({ entries })
19451
+ }
19452
+ );
19453
+ }
19454
+ async relayMessage(taskId, runId, text2) {
19455
+ const teamId = this.getTeamId();
19456
+ await this.apiRequest(
19457
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
19458
+ {
19459
+ method: "POST",
19460
+ body: JSON.stringify({ text: text2 })
19461
+ }
19462
+ );
19463
+ }
19464
+ async uploadTaskArtifacts(taskId, runId, artifacts) {
19465
+ if (!artifacts.length) {
19466
+ return [];
19467
+ }
19468
+ const teamId = this.getTeamId();
19469
+ const response = await this.apiRequest(
19470
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
19471
+ {
19472
+ method: "POST",
19473
+ body: JSON.stringify({ artifacts })
19474
+ }
19475
+ );
19476
+ const manifest = response.artifacts ?? [];
19477
+ return manifest.slice(-artifacts.length);
19478
+ }
19479
+ /**
19480
+ * Download artifact content by storage path
19481
+ * Streams the file through the PostHog backend so the sandbox does not need
19482
+ * direct access to object storage.
19483
+ */
19484
+ async downloadArtifact(taskId, runId, storagePath) {
19485
+ const teamId = this.getTeamId();
19486
+ try {
19487
+ const response = await this.performRequestWithRetry(
19488
+ `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/download/`,
19489
+ {
19490
+ method: "POST",
19491
+ body: JSON.stringify({ storage_path: storagePath })
19492
+ }
19493
+ );
19494
+ if (!response.ok) {
19495
+ throw new Error(`Failed to download artifact: ${response.status}`);
19496
+ }
19497
+ return response.arrayBuffer();
19498
+ } catch {
19499
+ return null;
19500
+ }
19501
+ }
19502
+ /**
19503
+ * Fetch logs for a task run via the logs API endpoint
19504
+ * @param taskRun - The task run to fetch logs for
19505
+ * @returns Array of stored entries, or empty array if no logs available
19506
+ */
19507
+ async fetchTaskRunLogs(taskRun) {
19508
+ const teamId = this.getTeamId();
19509
+ const endpoint = `/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`;
19510
+ try {
19511
+ const response = await this.performRequestWithRetry(endpoint);
19512
+ if (!response.ok) {
19513
+ if (response.status === 404) {
19514
+ return [];
19515
+ }
19516
+ throw new Error(
19517
+ `Failed to fetch logs: ${response.status} ${response.statusText}`
19518
+ );
19519
+ }
19520
+ const content = await response.text();
19521
+ if (!content.trim()) {
19522
+ return [];
19523
+ }
19524
+ return content.trim().split("\n").map((line) => JSON.parse(line));
19525
+ } catch (error) {
19526
+ throw new Error(
19527
+ `Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`
19528
+ );
19529
+ }
19530
+ }
19531
+ };
19532
+
19533
+ // src/adapters/claude/session/jsonl-hydration.ts
19534
+ var import_node_crypto3 = require("crypto");
19535
+ var fs11 = __toESM(require("fs/promises"), 1);
19536
+ var os6 = __toESM(require("os"), 1);
19537
+ var path14 = __toESM(require("path"), 1);
19538
+ var CHARS_PER_TOKEN = 4;
19539
+ var DEFAULT_MAX_TOKENS = 15e4;
19540
+ function estimateTurnTokens(turn) {
19541
+ let chars = 0;
19542
+ for (const block of turn.content) {
19543
+ if ("text" in block && typeof block.text === "string") {
19544
+ chars += block.text.length;
19545
+ }
19546
+ }
19547
+ if (turn.toolCalls) {
19548
+ for (const tc of turn.toolCalls) {
19549
+ chars += JSON.stringify(tc.input ?? "").length;
19550
+ if (tc.result !== void 0) {
19551
+ chars += typeof tc.result === "string" ? tc.result.length : JSON.stringify(tc.result).length;
19552
+ }
19553
+ }
19554
+ }
19555
+ return Math.ceil(chars / CHARS_PER_TOKEN);
19556
+ }
19557
+ function selectRecentTurns(turns, maxTokens = DEFAULT_MAX_TOKENS) {
19558
+ let budget = maxTokens;
19559
+ let startIndex = turns.length;
19560
+ for (let i2 = turns.length - 1; i2 >= 0; i2--) {
19561
+ const cost = estimateTurnTokens(turns[i2]);
19562
+ if (cost > budget) break;
19563
+ budget -= cost;
19564
+ startIndex = i2;
19565
+ }
19566
+ while (startIndex < turns.length && turns[startIndex].role !== "user") {
19567
+ startIndex++;
19568
+ }
19569
+ return turns.slice(startIndex);
19570
+ }
19571
+
19572
+ // src/sagas/apply-snapshot-saga.ts
19573
+ var import_promises4 = require("fs/promises");
19574
+ var import_node_path7 = require("path");
19575
+
19576
+ // ../git/dist/sagas/tree.js
19577
+ var import_node_fs3 = require("fs");
19578
+ var fs12 = __toESM(require("fs/promises"), 1);
19579
+ var path15 = __toESM(require("path"), 1);
19580
+ var tar = __toESM(require("tar"), 1);
19581
+ var CaptureTreeSaga = class extends GitSaga {
19582
+ sagaName = "CaptureTreeSaga";
19583
+ tempIndexPath = null;
19584
+ async executeGitOperations(input) {
19585
+ const { baseDir, lastTreeHash, archivePath, signal } = input;
19586
+ const tmpDir = path15.join(baseDir, ".git", "posthog-code-tmp");
19587
+ await this.step({
19588
+ name: "create_tmp_dir",
19589
+ execute: () => fs12.mkdir(tmpDir, { recursive: true }),
18754
19590
  rollback: async () => {
18755
19591
  }
18756
19592
  });
18757
- this.tempIndexPath = path13.join(tmpDir, `index-${Date.now()}`);
19593
+ this.tempIndexPath = path15.join(tmpDir, `index-${Date.now()}`);
18758
19594
  const tempIndexGit = this.git.env({
18759
19595
  ...process.env,
18760
19596
  GIT_INDEX_FILE: this.tempIndexPath
@@ -18764,7 +19600,7 @@ var CaptureTreeSaga = class extends GitSaga {
18764
19600
  execute: () => tempIndexGit.raw(["read-tree", "HEAD"]),
18765
19601
  rollback: async () => {
18766
19602
  if (this.tempIndexPath) {
18767
- await fs11.rm(this.tempIndexPath, { force: true }).catch(() => {
19603
+ await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
18768
19604
  });
18769
19605
  }
18770
19606
  }
@@ -18773,7 +19609,7 @@ var CaptureTreeSaga = class extends GitSaga {
18773
19609
  const treeHash = await this.readOnlyStep("write_tree", () => tempIndexGit.raw(["write-tree"]));
18774
19610
  if (lastTreeHash && treeHash === lastTreeHash) {
18775
19611
  this.log.debug("No changes since last capture", { treeHash });
18776
- await fs11.rm(this.tempIndexPath, { force: true }).catch(() => {
19612
+ await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
18777
19613
  });
18778
19614
  return { snapshot: null, changed: false };
18779
19615
  }
@@ -18785,7 +19621,7 @@ var CaptureTreeSaga = class extends GitSaga {
18785
19621
  }
18786
19622
  });
18787
19623
  const changes = await this.readOnlyStep("get_changes", () => this.getChanges(this.git, baseCommit, treeHash));
18788
- await fs11.rm(this.tempIndexPath, { force: true }).catch(() => {
19624
+ await fs12.rm(this.tempIndexPath, { force: true }).catch(() => {
18789
19625
  });
18790
19626
  const snapshot = {
18791
19627
  treeHash,
@@ -18809,15 +19645,15 @@ var CaptureTreeSaga = class extends GitSaga {
18809
19645
  if (filesToArchive.length === 0) {
18810
19646
  return void 0;
18811
19647
  }
18812
- const existingFiles = filesToArchive.filter((f) => (0, import_node_fs3.existsSync)(path13.join(baseDir, f)));
19648
+ const existingFiles = filesToArchive.filter((f) => (0, import_node_fs3.existsSync)(path15.join(baseDir, f)));
18813
19649
  if (existingFiles.length === 0) {
18814
19650
  return void 0;
18815
19651
  }
18816
19652
  await this.step({
18817
19653
  name: "create_archive",
18818
19654
  execute: async () => {
18819
- const archiveDir = path13.dirname(archivePath);
18820
- await fs11.mkdir(archiveDir, { recursive: true });
19655
+ const archiveDir = path15.dirname(archivePath);
19656
+ await fs12.mkdir(archiveDir, { recursive: true });
18821
19657
  await tar.create({
18822
19658
  gzip: true,
18823
19659
  file: archivePath,
@@ -18825,7 +19661,7 @@ var CaptureTreeSaga = class extends GitSaga {
18825
19661
  }, existingFiles);
18826
19662
  },
18827
19663
  rollback: async () => {
18828
- await fs11.rm(archivePath, { force: true }).catch(() => {
19664
+ await fs12.rm(archivePath, { force: true }).catch(() => {
18829
19665
  });
18830
19666
  }
18831
19667
  });
@@ -18925,9 +19761,9 @@ var ApplyTreeSaga = class extends GitSaga {
18925
19761
  const filesToExtract = changes.filter((c) => c.status !== "D").map((c) => c.path);
18926
19762
  await this.readOnlyStep("backup_existing_files", async () => {
18927
19763
  for (const filePath of filesToExtract) {
18928
- const fullPath = path13.join(baseDir, filePath);
19764
+ const fullPath = path15.join(baseDir, filePath);
18929
19765
  try {
18930
- const content = await fs11.readFile(fullPath);
19766
+ const content = await fs12.readFile(fullPath);
18931
19767
  this.fileBackups.set(filePath, content);
18932
19768
  } catch {
18933
19769
  }
@@ -18944,16 +19780,16 @@ var ApplyTreeSaga = class extends GitSaga {
18944
19780
  },
18945
19781
  rollback: async () => {
18946
19782
  for (const filePath of this.extractedFiles) {
18947
- const fullPath = path13.join(baseDir, filePath);
19783
+ const fullPath = path15.join(baseDir, filePath);
18948
19784
  const backup = this.fileBackups.get(filePath);
18949
19785
  if (backup) {
18950
- const dir = path13.dirname(fullPath);
18951
- await fs11.mkdir(dir, { recursive: true }).catch(() => {
19786
+ const dir = path15.dirname(fullPath);
19787
+ await fs12.mkdir(dir, { recursive: true }).catch(() => {
18952
19788
  });
18953
- await fs11.writeFile(fullPath, backup).catch(() => {
19789
+ await fs12.writeFile(fullPath, backup).catch(() => {
18954
19790
  });
18955
19791
  } else {
18956
- await fs11.rm(fullPath, { force: true }).catch(() => {
19792
+ await fs12.rm(fullPath, { force: true }).catch(() => {
18957
19793
  });
18958
19794
  }
18959
19795
  }
@@ -18961,10 +19797,10 @@ var ApplyTreeSaga = class extends GitSaga {
18961
19797
  });
18962
19798
  }
18963
19799
  for (const change of changes.filter((c) => c.status === "D")) {
18964
- const fullPath = path13.join(baseDir, change.path);
19800
+ const fullPath = path15.join(baseDir, change.path);
18965
19801
  const backupContent = await this.readOnlyStep(`backup_${change.path}`, async () => {
18966
19802
  try {
18967
- return await fs11.readFile(fullPath);
19803
+ return await fs12.readFile(fullPath);
18968
19804
  } catch {
18969
19805
  return null;
18970
19806
  }
@@ -18972,15 +19808,15 @@ var ApplyTreeSaga = class extends GitSaga {
18972
19808
  await this.step({
18973
19809
  name: `delete_${change.path}`,
18974
19810
  execute: async () => {
18975
- await fs11.rm(fullPath, { force: true });
19811
+ await fs12.rm(fullPath, { force: true });
18976
19812
  this.log.debug(`Deleted file: ${change.path}`);
18977
19813
  },
18978
19814
  rollback: async () => {
18979
19815
  if (backupContent) {
18980
- const dir = path13.dirname(fullPath);
18981
- await fs11.mkdir(dir, { recursive: true }).catch(() => {
19816
+ const dir = path15.dirname(fullPath);
19817
+ await fs12.mkdir(dir, { recursive: true }).catch(() => {
18982
19818
  });
18983
- await fs11.writeFile(fullPath, backupContent).catch(() => {
19819
+ await fs12.writeFile(fullPath, backupContent).catch(() => {
18984
19820
  });
18985
19821
  }
18986
19822
  }
@@ -19003,18 +19839,18 @@ var ApplySnapshotSaga = class extends Saga {
19003
19839
  archivePath = null;
19004
19840
  async execute(input) {
19005
19841
  const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
19006
- const tmpDir = (0, import_node_path5.join)(repositoryPath, ".posthog", "tmp");
19842
+ const tmpDir = (0, import_node_path7.join)(repositoryPath, ".posthog", "tmp");
19007
19843
  if (!snapshot.archiveUrl) {
19008
19844
  throw new Error("Cannot apply snapshot: no archive URL");
19009
19845
  }
19010
19846
  const archiveUrl = snapshot.archiveUrl;
19011
19847
  await this.step({
19012
19848
  name: "create_tmp_dir",
19013
- execute: () => (0, import_promises2.mkdir)(tmpDir, { recursive: true }),
19849
+ execute: () => (0, import_promises4.mkdir)(tmpDir, { recursive: true }),
19014
19850
  rollback: async () => {
19015
19851
  }
19016
19852
  });
19017
- const archivePath = (0, import_node_path5.join)(tmpDir, `${snapshot.treeHash}.tar.gz`);
19853
+ const archivePath = (0, import_node_path7.join)(tmpDir, `${snapshot.treeHash}.tar.gz`);
19018
19854
  this.archivePath = archivePath;
19019
19855
  await this.step({
19020
19856
  name: "download_archive",
@@ -19029,11 +19865,18 @@ var ApplySnapshotSaga = class extends Saga {
19029
19865
  }
19030
19866
  const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
19031
19867
  const binaryContent = Buffer.from(base64Content, "base64");
19032
- await (0, import_promises2.writeFile)(archivePath, binaryContent);
19868
+ await (0, import_promises4.writeFile)(archivePath, binaryContent);
19869
+ this.log.info("Tree archive downloaded", {
19870
+ treeHash: snapshot.treeHash,
19871
+ snapshotBytes: binaryContent.byteLength,
19872
+ snapshotWireBytes: arrayBuffer.byteLength,
19873
+ totalBytes: binaryContent.byteLength,
19874
+ totalWireBytes: arrayBuffer.byteLength
19875
+ });
19033
19876
  },
19034
19877
  rollback: async () => {
19035
19878
  if (this.archivePath) {
19036
- await (0, import_promises2.rm)(this.archivePath, { force: true }).catch(() => {
19879
+ await (0, import_promises4.rm)(this.archivePath, { force: true }).catch(() => {
19037
19880
  });
19038
19881
  }
19039
19882
  }
@@ -19049,7 +19892,7 @@ var ApplySnapshotSaga = class extends Saga {
19049
19892
  if (!applyResult.success) {
19050
19893
  throw new Error(`Failed to apply tree: ${applyResult.error}`);
19051
19894
  }
19052
- await (0, import_promises2.rm)(this.archivePath, { force: true }).catch(() => {
19895
+ await (0, import_promises4.rm)(this.archivePath, { force: true }).catch(() => {
19053
19896
  });
19054
19897
  this.log.info("Tree snapshot applied", {
19055
19898
  treeHash: snapshot.treeHash,
@@ -19062,8 +19905,8 @@ var ApplySnapshotSaga = class extends Saga {
19062
19905
 
19063
19906
  // src/sagas/capture-tree-saga.ts
19064
19907
  var import_node_fs4 = require("fs");
19065
- var import_promises3 = require("fs/promises");
19066
- var import_node_path6 = require("path");
19908
+ var import_promises5 = require("fs/promises");
19909
+ var import_node_path8 = require("path");
19067
19910
  var CaptureTreeSaga2 = class extends Saga {
19068
19911
  sagaName = "CaptureTreeSaga";
19069
19912
  async execute(input) {
@@ -19075,14 +19918,14 @@ var CaptureTreeSaga2 = class extends Saga {
19075
19918
  taskId,
19076
19919
  runId
19077
19920
  } = 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"))) {
19921
+ const tmpDir = (0, import_node_path8.join)(repositoryPath, ".posthog", "tmp");
19922
+ if ((0, import_node_fs4.existsSync)((0, import_node_path8.join)(repositoryPath, ".gitmodules"))) {
19080
19923
  this.log.warn(
19081
19924
  "Repository has submodules - snapshot may not capture submodule state"
19082
19925
  );
19083
19926
  }
19084
19927
  const shouldArchive = !!apiClient;
19085
- const archivePath = shouldArchive ? (0, import_node_path6.join)(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
19928
+ const archivePath = shouldArchive ? (0, import_node_path8.join)(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
19086
19929
  const gitCaptureSaga = new CaptureTreeSaga(this.log);
19087
19930
  const captureResult = await gitCaptureSaga.run({
19088
19931
  baseDir: repositoryPath,
@@ -19112,7 +19955,7 @@ var CaptureTreeSaga2 = class extends Saga {
19112
19955
  runId
19113
19956
  );
19114
19957
  } finally {
19115
- await (0, import_promises3.rm)(createdArchivePath, { force: true }).catch(() => {
19958
+ await (0, import_promises5.rm)(createdArchivePath, { force: true }).catch(() => {
19116
19959
  });
19117
19960
  }
19118
19961
  }
@@ -19136,8 +19979,10 @@ var CaptureTreeSaga2 = class extends Saga {
19136
19979
  const archiveUrl = await this.step({
19137
19980
  name: "upload_archive",
19138
19981
  execute: async () => {
19139
- const archiveContent = await (0, import_promises3.readFile)(archivePath);
19982
+ const archiveContent = await (0, import_promises5.readFile)(archivePath);
19140
19983
  const base64Content = archiveContent.toString("base64");
19984
+ const snapshotBytes = archiveContent.byteLength;
19985
+ const snapshotWireBytes = Buffer.byteLength(base64Content, "utf-8");
19141
19986
  const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
19142
19987
  {
19143
19988
  name: `trees/${treeHash}.tar.gz`,
@@ -19146,17 +19991,22 @@ var CaptureTreeSaga2 = class extends Saga {
19146
19991
  content_type: "application/gzip"
19147
19992
  }
19148
19993
  ]);
19149
- if (artifacts.length > 0 && artifacts[0].storage_path) {
19994
+ const uploadedArtifact = artifacts[0];
19995
+ if (uploadedArtifact?.storage_path) {
19150
19996
  this.log.info("Tree archive uploaded", {
19151
- storagePath: artifacts[0].storage_path,
19152
- treeHash
19997
+ storagePath: uploadedArtifact.storage_path,
19998
+ treeHash,
19999
+ snapshotBytes,
20000
+ snapshotWireBytes,
20001
+ totalBytes: snapshotBytes,
20002
+ totalWireBytes: snapshotWireBytes
19153
20003
  });
19154
- return artifacts[0].storage_path;
20004
+ return uploadedArtifact.storage_path;
19155
20005
  }
19156
20006
  return void 0;
19157
20007
  },
19158
20008
  rollback: async () => {
19159
- await (0, import_promises3.rm)(archivePath, { force: true }).catch(() => {
20009
+ await (0, import_promises5.rm)(archivePath, { force: true }).catch(() => {
19160
20010
  });
19161
20011
  }
19162
20012
  });
@@ -19284,6 +20134,10 @@ var ResumeSaga = class extends Saga {
19284
20134
  "find_snapshot",
19285
20135
  () => Promise.resolve(this.findLatestTreeSnapshot(entries))
19286
20136
  );
20137
+ const latestGitCheckpoint = await this.readOnlyStep(
20138
+ "find_git_checkpoint",
20139
+ () => Promise.resolve(this.findLatestGitCheckpoint(entries))
20140
+ );
19287
20141
  let snapshotApplied = false;
19288
20142
  if (latestSnapshot?.archiveUrl && repositoryPath) {
19289
20143
  this.log.info("Found tree snapshot", {
@@ -19356,6 +20210,7 @@ var ResumeSaga = class extends Saga {
19356
20210
  return {
19357
20211
  conversation,
19358
20212
  latestSnapshot,
20213
+ latestGitCheckpoint,
19359
20214
  snapshotApplied,
19360
20215
  interrupted: latestSnapshot?.interrupted ?? false,
19361
20216
  lastDevice,
@@ -19366,6 +20221,7 @@ var ResumeSaga = class extends Saga {
19366
20221
  return {
19367
20222
  conversation: [],
19368
20223
  latestSnapshot: null,
20224
+ latestGitCheckpoint: null,
19369
20225
  snapshotApplied: false,
19370
20226
  interrupted: false,
19371
20227
  logEntryCount: 0
@@ -19386,6 +20242,20 @@ var ResumeSaga = class extends Saga {
19386
20242
  }
19387
20243
  return null;
19388
20244
  }
20245
+ findLatestGitCheckpoint(entries) {
20246
+ const sdkPrefixedMethod = `_${POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT}`;
20247
+ for (let i2 = entries.length - 1; i2 >= 0; i2--) {
20248
+ const entry = entries[i2];
20249
+ const method = entry.notification?.method;
20250
+ if (method === sdkPrefixedMethod || method === POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT) {
20251
+ const params = entry.notification?.params;
20252
+ if (params?.checkpointId && params?.checkpointRef) {
20253
+ return params;
20254
+ }
20255
+ }
20256
+ }
20257
+ return null;
20258
+ }
19389
20259
  findLastDeviceInfo(entries) {
19390
20260
  for (let i2 = entries.length - 1; i2 >= 0; i2--) {
19391
20261
  const entry = entries[i2];
@@ -19534,6 +20404,7 @@ async function resumeFromLog(config) {
19534
20404
  return {
19535
20405
  conversation: result.data.conversation,
19536
20406
  latestSnapshot: result.data.latestSnapshot,
20407
+ latestGitCheckpoint: result.data.latestGitCheckpoint,
19537
20408
  snapshotApplied: result.data.snapshotApplied,
19538
20409
  interrupted: result.data.interrupted,
19539
20410
  lastDevice: result.data.lastDevice,
@@ -19574,8 +20445,8 @@ ${toolSummary}`);
19574
20445
 
19575
20446
  // src/session-log-writer.ts
19576
20447
  var import_node_fs5 = __toESM(require("fs"), 1);
19577
- var import_promises4 = __toESM(require("fs/promises"), 1);
19578
- var import_node_path7 = __toESM(require("path"), 1);
20448
+ var import_promises6 = __toESM(require("fs/promises"), 1);
20449
+ var import_node_path9 = __toESM(require("path"), 1);
19579
20450
  var SessionLogWriter = class _SessionLogWriter {
19580
20451
  static FLUSH_DEBOUNCE_MS = 500;
19581
20452
  static FLUSH_MAX_INTERVAL_MS = 5e3;
@@ -19611,7 +20482,7 @@ var SessionLogWriter = class _SessionLogWriter {
19611
20482
  this.sessions.set(sessionId, { context, currentTurnMessages: [] });
19612
20483
  this.lastFlushAttemptTime.set(sessionId, Date.now());
19613
20484
  if (this.localCachePath) {
19614
- const sessionDir = import_node_path7.default.join(
20485
+ const sessionDir = import_node_path9.default.join(
19615
20486
  this.localCachePath,
19616
20487
  "sessions",
19617
20488
  context.runId
@@ -19869,7 +20740,7 @@ var SessionLogWriter = class _SessionLogWriter {
19869
20740
  if (!this.localCachePath) return;
19870
20741
  const session = this.sessions.get(sessionId);
19871
20742
  if (!session) return;
19872
- const logPath = import_node_path7.default.join(
20743
+ const logPath = import_node_path9.default.join(
19873
20744
  this.localCachePath,
19874
20745
  "sessions",
19875
20746
  session.context.runId,
@@ -19888,17 +20759,17 @@ var SessionLogWriter = class _SessionLogWriter {
19888
20759
  }
19889
20760
  }
19890
20761
  static async cleanupOldSessions(localCachePath) {
19891
- const sessionsDir = import_node_path7.default.join(localCachePath, "sessions");
20762
+ const sessionsDir = import_node_path9.default.join(localCachePath, "sessions");
19892
20763
  let deleted = 0;
19893
20764
  try {
19894
- const entries = await import_promises4.default.readdir(sessionsDir);
20765
+ const entries = await import_promises6.default.readdir(sessionsDir);
19895
20766
  const now = Date.now();
19896
20767
  for (const entry of entries) {
19897
- const entryPath = import_node_path7.default.join(sessionsDir, entry);
20768
+ const entryPath = import_node_path9.default.join(sessionsDir, entry);
19898
20769
  try {
19899
- const stats = await import_promises4.default.stat(entryPath);
20770
+ const stats = await import_promises6.default.stat(entryPath);
19900
20771
  if (stats.isDirectory() && now - stats.birthtimeMs > _SessionLogWriter.SESSIONS_MAX_AGE_MS) {
19901
- await import_promises4.default.rm(entryPath, { recursive: true, force: true });
20772
+ await import_promises6.default.rm(entryPath, { recursive: true, force: true });
19902
20773
  deleted++;
19903
20774
  }
19904
20775
  } catch {
@@ -19976,6 +20847,14 @@ var httpHeaderSchema = import_v4.z.object({
19976
20847
  name: import_v4.z.string(),
19977
20848
  value: import_v4.z.string()
19978
20849
  });
20850
+ var nullishString = import_v4.z.string().nullish().transform((value) => value ?? null);
20851
+ var handoffLocalGitStateSchema = import_v4.z.object({
20852
+ head: nullishString,
20853
+ branch: nullishString,
20854
+ upstreamHead: nullishString,
20855
+ upstreamRemote: nullishString,
20856
+ upstreamMergeRef: nullishString
20857
+ });
19979
20858
  var remoteMcpServerSchema = import_v4.z.object({
19980
20859
  type: import_v4.z.enum(["http", "sse"]),
19981
20860
  name: import_v4.z.string().min(1, "MCP server name is required"),
@@ -20027,13 +20906,16 @@ var setConfigOptionParamsSchema = import_v4.z.object({
20027
20906
  var refreshSessionParamsSchema = import_v4.z.object({
20028
20907
  mcpServers: mcpServersSchema
20029
20908
  });
20909
+ var closeParamsSchema = import_v4.z.object({
20910
+ localGitState: handoffLocalGitStateSchema.optional()
20911
+ }).optional();
20030
20912
  var commandParamsSchemas = {
20031
20913
  user_message: userMessageParamsSchema,
20032
20914
  "posthog/user_message": userMessageParamsSchema,
20033
20915
  cancel: import_v4.z.object({}).optional(),
20034
20916
  "posthog/cancel": import_v4.z.object({}).optional(),
20035
- close: import_v4.z.object({}).optional(),
20036
- "posthog/close": import_v4.z.object({}).optional(),
20917
+ close: closeParamsSchema,
20918
+ "posthog/close": closeParamsSchema,
20037
20919
  permission_response: permissionResponseParamsSchema,
20038
20920
  "posthog/permission_response": permissionResponseParamsSchema,
20039
20921
  set_config_option: setConfigOptionParamsSchema,
@@ -20355,7 +21237,7 @@ var AgentServer = class {
20355
21237
  return app;
20356
21238
  }
20357
21239
  async start() {
20358
- await new Promise((resolve6) => {
21240
+ await new Promise((resolve7) => {
20359
21241
  this.server = (0, import_node_server.serve)(
20360
21242
  {
20361
21243
  fetch: this.app.fetch,
@@ -20365,7 +21247,7 @@ var AgentServer = class {
20365
21247
  this.logger.debug(
20366
21248
  `HTTP server listening on port ${this.config.port}`
20367
21249
  );
20368
- resolve6();
21250
+ resolve7();
20369
21251
  }
20370
21252
  );
20371
21253
  });
@@ -20519,6 +21401,10 @@ var AgentServer = class {
20519
21401
  case POSTHOG_NOTIFICATIONS.CLOSE:
20520
21402
  case "close": {
20521
21403
  this.logger.debug("Close requested");
21404
+ const localGitState = this.extractHandoffLocalGitState(params);
21405
+ if (localGitState && this.session) {
21406
+ this.session.pendingHandoffGitState = localGitState;
21407
+ }
20522
21408
  await this.cleanupSession();
20523
21409
  return { closed: true };
20524
21410
  }
@@ -20745,7 +21631,8 @@ var AgentServer = class {
20745
21631
  deviceInfo,
20746
21632
  logWriter,
20747
21633
  permissionMode: initialPermissionMode,
20748
- hasDesktopConnected: sseController !== null
21634
+ hasDesktopConnected: sseController !== null,
21635
+ pendingHandoffGitState: void 0
20749
21636
  };
20750
21637
  this.logger = new Logger({
20751
21638
  debug: true,
@@ -21070,23 +21957,23 @@ Continue from where you left off. The user is waiting for your response.`
21070
21957
  throw new Error(`Failed to download artifact ${artifact.name}`);
21071
21958
  }
21072
21959
  const safeName = this.getSafeArtifactName(artifact.name);
21073
- const artifactDir = (0, import_node_path8.join)(
21960
+ const artifactDir = (0, import_node_path10.join)(
21074
21961
  this.config.repositoryPath ?? "/tmp/workspace",
21075
21962
  ".posthog",
21076
21963
  "attachments",
21077
21964
  runId,
21078
21965
  artifact.id ?? safeName
21079
21966
  );
21080
- await (0, import_promises5.mkdir)(artifactDir, { recursive: true });
21081
- const artifactPath = (0, import_node_path8.join)(artifactDir, safeName);
21082
- await (0, import_promises5.writeFile)(artifactPath, Buffer.from(data));
21967
+ await (0, import_promises7.mkdir)(artifactDir, { recursive: true });
21968
+ const artifactPath = (0, import_node_path10.join)(artifactDir, safeName);
21969
+ await (0, import_promises7.writeFile)(artifactPath, Buffer.from(data));
21083
21970
  return resourceLink((0, import_node_url2.pathToFileURL)(artifactPath).toString(), artifact.name, {
21084
21971
  ...artifact.content_type ? { mimeType: artifact.content_type } : {},
21085
21972
  ...typeof artifact.size === "number" ? { size: artifact.size } : {}
21086
21973
  });
21087
21974
  }
21088
21975
  getSafeArtifactName(name2) {
21089
- const baseName = (0, import_node_path8.basename)(name2).trim();
21976
+ const baseName = (0, import_node_path10.basename)(name2).trim();
21090
21977
  const normalizedName = baseName.replace(/[^\w.-]/g, "_");
21091
21978
  return normalizedName.length > 0 ? normalizedName : "attachment";
21092
21979
  }
@@ -21578,6 +22465,11 @@ ${attributionInstructions}
21578
22465
  async cleanupSession() {
21579
22466
  if (!this.session) return;
21580
22467
  this.logger.debug("Cleaning up session");
22468
+ try {
22469
+ await this.captureHandoffCheckpoint();
22470
+ } catch (error) {
22471
+ this.logger.error("Failed to capture handoff checkpoint", error);
22472
+ }
21581
22473
  try {
21582
22474
  await this.captureTreeState();
21583
22475
  } catch (error) {
@@ -21637,6 +22529,50 @@ ${attributionInstructions}
21637
22529
  this.logger.error("Failed to capture tree state", error);
21638
22530
  }
21639
22531
  }
22532
+ async captureHandoffCheckpoint() {
22533
+ if (!this.session?.treeTracker || !this.session.pendingHandoffGitState) {
22534
+ return;
22535
+ }
22536
+ if (!this.posthogAPI) {
22537
+ this.logger.warn(
22538
+ "Skipping handoff checkpoint capture: PostHog API client is not configured"
22539
+ );
22540
+ return;
22541
+ }
22542
+ const tracker = new HandoffCheckpointTracker({
22543
+ repositoryPath: this.config.repositoryPath ?? "/tmp/workspace",
22544
+ taskId: this.session.payload.task_id,
22545
+ runId: this.session.payload.run_id,
22546
+ apiClient: this.posthogAPI,
22547
+ logger: this.logger.child("HandoffCheckpoint")
22548
+ });
22549
+ const checkpoint = await tracker.captureForHandoff(
22550
+ this.session.pendingHandoffGitState
22551
+ );
22552
+ if (!checkpoint) return;
22553
+ const checkpointWithDevice = {
22554
+ ...checkpoint,
22555
+ device: this.session.deviceInfo
22556
+ };
22557
+ const notification = {
22558
+ jsonrpc: "2.0",
22559
+ method: POSTHOG_NOTIFICATIONS.GIT_CHECKPOINT,
22560
+ params: checkpointWithDevice
22561
+ };
22562
+ this.broadcastEvent({
22563
+ type: "notification",
22564
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
22565
+ notification
22566
+ });
22567
+ this.session.logWriter.appendRawLine(
22568
+ this.session.payload.run_id,
22569
+ JSON.stringify(notification)
22570
+ );
22571
+ }
22572
+ extractHandoffLocalGitState(params) {
22573
+ const result = handoffLocalGitStateSchema.safeParse(params.localGitState);
22574
+ return result.success ? result.data : null;
22575
+ }
21640
22576
  broadcastTurnComplete(stopReason) {
21641
22577
  if (!this.session) return;
21642
22578
  this.broadcastEvent({
@@ -21690,8 +22626,8 @@ ${attributionInstructions}
21690
22626
  options: params.options,
21691
22627
  toolCall: params.toolCall
21692
22628
  });
21693
- return new Promise((resolve6) => {
21694
- this.pendingPermissions.set(requestId, { resolve: resolve6 });
22629
+ return new Promise((resolve7) => {
22630
+ this.pendingPermissions.set(requestId, { resolve: resolve7 });
21695
22631
  });
21696
22632
  }
21697
22633
  resolvePermission(requestId, optionId, customInput, answers) {