@marcoscale98/pi-extensible-workflows 5.14.1-fork.1

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 (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
@@ -0,0 +1,732 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, open, readFile, rm } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
+ import { clearTrajectoryHost, setTrajectoryHost } from "../../src/trajectory-host-handle.js";
9
+ import { processAlive } from "../../src/session-lease.js";
10
+ import { errorText, isNodeError, object, positiveInteger } from "../../src/utils.js";
11
+ import { isTrajectoryAction, isTrajectoryTarget, trajectoryActionError, TRAJECTORY_MAX_TRANSCRIPT_BYTES } from "../../src/trajectory.js";
12
+ import { shareTrajectoryRun } from "./export.js";
13
+ const DEFAULT_TRAJECTORY_PORT = 7432;
14
+ const TRAJECTORY_IDLE_EXIT_MS = 5 * 60 * 1000;
15
+ const TRAJECTORY_LOCK_NAME = "trajectory.lock";
16
+ function trajectoryLockPath(agentDir) { return join(agentDir, "pi-extensible-workflows", TRAJECTORY_LOCK_NAME); }
17
+ export function trajectoryServerPath(moduleDirectory = dirname(fileURLToPath(import.meta.url))) {
18
+ // A spawned bare node runs the compiled server beside this extension entry.
19
+ const candidates = [join(moduleDirectory, "server.js"), join(moduleDirectory, "../../dist/trajectory/src/server.js")];
20
+ const path = candidates.find((candidate) => existsSync(candidate));
21
+ if (!path)
22
+ throw new Error("Trajectory server implementation is unavailable");
23
+ return path;
24
+ }
25
+ async function trajectoryFingerprint(serverPath) {
26
+ const [serverBytes, htmlBytes] = await Promise.all([readFile(serverPath), readFile(join(dirname(serverPath), "assets/index.html"))]);
27
+ return `${createHash("sha256").update(serverBytes).digest("hex")}:${createHash("sha256").update(htmlBytes).digest("hex")}`;
28
+ }
29
+ function publisherId(cwd, sessionId) { return createHash("sha256").update(`${cwd}\n${sessionId}`).digest("hex").slice(0, 16); }
30
+ function trajectoryPort(value) { return positiveInteger(value) && value <= 65535 ? value : DEFAULT_TRAJECTORY_PORT; }
31
+ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
32
+ async function serverHealthy(port) {
33
+ try {
34
+ const response = await fetch(`http://127.0.0.1:${String(port)}/health`, { signal: AbortSignal.timeout(300) });
35
+ return response.ok;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ function signalProcess(pid, signal) {
42
+ try {
43
+ process.kill(pid, signal);
44
+ }
45
+ catch (error) {
46
+ if (!isNodeError(error, "ESRCH"))
47
+ throw error;
48
+ }
49
+ }
50
+ async function stopStaleServer(lock) {
51
+ // During startup, the lock can name the current Pi process rather than the detached server.
52
+ if (lock.pid === process.pid)
53
+ return;
54
+ // NOTE: after a reboot the pid can belong to an unrelated process; startedAt (checked against /proc ctime) is the only proof it is still ours.
55
+ if (!await processAlive(lock.pid, lock.startedAt))
56
+ return;
57
+ signalProcess(lock.pid, "SIGTERM");
58
+ const deadline = Date.now() + 3000;
59
+ while (Date.now() < deadline) {
60
+ if (!await serverHealthy(lock.port))
61
+ break;
62
+ const remaining = deadline - Date.now();
63
+ if (remaining <= 0)
64
+ break;
65
+ await delay(Math.min(50, remaining));
66
+ }
67
+ if (await processAlive(lock.pid, lock.startedAt))
68
+ signalProcess(lock.pid, "SIGKILL");
69
+ }
70
+ async function readLock(path) {
71
+ try {
72
+ const parsed = JSON.parse(await readFile(path, "utf8"));
73
+ if (!object(parsed) || !positiveInteger(parsed.pid) || !positiveInteger(parsed.port) || parsed.port > 65535)
74
+ return undefined;
75
+ const fingerprint = typeof parsed.fingerprint === "string" ? parsed.fingerprint : undefined;
76
+ const startedAt = positiveInteger(parsed.startedAt) ? parsed.startedAt : undefined;
77
+ return { pid: parsed.pid, port: parsed.port, ...(fingerprint === undefined ? {} : { fingerprint }), ...(startedAt === undefined ? {} : { startedAt }) };
78
+ }
79
+ catch (error) {
80
+ if (isNodeError(error, "ENOENT"))
81
+ return undefined;
82
+ return undefined;
83
+ }
84
+ }
85
+ async function waitForServer(port) {
86
+ for (let attempt = 0; attempt < 60; attempt += 1) {
87
+ if (await serverHealthy(port))
88
+ return;
89
+ await delay(50);
90
+ }
91
+ throw new Error(`Trajectory server did not start on port ${String(port)}`);
92
+ }
93
+ async function resolveExistingServer(lockPath, existing, fingerprint) {
94
+ if (await serverHealthy(existing.port)) {
95
+ if (existing.fingerprint === fingerprint)
96
+ return existing;
97
+ await stopStaleServer(existing);
98
+ await rm(lockPath, { force: true });
99
+ return undefined;
100
+ }
101
+ if (await processAlive(existing.pid, existing.startedAt)) {
102
+ try {
103
+ await waitForServer(existing.port);
104
+ return existing;
105
+ }
106
+ catch {
107
+ // A live lock can still name the attaching process during startup; after the bounded wait, replace the unrecoverable startup owner and retry normally.
108
+ await stopStaleServer(existing);
109
+ await rm(lockPath, { force: true });
110
+ return undefined;
111
+ }
112
+ }
113
+ await rm(lockPath, { force: true });
114
+ return undefined;
115
+ }
116
+ async function ensureTrajectoryServer(agentDir, configuredPort) {
117
+ const lockPath = trajectoryLockPath(agentDir);
118
+ const serverPath = trajectoryServerPath();
119
+ const fingerprint = await trajectoryFingerprint(serverPath);
120
+ await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
121
+ const existing = await readLock(lockPath);
122
+ if (existing) {
123
+ const reused = await resolveExistingServer(lockPath, existing, fingerprint);
124
+ if (reused)
125
+ return reused;
126
+ }
127
+ let lockHandle;
128
+ try {
129
+ lockHandle = await open(lockPath, "wx", 0o600);
130
+ await lockHandle.writeFile(`${JSON.stringify({ pid: process.pid, port: configuredPort, fingerprint, startedAt: Date.now() })}\n`, "utf8");
131
+ }
132
+ catch (error) {
133
+ if (isNodeError(error, "EEXIST")) {
134
+ const raced = await readLock(lockPath);
135
+ if (raced) {
136
+ const reused = await resolveExistingServer(lockPath, raced, fingerprint);
137
+ if (reused)
138
+ return reused;
139
+ }
140
+ await rm(lockPath, { force: true });
141
+ lockHandle = await open(lockPath, "wx", 0o600);
142
+ await lockHandle.writeFile(`${JSON.stringify({ pid: process.pid, port: configuredPort, fingerprint, startedAt: Date.now() })}\n`, "utf8");
143
+ }
144
+ else {
145
+ throw error;
146
+ }
147
+ }
148
+ finally {
149
+ await lockHandle?.close();
150
+ }
151
+ try {
152
+ // NOTE: under a Bun-compiled pi binary process.execPath is the pi CLI, and Bun's node:http never writes the WebSocket 101 upgrade (oven-sh/bun#28157), so the server must run on a real node from PATH.
153
+ const child = spawn(process.versions.bun ? "node" : process.execPath, [serverPath, "--port", String(configuredPort), "--lock", lockPath, "--fingerprint", fingerprint], { detached: true, stdio: "ignore" });
154
+ const startupError = new Promise((_resolve, reject) => { child.once("error", reject); });
155
+ child.unref();
156
+ await Promise.race([waitForServer(configuredPort), startupError]);
157
+ return { port: configuredPort };
158
+ }
159
+ catch (error) {
160
+ await rm(lockPath, { force: true });
161
+ throw error;
162
+ }
163
+ }
164
+ function trajectoryWebSocket() {
165
+ const candidate = globalThis.WebSocket;
166
+ return typeof candidate === "function" ? candidate : undefined;
167
+ }
168
+ function openBrowser(url) {
169
+ const command = process.platform === "win32" ? "rundll32" : process.platform === "darwin" ? "open" : "xdg-open";
170
+ const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
171
+ try {
172
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
173
+ child.once("error", () => undefined);
174
+ child.unref();
175
+ }
176
+ catch { /* Browser launch is best effort; the URL remains in the notification. */ }
177
+ }
178
+ export function trajectoryUrl(port) { return `http://127.0.0.1:${String(port)}/`; }
179
+ export function openTrajectoryUrl(url) { openBrowser(url); }
180
+ const MAX_FRAME_BYTES = 32 * 1024 * 1024;
181
+ const MAX_LIVE_STATE_BYTES = MAX_FRAME_BYTES - 1024;
182
+ const MAX_TRANSCRIPT_REQUESTS = 64;
183
+ const TRANSCRIPT_REQUEST_TIMEOUT_MS = 10_000;
184
+ const RECONNECT_INITIAL_DELAY_MS = 100;
185
+ const RECONNECT_MAX_DELAY_MS = 5_000;
186
+ function isTimingEntry(value) { return object(value) && value.type === "custom" && value.customType === "pi-workflows:tool-timing"; }
187
+ function boundedString(value, maxLength = 1024) {
188
+ if (typeof value !== "string")
189
+ return value;
190
+ const bytes = Buffer.from(value);
191
+ return bytes.length > maxLength ? bytes.subarray(0, maxLength).toString("utf8") : value;
192
+ }
193
+ const MAX_LIVE_STRING_BYTES = 64 * 1024;
194
+ const MAX_LIVE_ARRAY_ENTRIES = 256;
195
+ const MAX_LIVE_OBJECT_KEYS = 64;
196
+ const LIVE_METADATA_ARRAY_KEYS = new Set(["agents", "runs", "subagents"]);
197
+ const LIVE_TRUNCATABLE_ARRAY_KEYS = new Set(["events", "phaseHistory"]);
198
+ const LIVE_METADATA_OBJECT_KEYS = new Set(["transcripts"]);
199
+ function boundedTiming(value) {
200
+ const entries = Array.isArray(value) ? value.filter(isTimingEntry) : [];
201
+ const retained = [];
202
+ let bytes = 2;
203
+ for (const entry of entries) {
204
+ let serialized;
205
+ try {
206
+ serialized = JSON.stringify(entry);
207
+ }
208
+ catch {
209
+ continue;
210
+ }
211
+ const nextBytes = bytes + (retained.length ? 1 : 0) + Buffer.byteLength(serialized);
212
+ if (nextBytes >= 64 * 1024)
213
+ break;
214
+ retained.push(entry);
215
+ bytes = nextBytes;
216
+ }
217
+ return retained;
218
+ }
219
+ function boundedLiveValue(value, key = "", depth = 0) {
220
+ if (typeof value === "string")
221
+ return boundedString(value, MAX_LIVE_STRING_BYTES);
222
+ if (typeof value === "number" || typeof value === "boolean" || value === null)
223
+ return value;
224
+ if (depth >= 12)
225
+ return undefined;
226
+ if (key === "timing")
227
+ return boundedTiming(value);
228
+ if (Array.isArray(value)) {
229
+ const array = value;
230
+ const maxEntries = LIVE_TRUNCATABLE_ARRAY_KEYS.has(key) ? MAX_LIVE_ARRAY_ENTRIES - 1 : MAX_LIVE_ARRAY_ENTRIES;
231
+ const entries = key === "attemptDetails" ? array.slice(-8) : LIVE_METADATA_ARRAY_KEYS.has(key) ? array : array.length <= maxEntries ? array : [...array.slice(0, 32), ...array.slice(-maxEntries + 32)];
232
+ const bounded = entries.map((entry) => boundedLiveValue(entry, "", depth + 1));
233
+ if (entries.length !== array.length && LIVE_TRUNCATABLE_ARRAY_KEYS.has(key))
234
+ bounded.push({ type: "trajectory:truncated", field: key, omitted: array.length - entries.length });
235
+ return bounded;
236
+ }
237
+ if (object(value)) {
238
+ const result = {};
239
+ const properties = LIVE_METADATA_OBJECT_KEYS.has(key) ? Object.keys(value).sort() : Object.keys(value).sort().slice(0, MAX_LIVE_OBJECT_KEYS);
240
+ for (const property of properties)
241
+ result[property] = boundedLiveValue(value[property], property, depth + 1);
242
+ return result;
243
+ }
244
+ return undefined;
245
+ }
246
+ function transcriptRevisionProjection(value, depth = 0) {
247
+ if (depth >= 8)
248
+ return typeof value === "string" ? boundedString(value, 1024) : typeof value === "number" || typeof value === "boolean" || value === null ? value : undefined;
249
+ if (Array.isArray(value)) {
250
+ const entries = value;
251
+ const sample = entries.length <= 16 ? entries : [...entries.slice(0, 8), ...entries.slice(-8)];
252
+ return { length: entries.length, values: sample.map((entry) => transcriptRevisionProjection(entry, depth + 1)) };
253
+ }
254
+ if (object(value)) {
255
+ const result = {};
256
+ for (const key of Object.keys(value).sort().slice(0, 32))
257
+ result[key] = transcriptRevisionProjection(value[key], depth + 1);
258
+ return result;
259
+ }
260
+ return value;
261
+ }
262
+ function sourceMetadata(value, key, revisions) {
263
+ if (object(value) && !Array.isArray(value) && typeof value.revision === "number" && typeof value.status === "string")
264
+ return { ...value, timing: boundedTiming(value.timing) };
265
+ const entries = Array.isArray(value) ? value : [];
266
+ const signature = createHash("sha256").update(JSON.stringify(transcriptRevisionProjection(entries))).digest("hex");
267
+ const previous = revisions.get(key);
268
+ const revision = previous?.signature === signature ? previous.revision : (previous?.revision ?? 0) + 1;
269
+ revisions.set(key, { signature, revision });
270
+ return { revision, status: entries.length ? "available" : "empty", timing: boundedTiming(entries) };
271
+ }
272
+ function projectRun(value, key, revisions) {
273
+ const run = object(value.run) && !Array.isArray(value.run) ? value.run : {};
274
+ const rawAgents = Array.isArray(run.agents) ? run.agents : [];
275
+ const agents = rawAgents.map((agent) => {
276
+ if (!object(agent))
277
+ return agent;
278
+ if (agent.activity === undefined)
279
+ return agent;
280
+ const activity = object(agent.activity) ? agent.activity : {};
281
+ return { ...agent, activity: { ...activity, text: boundedString(activity.text) } };
282
+ });
283
+ const transcripts = {};
284
+ const source = object(value.transcripts) && !Array.isArray(value.transcripts) ? value.transcripts : {};
285
+ for (const [agentId, transcript] of Object.entries(source))
286
+ transcripts[agentId] = sourceMetadata(transcript, `${key}\t${agentId}`, revisions);
287
+ return { ...value, run: { ...run, agents }, transcripts };
288
+ }
289
+ function projectSubagent(value, key, revisions) {
290
+ return { ...value, transcript: sourceMetadata(value.transcript, key, revisions) };
291
+ }
292
+ function projectPublisher(metadata, publisher, revisions) {
293
+ const publisherId = typeof publisher.id === "string" ? publisher.id : "";
294
+ return { ...publisher, runs: metadata.runs.map((run) => projectRun(run, `${publisherId}\t${run.run.id}`, revisions)), subagents: metadata.subagents.map((subagent) => projectSubagent(subagent, `${publisherId}\tsubagent\t${subagent.id}`, revisions)) };
295
+ }
296
+ function minimalStatePublisher(publisher) {
297
+ return boundedLiveValue(publisher) ?? {};
298
+ }
299
+ function publisherStateFrame(publisher, runs, subagents, truncated = false) {
300
+ const publisherSummary = { ...publisher };
301
+ delete publisherSummary.runs;
302
+ delete publisherSummary.subagents;
303
+ return { type: "publisher:state", publisher: { ...publisherSummary, ...(truncated ? { truncated: true } : {}) }, runs, subagents, ...(truncated ? { truncated: true } : {}) };
304
+ }
305
+ function encodeLiveState(publisher) {
306
+ const projected = minimalStatePublisher(publisher);
307
+ const publisherJson = JSON.stringify(publisherStateFrame(projected, [], []).publisher);
308
+ const runs = Array.isArray(projected.runs) ? projected.runs : [];
309
+ const subagents = Array.isArray(projected.subagents) ? projected.subagents : [];
310
+ const prefix = `{"type":"publisher:state","publisher":${publisherJson},"runs":[`;
311
+ const baseBytes = Buffer.byteLength(`${prefix}],"subagents":[]}`);
312
+ const truncatedBytes = Buffer.byteLength(',"truncated":true');
313
+ const selectedRuns = [];
314
+ const selectedSubagents = [];
315
+ let bytes = baseBytes;
316
+ const state = { truncated: false };
317
+ const add = (serialized, target) => {
318
+ const nextBytes = bytes + Buffer.byteLength(serialized) + (target.length ? 1 : 0);
319
+ if (nextBytes + truncatedBytes >= MAX_LIVE_STATE_BYTES)
320
+ return false;
321
+ target.push(serialized);
322
+ bytes = nextBytes;
323
+ return true;
324
+ };
325
+ const addValue = (value, target) => {
326
+ if (bytes + truncatedBytes >= MAX_LIVE_STATE_BYTES) {
327
+ state.truncated = true;
328
+ return;
329
+ }
330
+ let serializedValue;
331
+ try {
332
+ serializedValue = JSON.stringify(value);
333
+ }
334
+ catch {
335
+ state.truncated = true;
336
+ return;
337
+ }
338
+ if (typeof serializedValue !== "string" || !add(serializedValue, target))
339
+ state.truncated = true;
340
+ };
341
+ for (let index = 0; index < Math.max(runs.length, subagents.length); index += 1) {
342
+ const run = runs[index];
343
+ if (run !== undefined)
344
+ addValue(run, selectedRuns);
345
+ const subagent = subagents[index];
346
+ if (subagent !== undefined)
347
+ addValue(subagent, selectedSubagents);
348
+ }
349
+ return `${prefix}${selectedRuns.join(",")}],"subagents":[${selectedSubagents.join(",")}]${state.truncated ? ',"truncated":true' : ""}}`;
350
+ }
351
+ export function createTrajectoryController(agentDir) {
352
+ let socket;
353
+ let connectionGeneration = 0;
354
+ let pollTimer;
355
+ let reconnectTimer;
356
+ let reconnectLoop;
357
+ let currentInput;
358
+ let configuredPort;
359
+ let closing = false;
360
+ const transcriptRevisions = new Map();
361
+ const pendingTranscripts = new Map();
362
+ const stopPolling = () => { if (pollTimer !== undefined) {
363
+ clearInterval(pollTimer);
364
+ pollTimer = undefined;
365
+ } };
366
+ const clearTranscriptRequests = () => { for (const request of pendingTranscripts.values())
367
+ clearTimeout(request.timer); pendingTranscripts.clear(); };
368
+ const startPolling = () => { stopPolling(); pollTimer = setInterval(() => { void sendState().catch((error) => { if (!closing)
369
+ console.error(`Trajectory state publish failed: ${errorText(error)}`); }); }, 1000); pollTimer.unref(); };
370
+ let stateLoad;
371
+ let lastState;
372
+ const publisherValue = (input) => ({ id: publisherId(input.cwd, input.sessionId), title: `session ${input.sessionId.slice(0, 8)}`, cwd: input.cwd, sessionId: input.sessionId, themes: input.themes, connected: true });
373
+ const sendState = async () => {
374
+ const activeSocket = socket;
375
+ if (activeSocket !== undefined && stateLoad?.socket === activeSocket)
376
+ return stateLoad.task;
377
+ const task = (async () => {
378
+ const input = currentInput;
379
+ if (!activeSocket || !input || activeSocket.readyState !== 1)
380
+ return;
381
+ const metadata = await input.loadMetadata();
382
+ if (closing || socket !== activeSocket)
383
+ return;
384
+ const publisher = projectPublisher(metadata, publisherValue(input), transcriptRevisions);
385
+ const serialized = encodeLiveState(publisher);
386
+ if (serialized === lastState)
387
+ return;
388
+ lastState = serialized;
389
+ activeSocket.send(serialized);
390
+ })();
391
+ if (activeSocket !== undefined)
392
+ stateLoad = { socket: activeSocket, task };
393
+ try {
394
+ await task;
395
+ }
396
+ finally {
397
+ if (stateLoad?.task === task)
398
+ stateLoad = undefined;
399
+ }
400
+ };
401
+ const abandonSocket = (candidate) => {
402
+ if (socket !== candidate)
403
+ return;
404
+ socket = undefined;
405
+ lastState = undefined;
406
+ stopPolling();
407
+ clearTranscriptRequests();
408
+ candidate.close();
409
+ };
410
+ const publishConnectedState = async (next, input) => {
411
+ startPolling();
412
+ try {
413
+ await sendState();
414
+ if (closing || currentInput !== input)
415
+ return;
416
+ if (socket !== next || next.readyState !== 1)
417
+ throw new Error("Trajectory connection closed before state publish");
418
+ }
419
+ catch (error) {
420
+ if (!closing && socket === next && currentInput === input)
421
+ abandonSocket(next);
422
+ throw error;
423
+ }
424
+ };
425
+ const transcriptFromFallback = async (input, request) => {
426
+ const boundedTranscriptResult = (result) => {
427
+ try {
428
+ if (result.status === "available" && Buffer.byteLength(JSON.stringify(result.entries)) > TRAJECTORY_MAX_TRANSCRIPT_BYTES)
429
+ return { status: "oversized", revision: result.revision, entries: [], error: "Transcript is too large" };
430
+ }
431
+ catch {
432
+ return { status: "failed", revision: result.revision, entries: [], error: "Transcript failed" };
433
+ }
434
+ return result;
435
+ };
436
+ const boundedResult = (entries, revision) => { try {
437
+ if (Buffer.byteLength(JSON.stringify(entries)) > TRAJECTORY_MAX_TRANSCRIPT_BYTES)
438
+ return { status: "oversized", revision, entries: [], error: "Transcript is too large" };
439
+ }
440
+ catch {
441
+ return { status: "failed", revision, entries: [], error: "Transcript failed" };
442
+ } return { status: entries.length ? "available" : "empty", revision, entries }; };
443
+ if (input.loadTranscript) {
444
+ const result = await input.loadTranscript(request);
445
+ const revisionChanged = request.revision !== undefined && result.revision !== request.revision && result.status !== "missing" && result.status !== "failed" && result.status !== "oversized" && result.status !== "disconnected";
446
+ return boundedTranscriptResult(revisionChanged ? { ...result, status: "available", entries: [], error: "Transcript revision is stale" } : result);
447
+ }
448
+ if (request.runId !== undefined && request.agentId !== undefined && request.subagentId === undefined) {
449
+ const runs = await input.loadRuns();
450
+ const run = runs.find((candidate) => candidate.run.id === request.runId);
451
+ const entries = run?.transcripts[request.agentId];
452
+ const metadata = sourceMetadata(entries, `${publisherId(input.cwd, input.sessionId)}\t${request.runId}\t${request.agentId}`, transcriptRevisions);
453
+ if (request.revision !== undefined && request.revision !== metadata.revision)
454
+ return { status: "available", revision: Number(metadata.revision), entries: [], error: "Transcript revision is stale" };
455
+ if (run === undefined || entries === undefined)
456
+ return { status: "missing", revision: Number(metadata.revision), entries: [], error: "Transcript not found" };
457
+ return boundedResult(entries, Number(metadata.revision));
458
+ }
459
+ if (request.subagentId !== undefined && request.runId === undefined && request.agentId === undefined) {
460
+ const subagents = await input.loadSubagents();
461
+ const subagent = subagents.find((candidate) => candidate.id === request.subagentId);
462
+ const entries = subagent?.transcript;
463
+ const metadata = sourceMetadata(entries, `${publisherId(input.cwd, input.sessionId)}\tsubagent\t${request.subagentId}`, transcriptRevisions);
464
+ if (request.revision !== undefined && request.revision !== metadata.revision)
465
+ return { status: "available", revision: Number(metadata.revision), entries: [], error: "Transcript revision is stale" };
466
+ if (subagent === undefined || entries === undefined)
467
+ return { status: "missing", revision: Number(metadata.revision), entries: [], error: "Transcript not found" };
468
+ return boundedResult(entries, Number(metadata.revision));
469
+ }
470
+ return { status: "missing", revision: 0, entries: [], error: "Transcript not found" };
471
+ };
472
+ const sendTranscriptResult = (next, generation, message, result) => {
473
+ const requestId = String(message.requestId);
474
+ const pending = pendingTranscripts.get(requestId);
475
+ if (!pending || pending.generation !== generation || socket !== next || closing || pending.publisherId !== message.publisherId || pending.runId !== message.runId || pending.agentId !== message.agentId || pending.subagentId !== message.subagentId || pending.revision !== message.revision)
476
+ return;
477
+ clearTimeout(pending.timer);
478
+ pendingTranscripts.delete(requestId);
479
+ const failed = result.error !== undefined || result.status === "missing" || result.status === "failed" || result.status === "oversized" || result.status === "disconnected";
480
+ let safeResult = result;
481
+ try {
482
+ if (!failed && Buffer.byteLength(JSON.stringify(result.entries)) > TRAJECTORY_MAX_TRANSCRIPT_BYTES)
483
+ safeResult = { status: "oversized", revision: result.revision, entries: [], error: "Transcript is too large" };
484
+ }
485
+ catch {
486
+ safeResult = { status: "failed", revision: result.revision, entries: [], error: "Transcript failed" };
487
+ }
488
+ const safeFailed = safeResult.error !== undefined || safeResult.status === "missing" || safeResult.status === "failed" || safeResult.status === "oversized" || safeResult.status === "disconnected";
489
+ const error = typeof safeResult.error === "string" ? safeResult.error.slice(0, 1024) : safeResult.status;
490
+ next.send(JSON.stringify({ type: "publisher:transcript-result", requestId: message.requestId, publisherId: message.publisherId, ...(message.runId === undefined ? {} : { runId: message.runId }), ...(message.agentId === undefined ? {} : { agentId: message.agentId }), ...(message.subagentId === undefined ? {} : { subagentId: message.subagentId }), ...(message.revision === undefined ? {} : { requestedRevision: message.revision }), ok: !safeFailed, status: safeResult.status, revision: safeResult.revision, ...(safeFailed ? { error } : { entries: safeResult.entries }) }));
491
+ };
492
+ const handleTranscriptRequest = (next, generation, message, input) => {
493
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
494
+ const revision = typeof message.revision === "number" && Number.isSafeInteger(message.revision) && message.revision >= 0 ? message.revision : undefined;
495
+ const target = { ...(typeof message.runId === "string" ? { runId: message.runId } : {}), ...(typeof message.agentId === "string" ? { agentId: message.agentId } : {}), ...(typeof message.subagentId === "string" ? { subagentId: message.subagentId } : {}), ...(revision === undefined ? {} : { requestedRevision: revision }) };
496
+ const existing = pendingTranscripts.get(requestId);
497
+ if (!requestId || (existing === undefined && pendingTranscripts.size >= MAX_TRANSCRIPT_REQUESTS)) {
498
+ next.send(JSON.stringify({ type: "publisher:transcript-result", requestId, publisherId: message.publisherId, ...target, ok: false, status: "failed", revision: revision ?? 0, error: "Too many transcript requests" }));
499
+ return;
500
+ }
501
+ if (existing !== undefined) {
502
+ const original = { requestId, publisherId: existing.publisherId, ...(existing.runId === undefined ? {} : { runId: existing.runId }), ...(existing.agentId === undefined ? {} : { agentId: existing.agentId }), ...(existing.subagentId === undefined ? {} : { subagentId: existing.subagentId }), ...(existing.revision === undefined ? {} : { revision: existing.revision }) };
503
+ sendTranscriptResult(next, generation, original, { status: "failed", revision: existing.revision ?? 0, entries: [], error: "Duplicate transcript request" });
504
+ return;
505
+ }
506
+ const timer = setTimeout(() => {
507
+ const pending = pendingTranscripts.get(requestId);
508
+ if (!pending || pending.timer !== timer)
509
+ return;
510
+ pendingTranscripts.delete(requestId);
511
+ if (socket === next && !closing && next.readyState === 1)
512
+ next.send(JSON.stringify({ type: "publisher:transcript-result", requestId, publisherId: pending.publisherId, ...(pending.runId === undefined ? {} : { runId: pending.runId }), ...(pending.agentId === undefined ? {} : { agentId: pending.agentId }), ...(pending.subagentId === undefined ? {} : { subagentId: pending.subagentId }), ...(pending.revision === undefined ? {} : { requestedRevision: pending.revision }), ok: false, status: "failed", revision: pending.revision ?? 0, error: "Transcript request timed out" }));
513
+ }, TRANSCRIPT_REQUEST_TIMEOUT_MS);
514
+ const pending = { generation, timer, publisherId: message.publisherId, ...(typeof message.runId === "string" ? { runId: message.runId } : {}), ...(typeof message.agentId === "string" ? { agentId: message.agentId } : {}), ...(typeof message.subagentId === "string" ? { subagentId: message.subagentId } : {}), ...(revision === undefined ? {} : { revision }) };
515
+ pendingTranscripts.set(requestId, pending);
516
+ const request = { ...(typeof message.runId === "string" ? { runId: message.runId } : {}), ...(typeof message.agentId === "string" ? { agentId: message.agentId } : {}), ...(typeof message.subagentId === "string" ? { subagentId: message.subagentId } : {}), ...(revision === undefined ? {} : { revision }) };
517
+ const requestMessage = { ...message, revision };
518
+ void transcriptFromFallback(input, request).then((result) => { sendTranscriptResult(next, generation, requestMessage, result); }, (error) => { sendTranscriptResult(next, generation, requestMessage, { status: "failed", revision: 0, entries: [], error: errorText(error) }); }).catch(() => undefined);
519
+ };
520
+ const connect = async (port, input) => {
521
+ const Constructor = trajectoryWebSocket();
522
+ if (!Constructor)
523
+ throw new Error("Trajectory requires a WebSocket-capable Node runtime");
524
+ const next = new Constructor(`ws://127.0.0.1:${String(port)}/ws`);
525
+ const generation = connectionGeneration + 1;
526
+ let established = false;
527
+ connectionGeneration = generation;
528
+ socket = next;
529
+ const onClose = () => {
530
+ if (socket !== next)
531
+ return;
532
+ socket = undefined;
533
+ lastState = undefined;
534
+ stopPolling();
535
+ clearTranscriptRequests();
536
+ if (established && !closing)
537
+ scheduleReconnect();
538
+ };
539
+ next.addEventListener("close", onClose);
540
+ next.addEventListener("error", onClose);
541
+ next.addEventListener("message", (event) => {
542
+ try {
543
+ const message = JSON.parse(typeof event === "object" && event !== null && "data" in event ? String(event.data) : "");
544
+ if (!object(message))
545
+ return;
546
+ if (message.type === "publisher:replaced") {
547
+ established = false;
548
+ return;
549
+ }
550
+ if (message.type === "publisher:transcript" && typeof message.requestId === "string" && typeof message.publisherId === "string" && (typeof message.runId === "string" && typeof message.agentId === "string" || typeof message.subagentId === "string")) {
551
+ handleTranscriptRequest(next, generation, message, input);
552
+ return;
553
+ }
554
+ if (message.type !== "publisher:action" || typeof message.requestId !== "string")
555
+ return;
556
+ const sendActionResponse = (response) => { if (socket !== next || closing || next.readyState !== 1)
557
+ return; let serialized; try {
558
+ serialized = JSON.stringify(response);
559
+ }
560
+ catch {
561
+ serialized = JSON.stringify({ type: "publisher:action-result", requestId: response.requestId, publisherId: response.publisherId, ok: false, error: "Trajectory action result is invalid" });
562
+ } if (Buffer.byteLength(serialized) >= MAX_FRAME_BYTES)
563
+ serialized = JSON.stringify({ type: "publisher:action-result", requestId: response.requestId, publisherId: response.publisherId, ok: false, error: "Trajectory action result is too large" }); next.send(serialized); };
564
+ const sendActionResult = (value) => { sendActionResponse({ type: "publisher:action-result", requestId: message.requestId, publisherId: publisherId(input.cwd, input.sessionId), ...object(value) ? { ok: true, result: value } : { ok: true } }); };
565
+ if (!isTrajectoryAction(message.action)) {
566
+ sendActionResponse({ type: "publisher:action-result", requestId: message.requestId, publisherId: publisherId(input.cwd, input.sessionId), ok: false, error: "Unsupported Trajectory action" });
567
+ return;
568
+ }
569
+ if (!isTrajectoryTarget(message.target)) {
570
+ sendActionResponse({ type: "publisher:action-result", requestId: message.requestId, publisherId: publisherId(input.cwd, input.sessionId), ok: false, error: "Invalid Trajectory action target" });
571
+ return;
572
+ }
573
+ const target = message.target;
574
+ const actionError = trajectoryActionError(message.action, target);
575
+ if (actionError !== undefined) {
576
+ sendActionResponse({ type: "publisher:action-result", requestId: message.requestId, publisherId: publisherId(input.cwd, input.sessionId), ok: false, error: actionError });
577
+ return;
578
+ }
579
+ if (message.action === "share") {
580
+ void shareTrajectoryRun({ cwd: input.cwd, sessionId: input.sessionId, runId: target.id }).then((result) => { sendActionResult(result); }, (error) => { sendActionResponse({ type: "publisher:action-result", requestId: message.requestId, publisherId: publisherId(input.cwd, input.sessionId), ok: false, error: errorText(error).slice(0, 1024) }); }).catch(() => undefined);
581
+ return;
582
+ }
583
+ void input.handleAction({ action: message.action, target, ...(typeof message.name === "string" ? { name: message.name } : {}), ...(message.payload === undefined ? {} : { payload: message.payload }) }).then((result) => { sendActionResult(result); }, (error) => { sendActionResponse({ type: "publisher:action-result", requestId: message.requestId, publisherId: publisherId(input.cwd, input.sessionId), ok: false, error: errorText(error).slice(0, 1024) }); }).catch(() => undefined);
584
+ }
585
+ catch { /* Ignore malformed local browser messages. */ }
586
+ });
587
+ await new Promise((resolve, reject) => {
588
+ let settled = false;
589
+ const finish = (error) => { if (settled)
590
+ return; settled = true; if (error)
591
+ reject(error);
592
+ else
593
+ resolve(); };
594
+ next.addEventListener("open", () => { finish(); });
595
+ next.addEventListener("error", () => { finish(new Error("Could not connect to Trajectory server")); });
596
+ next.addEventListener("close", () => { finish(new Error("Could not connect to Trajectory server")); });
597
+ });
598
+ if (closing || currentInput !== input || socket !== next || next.readyState !== 1) {
599
+ next.close();
600
+ throw new Error("Trajectory connection was replaced");
601
+ }
602
+ established = true;
603
+ next.send(JSON.stringify({ type: "publisher:attach", publisherId: publisherId(input.cwd, input.sessionId) }));
604
+ };
605
+ let reconnectCancel;
606
+ const waitReconnect = (ms) => new Promise((resolve) => { const timer = setTimeout(() => { if (reconnectTimer === timer)
607
+ reconnectTimer = undefined; reconnectCancel = undefined; resolve(); }, ms); timer.unref(); reconnectTimer = timer; reconnectCancel = () => { clearTimeout(timer); if (reconnectTimer === timer)
608
+ reconnectTimer = undefined; reconnectCancel = undefined; resolve(); }; });
609
+ const scheduleReconnect = () => {
610
+ if (closing || reconnectLoop !== undefined || configuredPort === undefined)
611
+ return;
612
+ const reconnectInput = () => closing || socket !== undefined || currentInput === undefined || configuredPort === undefined ? undefined : { port: configuredPort, input: currentInput };
613
+ reconnectLoop = (async () => {
614
+ let backoff = RECONNECT_INITIAL_DELAY_MS;
615
+ while (reconnectInput() !== undefined) {
616
+ await waitReconnect(backoff);
617
+ const active = reconnectInput();
618
+ if (active === undefined)
619
+ return;
620
+ const { port, input } = active;
621
+ const isCurrent = () => !closing && currentInput === input && configuredPort === port;
622
+ try {
623
+ const server = await ensureTrajectoryServer(agentDir, port);
624
+ if (!isCurrent())
625
+ return;
626
+ await connect(server.port, input);
627
+ if (!isCurrent())
628
+ return;
629
+ const activeSocket = socket;
630
+ if (!activeSocket)
631
+ throw new Error("Trajectory connection closed before state publish");
632
+ await publishConnectedState(activeSocket, input);
633
+ return;
634
+ }
635
+ catch (error) {
636
+ console.error(`Trajectory reconnect failed: ${errorText(error)}`);
637
+ backoff = Math.min(RECONNECT_MAX_DELAY_MS, backoff * 2);
638
+ }
639
+ }
640
+ })().finally(() => { reconnectLoop = undefined; if (!closing && socket === undefined && currentInput !== undefined && configuredPort !== undefined)
641
+ scheduleReconnect(); });
642
+ };
643
+ return {
644
+ async open(input) {
645
+ closing = false;
646
+ currentInput = input;
647
+ const envPort = process.env.PI_WORKFLOW_TRAJECTORY_PORT;
648
+ configuredPort = envPort !== undefined && /^\d+$/.test(envPort) ? trajectoryPort(Number(envPort)) : trajectoryPort(input.port);
649
+ const server = await ensureTrajectoryServer(agentDir, configuredPort);
650
+ if (!socket || socket.readyState !== 1)
651
+ await connect(server.port, input);
652
+ stopPolling();
653
+ const activeSocket = socket;
654
+ if (!activeSocket)
655
+ throw new Error("Trajectory connection closed before state publish");
656
+ await publishConnectedState(activeSocket, input);
657
+ return server;
658
+ },
659
+ async close() {
660
+ closing = true;
661
+ const input = currentInput;
662
+ const reconnect = reconnectLoop;
663
+ currentInput = undefined;
664
+ configuredPort = undefined;
665
+ reconnectCancel?.();
666
+ stopPolling();
667
+ clearTranscriptRequests();
668
+ lastState = undefined;
669
+ connectionGeneration += 1;
670
+ const activeSocket = socket;
671
+ if (activeSocket?.readyState === 1 && input)
672
+ activeSocket.send(JSON.stringify({ type: "publisher:detach", publisherId: publisherId(input.cwd, input.sessionId) }));
673
+ socket = undefined;
674
+ activeSocket?.close();
675
+ if (reconnect)
676
+ await reconnect;
677
+ },
678
+ };
679
+ }
680
+ export { DEFAULT_TRAJECTORY_PORT, TRAJECTORY_IDLE_EXIT_MS, TRAJECTORY_LOCK_NAME };
681
+ export { exportTrajectoryRunHtml, shareTrajectoryRun } from "./export.js";
682
+ export function registerTrajectoryExtension(pi, options = {}) {
683
+ const controller = options.controller ?? createTrajectoryController(options.agentDir ?? getAgentDir());
684
+ const openUrl = options.openUrl ?? openTrajectoryUrl;
685
+ let autoAttached = false;
686
+ const notify = (context, message, level) => {
687
+ const current = object(context) ? context : undefined;
688
+ const ui = current && object(current.ui) ? current.ui : undefined;
689
+ if (typeof ui?.notify === "function")
690
+ Reflect.apply(ui.notify, ui, [message, level]);
691
+ };
692
+ const attach = async (provider, context) => {
693
+ try {
694
+ return await controller.open(provider(context));
695
+ }
696
+ catch (error) {
697
+ notify(context, `Unable to attach Trajectory: ${errorText(error)}`, "error");
698
+ return undefined;
699
+ }
700
+ };
701
+ const open = async (provider, context) => {
702
+ const server = await attach(provider, context);
703
+ if (server === undefined)
704
+ return;
705
+ const url = trajectoryUrl(server.port);
706
+ openUrl(url);
707
+ notify(context, `Trajectory opened at ${url}`, "info");
708
+ };
709
+ const host = {
710
+ open,
711
+ autoAttach(provider, context) {
712
+ if (autoAttached || !context.hasUI)
713
+ return;
714
+ autoAttached = true;
715
+ void attach(provider, context);
716
+ },
717
+ close: () => controller.close(),
718
+ };
719
+ setTrajectoryHost(host);
720
+ pi.on("session_shutdown", async () => {
721
+ try {
722
+ await host.close();
723
+ }
724
+ finally {
725
+ clearTrajectoryHost(host);
726
+ }
727
+ });
728
+ return host;
729
+ }
730
+ export default function extension(pi) {
731
+ registerTrajectoryExtension(pi);
732
+ }