@agimon-ai/po-file-edit 0.0.0-alpha.44 → 0.0.0-alpha.46

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.
@@ -34,9 +34,8 @@ let node_path = require("node:path");
34
34
  node_path = __toESM(node_path, 1);
35
35
  let _agimon_ai_po_hashline = require("@agimon-ai/po-hashline");
36
36
  let _agimon_ai_po_hashline_files = require("@agimon-ai/po-hashline/files");
37
- let node_fs = require("node:fs");
38
- node_fs = __toESM(node_fs, 1);
39
37
  let _agimon_ai_po_core_childProcess = require("@agimon-ai/po-core/childProcess");
38
+ let _agimon_ai_po_core_platform = require("@agimon-ai/po-core/platform");
40
39
  let _agimon_ai_po_core_telemetry = require("@agimon-ai/po-core/telemetry");
41
40
  let node_crypto = require("node:crypto");
42
41
  let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
@@ -47,7 +46,6 @@ ignore = __toESM(ignore, 1);
47
46
  let node_os = require("node:os");
48
47
  node_os = __toESM(node_os, 1);
49
48
  let _agimon_ai_po_config = require("@agimon-ai/po-config");
50
- let _agimon_ai_po_core_platform = require("@agimon-ai/po-core/platform");
51
49
  let hono = require("hono");
52
50
  //#region src/constants/editPresence.ts
53
51
  const PRESENCE_NOTE_TITLE = "[Po: other active agents edited files in this result]";
@@ -717,17 +715,17 @@ var FileEditPaths = class {
717
715
  * the hub and the session API can find it from another process knowing only
718
716
  * those two things.
719
717
  */
720
- timelinePath(cwd, sessionKey) {
718
+ async timelinePath(cwd, sessionKey) {
721
719
  const directory = this.stateDirectory();
722
- node_fs.default.mkdirSync(directory, { recursive: true });
723
- return node_path.default.join(directory, `${hash(node_fs.default.realpathSync(cwd))}-${hash(sessionKey)}.jsonl`);
720
+ await node_fs_promises.default.mkdir(directory, { recursive: true });
721
+ return node_path.default.join(directory, `${hash(await node_fs_promises.default.realpath(cwd))}-${hash(sessionKey)}.jsonl`);
724
722
  }
725
723
  /**
726
724
  * Where this session's content snapshots live: beside its timeline and named
727
725
  * after it, so the two are found together and cleared together.
728
726
  */
729
- snapshotsPath(cwd, sessionKey) {
730
- return `${this.timelinePath(cwd, sessionKey).replace(/\.jsonl$/u, "")}.blobs`;
727
+ async snapshotsPath(cwd, sessionKey) {
728
+ return `${(await this.timelinePath(cwd, sessionKey)).replace(/\.jsonl$/u, "")}.blobs`;
731
729
  }
732
730
  };
733
731
  //#endregion
@@ -1576,8 +1574,15 @@ function createFileEditsApi(options = {}) {
1576
1574
  const app = new hono.Hono();
1577
1575
  if (options.sessionId !== void 0 && cwd !== "") {
1578
1576
  const sessionKey = paths.sessionKey(options.sessionId);
1579
- if (options.timeline === void 0) timeline.initialize(paths.timelinePath(cwd, sessionKey));
1580
- if (options.snapshots === void 0) snapshots.initialize(paths.snapshotsPath(cwd, sessionKey));
1577
+ let initializing;
1578
+ app.use(async (_context, next) => {
1579
+ initializing ??= (async () => {
1580
+ if (options.timeline === void 0) timeline.initialize(await paths.timelinePath(cwd, sessionKey));
1581
+ if (options.snapshots === void 0) snapshots.initialize(await paths.snapshotsPath(cwd, sessionKey));
1582
+ })();
1583
+ await initializing;
1584
+ await next();
1585
+ });
1581
1586
  }
1582
1587
  /**
1583
1588
  * The file a request names, rejected unless this session actually recorded a
@@ -1834,9 +1839,9 @@ async function sweepSessionState(options) {
1834
1839
  * startup.
1835
1840
  */
1836
1841
  /** Whether the file is still there to be opened. */
1837
- function stillExists(filePath) {
1842
+ async function stillExists(filePath) {
1838
1843
  try {
1839
- return node_fs.default.statSync(filePath).isFile();
1844
+ return (await node_fs_promises.default.stat(filePath)).isFile();
1840
1845
  } catch {
1841
1846
  return false;
1842
1847
  }
@@ -1851,15 +1856,17 @@ function stillExists(filePath) {
1851
1856
  * is gone" is a dead end; the timeline still holds the change, so a tab already
1852
1857
  * open on the file keeps working and says so.
1853
1858
  */
1854
- function readSessionFiles(timelinePath, cwd) {
1859
+ async function readSessionFiles(timelinePath, cwd) {
1855
1860
  let content;
1856
1861
  try {
1857
- content = node_fs.default.readFileSync(timelinePath, "utf8");
1862
+ content = await node_fs_promises.default.readFile(timelinePath, "utf8");
1858
1863
  } catch {
1859
1864
  return [];
1860
1865
  }
1861
1866
  const events = confirmedChanges(parseTimeline(content));
1862
- const items = foldEntries(events).filter((entry) => stillExists(entry.path)).map((entry) => ({
1867
+ const entries = foldEntries(events);
1868
+ const existing = await Promise.all(entries.map((entry) => stillExists(entry.path)));
1869
+ const items = entries.filter((_entry, index) => existing[index]).map((entry) => ({
1863
1870
  path: entry.path,
1864
1871
  relPath: node_path.default.relative(cwd, entry.path) || entry.path,
1865
1872
  tool: entry.tool,
@@ -1868,7 +1875,7 @@ function readSessionFiles(timelinePath, cwd) {
1868
1875
  diffable: isDiffable(foldVersions(events, entry.path))
1869
1876
  }));
1870
1877
  try {
1871
- return filterPoIgnoredFiles(items, node_fs.default.readFileSync(node_path.default.join(cwd, ".poignore"), "utf8"));
1878
+ return filterPoIgnoredFiles(items, await node_fs_promises.default.readFile(node_path.default.join(cwd, ".poignore"), "utf8"));
1872
1879
  } catch {
1873
1880
  return items;
1874
1881
  }
@@ -1894,19 +1901,27 @@ function createFilesChannel() {
1894
1901
  sessionAdded(scope) {
1895
1902
  subscriptions.get(scope.sessionId)?.();
1896
1903
  const paths = new FileEditPaths(scope.environment);
1897
- let timelinePath;
1898
- try {
1899
- timelinePath = paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId));
1900
- } catch {}
1901
- const items = timelinePath === void 0 ? [] : readSessionFiles(timelinePath, scope.cwd);
1902
- latest.set(scope.sessionId, items);
1903
- host.publish(scope.sessionId, { items });
1904
+ let current = true;
1905
+ let receivedDirect = false;
1904
1906
  const unsubscribe = host.directEvents.subscribe(filesChannelType, scope.sessionId, (payload) => {
1905
1907
  if (!filesPayload(payload)) return;
1908
+ receivedDirect = true;
1906
1909
  latest.set(scope.sessionId, payload.items);
1907
1910
  host.publish(scope.sessionId, payload);
1908
1911
  }, { replayLatest: true });
1909
- subscriptions.set(scope.sessionId, unsubscribe);
1912
+ subscriptions.set(scope.sessionId, () => {
1913
+ current = false;
1914
+ unsubscribe();
1915
+ });
1916
+ (async () => {
1917
+ let items = [];
1918
+ try {
1919
+ items = await readSessionFiles(await paths.timelinePath(scope.cwd, paths.sessionKey(scope.sessionId)), scope.cwd);
1920
+ } catch {}
1921
+ if (!current || receivedDirect) return;
1922
+ latest.set(scope.sessionId, items);
1923
+ host.publish(scope.sessionId, { items });
1924
+ })().catch((error) => host.onNotice(`File-edit channel seed failed: ${String(error)}`));
1910
1925
  },
1911
1926
  sessionRemoved(sessionId) {
1912
1927
  subscriptions.get(sessionId)?.();
@@ -1925,9 +1940,11 @@ function createFilesChannel() {
1925
1940
  //#endregion
1926
1941
  //#region src/services/fileEditSession/index.ts
1927
1942
  const PO_IGNORE_FILE = ".poignore";
1928
- function readPoIgnore(cwd) {
1943
+ async function readPoIgnore(cwd, context) {
1929
1944
  try {
1930
- const matcher = createPoIgnoreMatcher(node_fs.default.readFileSync(node_path.default.join(cwd, PO_IGNORE_FILE), "utf8"));
1945
+ const content = await (0, _agimon_ai_po_core_platform.readTextIfExists)((0, _agimon_ai_po_core_platform.requirePoFs)(context), node_path.default.join(cwd, PO_IGNORE_FILE), _agimon_ai_po_core_platform.BACKGROUND_CONTEXT);
1946
+ if (content === void 0) return void 0;
1947
+ const matcher = createPoIgnoreMatcher(content);
1931
1948
  return matcher === void 0 ? void 0 : (filePath) => matcher(node_path.default.relative(cwd, filePath));
1932
1949
  } catch {
1933
1950
  return;
@@ -1937,7 +1954,7 @@ function stringValue(value, key) {
1937
1954
  const candidate = value[key];
1938
1955
  return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
1939
1956
  }
1940
- function createFileEditSession({ agent, host }) {
1957
+ function createFileEditSession({ agent, host, context }) {
1941
1958
  if (!agent) return { api: [api] };
1942
1959
  const environment = Object.freeze({ ...host.context.environment });
1943
1960
  const paths = new FileEditPaths(environment);
@@ -1958,6 +1975,7 @@ function createFileEditSession({ agent, host }) {
1958
1975
  let closed = false;
1959
1976
  let disposal;
1960
1977
  let stopping;
1978
+ let starting;
1961
1979
  let initialized = false;
1962
1980
  const track = (work) => {
1963
1981
  const task = work();
@@ -1975,8 +1993,9 @@ function createFileEditSession({ agent, host }) {
1975
1993
  * string is deliberate: the group declares `hideWhenEmpty`, so a session that
1976
1994
  * has edited nothing publishes a key with no content and stays hidden.
1977
1995
  */
1978
- const publish = (runtime) => {
1979
- const items = readSessionFiles(runtime.timelinePath, runtime.context.cwd);
1996
+ const publish = async (runtime) => {
1997
+ const items = await readSessionFiles(runtime.timelinePath, runtime.context.cwd);
1998
+ if (active !== runtime || closed || stopping) return;
1980
1999
  directEvents.publish(filesChannelType, runtime.context.sessionId, { items });
1981
2000
  runtime.context.client.setStatus(filesStatusKey, items.length === 0 ? "" : `${items.length} ${items.length === 1 ? "file" : "files"}`);
1982
2001
  };
@@ -1985,11 +2004,13 @@ function createFileEditSession({ agent, host }) {
1985
2004
  const runtime = active;
1986
2005
  active = void 0;
1987
2006
  stopping = (async () => {
1988
- await Promise.allSettled(pending);
2007
+ while (pending.size > 0) await Promise.allSettled(pending);
2008
+ const startedRuntime = active;
2009
+ active = void 0;
1989
2010
  await editTracker.flush();
1990
2011
  editTracker.reset();
1991
2012
  toolStartedAt.clear();
1992
- runtime?.context.client.setStatus(filesStatusKey, void 0);
2013
+ (runtime ?? startedRuntime)?.context.client.setStatus(filesStatusKey, void 0);
1993
2014
  })();
1994
2015
  return stopping;
1995
2016
  };
@@ -2017,50 +2038,60 @@ function createFileEditSession({ agent, host }) {
2017
2038
  name: "file-edits",
2018
2039
  start: async (executionContext) => {
2019
2040
  if (closed) throw new Error("File-edit session is closed.");
2041
+ if (starting) return starting;
2020
2042
  if (active) return stopActivity;
2021
2043
  if (stopping) await stopping;
2022
2044
  if (closed) throw new Error("File-edit session is closed.");
2045
+ if (starting) return starting;
2023
2046
  stopping = void 0;
2024
- const sessionKey = paths.sessionKey(executionContext.sessionId);
2025
- const timelinePath = paths.timelinePath(executionContext.cwd, sessionKey);
2026
- const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);
2027
- timeline.initialize(timelinePath);
2028
- snapshots.initialize(snapshotsPath);
2029
- const isIgnored = readPoIgnore(executionContext.cwd);
2030
- editTracker.reset({
2031
- exclude: [
2047
+ starting = track(async () => {
2048
+ const sessionKey = paths.sessionKey(executionContext.sessionId);
2049
+ const timelinePath = await paths.timelinePath(executionContext.cwd, sessionKey);
2050
+ const snapshotsPath = await paths.snapshotsPath(executionContext.cwd, sessionKey);
2051
+ timeline.initialize(timelinePath);
2052
+ snapshots.initialize(snapshotsPath);
2053
+ const isIgnored = await readPoIgnore(executionContext.cwd, context);
2054
+ editTracker.reset({
2055
+ exclude: [
2056
+ timelinePath,
2057
+ `${timelinePath}.lock`,
2058
+ snapshotsPath
2059
+ ],
2060
+ ...isIgnored === void 0 ? {} : { isIgnored }
2061
+ });
2062
+ const runtime = {
2063
+ context: executionContext,
2032
2064
  timelinePath,
2033
- `${timelinePath}.lock`,
2034
2065
  snapshotsPath
2035
- ],
2036
- ...isIgnored === void 0 ? {} : { isIgnored }
2037
- });
2038
- const runtime = {
2039
- context: executionContext,
2040
- timelinePath,
2041
- snapshotsPath
2042
- };
2043
- active = runtime;
2044
- lastRuntime = runtime;
2045
- publish(runtime);
2046
- track(async () => {
2047
- try {
2048
- await sweepSessionState({
2049
- directory: paths.stateDirectory(),
2050
- keep: timelinePath
2051
- });
2052
- } catch {}
2066
+ };
2067
+ active = runtime;
2068
+ lastRuntime = runtime;
2069
+ await publish(runtime);
2070
+ track(async () => {
2071
+ try {
2072
+ await sweepSessionState({
2073
+ directory: paths.stateDirectory(),
2074
+ keep: timelinePath
2075
+ });
2076
+ } catch {}
2077
+ });
2078
+ if (!initialized) {
2079
+ initialized = true;
2080
+ await track(() => telemetry.recordEvent("po_file_edit.timeline_started", { outcome: "initialized" }));
2081
+ }
2082
+ return () => active === runtime ? stopActivity() : Promise.resolve();
2053
2083
  });
2054
- if (!initialized) {
2055
- initialized = true;
2056
- await track(() => telemetry.recordEvent("po_file_edit.timeline_started", { outcome: "initialized" }));
2084
+ try {
2085
+ return await starting;
2086
+ } finally {
2087
+ starting = void 0;
2057
2088
  }
2058
- return () => active === runtime ? stopActivity() : Promise.resolve();
2059
2089
  }
2060
2090
  }],
2061
2091
  hooks: [{
2062
2092
  event: "tool_execution_start",
2063
2093
  handle: (event, executionContext) => {
2094
+ if (closed || stopping) return;
2064
2095
  const runtime = active;
2065
2096
  const toolCallId = stringValue(event, "toolCallId");
2066
2097
  const toolName = stringValue(event, "toolName");
@@ -2079,6 +2110,7 @@ function createFileEditSession({ agent, host }) {
2079
2110
  }, {
2080
2111
  event: "tool_execution_end",
2081
2112
  handle: (event, executionContext) => {
2113
+ if (closed || stopping) return;
2082
2114
  const runtime = active;
2083
2115
  const toolCallId = stringValue(event, "toolCallId");
2084
2116
  const toolName = stringValue(event, "toolName");
@@ -2090,7 +2122,7 @@ function createFileEditSession({ agent, host }) {
2090
2122
  const isError = event.isError === true;
2091
2123
  await editTracker.end(toolCallId, isError, executionContext.cwd);
2092
2124
  if (active !== runtime) return;
2093
- publish(runtime);
2125
+ await publish(runtime);
2094
2126
  await telemetry.recordEvent("po_file_edit.edit_finished", {
2095
2127
  "tool.name": toolName,
2096
2128
  "tool.result.error": isError,