@agimon-ai/po-file-edit 0.0.0-alpha.2 → 0.0.0-alpha.21

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.
@@ -24,56 +24,211 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- let _agimon_ai_po_core_serverFacet = require("@agimon-ai/po-core/serverFacet");
27
+ let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
28
28
  let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
29
- let node_fs = require("node:fs");
30
- node_fs = __toESM(node_fs, 1);
29
+ let _agimon_ai_po_core_childSession = require("@agimon-ai/po-core/childSession");
30
+ let _agimon_ai_po_core_hubChannel = require("@agimon-ai/po-core/hubChannel");
31
+ let node_fs_promises = require("node:fs/promises");
32
+ node_fs_promises = __toESM(node_fs_promises, 1);
31
33
  let node_path = require("node:path");
32
34
  node_path = __toESM(node_path, 1);
35
+ let _agimon_ai_po_hashline = require("@agimon-ai/po-hashline");
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);
33
39
  let _agimon_ai_po_core_childProcess = require("@agimon-ai/po-core/childProcess");
34
40
  let _agimon_ai_po_telemetry = require("@agimon-ai/po-telemetry");
35
- let ignore = require("ignore");
36
- ignore = __toESM(ignore, 1);
37
- let node_child_process = require("node:child_process");
38
41
  let node_crypto = require("node:crypto");
39
42
  let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
43
+ let node_child_process = require("node:child_process");
40
44
  let node_util = require("node:util");
41
- let node_fs_promises = require("node:fs/promises");
42
- node_fs_promises = __toESM(node_fs_promises, 1);
45
+ let ignore = require("ignore");
46
+ ignore = __toESM(ignore, 1);
47
+ let node_os = require("node:os");
48
+ node_os = __toESM(node_os, 1);
49
+ let _agimon_ai_po_config = require("@agimon-ai/po-config");
43
50
  let hono = require("hono");
44
- //#region src/services/doomIgnore/index.ts
45
- function matchablePath(relPath) {
46
- const normalized = relPath.replace(/\\/gu, "/");
47
- if (normalized.length === 0 || normalized.includes("\0") || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) return;
48
- return normalized;
51
+ //#region src/constants/editPresence.ts
52
+ const PRESENCE_NOTE_TITLE = "[Po: other active agents edited files in this result]";
53
+ const PRESENCE_NOTE_GUIDANCE = "Preserve unrelated edits. If coordination would help, you can use intercom to send to the session id, or ask the subagent by name if it is on your team. Contact is optional; this notice does not block editing.";
54
+ //#endregion
55
+ //#region src/services/editPresence/index.ts
56
+ const PATH_TOOLS = /* @__PURE__ */ new Set([
57
+ "read",
58
+ "edit",
59
+ "write"
60
+ ]);
61
+ const EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write"]);
62
+ const MAX_GREP_FILES = 50;
63
+ const MAX_NOTE_LINES = 10;
64
+ const MINUTE_MS = 6e4;
65
+ const SHORT_ID_LENGTH = 8;
66
+ /** Pi's built-in grep prints match rows as `relative/path:line: text`. */
67
+ const PI_GREP_ROW = /^(.+?):(\d+): /u;
68
+ function forgetPresence(ledger, agentKey) {
69
+ for (const [file, entries] of ledger) {
70
+ entries.delete(agentKey);
71
+ if (entries.size === 0) ledger.delete(file);
72
+ }
73
+ }
74
+ function stringArg(args, key) {
75
+ if (typeof args !== "object" || args === null) return void 0;
76
+ const value = args[key];
77
+ return typeof value === "string" && value.length > 0 ? value : void 0;
78
+ }
79
+ function textOf(content) {
80
+ return content.map((part) => part.type === "text" && "text" in part && typeof part.text === "string" ? part.text : "").join("\n");
81
+ }
82
+ async function grepFiles(call) {
83
+ const lines = textOf(call.content).split("\n");
84
+ const headers = lines.flatMap((line) => {
85
+ const header = (0, _agimon_ai_po_hashline.parseFileHeader)(line);
86
+ return header === void 0 ? [] : [(0, _agimon_ai_po_hashline_files.resolveInputPath)(header.path, call.cwd)];
87
+ });
88
+ if (headers.length > 0) return [...new Set(headers)].slice(0, MAX_GREP_FILES);
89
+ const rows = lines.flatMap((line) => {
90
+ const match = PI_GREP_ROW.exec(line)?.[1];
91
+ return match === void 0 ? [] : [match];
92
+ });
93
+ if (rows.length === 0) return [];
94
+ const searchPath = (0, _agimon_ai_po_hashline_files.resolveInputPath)(stringArg(call.args, "path") ?? ".", call.cwd);
95
+ if ((await node_fs_promises.default.stat(searchPath).catch(() => void 0))?.isFile()) return [searchPath];
96
+ return [...new Set(rows.map((row) => node_path.default.resolve(searchPath, row)))].slice(0, MAX_GREP_FILES);
97
+ }
98
+ async function touchedFiles(call) {
99
+ if (PATH_TOOLS.has(call.toolName)) {
100
+ const input = stringArg(call.args, "path");
101
+ return input === void 0 ? [] : [await (0, _agimon_ai_po_hashline_files.resolveReadInputPath)(input, call.cwd)];
102
+ }
103
+ return call.toolName === "grep" ? grepFiles(call) : [];
104
+ }
105
+ function ago(elapsed) {
106
+ const minutes = Math.floor(elapsed / MINUTE_MS);
107
+ return minutes < 1 ? "under 1m ago" : `${minutes}m ago`;
108
+ }
109
+ function liveOthers(ledger, file, self) {
110
+ const entries = ledger.get(file);
111
+ if (entries === void 0) return [];
112
+ const live = [];
113
+ for (const [key, entry] of entries) {
114
+ if (key === self) continue;
115
+ if (entry.agent.isLive()) live.push(entry);
116
+ else entries.delete(key);
117
+ }
118
+ return live.sort((left, right) => right.at - left.at);
119
+ }
120
+ function record(ledger, file, agent, tag, at) {
121
+ const entries = ledger.get(file) ?? /* @__PURE__ */ new Map();
122
+ entries.set(agent.key, {
123
+ agent,
124
+ tag,
125
+ at
126
+ });
127
+ ledger.set(file, entries);
128
+ }
129
+ function renderNote(lines) {
130
+ const shown = lines.slice(0, MAX_NOTE_LINES);
131
+ const more = lines.length - shown.length;
132
+ return [
133
+ PRESENCE_NOTE_TITLE,
134
+ ...shown,
135
+ ...more > 0 ? [`- and ${more} more`] : [],
136
+ PRESENCE_NOTE_GUIDANCE
137
+ ].join("\n");
49
138
  }
50
139
  /**
51
- * Builds the project's ignore test, or nothing when there are no rules.
52
- *
53
- * Shared because the same rules are needed twice: once when deciding what to
54
- * record, and once when deciding what to list. A caller that built its own
55
- * matcher would let the two drift apart.
56
- *
57
- * The test answers false for anything it cannot safely match, so a path that
58
- * escapes the project stays visible rather than being hidden by accident.
140
+ * Notes live editors of the files a tool call touched, then records the caller's own successful edit.
141
+ * Returns the content with one note appended, or undefined when there is nothing to add, so callers
142
+ * never rewrite a result needlessly.
59
143
  */
60
- function createDoomIgnoreMatcher(content) {
61
- try {
62
- if (content.trim().length === 0) return void 0;
63
- const matcher = (0, ignore.default)().add(content);
64
- return (relPath) => {
65
- const candidate = matchablePath(relPath);
66
- return candidate !== void 0 && matcher.ignores(candidate);
67
- };
68
- } catch {
69
- return;
144
+ async function observePresence(ledger, agent, call, now = Date.now()) {
145
+ const recording = EDIT_TOOLS.has(call.toolName) && !call.isError;
146
+ if (!recording && ledger.size === 0) return void 0;
147
+ const lines = [];
148
+ for (const file of await touchedFiles(call)) {
149
+ const real = await node_fs_promises.default.realpath(file).catch(() => void 0);
150
+ if (real === void 0) continue;
151
+ const others = liveOthers(ledger, real, agent.key);
152
+ let bytes;
153
+ if (others.length > 0) {
154
+ bytes = await node_fs_promises.default.readFile(real).catch(() => void 0);
155
+ const current = bytes === void 0 ? void 0 : (0, _agimon_ai_po_hashline_files.computeFileTag)(bytes);
156
+ const shown = (0, _agimon_ai_po_hashline_files.displayPath)(file, call.cwd);
157
+ for (const entry of others) {
158
+ const state = entry.tag === current ? "unchanged" : "changed";
159
+ lines.push(`- ${shown}: ${entry.agent.label()}, ${ago(now - entry.at)}, file ${state} since their edit.`);
160
+ }
161
+ }
162
+ if (recording) {
163
+ bytes ??= await node_fs_promises.default.readFile(real).catch(() => void 0);
164
+ if (bytes !== void 0) record(ledger, real, agent, (0, _agimon_ai_po_hashline_files.computeFileTag)(bytes), now);
165
+ }
70
166
  }
167
+ if (lines.length === 0) return void 0;
168
+ return [...call.content, {
169
+ type: "text",
170
+ text: renderNote(lines)
171
+ }];
71
172
  }
72
- /** Applies project-local gitignore rules while keeping unsafe candidates visible. */
73
- function filterDoomIgnoredFiles(items, content) {
74
- const ignored = createDoomIgnoreMatcher(content);
75
- if (ignored === void 0) return items;
76
- return items.filter((item) => !ignored(item.relPath));
173
+ /** Edit presence for one hub session and the in-process children it spawns. */
174
+ function createSessionPresence(options) {
175
+ const { sessionId, ledger } = options;
176
+ const sessionAgent = {
177
+ key: `session:${sessionId}`,
178
+ label: () => `session ${sessionId}${options.workflowStep ? " (workflow step)" : ""}`,
179
+ contact: sessionId,
180
+ isLive: options.isLive
181
+ };
182
+ const safely = async (work) => {
183
+ try {
184
+ return await work();
185
+ } catch (error) {
186
+ options.onError?.(error);
187
+ return;
188
+ }
189
+ };
190
+ return {
191
+ toolResult: {
192
+ event: "tool_result",
193
+ async handle(event, execution) {
194
+ const toolName = event.toolName;
195
+ if (execution.sessionId !== sessionId || typeof toolName !== "string" || !Array.isArray(event.content)) return;
196
+ const content = event.content;
197
+ const next = await safely(() => observePresence(ledger, sessionAgent, {
198
+ toolName,
199
+ args: event.args,
200
+ content,
201
+ isError: event.isError === true,
202
+ cwd: execution.cwd
203
+ }));
204
+ return next === void 0 ? void 0 : { content: next };
205
+ }
206
+ },
207
+ childHooks: { bind(scope) {
208
+ const { runId, parentSessionId, agent } = scope.request;
209
+ const child = {
210
+ key: `run:${runId}`,
211
+ label: () => `${options.labelFor(runId) ?? `${agent} (run ${runId.slice(0, SHORT_ID_LENGTH)})`} (subagent of session ${parentSessionId})`,
212
+ contact: parentSessionId,
213
+ isLive: () => !scope.signal.aborted
214
+ };
215
+ return {
216
+ beforeTool: () => void 0,
217
+ async afterTool(event) {
218
+ const next = await safely(() => observePresence(ledger, child, {
219
+ toolName: event.toolName,
220
+ args: event.args,
221
+ content: event.content,
222
+ isError: event.isError,
223
+ cwd: scope.request.cwd || options.cwd
224
+ }));
225
+ return next === void 0 ? void 0 : { content: next };
226
+ },
227
+ dispose: async () => forgetPresence(ledger, child.key)
228
+ };
229
+ } },
230
+ dispose: () => forgetPresence(ledger, sessionAgent.key)
231
+ };
77
232
  }
78
233
  //#endregion
79
234
  //#region src/services/lineDiff/index.ts
@@ -523,7 +678,7 @@ var EditTracker = class {
523
678
  //#endregion
524
679
  //#region src/services/fileEditPaths/index.ts
525
680
  /** The directory this package owns, under whichever root holds agent state. */
526
- const STATE_DIRECTORY = "doom-file-edit";
681
+ const STATE_DIRECTORY = "po-file-edit";
527
682
  function hash(value) {
528
683
  return (0, node_crypto.createHash)("sha256").update(value).digest("hex").slice(0, 16);
529
684
  }
@@ -541,7 +696,7 @@ var FileEditPaths = class {
541
696
  }
542
697
  /**
543
698
  * Where every session's state lives: the Pi agent directory, the same root
544
- * every other Doom package writes to, and never inside the working tree.
699
+ * every other Po package writes to, and never inside the working tree.
545
700
  *
546
701
  * An earlier layout put this in the repository's git common directory. That
547
702
  * is git's own storage: nothing in git's lifecycle prunes a directory we add
@@ -571,31 +726,6 @@ var FileEditPaths = class {
571
726
  snapshotsPath(cwd, sessionKey) {
572
727
  return `${this.timelinePath(cwd, sessionKey).replace(/\.jsonl$/u, "")}.blobs`;
573
728
  }
574
- legacyStateDirectory(cwd) {
575
- const common = this.gitCommonDirectory(cwd);
576
- return common === void 0 ? void 0 : node_path.default.join(common, STATE_DIRECTORY);
577
- }
578
- /** Only still read so the state an older build left behind can be cleared away. */
579
- gitCommonDirectory(cwd) {
580
- try {
581
- return (0, node_child_process.execFileSync)("git", [
582
- "rev-parse",
583
- "--path-format=absolute",
584
- "--git-common-dir"
585
- ], {
586
- cwd,
587
- encoding: "utf8",
588
- stdio: [
589
- "ignore",
590
- "pipe",
591
- "ignore"
592
- ]
593
- }).trim() || void 0;
594
- } catch (error) {
595
- if (error instanceof Error) return void 0;
596
- throw error;
597
- }
598
- }
599
729
  };
600
730
  //#endregion
601
731
  //#region src/services/gitStatus/index.ts
@@ -683,6 +813,41 @@ var NodeGitStatusAdapter = class {
683
813
  }
684
814
  }
685
815
  };
816
+ //#endregion
817
+ //#region src/services/poIgnore/index.ts
818
+ function matchablePath(relPath) {
819
+ const normalized = relPath.replace(/\\/gu, "/");
820
+ if (normalized.length === 0 || normalized.includes("\0") || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) return;
821
+ return normalized;
822
+ }
823
+ /**
824
+ * Builds the project's ignore test, or nothing when there are no rules.
825
+ *
826
+ * Shared because the same rules are needed twice: once when deciding what to
827
+ * record, and once when deciding what to list. A caller that built its own
828
+ * matcher would let the two drift apart.
829
+ *
830
+ * The test answers false for anything it cannot safely match, so a path that
831
+ * escapes the project stays visible rather than being hidden by accident.
832
+ */
833
+ function createPoIgnoreMatcher(content) {
834
+ try {
835
+ if (content.trim().length === 0) return void 0;
836
+ const matcher = (0, ignore.default)().add(content);
837
+ return (relPath) => {
838
+ const candidate = matchablePath(relPath);
839
+ return candidate !== void 0 && matcher.ignores(candidate);
840
+ };
841
+ } catch {
842
+ return;
843
+ }
844
+ }
845
+ /** Applies project-local gitignore rules while keeping unsafe candidates visible. */
846
+ function filterPoIgnoredFiles(items, content) {
847
+ const ignored = createPoIgnoreMatcher(content);
848
+ if (ignored === void 0) return items;
849
+ return items.filter((item) => !ignored(item.relPath));
850
+ }
686
851
  /** How much of a file is read before deciding it is binary. */
687
852
  const BINARY_SAMPLE_BYTES$1 = 8192;
688
853
  const MISSING_CODES = /* @__PURE__ */ new Set([
@@ -1112,7 +1277,7 @@ var NodeTreeManifestAdapter = class {
1112
1277
  /** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */
1113
1278
  const filesChannelType = "file_edits";
1114
1279
  /** Footer status key whose presence shows the activity group; the extension publishes it. */
1115
- const filesStatusKey = "doom-file-edit-files";
1280
+ const filesStatusKey = "po-file-edit-files";
1116
1281
  //#endregion
1117
1282
  //#region src/types/apiRoutes.ts
1118
1283
  /**
@@ -1399,6 +1564,13 @@ function createFileEditsApi(options = {}) {
1399
1564
  const root = node_path.default.resolve(cwd);
1400
1565
  return isInside(root, filePath) && await isRealPathInside(root, filePath);
1401
1566
  };
1567
+ /** The Po-owned folder holding the file, checked the same two ways as the working directory. */
1568
+ const readableRootOf = async (filePath) => {
1569
+ for (const root of options.readableRoots?.() ?? []) {
1570
+ const resolved = node_path.default.resolve(root);
1571
+ if (isInside(resolved, filePath) && await isRealPathInside(resolved, filePath)) return resolved;
1572
+ }
1573
+ };
1402
1574
  app.get(routes.detail.path, async (context) => {
1403
1575
  const requested = context.req.query(PATH_QUERY_PARAM);
1404
1576
  const versions = await recordedVersions(requested);
@@ -1431,10 +1603,11 @@ function createFileEditsApi(options = {}) {
1431
1603
  if (cwd === "") return context.json({ error: "This session has no working directory to read from." }, 403);
1432
1604
  const root = node_path.default.resolve(cwd);
1433
1605
  const filePath = node_path.default.resolve(root, requested);
1434
- if (!await isInsideSession(filePath)) return context.json({ error: "The path leaves the session directory." }, 403);
1606
+ const inSession = await isInsideSession(filePath);
1607
+ if (!inSession && await readableRootOf(filePath) === void 0) return context.json({ error: "The path leaves the session directory." }, 403);
1435
1608
  const body = {
1436
1609
  path: filePath,
1437
- relPath: node_path.default.relative(root, filePath),
1610
+ relPath: inSession ? node_path.default.relative(root, filePath) : filePath,
1438
1611
  working: await readWorking(filePath)
1439
1612
  };
1440
1613
  return context.json(body);
@@ -1478,10 +1651,15 @@ function createFileEditsApi(options = {}) {
1478
1651
  const api = {
1479
1652
  basePath: API_BASE_PATH,
1480
1653
  start(context) {
1654
+ const environment = context.environment ?? process.env;
1655
+ const cwd = context.cwd;
1481
1656
  const app = createFileEditsApi({
1482
1657
  paths: new FileEditPaths(context.environment),
1483
1658
  ...context.sessionId === void 0 ? {} : { sessionId: context.sessionId },
1484
- ...context.cwd === void 0 ? {} : { cwd: context.cwd }
1659
+ ...cwd === void 0 ? {} : {
1660
+ cwd,
1661
+ readableRoots: () => plansRoots(cwd, environment)
1662
+ }
1485
1663
  });
1486
1664
  return {
1487
1665
  fetch: (request) => app.fetch(request),
@@ -1489,6 +1667,20 @@ const api = {
1489
1667
  };
1490
1668
  }
1491
1669
  };
1670
+ /**
1671
+ * The plans directory Plan mode writes to, resolved the way Plan resolves it, so
1672
+ * a plan link in the transcript opens in the file view. A config that cannot be
1673
+ * read admits nothing extra rather than failing the preview.
1674
+ */
1675
+ function plansRoots(cwd, environment) {
1676
+ const home = environment.HOME ?? node_os.default.homedir();
1677
+ try {
1678
+ const configured = (0, _agimon_ai_po_config.loadPoConfig)(cwd, home, environment).modes?.planning?.plansDirectory;
1679
+ return [(0, _agimon_ai_po_config.resolvePlanningPlansDirectory)(configured, cwd, home)];
1680
+ } catch {
1681
+ return [];
1682
+ }
1683
+ }
1492
1684
  //#endregion
1493
1685
  //#region src/services/sessionStateSweep/index.ts
1494
1686
  /**
@@ -1572,8 +1764,9 @@ async function sweepSessionState(options) {
1572
1764
  //#endregion
1573
1765
  //#region src/services/webFilesChannel/index.ts
1574
1766
  /**
1575
- * The changed-files channel consumes snapshots emitted by the owning session
1576
- * facet. Durable timeline reads seed a channel that starts after session startup.
1767
+ * The changed-files channel consumes snapshots emitted by the owning session's
1768
+ * server plugin. Durable timeline reads seed a channel that starts after session
1769
+ * startup.
1577
1770
  */
1578
1771
  /** Whether the file is still there to be opened. */
1579
1772
  function stillExists(filePath) {
@@ -1610,7 +1803,7 @@ function readSessionFiles(timelinePath, cwd) {
1610
1803
  diffable: isDiffable(foldVersions(events, entry.path))
1611
1804
  }));
1612
1805
  try {
1613
- return filterDoomIgnoredFiles(items, node_fs.default.readFileSync(node_path.default.join(cwd, ".poignore"), "utf8"));
1806
+ return filterPoIgnoredFiles(items, node_fs.default.readFileSync(node_path.default.join(cwd, ".poignore"), "utf8"));
1614
1807
  } catch {
1615
1808
  return items;
1616
1809
  }
@@ -1666,10 +1859,10 @@ function createFilesChannel() {
1666
1859
  }
1667
1860
  //#endregion
1668
1861
  //#region src/services/fileEditSession/index.ts
1669
- const DOOM_IGNORE_FILE = ".poignore";
1670
- function readDoomIgnore(cwd) {
1862
+ const PO_IGNORE_FILE = ".poignore";
1863
+ function readPoIgnore(cwd) {
1671
1864
  try {
1672
- const matcher = createDoomIgnoreMatcher(node_fs.default.readFileSync(node_path.default.join(cwd, DOOM_IGNORE_FILE), "utf8"));
1865
+ const matcher = createPoIgnoreMatcher(node_fs.default.readFileSync(node_path.default.join(cwd, PO_IGNORE_FILE), "utf8"));
1673
1866
  return matcher === void 0 ? void 0 : (filePath) => matcher(node_path.default.relative(cwd, filePath));
1674
1867
  } catch {
1675
1868
  return;
@@ -1686,10 +1879,10 @@ function createFileEditSession({ agent, host }) {
1686
1879
  const timeline = new TimelineStore();
1687
1880
  const snapshots = new NodeSnapshotStoreAdapter();
1688
1881
  const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), { git: new NodeGitStatusAdapter() });
1689
- if (host.context.directEvents === void 0) throw new Error("File-edit headless facet requires the session direct event bus.");
1882
+ if (host.context.directEvents === void 0) throw new Error("File-edit server plugin requires the session direct event bus.");
1690
1883
  const directEvents = host.context.directEvents;
1691
- const telemetry = (0, _agimon_ai_po_telemetry.createDoomTelemetry)({
1692
- serviceName: "doom-file-edit",
1884
+ const telemetry = (0, _agimon_ai_po_telemetry.createPoTelemetry)({
1885
+ serviceName: "po-file-edit",
1693
1886
  packageName: "@agimon-ai/po-file-edit",
1694
1887
  env: environment,
1695
1888
  enableLogs: true,
@@ -1712,7 +1905,7 @@ function createFileEditSession({ agent, host }) {
1712
1905
  /**
1713
1906
  * The channel carries the rows; the status decides whether the group exists.
1714
1907
  *
1715
- * A cockpit drives this facet and never the Pi runtime, so without the status
1908
+ * A cockpit drives this server plugin and never the Pi runtime, so without the status
1716
1909
  * the dock had the file list on the wire and no frame to draw it in. The empty
1717
1910
  * string is deliberate: the group declares `hideWhenEmpty`, so a session that
1718
1911
  * has edited nothing publishes a key with no content and stays hidden.
@@ -1738,20 +1931,19 @@ function createFileEditSession({ agent, host }) {
1738
1931
  const dispose = () => {
1739
1932
  if (disposal) return disposal;
1740
1933
  closed = true;
1741
- disposal = (async () => {
1742
- try {
1743
- await stopActivity();
1744
- if (lastRuntime) {
1745
- if (!environment[_agimon_ai_po_core_childProcess.SUBAGENT_CHILD_ENV]) {
1746
- await timeline.clear();
1747
- await snapshots.clear();
1748
- }
1749
- await telemetry.recordEvent("doom_file_edit.timeline_finished", { outcome: "closed" });
1750
- }
1751
- } finally {
1752
- await telemetry.shutdown();
1753
- }
1754
- })();
1934
+ disposal = (0, _agimon_ai_po_core_extensionFile.runCleanups)([
1935
+ stopActivity,
1936
+ async () => {
1937
+ if (lastRuntime && !environment[_agimon_ai_po_core_childProcess.SUBAGENT_CHILD_ENV]) await timeline.clear();
1938
+ },
1939
+ async () => {
1940
+ if (lastRuntime && !environment[_agimon_ai_po_core_childProcess.SUBAGENT_CHILD_ENV]) await snapshots.clear();
1941
+ },
1942
+ async () => {
1943
+ if (lastRuntime) await telemetry.recordEvent("po_file_edit.timeline_finished", { outcome: "closed" });
1944
+ },
1945
+ () => telemetry.shutdown()
1946
+ ], "File-edit session cleanup failed.");
1755
1947
  return disposal;
1756
1948
  };
1757
1949
  return {
@@ -1769,7 +1961,7 @@ function createFileEditSession({ agent, host }) {
1769
1961
  const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);
1770
1962
  timeline.initialize(timelinePath);
1771
1963
  snapshots.initialize(snapshotsPath);
1772
- const isIgnored = readDoomIgnore(executionContext.cwd);
1964
+ const isIgnored = readPoIgnore(executionContext.cwd);
1773
1965
  editTracker.reset({
1774
1966
  exclude: [
1775
1967
  timelinePath,
@@ -1792,16 +1984,11 @@ function createFileEditSession({ agent, host }) {
1792
1984
  directory: paths.stateDirectory(),
1793
1985
  keep: timelinePath
1794
1986
  });
1795
- const legacy = paths.legacyStateDirectory(executionContext.cwd);
1796
- if (legacy !== void 0 && legacy !== paths.stateDirectory()) await sweepSessionState({
1797
- directory: legacy,
1798
- removeDirectory: true
1799
- });
1800
1987
  } catch {}
1801
1988
  });
1802
1989
  if (!initialized) {
1803
1990
  initialized = true;
1804
- await track(() => telemetry.recordEvent("doom_file_edit.timeline_started", { outcome: "initialized" }));
1991
+ await track(() => telemetry.recordEvent("po_file_edit.timeline_started", { outcome: "initialized" }));
1805
1992
  }
1806
1993
  return () => active === runtime ? stopActivity() : Promise.resolve();
1807
1994
  }
@@ -1818,7 +2005,7 @@ function createFileEditSession({ agent, host }) {
1818
2005
  const hookStartedAt = Date.now();
1819
2006
  await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);
1820
2007
  if (active !== runtime) return;
1821
- await telemetry.recordEvent("doom_file_edit.edit_started", {
2008
+ await telemetry.recordEvent("po_file_edit.edit_started", {
1822
2009
  "tool.name": toolName,
1823
2010
  hook_ms: Date.now() - hookStartedAt
1824
2011
  });
@@ -1839,7 +2026,7 @@ function createFileEditSession({ agent, host }) {
1839
2026
  await editTracker.end(toolCallId, isError, executionContext.cwd);
1840
2027
  if (active !== runtime) return;
1841
2028
  publish(runtime);
1842
- await telemetry.recordEvent("doom_file_edit.edit_finished", {
2029
+ await telemetry.recordEvent("po_file_edit.edit_finished", {
1843
2030
  "tool.name": toolName,
1844
2031
  "tool.result.error": isError,
1845
2032
  ...startedAt === void 0 ? {} : { duration_ms: Date.now() - startedAt },
@@ -1854,12 +2041,38 @@ function createFileEditSession({ agent, host }) {
1854
2041
  }
1855
2042
  //#endregion
1856
2043
  //#region src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts
2044
+ function createPresence({ agent, host, context }) {
2045
+ const ledger = host.context.editPresence;
2046
+ if (!agent || !ledger) return void 0;
2047
+ const execution = agent.context;
2048
+ return createSessionPresence({
2049
+ ledger,
2050
+ sessionId: execution.sessionId,
2051
+ cwd: execution.cwd,
2052
+ workflowStep: execution.sessionContext?.provenance === _agimon_ai_po_core_hubChannel.WORKFLOW_STEP_SESSION_PROVENANCE,
2053
+ isLive: () => host.context.sessionService?.isLive(execution.sessionId) ?? true,
2054
+ labelFor: (runId) => (0, _agimon_ai_po_core_childSession.readPoChildSessionLabels)(context)?.labelFor(runId),
2055
+ onError: (error) => host.context.onNotice(`Edit presence failed: ${error instanceof Error ? error.message : String(error)}`)
2056
+ });
2057
+ }
1857
2058
  const createFileEditServerRoot = (context) => {
1858
2059
  const runtime = createFileEditSession(context);
2060
+ const presence = createPresence(context);
1859
2061
  return {
1860
- value: runtime,
2062
+ value: {
2063
+ ...runtime,
2064
+ ...presence === void 0 ? {} : { presence }
2065
+ },
1861
2066
  activities: runtime.activities,
1862
- onDispose: runtime.onDispose
2067
+ ...presence === void 0 ? {} : { services: [(cordis) => {
2068
+ cordis.inject([_agimon_ai_po_core_childSession.PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE], (registryContext) => {
2069
+ const registration = registryContext.get(_agimon_ai_po_core_childSession.PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE).register(presence.childHooks);
2070
+ return () => registration.dispose();
2071
+ });
2072
+ }] },
2073
+ onDispose: async (pluginContext) => {
2074
+ await (0, _agimon_ai_po_core_extensionFile.runCleanups)([() => presence?.dispose(), () => runtime.onDispose?.(pluginContext)], "File-edit root cleanup failed.");
2075
+ }
1863
2076
  };
1864
2077
  };
1865
2078
  //#endregion
@@ -1888,23 +2101,32 @@ var tool_execution_end_server_default = (0, _agimon_ai_po_core_extensionFile.def
1888
2101
  //#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts
1889
2102
  var tool_execution_start_server_default = (0, _agimon_ai_po_core_extensionFile.defineHook)((context) => context.root.hooks[0]);
1890
2103
  //#endregion
2104
+ //#region src/extensions/workspaces/sessions/(backend)/hook/tool-result.server.ts
2105
+ var tool_result_server_default = (0, _agimon_ai_po_core_extensionFile.defineHook)(({ root }) => ({
2106
+ event: "tool_result",
2107
+ handle: async (event, execution) => root.presence?.toolResult.handle(event, execution)
2108
+ }));
2109
+ //#endregion
1891
2110
  //#region generated/server.ts
1892
2111
  const at = (value, context) => typeof value === "function" ? value(context) : value;
2112
+ /* v8 ignore next -- generated normalization branch */
2113
+ const defined = (values) => values.filter((value) => value !== void 0);
1893
2114
  const via = (identity, value) => value === void 0 ? void 0 : {
1894
2115
  ...identity,
1895
2116
  ...value
1896
2117
  };
1897
- const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
2118
+ const serverPlugin = (0, _agimon_ai_po_core_serverPlugin.defineServerPlugin)({
1898
2119
  name: "@agimon-ai/po-file-edit",
1899
- providerOverrides: (0, _agimon_ai_po_core_serverFacet.composeServerProviderOverrides)(root_server_default),
2120
+ providerOverrides: (0, _agimon_ai_po_core_serverPlugin.composeServerProviderOverrides)(root_server_default),
2121
+ injects: (0, _agimon_ai_po_core_extensionFile.composeRootInjects)({ session: [root_server_default] }),
1900
2122
  global: () => ({ get channels() {
1901
2123
  return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
1902
2124
  } }),
1903
- workspace: () => ({ get channels() {
1904
- return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
2125
+ workspace: (context) => ({ get channels() {
2126
+ return defined([context.hosted("global") ? void 0 : () => via({ frameType: "file_edits" }, fileEdits_server_default())]);
1905
2127
  } }),
1906
2128
  session: async (context) => {
1907
- const scopeSession = await root_server_default(context);
2129
+ const scopeSession = await context.scopeRoot("session", root_server_default, context);
1908
2130
  const contextSession = {
1909
2131
  ...context,
1910
2132
  root: scopeSession.value
@@ -1915,10 +2137,14 @@ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
1915
2137
  return [at(route_server_default, contextSession)];
1916
2138
  },
1917
2139
  get channels() {
1918
- return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
2140
+ return defined([context.hosted("global") ? void 0 : () => via({ frameType: "file_edits" }, fileEdits_server_default())]);
1919
2141
  },
1920
2142
  get hooks() {
1921
- return [via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession)), via({ event: "tool_execution_start" }, at(tool_execution_start_server_default, contextSession))];
2143
+ return [
2144
+ via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession)),
2145
+ via({ event: "tool_execution_start" }, at(tool_execution_start_server_default, contextSession)),
2146
+ via({ event: "tool_result" }, at(tool_result_server_default, contextSession))
2147
+ ];
1922
2148
  },
1923
2149
  services: [...scopeSession.services ?? []],
1924
2150
  get activities() {
@@ -1928,7 +2154,7 @@ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
1928
2154
  }
1929
2155
  });
1930
2156
  //#endregion
1931
- exports.default = facet;
1932
- exports.facet = facet;
2157
+ exports.default = serverPlugin;
2158
+ exports.serverPlugin = serverPlugin;
1933
2159
 
1934
2160
  //# sourceMappingURL=server.cjs.map