@bastani/atomic 0.9.4-alpha.9 → 0.9.4

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 (78) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +6 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +27 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/src/slash/slash-commands.ts +2 -2
  11. package/dist/builtin/subagents/src/tui/render-result-compact.ts +3 -2
  12. package/dist/builtin/subagents/src/tui/render-result.ts +3 -3
  13. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +4 -3
  14. package/dist/builtin/subagents/src/tui/render-widget.ts +10 -4
  15. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  16. package/dist/builtin/web-access/package.json +1 -1
  17. package/dist/builtin/workflows/CHANGELOG.md +22 -0
  18. package/dist/builtin/workflows/package.json +1 -1
  19. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +4 -0
  20. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -0
  21. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +12 -1
  22. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +2 -0
  23. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +6 -0
  24. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +4 -0
  25. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +4 -0
  26. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -9
  27. package/dist/core/agent-session-events.d.ts.map +1 -1
  28. package/dist/core/agent-session-events.js +2 -0
  29. package/dist/core/agent-session-events.js.map +1 -1
  30. package/dist/core/agent-session-methods.d.ts +4 -1
  31. package/dist/core/agent-session-methods.d.ts.map +1 -1
  32. package/dist/core/agent-session-methods.js.map +1 -1
  33. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  34. package/dist/core/agent-session-tool-registry.js +4 -0
  35. package/dist/core/agent-session-tool-registry.js.map +1 -1
  36. package/dist/core/agent-session.d.ts +4 -1
  37. package/dist/core/agent-session.d.ts.map +1 -1
  38. package/dist/core/agent-session.js +4 -0
  39. package/dist/core/agent-session.js.map +1 -1
  40. package/dist/core/async/format.d.ts +3 -0
  41. package/dist/core/async/format.d.ts.map +1 -0
  42. package/dist/core/async/format.js +50 -0
  43. package/dist/core/async/format.js.map +1 -0
  44. package/dist/core/async/job-manager.d.ts +39 -0
  45. package/dist/core/async/job-manager.d.ts.map +1 -0
  46. package/dist/core/async/job-manager.js +254 -0
  47. package/dist/core/async/job-manager.js.map +1 -0
  48. package/dist/core/async/session-manager.d.ts +18 -0
  49. package/dist/core/async/session-manager.d.ts.map +1 -0
  50. package/dist/core/async/session-manager.js +61 -0
  51. package/dist/core/async/session-manager.js.map +1 -0
  52. package/dist/core/async/types.d.ts +23 -0
  53. package/dist/core/async/types.d.ts.map +1 -0
  54. package/dist/core/async/types.js +2 -0
  55. package/dist/core/async/types.js.map +1 -0
  56. package/dist/core/extensions/loader-virtual-modules.d.ts +11 -0
  57. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  58. package/dist/core/extensions/loader-virtual-modules.js +20 -5
  59. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  60. package/dist/core/tools/bash-async-execution.d.ts +25 -0
  61. package/dist/core/tools/bash-async-execution.d.ts.map +1 -0
  62. package/dist/core/tools/bash-async-execution.js +71 -0
  63. package/dist/core/tools/bash-async-execution.js.map +1 -0
  64. package/dist/core/tools/bash-async-jobs.d.ts +10 -0
  65. package/dist/core/tools/bash-async-jobs.d.ts.map +1 -1
  66. package/dist/core/tools/bash-async-jobs.js +18 -2
  67. package/dist/core/tools/bash-async-jobs.js.map +1 -1
  68. package/dist/core/tools/bash-async-output.d.ts +3 -1
  69. package/dist/core/tools/bash-async-output.d.ts.map +1 -1
  70. package/dist/core/tools/bash-async-output.js +4 -1
  71. package/dist/core/tools/bash-async-output.js.map +1 -1
  72. package/dist/core/tools/bash.d.ts +10 -4
  73. package/dist/core/tools/bash.d.ts.map +1 -1
  74. package/dist/core/tools/bash.js +20 -38
  75. package/dist/core/tools/bash.js.map +1 -1
  76. package/docs/tools.md +4 -2
  77. package/npm-shrinkwrap.json +23 -23
  78. package/package.json +2 -2
@@ -0,0 +1,254 @@
1
+ import { COMPLETED_JOB_TTL_MS, MAX_MANAGED_BASH_JOBS } from "../tools/bash-async-jobs.js";
2
+ import { formatAsyncResultForFollowUp } from "./format.js";
3
+ const DEFAULT_MAX_RUNNING_JOBS = 15;
4
+ const DELIVERY_RETRY_BASE_MS = 500;
5
+ const DELIVERY_RETRY_MAX_MS = 30_000;
6
+ const DELIVERY_RETRY_JITTER_MS = 200;
7
+ export class AsyncJobManager {
8
+ static #instance;
9
+ static instance() {
10
+ return AsyncJobManager.#instance;
11
+ }
12
+ static setInstance(value) {
13
+ AsyncJobManager.#instance = value;
14
+ }
15
+ static resetForTests() {
16
+ AsyncJobManager.#instance = undefined;
17
+ }
18
+ #jobs = new Map();
19
+ #deliveries = [];
20
+ #suppressedDeliveries = new Set();
21
+ #inFlightDeliveries = new Map();
22
+ #deliveryHandlers = new Map();
23
+ #jobSessions = new Map();
24
+ #sessions = new Map();
25
+ #onJobComplete;
26
+ #maxRunningJobs;
27
+ #maxRetainedJobs;
28
+ #completedJobTtlMs;
29
+ #timer;
30
+ #disposed = false;
31
+ #runningDeliveryLoop = false;
32
+ constructor(options) {
33
+ this.#onJobComplete = options.onJobComplete;
34
+ this.#maxRunningJobs = Math.max(1, Math.floor(options.maxRunningJobs ?? DEFAULT_MAX_RUNNING_JOBS));
35
+ this.#maxRetainedJobs = Math.max(1, Math.floor(options.maxRetainedJobs ?? MAX_MANAGED_BASH_JOBS));
36
+ this.#completedJobTtlMs = Math.max(0, Math.floor(options.completedJobTtlMs ?? COMPLETED_JOB_TTL_MS));
37
+ }
38
+ get disposed() {
39
+ return this.#disposed;
40
+ }
41
+ get atCapacity() {
42
+ let running = 0;
43
+ for (const job of this.#jobs.values())
44
+ if (job.status === "running")
45
+ running += 1;
46
+ return running >= this.#maxRunningJobs;
47
+ }
48
+ registerSession() {
49
+ if (this.#disposed)
50
+ throw new Error("Async job manager is disposed");
51
+ const sessionId = Symbol("async-job-session");
52
+ this.#sessions.set(sessionId, { disposed: false, activeJobIds: new Set() });
53
+ return sessionId;
54
+ }
55
+ releaseSession(sessionId) {
56
+ const session = this.#sessions.get(sessionId);
57
+ if (!session)
58
+ return;
59
+ session.disposed = true;
60
+ this.acknowledgeDeliveries([...session.activeJobIds]);
61
+ this.#sessions.delete(sessionId);
62
+ if (this.#sessions.size === 0)
63
+ this.dispose();
64
+ }
65
+ isSessionDisposed(sessionId) {
66
+ return this.#disposed || this.#sessions.get(sessionId)?.disposed !== false;
67
+ }
68
+ registerBashJob(job, onComplete, sessionId) {
69
+ if (this.#disposed)
70
+ throw new Error("Async job manager is disposed");
71
+ this.#pruneRetention();
72
+ if (this.atCapacity)
73
+ throw new Error(`Background job limit reached (${this.#maxRunningJobs}). Wait for running jobs to finish or cancel one.`);
74
+ if (sessionId !== undefined && this.isSessionDisposed(sessionId))
75
+ throw new Error("Async job session is disposed");
76
+ this.#suppressedDeliveries.delete(job.jobId);
77
+ if (onComplete)
78
+ this.#deliveryHandlers.set(job.jobId, onComplete);
79
+ else
80
+ this.#deliveryHandlers.delete(job.jobId);
81
+ if (sessionId !== undefined) {
82
+ this.#jobSessions.set(job.jobId, sessionId);
83
+ this.#sessions.get(sessionId)?.activeJobIds.add(job.jobId);
84
+ }
85
+ else
86
+ this.#jobSessions.delete(job.jobId);
87
+ this.#jobs.set(job.jobId, job);
88
+ this.#pruneRetention();
89
+ }
90
+ completeBashJob(job) {
91
+ if (this.#disposed || this.#suppressedDeliveries.has(job.jobId))
92
+ return;
93
+ if (job.status === "running")
94
+ return;
95
+ this.#jobs.set(job.jobId, job);
96
+ this.#pruneRetention();
97
+ if (this.#jobs.has(job.jobId))
98
+ this.#enqueueDelivery(job);
99
+ }
100
+ acknowledgeDeliveries(jobIds) {
101
+ for (const jobId of jobIds) {
102
+ this.#suppressedDeliveries.add(jobId);
103
+ for (let index = this.#deliveries.length - 1; index >= 0; index -= 1) {
104
+ if (this.#deliveries[index]?.jobId === jobId)
105
+ this.#deliveries.splice(index, 1);
106
+ }
107
+ this.#deliveryHandlers.delete(jobId);
108
+ this.#completeTrackedJob(jobId);
109
+ }
110
+ this.#pruneRetention();
111
+ }
112
+ isDeliverySuppressed(jobId) {
113
+ return this.#suppressedDeliveries.has(jobId);
114
+ }
115
+ deliveryState() {
116
+ this.#pruneRetention();
117
+ return {
118
+ queued: this.#deliveries.length,
119
+ delivering: this.#runningDeliveryLoop || this.#inFlightDeliveries.size > 0,
120
+ pendingJobIds: [...this.#deliveries.map((delivery) => delivery.jobId), ...this.#inFlightDeliveries.keys()],
121
+ };
122
+ }
123
+ retentionState() {
124
+ this.#pruneRetention();
125
+ return { jobs: this.#jobs.size, suppressions: this.#suppressedDeliveries.size, handlers: this.#deliveryHandlers.size, queued: this.#deliveries.length + this.#inFlightDeliveries.size, sessions: this.#sessions.size };
126
+ }
127
+ dispose() {
128
+ this.#disposed = true;
129
+ if (this.#timer)
130
+ clearTimeout(this.#timer);
131
+ this.#timer = undefined;
132
+ this.#deliveries.length = 0;
133
+ this.#inFlightDeliveries.clear();
134
+ this.#deliveryHandlers.clear();
135
+ this.#jobSessions.clear();
136
+ this.#sessions.clear();
137
+ this.#jobs.clear();
138
+ this.#suppressedDeliveries.clear();
139
+ }
140
+ #enqueueDelivery(job) {
141
+ if (this.#inFlightDeliveries.has(job.jobId))
142
+ return;
143
+ for (let index = this.#deliveries.length - 1; index >= 0; index -= 1)
144
+ if (this.#deliveries[index]?.jobId === job.jobId)
145
+ this.#deliveries.splice(index, 1);
146
+ this.#deliveries.push({ jobId: job.jobId, message: formatAsyncResultForFollowUp(job), attempt: 0, nextAttemptAt: Date.now() });
147
+ this.#pruneRetention();
148
+ this.#scheduleDeliveryLoop(25);
149
+ }
150
+ #completeTrackedJob(jobId) {
151
+ const sessionId = this.#jobSessions.get(jobId);
152
+ if (sessionId !== undefined)
153
+ this.#sessions.get(sessionId)?.activeJobIds.delete(jobId);
154
+ this.#jobSessions.delete(jobId);
155
+ }
156
+ #pruneRetention(now = Date.now()) {
157
+ for (const [jobId, job] of this.#jobs) {
158
+ if (job.status !== "running" && job.endedAt !== undefined && now - job.endedAt > this.#completedJobTtlMs) {
159
+ this.#jobs.delete(jobId);
160
+ this.#completeTrackedJob(jobId);
161
+ }
162
+ }
163
+ for (const jobId of this.#suppressedDeliveries) {
164
+ if (!this.#jobs.has(jobId))
165
+ this.#suppressedDeliveries.delete(jobId);
166
+ }
167
+ const oldestTerminalJobs = [...this.#jobs.values()]
168
+ .filter((job) => job.status !== "running")
169
+ .sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt));
170
+ while (this.#jobs.size > this.#maxRetainedJobs && oldestTerminalJobs.length > 0) {
171
+ const job = oldestTerminalJobs.shift();
172
+ if (job) {
173
+ this.#jobs.delete(job.jobId);
174
+ this.#completeTrackedJob(job.jobId);
175
+ }
176
+ }
177
+ for (let index = this.#deliveries.length - 1; index >= 0; index -= 1) {
178
+ const delivery = this.#deliveries[index];
179
+ if (!delivery || !this.#jobs.has(delivery.jobId) || this.#suppressedDeliveries.has(delivery.jobId))
180
+ this.#deliveries.splice(index, 1);
181
+ }
182
+ while (this.#deliveries.length + this.#inFlightDeliveries.size > this.#maxRetainedJobs && this.#deliveries.length > 0)
183
+ this.#deliveries.shift();
184
+ for (const jobId of this.#deliveryHandlers.keys())
185
+ if (!this.#jobs.has(jobId) || this.#suppressedDeliveries.has(jobId))
186
+ this.#deliveryHandlers.delete(jobId);
187
+ for (const session of this.#sessions.values())
188
+ for (const jobId of session.activeJobIds)
189
+ if (!this.#jobs.has(jobId))
190
+ session.activeJobIds.delete(jobId);
191
+ }
192
+ #scheduleDeliveryLoop(delayMs) {
193
+ if (this.#disposed)
194
+ return;
195
+ if (this.#timer)
196
+ clearTimeout(this.#timer);
197
+ this.#timer = setTimeout(() => {
198
+ this.#timer = undefined;
199
+ void this.#runDeliveryLoop();
200
+ }, delayMs);
201
+ this.#timer.unref?.();
202
+ }
203
+ async #runDeliveryLoop() {
204
+ if (this.#runningDeliveryLoop || this.#disposed)
205
+ return;
206
+ this.#runningDeliveryLoop = true;
207
+ try {
208
+ while (!this.#disposed) {
209
+ const now = Date.now();
210
+ const delivery = this.#deliveries.find((candidate) => candidate.nextAttemptAt <= now);
211
+ if (!delivery)
212
+ break;
213
+ this.#deliveries.splice(this.#deliveries.indexOf(delivery), 1);
214
+ this.#startDelivery(delivery);
215
+ }
216
+ }
217
+ finally {
218
+ this.#runningDeliveryLoop = false;
219
+ }
220
+ this.#scheduleNextDelivery();
221
+ }
222
+ #startDelivery(delivery) {
223
+ if (this.#suppressedDeliveries.has(delivery.jobId) || !this.#jobs.has(delivery.jobId) || this.#inFlightDeliveries.has(delivery.jobId))
224
+ return;
225
+ const handler = this.#deliveryHandlers.get(delivery.jobId) ?? this.#onJobComplete;
226
+ this.#inFlightDeliveries.set(delivery.jobId, delivery);
227
+ delivery.promise = (async () => handler(delivery.message))()
228
+ .then(() => {
229
+ if (!this.#suppressedDeliveries.has(delivery.jobId) && this.#jobs.has(delivery.jobId)) {
230
+ this.#deliveryHandlers.delete(delivery.jobId);
231
+ this.#completeTrackedJob(delivery.jobId);
232
+ }
233
+ })
234
+ .catch(() => {
235
+ if (this.#disposed || this.#suppressedDeliveries.has(delivery.jobId) || !this.#jobs.has(delivery.jobId))
236
+ return;
237
+ delivery.attempt += 1;
238
+ const jitter = Math.floor(Math.random() * DELIVERY_RETRY_JITTER_MS);
239
+ delivery.nextAttemptAt = Date.now() + Math.min(DELIVERY_RETRY_MAX_MS, DELIVERY_RETRY_BASE_MS * 2 ** delivery.attempt) + jitter;
240
+ this.#deliveries.push(delivery);
241
+ })
242
+ .finally(() => {
243
+ this.#inFlightDeliveries.delete(delivery.jobId);
244
+ this.#pruneRetention();
245
+ this.#scheduleNextDelivery();
246
+ });
247
+ }
248
+ #scheduleNextDelivery() {
249
+ const next = this.#deliveries.reduce((soonest, delivery) => soonest === undefined ? delivery.nextAttemptAt : Math.min(soonest, delivery.nextAttemptAt), undefined);
250
+ if (next !== undefined)
251
+ this.#scheduleDeliveryLoop(Math.max(0, next - Date.now()));
252
+ }
253
+ }
254
+ //# sourceMappingURL=job-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-manager.js","sourceRoot":"","sources":["../../../src/core/async/job-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { COMPLETED_JOB_TTL_MS, MAX_MANAGED_BASH_JOBS, type ManagedBashJob } from \"../tools/bash-async-jobs.js\";\nimport { formatAsyncResultForFollowUp } from \"./format.js\";\nimport type { AsyncJobDeliveryCallback, AsyncJobDeliveryHandler, AsyncJobDeliveryMessage, ManagedAsyncBashJob } from \"./types.js\";\n\nconst DEFAULT_MAX_RUNNING_JOBS = 15;\nconst DELIVERY_RETRY_BASE_MS = 500;\nconst DELIVERY_RETRY_MAX_MS = 30_000;\nconst DELIVERY_RETRY_JITTER_MS = 200;\n\ninterface AsyncJobManagerOptions {\n\tonJobComplete: AsyncJobDeliveryCallback;\n\tmaxRunningJobs?: number;\n\tmaxRetainedJobs?: number;\n\tcompletedJobTtlMs?: number;\n}\n\ninterface Delivery {\n\tjobId: string;\n\tmessage: AsyncJobDeliveryMessage;\n\tattempt: number;\n\tnextAttemptAt: number;\n\tpromise?: Promise<void>;\n}\n\ninterface RegisteredSession {\n\tdisposed: boolean;\n\tactiveJobIds: Set<string>;\n}\n\nexport class AsyncJobManager {\n\tstatic #instance: AsyncJobManager | undefined;\n\n\tstatic instance(): AsyncJobManager | undefined {\n\t\treturn AsyncJobManager.#instance;\n\t}\n\n\tstatic setInstance(value: AsyncJobManager | undefined): void {\n\t\tAsyncJobManager.#instance = value;\n\t}\n\n\tstatic resetForTests(): void {\n\t\tAsyncJobManager.#instance = undefined;\n\t}\n\n\treadonly #jobs = new Map<string, ManagedBashJob>();\n\treadonly #deliveries: Delivery[] = [];\n\treadonly #suppressedDeliveries = new Set<string>();\n\treadonly #inFlightDeliveries = new Map<string, Delivery>();\n\treadonly #deliveryHandlers = new Map<string, AsyncJobDeliveryHandler>();\n\treadonly #jobSessions = new Map<string, symbol>();\n\treadonly #sessions = new Map<symbol, RegisteredSession>();\n\treadonly #onJobComplete: AsyncJobDeliveryCallback;\n\treadonly #maxRunningJobs: number;\n\treadonly #maxRetainedJobs: number;\n\treadonly #completedJobTtlMs: number;\n\t#timer: NodeJS.Timeout | undefined;\n\t#disposed = false;\n\t#runningDeliveryLoop = false;\n\n\tconstructor(options: AsyncJobManagerOptions) {\n\t\tthis.#onJobComplete = options.onJobComplete;\n\t\tthis.#maxRunningJobs = Math.max(1, Math.floor(options.maxRunningJobs ?? DEFAULT_MAX_RUNNING_JOBS));\n\t\tthis.#maxRetainedJobs = Math.max(1, Math.floor(options.maxRetainedJobs ?? MAX_MANAGED_BASH_JOBS));\n\t\tthis.#completedJobTtlMs = Math.max(0, Math.floor(options.completedJobTtlMs ?? COMPLETED_JOB_TTL_MS));\n\t}\n\n\tget disposed(): boolean {\n\t\treturn this.#disposed;\n\t}\n\n\tget atCapacity(): boolean {\n\t\tlet running = 0;\n\t\tfor (const job of this.#jobs.values()) if (job.status === \"running\") running += 1;\n\t\treturn running >= this.#maxRunningJobs;\n\t}\n\n\n\tregisterSession(): symbol {\n\t\tif (this.#disposed) throw new Error(\"Async job manager is disposed\");\n\t\tconst sessionId = Symbol(\"async-job-session\");\n\t\tthis.#sessions.set(sessionId, { disposed: false, activeJobIds: new Set() });\n\t\treturn sessionId;\n\t}\n\n\treleaseSession(sessionId: symbol): void {\n\t\tconst session = this.#sessions.get(sessionId);\n\t\tif (!session) return;\n\t\tsession.disposed = true;\n\t\tthis.acknowledgeDeliveries([...session.activeJobIds]);\n\t\tthis.#sessions.delete(sessionId);\n\t\tif (this.#sessions.size === 0) this.dispose();\n\t}\n\n\tisSessionDisposed(sessionId: symbol): boolean {\n\t\treturn this.#disposed || this.#sessions.get(sessionId)?.disposed !== false;\n\t}\n\tregisterBashJob(job: ManagedBashJob, onComplete?: AsyncJobDeliveryHandler, sessionId?: symbol): void {\n\t\tif (this.#disposed) throw new Error(\"Async job manager is disposed\");\n\t\tthis.#pruneRetention();\n\t\tif (this.atCapacity) throw new Error(`Background job limit reached (${this.#maxRunningJobs}). Wait for running jobs to finish or cancel one.`);\n\t\tif (sessionId !== undefined && this.isSessionDisposed(sessionId)) throw new Error(\"Async job session is disposed\");\n\t\tthis.#suppressedDeliveries.delete(job.jobId);\n\t\tif (onComplete) this.#deliveryHandlers.set(job.jobId, onComplete);\n\t\telse this.#deliveryHandlers.delete(job.jobId);\n\t\tif (sessionId !== undefined) {\n\t\t\tthis.#jobSessions.set(job.jobId, sessionId);\n\t\t\tthis.#sessions.get(sessionId)?.activeJobIds.add(job.jobId);\n\t\t} else this.#jobSessions.delete(job.jobId);\n\t\tthis.#jobs.set(job.jobId, job);\n\t\tthis.#pruneRetention();\n\t}\n\n\tcompleteBashJob(job: ManagedBashJob): void {\n\t\tif (this.#disposed || this.#suppressedDeliveries.has(job.jobId)) return;\n\t\tif (job.status === \"running\") return;\n\t\tthis.#jobs.set(job.jobId, job);\n\t\tthis.#pruneRetention();\n\t\tif (this.#jobs.has(job.jobId)) this.#enqueueDelivery(job as ManagedAsyncBashJob);\n\t}\n\n\tacknowledgeDeliveries(jobIds: readonly string[]): void {\n\t\tfor (const jobId of jobIds) {\n\t\t\tthis.#suppressedDeliveries.add(jobId);\n\t\t\tfor (let index = this.#deliveries.length - 1; index >= 0; index -= 1) {\n\t\t\t\tif (this.#deliveries[index]?.jobId === jobId) this.#deliveries.splice(index, 1);\n\t\t\t}\n\t\t\tthis.#deliveryHandlers.delete(jobId);\n\t\t\tthis.#completeTrackedJob(jobId);\n\t\t}\n\t\tthis.#pruneRetention();\n\t}\n\n\tisDeliverySuppressed(jobId: string): boolean {\n\t\treturn this.#suppressedDeliveries.has(jobId);\n\t}\n\n\tdeliveryState(): { queued: number; delivering: boolean; pendingJobIds: string[] } {\n\t\tthis.#pruneRetention();\n\t\treturn {\n\t\t\tqueued: this.#deliveries.length,\n\t\t\tdelivering: this.#runningDeliveryLoop || this.#inFlightDeliveries.size > 0,\n\t\t\tpendingJobIds: [...this.#deliveries.map((delivery) => delivery.jobId), ...this.#inFlightDeliveries.keys()],\n\t\t};\n\t}\n\n\tretentionState(): { jobs: number; suppressions: number; handlers: number; queued: number; sessions: number } {\n\t\tthis.#pruneRetention();\n\t\treturn { jobs: this.#jobs.size, suppressions: this.#suppressedDeliveries.size, handlers: this.#deliveryHandlers.size, queued: this.#deliveries.length + this.#inFlightDeliveries.size, sessions: this.#sessions.size };\n\t}\n\n\tdispose(): void {\n\t\tthis.#disposed = true;\n\t\tif (this.#timer) clearTimeout(this.#timer);\n\t\tthis.#timer = undefined;\n\t\tthis.#deliveries.length = 0;\n\t\tthis.#inFlightDeliveries.clear();\n\t\tthis.#deliveryHandlers.clear();\n\t\tthis.#jobSessions.clear();\n\t\tthis.#sessions.clear();\n\t\tthis.#jobs.clear();\n\t\tthis.#suppressedDeliveries.clear();\n\t}\n\n\t#enqueueDelivery(job: ManagedAsyncBashJob): void {\n\t\tif (this.#inFlightDeliveries.has(job.jobId)) return;\n\t\tfor (let index = this.#deliveries.length - 1; index >= 0; index -= 1) if (this.#deliveries[index]?.jobId === job.jobId) this.#deliveries.splice(index, 1);\n\t\tthis.#deliveries.push({ jobId: job.jobId, message: formatAsyncResultForFollowUp(job), attempt: 0, nextAttemptAt: Date.now() });\n\t\tthis.#pruneRetention();\n\t\tthis.#scheduleDeliveryLoop(25);\n\t}\n\n\n\t#completeTrackedJob(jobId: string): void {\n\t\tconst sessionId = this.#jobSessions.get(jobId);\n\t\tif (sessionId !== undefined) this.#sessions.get(sessionId)?.activeJobIds.delete(jobId);\n\t\tthis.#jobSessions.delete(jobId);\n\t}\n\t#pruneRetention(now = Date.now()): void {\n\t\tfor (const [jobId, job] of this.#jobs) {\n\t\t\tif (job.status !== \"running\" && job.endedAt !== undefined && now - job.endedAt > this.#completedJobTtlMs) {\n\t\t\t\tthis.#jobs.delete(jobId);\n\t\t\t\tthis.#completeTrackedJob(jobId);\n\t\t\t}\n\t\t}\n\t\tfor (const jobId of this.#suppressedDeliveries) {\n\t\t\tif (!this.#jobs.has(jobId)) this.#suppressedDeliveries.delete(jobId);\n\t\t}\n\t\tconst oldestTerminalJobs = [...this.#jobs.values()]\n\t\t\t.filter((job) => job.status !== \"running\")\n\t\t\t.sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt));\n\t\twhile (this.#jobs.size > this.#maxRetainedJobs && oldestTerminalJobs.length > 0) {\n\t\t\tconst job = oldestTerminalJobs.shift();\n\t\t\tif (job) {\n\t\t\t\tthis.#jobs.delete(job.jobId);\n\t\t\t\tthis.#completeTrackedJob(job.jobId);\n\t\t\t}\n\t\t}\n\t\tfor (let index = this.#deliveries.length - 1; index >= 0; index -= 1) {\n\t\t\tconst delivery = this.#deliveries[index];\n\t\t\tif (!delivery || !this.#jobs.has(delivery.jobId) || this.#suppressedDeliveries.has(delivery.jobId)) this.#deliveries.splice(index, 1);\n\t\t}\n\t\twhile (this.#deliveries.length + this.#inFlightDeliveries.size > this.#maxRetainedJobs && this.#deliveries.length > 0) this.#deliveries.shift();\n\t\tfor (const jobId of this.#deliveryHandlers.keys()) if (!this.#jobs.has(jobId) || this.#suppressedDeliveries.has(jobId)) this.#deliveryHandlers.delete(jobId);\n\t\tfor (const session of this.#sessions.values()) for (const jobId of session.activeJobIds) if (!this.#jobs.has(jobId)) session.activeJobIds.delete(jobId);\n\t}\n\n\t#scheduleDeliveryLoop(delayMs: number): void {\n\t\tif (this.#disposed) return;\n\t\tif (this.#timer) clearTimeout(this.#timer);\n\t\tthis.#timer = setTimeout(() => {\n\t\t\tthis.#timer = undefined;\n\t\t\tvoid this.#runDeliveryLoop();\n\t\t}, delayMs);\n\t\tthis.#timer.unref?.();\n\t}\n\n\tasync #runDeliveryLoop(): Promise<void> {\n\t\tif (this.#runningDeliveryLoop || this.#disposed) return;\n\t\tthis.#runningDeliveryLoop = true;\n\t\ttry {\n\t\t\twhile (!this.#disposed) {\n\t\t\t\tconst now = Date.now();\n\t\t\t\tconst delivery = this.#deliveries.find((candidate) => candidate.nextAttemptAt <= now);\n\t\t\t\tif (!delivery) break;\n\t\t\t\tthis.#deliveries.splice(this.#deliveries.indexOf(delivery), 1);\n\t\t\t\tthis.#startDelivery(delivery);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.#runningDeliveryLoop = false;\n\t\t}\n\t\tthis.#scheduleNextDelivery();\n\t}\n\n\t#startDelivery(delivery: Delivery): void {\n\t\tif (this.#suppressedDeliveries.has(delivery.jobId) || !this.#jobs.has(delivery.jobId) || this.#inFlightDeliveries.has(delivery.jobId)) return;\n\t\tconst handler = this.#deliveryHandlers.get(delivery.jobId) ?? this.#onJobComplete;\n\t\tthis.#inFlightDeliveries.set(delivery.jobId, delivery);\n\t\tdelivery.promise = (async () => handler(delivery.message))()\n\t\t\t.then(() => {\n\t\t\t\tif (!this.#suppressedDeliveries.has(delivery.jobId) && this.#jobs.has(delivery.jobId)) {\n\t\t\t\t\tthis.#deliveryHandlers.delete(delivery.jobId);\n\t\t\t\t\tthis.#completeTrackedJob(delivery.jobId);\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\tif (this.#disposed || this.#suppressedDeliveries.has(delivery.jobId) || !this.#jobs.has(delivery.jobId)) return;\n\t\t\t\tdelivery.attempt += 1;\n\t\t\t\tconst jitter = Math.floor(Math.random() * DELIVERY_RETRY_JITTER_MS);\n\t\t\t\tdelivery.nextAttemptAt = Date.now() + Math.min(DELIVERY_RETRY_MAX_MS, DELIVERY_RETRY_BASE_MS * 2 ** delivery.attempt) + jitter;\n\t\t\t\tthis.#deliveries.push(delivery);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.#inFlightDeliveries.delete(delivery.jobId);\n\t\t\t\tthis.#pruneRetention();\n\t\t\t\tthis.#scheduleNextDelivery();\n\t\t\t});\n\t}\n\n\t#scheduleNextDelivery(): void {\n\t\tconst next = this.#deliveries.reduce<number | undefined>((soonest, delivery) => soonest === undefined ? delivery.nextAttemptAt : Math.min(soonest, delivery.nextAttemptAt), undefined);\n\t\tif (next !== undefined) this.#scheduleDeliveryLoop(Math.max(0, next - Date.now()));\n\t}\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import type { CustomMessage } from "../messages.js";
2
+ import type { SendMessageOptions } from "../extensions/index.js";
3
+ import { AsyncJobManager } from "./job-manager.js";
4
+ import type { AsyncJobDeliveryHandler } from "./types.js";
5
+ export interface SessionAsyncJobManagerHandle {
6
+ manager: AsyncJobManager;
7
+ owns: boolean;
8
+ sessionId: symbol;
9
+ }
10
+ interface AsyncDeliverySession {
11
+ readonly isStreaming?: boolean;
12
+ sendCustomMessage<T>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: SendMessageOptions): Promise<void>;
13
+ }
14
+ export declare function createSessionAsyncDeliveryHandler(session: AsyncDeliverySession, manager?: AsyncJobManager, sessionId?: symbol): AsyncJobDeliveryHandler;
15
+ export declare function createSessionAsyncJobManager(session: AsyncDeliverySession): SessionAsyncJobManagerHandle;
16
+ export declare function disposeSessionAsyncJobManager(manager: AsyncJobManager | undefined, sessionId: symbol | undefined): void;
17
+ export {};
18
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../../src/core/async/session-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,uBAAuB,EAA2B,MAAM,YAAY,CAAC;AAEnF,MAAM,WAAW,4BAA4B;IAC5C,OAAO,EAAE,eAAe,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CAClB;AACD,UAAU,oBAAoB;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,CAAC,EAClB,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC,EACjF,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;CACjB;AAiCD,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,oBAAoB,EAAE,OAAO,CAAC,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,uBAAuB,CAUvJ;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,oBAAoB,GAAG,4BAA4B,CASxG;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,eAAe,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAIvH","sourcesContent":["import type { CustomMessage } from \"../messages.js\";\nimport type { SendMessageOptions } from \"../extensions/index.js\";\nimport { AsyncJobManager } from \"./job-manager.js\";\nimport type { AsyncJobDeliveryHandler, AsyncJobDeliveryMessage } from \"./types.js\";\n\nexport interface SessionAsyncJobManagerHandle {\n\tmanager: AsyncJobManager;\n\towns: boolean;\n\tsessionId: symbol;\n}\ninterface AsyncDeliverySession {\n\treadonly isStreaming?: boolean;\n\tsendCustomMessage<T>(\n\t\tmessage: Pick<CustomMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\t\toptions?: SendMessageOptions,\n\t): Promise<void>;\n}\n\nconst STREAMING_DELIVERY_POLL_MS = 10;\n\nfunction scheduleBoundaryCheck(callback: () => void): NodeJS.Timeout {\n\tconst timer = setTimeout(callback, STREAMING_DELIVERY_POLL_MS);\n\ttimer.unref?.();\n\treturn timer;\n}\n\nfunction waitForStreamingBoundary(session: AsyncDeliverySession, isStale: () => boolean): Promise<\"ready\" | \"stale\"> {\n\treturn new Promise((resolve) => {\n\t\tlet timer: NodeJS.Timeout | undefined;\n\t\tconst settle = (value: \"ready\" | \"stale\") => {\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\tresolve(value);\n\t\t};\n\t\tconst check = () => {\n\t\t\ttimer = undefined;\n\t\t\tif (isStale()) {\n\t\t\t\tsettle(\"stale\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (session.isStreaming !== true) {\n\t\t\t\tsettle(\"ready\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttimer = scheduleBoundaryCheck(check);\n\t\t};\n\t\tcheck();\n\t});\n}\n\nexport function createSessionAsyncDeliveryHandler(session: AsyncDeliverySession, manager?: AsyncJobManager, sessionId?: symbol): AsyncJobDeliveryHandler {\n\treturn async (message: AsyncJobDeliveryMessage) => {\n\t\tconst isStale = () =>\n\t\t\tmanager?.disposed === true ||\n\t\t\tmanager?.isDeliverySuppressed(message.details.jobId) === true ||\n\t\t\t(sessionId !== undefined && manager?.isSessionDisposed(sessionId) === true);\n\t\tif (await waitForStreamingBoundary(session, isStale) === \"stale\") return;\n\t\tif (isStale()) return;\n\t\tawait session.sendCustomMessage(message, { deliverAs: \"followUp\", triggerTurn: true });\n\t};\n}\n\nexport function createSessionAsyncJobManager(session: AsyncDeliverySession): SessionAsyncJobManagerHandle {\n\tconst existing = AsyncJobManager.instance();\n\tif (existing) return { manager: existing, owns: false, sessionId: existing.registerSession() };\n\tlet manager: AsyncJobManager;\n\tmanager = new AsyncJobManager({\n\t\tonJobComplete: (message) => createSessionAsyncDeliveryHandler(session, manager)(message),\n\t});\n\tAsyncJobManager.setInstance(manager);\n\treturn { manager, owns: true, sessionId: manager.registerSession() };\n}\n\nexport function disposeSessionAsyncJobManager(manager: AsyncJobManager | undefined, sessionId: symbol | undefined): void {\n\tif (!manager || !sessionId) return;\n\tmanager.releaseSession(sessionId);\n\tif (manager.disposed && AsyncJobManager.instance() === manager) AsyncJobManager.setInstance(undefined);\n}\n\n"]}
@@ -0,0 +1,61 @@
1
+ import { AsyncJobManager } from "./job-manager.js";
2
+ const STREAMING_DELIVERY_POLL_MS = 10;
3
+ function scheduleBoundaryCheck(callback) {
4
+ const timer = setTimeout(callback, STREAMING_DELIVERY_POLL_MS);
5
+ timer.unref?.();
6
+ return timer;
7
+ }
8
+ function waitForStreamingBoundary(session, isStale) {
9
+ return new Promise((resolve) => {
10
+ let timer;
11
+ const settle = (value) => {
12
+ if (timer)
13
+ clearTimeout(timer);
14
+ resolve(value);
15
+ };
16
+ const check = () => {
17
+ timer = undefined;
18
+ if (isStale()) {
19
+ settle("stale");
20
+ return;
21
+ }
22
+ if (session.isStreaming !== true) {
23
+ settle("ready");
24
+ return;
25
+ }
26
+ timer = scheduleBoundaryCheck(check);
27
+ };
28
+ check();
29
+ });
30
+ }
31
+ export function createSessionAsyncDeliveryHandler(session, manager, sessionId) {
32
+ return async (message) => {
33
+ const isStale = () => manager?.disposed === true ||
34
+ manager?.isDeliverySuppressed(message.details.jobId) === true ||
35
+ (sessionId !== undefined && manager?.isSessionDisposed(sessionId) === true);
36
+ if (await waitForStreamingBoundary(session, isStale) === "stale")
37
+ return;
38
+ if (isStale())
39
+ return;
40
+ await session.sendCustomMessage(message, { deliverAs: "followUp", triggerTurn: true });
41
+ };
42
+ }
43
+ export function createSessionAsyncJobManager(session) {
44
+ const existing = AsyncJobManager.instance();
45
+ if (existing)
46
+ return { manager: existing, owns: false, sessionId: existing.registerSession() };
47
+ let manager;
48
+ manager = new AsyncJobManager({
49
+ onJobComplete: (message) => createSessionAsyncDeliveryHandler(session, manager)(message),
50
+ });
51
+ AsyncJobManager.setInstance(manager);
52
+ return { manager, owns: true, sessionId: manager.registerSession() };
53
+ }
54
+ export function disposeSessionAsyncJobManager(manager, sessionId) {
55
+ if (!manager || !sessionId)
56
+ return;
57
+ manager.releaseSession(sessionId);
58
+ if (manager.disposed && AsyncJobManager.instance() === manager)
59
+ AsyncJobManager.setInstance(undefined);
60
+ }
61
+ //# sourceMappingURL=session-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../../../src/core/async/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CustomMessage } from \"../messages.js\";\nimport type { SendMessageOptions } from \"../extensions/index.js\";\nimport { AsyncJobManager } from \"./job-manager.js\";\nimport type { AsyncJobDeliveryHandler, AsyncJobDeliveryMessage } from \"./types.js\";\n\nexport interface SessionAsyncJobManagerHandle {\n\tmanager: AsyncJobManager;\n\towns: boolean;\n\tsessionId: symbol;\n}\ninterface AsyncDeliverySession {\n\treadonly isStreaming?: boolean;\n\tsendCustomMessage<T>(\n\t\tmessage: Pick<CustomMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\t\toptions?: SendMessageOptions,\n\t): Promise<void>;\n}\n\nconst STREAMING_DELIVERY_POLL_MS = 10;\n\nfunction scheduleBoundaryCheck(callback: () => void): NodeJS.Timeout {\n\tconst timer = setTimeout(callback, STREAMING_DELIVERY_POLL_MS);\n\ttimer.unref?.();\n\treturn timer;\n}\n\nfunction waitForStreamingBoundary(session: AsyncDeliverySession, isStale: () => boolean): Promise<\"ready\" | \"stale\"> {\n\treturn new Promise((resolve) => {\n\t\tlet timer: NodeJS.Timeout | undefined;\n\t\tconst settle = (value: \"ready\" | \"stale\") => {\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\tresolve(value);\n\t\t};\n\t\tconst check = () => {\n\t\t\ttimer = undefined;\n\t\t\tif (isStale()) {\n\t\t\t\tsettle(\"stale\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (session.isStreaming !== true) {\n\t\t\t\tsettle(\"ready\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttimer = scheduleBoundaryCheck(check);\n\t\t};\n\t\tcheck();\n\t});\n}\n\nexport function createSessionAsyncDeliveryHandler(session: AsyncDeliverySession, manager?: AsyncJobManager, sessionId?: symbol): AsyncJobDeliveryHandler {\n\treturn async (message: AsyncJobDeliveryMessage) => {\n\t\tconst isStale = () =>\n\t\t\tmanager?.disposed === true ||\n\t\t\tmanager?.isDeliverySuppressed(message.details.jobId) === true ||\n\t\t\t(sessionId !== undefined && manager?.isSessionDisposed(sessionId) === true);\n\t\tif (await waitForStreamingBoundary(session, isStale) === \"stale\") return;\n\t\tif (isStale()) return;\n\t\tawait session.sendCustomMessage(message, { deliverAs: \"followUp\", triggerTurn: true });\n\t};\n}\n\nexport function createSessionAsyncJobManager(session: AsyncDeliverySession): SessionAsyncJobManagerHandle {\n\tconst existing = AsyncJobManager.instance();\n\tif (existing) return { manager: existing, owns: false, sessionId: existing.registerSession() };\n\tlet manager: AsyncJobManager;\n\tmanager = new AsyncJobManager({\n\t\tonJobComplete: (message) => createSessionAsyncDeliveryHandler(session, manager)(message),\n\t});\n\tAsyncJobManager.setInstance(manager);\n\treturn { manager, owns: true, sessionId: manager.registerSession() };\n}\n\nexport function disposeSessionAsyncJobManager(manager: AsyncJobManager | undefined, sessionId: symbol | undefined): void {\n\tif (!manager || !sessionId) return;\n\tmanager.releaseSession(sessionId);\n\tif (manager.disposed && AsyncJobManager.instance() === manager) AsyncJobManager.setInstance(undefined);\n}\n\n"]}
@@ -0,0 +1,23 @@
1
+ import type { ManagedBashJob } from "../tools/bash-async-jobs.js";
2
+ export type AsyncJobStatus = "running" | "completed" | "failed";
3
+ export interface AsyncJobDeliveryDetails {
4
+ jobId: string;
5
+ type: "bash";
6
+ status: AsyncJobStatus;
7
+ command: string;
8
+ exitCode?: number | null;
9
+ fullOutputPath?: string;
10
+ wallTimeMs?: number;
11
+ }
12
+ export interface AsyncJobDeliveryMessage {
13
+ customType: "async-job-result";
14
+ content: string;
15
+ display: true;
16
+ details: AsyncJobDeliveryDetails;
17
+ }
18
+ export type AsyncJobDeliveryCallback = (message: AsyncJobDeliveryMessage) => void | Promise<void>;
19
+ export type AsyncJobDeliveryHandler = (message: AsyncJobDeliveryMessage) => void | Promise<void>;
20
+ export interface ManagedAsyncBashJob extends ManagedBashJob {
21
+ status: AsyncJobStatus;
22
+ }
23
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/async/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEhE,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACvC,UAAU,EAAE,kBAAkB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,uBAAuB,CAAC;CACjC;AAED,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAElG,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjG,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IAC1D,MAAM,EAAE,cAAc,CAAC;CACvB","sourcesContent":["import type { ManagedBashJob } from \"../tools/bash-async-jobs.js\";\n\nexport type AsyncJobStatus = \"running\" | \"completed\" | \"failed\";\n\nexport interface AsyncJobDeliveryDetails {\n\tjobId: string;\n\ttype: \"bash\";\n\tstatus: AsyncJobStatus;\n\tcommand: string;\n\texitCode?: number | null;\n\tfullOutputPath?: string;\n\twallTimeMs?: number;\n}\n\nexport interface AsyncJobDeliveryMessage {\n\tcustomType: \"async-job-result\";\n\tcontent: string;\n\tdisplay: true;\n\tdetails: AsyncJobDeliveryDetails;\n}\n\nexport type AsyncJobDeliveryCallback = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport type AsyncJobDeliveryHandler = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport interface ManagedAsyncBashJob extends ManagedBashJob {\n\tstatus: AsyncJobStatus;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/async/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ManagedBashJob } from \"../tools/bash-async-jobs.js\";\n\nexport type AsyncJobStatus = \"running\" | \"completed\" | \"failed\";\n\nexport interface AsyncJobDeliveryDetails {\n\tjobId: string;\n\ttype: \"bash\";\n\tstatus: AsyncJobStatus;\n\tcommand: string;\n\texitCode?: number | null;\n\tfullOutputPath?: string;\n\twallTimeMs?: number;\n}\n\nexport interface AsyncJobDeliveryMessage {\n\tcustomType: \"async-job-result\";\n\tcontent: string;\n\tdisplay: true;\n\tdetails: AsyncJobDeliveryDetails;\n}\n\nexport type AsyncJobDeliveryCallback = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport type AsyncJobDeliveryHandler = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport interface ManagedAsyncBashJob extends ManagedBashJob {\n\tstatus: AsyncJobStatus;\n}\n"]}
@@ -6,6 +6,16 @@ export interface ExtensionCacheToken {
6
6
  }
7
7
  export declare function clearExtensionCache(): void;
8
8
  export declare function useExtensionCacheCwd(cwd: string): ExtensionCacheToken;
9
+ /**
10
+ * Locate an installed package's root directory without consulting its
11
+ * "exports" map: require.resolve("<pkg>/package.json") throws
12
+ * ERR_PACKAGE_PATH_NOT_EXPORTED under Node for packages that do not export
13
+ * "./package.json" (pi-ai does not), and import.meta.resolve() cannot be
14
+ * used because its mere presence silently disables bytecode generation for
15
+ * the compiled binary (CJS bundle). Scanning require.resolve.paths() walks
16
+ * the same node_modules chain Node would, without exports-map encapsulation.
17
+ */
18
+ declare function findPackageRoot(packageName: string, searchPaths?: string[]): string;
9
19
  /**
10
20
  * Get aliases for jiti (used in Node.js/development mode).
11
21
  * In Bun binary mode, virtualModules is used instead.
@@ -15,6 +25,7 @@ declare function getAliases(): Record<string, string>;
15
25
  export declare const extensionLoaderTestHooks: {
16
26
  loadVirtualModules: typeof loadVirtualModules;
17
27
  getAliases: typeof getAliases;
28
+ findPackageRoot: typeof findPackageRoot;
18
29
  };
19
30
  export declare function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken): Promise<ExtensionFactory | undefined>;
20
31
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"loader-virtual-modules.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader-virtual-modules.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAMnD,iBAAe,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqCnE;AAoDD,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAOrE;AAiBD;;;GAGG;AACH,iBAAS,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwD5C;AAED,kEAAkE;AAClE,eAAO,MAAM,wBAAwB;;;CAGpC,CAAC;AAUF,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,mBAAmB,GAC/B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmCvC","sourcesContent":["import * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { createJiti } from \"jiti/static\";\nimport { getExtensionTranspileCacheDir, isBunBinary, isBundledBuild } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport type { ExtensionFactory } from \"./types.ts\";\n\nconst require = createRequire(import.meta.url);\nlet _virtualModules: Record<string, object> | null = null;\nlet _virtualModulesPromise: Promise<Record<string, object>> | null = null;\n\nasync function loadVirtualModules(): Promise<Record<string, object>> {\n const [typebox, typeboxCompile, typeboxValue, piAgentCore, piTui, piAi, piAiOauth, piCodingAgent] = await Promise.all([\n import(\"typebox\"),\n import(\"typebox/compile\"),\n import(\"typebox/value\"),\n import(\"@earendil-works/pi-agent-core\"),\n import(\"@earendil-works/pi-tui\"),\n // pi 0.80.2: the old global pi-ai API moved off the root entrypoint onto\n // `/compat` (a strict superset). Extensions still `import ... from\n // \"@earendil-works/pi-ai\"`, so we load the compat module here and key it\n // under the root specifier below to keep every extension working unchanged.\n import(\"@earendil-works/pi-ai/compat\"),\n import(\"@earendil-works/pi-ai/oauth\"),\n // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n // avoiding a circular dependency while preserving the package-name extension import path.\n import(\"../../index.ts\"),\n ]);\n\n return {\n typebox,\n \"typebox/compile\": typeboxCompile,\n \"typebox/value\": typeboxValue,\n \"@sinclair/typebox\": typebox,\n \"@sinclair/typebox/compile\": typeboxCompile,\n \"@sinclair/typebox/value\": typeboxValue,\n \"@earendil-works/pi-agent-core\": piAgentCore,\n \"@earendil-works/pi-tui\": piTui,\n \"@earendil-works/pi-ai\": piAi,\n \"@earendil-works/pi-ai/compat\": piAi,\n \"@earendil-works/pi-ai/oauth\": piAiOauth,\n \"@bastani/atomic\": piCodingAgent,\n \"@mariozechner/pi-agent-core\": piAgentCore,\n \"@mariozechner/pi-tui\": piTui,\n \"@mariozechner/pi-ai\": piAi,\n \"@mariozechner/pi-ai/compat\": piAi,\n \"@mariozechner/pi-ai/oauth\": piAiOauth,\n };\n}\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary). */\nasync function getVirtualModules(): Promise<Record<string, object>> {\n if (_virtualModules) return _virtualModules;\n _virtualModulesPromise ??= loadVirtualModules().then(\n (virtualModules) => {\n _virtualModules = virtualModules;\n return virtualModules;\n },\n (error: Error) => {\n _virtualModulesPromise = null;\n throw error;\n },\n );\n return _virtualModulesPromise;\n}\nlet _aliases: Record<string, string> | null = null;\nlet _transpileCacheDir: string | null = null;\n\n/**\n * Persistent on-disk cache for jiti-transpiled extension modules.\n * jiti keys cache entries by source-content hash, so entries self-invalidate\n * when extension sources change; stale sibling version dirs are pruned\n * in the background.\n */\nfunction getTranspileCacheDir(): string {\n if (_transpileCacheDir) return _transpileCacheDir;\n _transpileCacheDir = getExtensionTranspileCacheDir();\n pruneStaleTranspileCaches(_transpileCacheDir);\n return _transpileCacheDir;\n}\n\nfunction pruneStaleTranspileCaches(currentDir: string): void {\n const parent = path.dirname(currentDir);\n const keep = path.basename(currentDir);\n void fs.promises\n .readdir(parent)\n .then((entries) =>\n Promise.all(\n entries\n .filter((entry) => entry !== keep)\n .map((entry) => fs.promises.rm(path.join(parent, entry), { recursive: true, force: true })),\n ),\n )\n .catch(() => {});\n}\n\nlet extensionCacheCwd: string | undefined;\nlet extensionCacheGeneration = 0;\nconst extensionCache = new Map<string, ExtensionFactory>();\n\nexport interface ExtensionCacheToken {\n cwd: string;\n generation: number;\n}\n\nexport function clearExtensionCache(): void {\n extensionCache.clear();\n extensionCacheCwd = undefined;\n extensionCacheGeneration++;\n}\n\nexport function useExtensionCacheCwd(cwd: string): ExtensionCacheToken {\n const resolvedCwd = resolvePath(cwd);\n if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {\n clearExtensionCache();\n }\n extensionCacheCwd = resolvedCwd;\n return { cwd: resolvedCwd, generation: extensionCacheGeneration };\n}\n\nfunction isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {\n return (\n cacheToken !== undefined &&\n extensionCacheCwd === cacheToken.cwd &&\n extensionCacheGeneration === cacheToken.generation\n );\n}\n\nfunction extensionImportSpecifier(extensionPath: string, cacheToken: ExtensionCacheToken | undefined): string {\n const url = pathToFileURL(extensionPath);\n const cacheKey = cacheToken ? `${cacheToken.generation}:${cacheToken.cwd}` : `${Date.now()}:${Math.random()}`;\n url.searchParams.set(\"atomicExtensionCache\", cacheKey);\n return url.href;\n}\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nfunction getAliases(): Record<string, string> {\n if (_aliases) return _aliases;\n\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n const typeboxEntry = require.resolve(\"typebox\");\n const typeboxCompileEntry = require.resolve(\"typebox/compile\");\n const typeboxValueEntry = require.resolve(\"typebox/value\");\n\n const packagesRoot = path.resolve(__dirname, \"../../../../\");\n const resolveWorkspaceOrImport = (workspaceRelativePath: string, packageName: string): string => {\n const workspacePath = path.join(packagesRoot, workspaceRelativePath);\n if (fs.existsSync(workspacePath)) {\n return workspacePath;\n }\n // Resolve via the package.json export and join the built-entry path, not\n // import.meta.resolve: its mere presence silently breaks bytecode\n // generation for the compiled binary (CJS bundle), and require.resolve\n // fails on these ESM-only packages.\n const packageRoot = path.dirname(require.resolve(`${packageName}/package.json`));\n const entryRelativePath = workspaceRelativePath.split(\"/\").slice(1).join(\"/\");\n return path.join(packageRoot, entryRelativePath);\n };\n\n const piCodingAgentEntry = packageIndex;\n const piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/dist/index.js\", \"@earendil-works/pi-agent-core\");\n const piTuiEntry = resolveWorkspaceOrImport(\"tui/dist/index.js\", \"@earendil-works/pi-tui\");\n // The workspace path mirrors pi-ai 0.80.x's built dist layout. If an\n // upstream layout change moves these files, this join needs updating to\n // match the package's real dist paths.\n const piAiEntry = resolveWorkspaceOrImport(\"ai/dist/compat.js\", \"@earendil-works/pi-ai\");\n const piAiOauthEntry = resolveWorkspaceOrImport(\"ai/dist/oauth.js\", \"@earendil-works/pi-ai\");\n\n _aliases = {\n \"@bastani/atomic\": piCodingAgentEntry,\n \"@earendil-works/pi-coding-agent\": piCodingAgentEntry,\n \"@earendil-works/pi-agent-core\": piAgentCoreEntry,\n \"@earendil-works/pi-tui\": piTuiEntry,\n \"@earendil-works/pi-ai\": piAiEntry,\n \"@earendil-works/pi-ai/compat\": piAiEntry,\n \"@earendil-works/pi-ai/oauth\": piAiOauthEntry,\n \"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n \"@mariozechner/pi-tui\": piTuiEntry,\n \"@mariozechner/pi-ai\": piAiEntry,\n \"@mariozechner/pi-ai/compat\": piAiEntry,\n \"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n typebox: typeboxEntry,\n \"typebox/compile\": typeboxCompileEntry,\n \"typebox/value\": typeboxValueEntry,\n \"@sinclair/typebox\": typeboxEntry,\n \"@sinclair/typebox/compile\": typeboxCompileEntry,\n \"@sinclair/typebox/value\": typeboxValueEntry,\n };\n\n return _aliases;\n}\n\n/** Internal hooks for extension-loader alias regression tests. */\nexport const extensionLoaderTestHooks = {\n loadVirtualModules,\n getAliases,\n};\n\n/**\n * Extension paths already evaluated via native import() in this process. Bun on\n * Windows ignores the cache-busting query on file URLs, so re-loads of these\n * paths (e.g. /reload) must go through jiti's transformed-import path to get a\n * fresh module evaluation.\n */\nconst nativelyImportedPaths = new Set<string>();\n\nexport async function loadExtensionModule(\n extensionPath: string,\n cacheToken?: ExtensionCacheToken,\n): Promise<ExtensionFactory | undefined> {\n if (isCurrentCacheToken(cacheToken)) {\n const cachedFactory = extensionCache.get(extensionPath);\n if (cachedFactory) return cachedFactory;\n }\n\n const isWindows = process.platform === \"win32\";\n // Single-file builds (compiled binary or dev bundle) cannot alias host\n // package specifiers to files on disk: extensions must share the live\n // module instances baked into the build, so virtualModules is used instead\n // (which requires jiti's transformed-import path).\n const isSingleFileBuild = isBunBinary || isBundledBuild;\n // Windows first-load fast path: native import() (jiti's default tryNative)\n // skips per-launch transpilation of the extension module graph. Re-loads of\n // the same path fall back to transformed imports for fresh evaluation.\n const forceTransformedImports = isSingleFileBuild || (isWindows && nativelyImportedPaths.has(extensionPath));\n const jiti = createJiti(import.meta.url, {\n moduleCache: false,\n ...(forceTransformedImports\n ? { fsCache: getTranspileCacheDir(), tryNative: false }\n : isWindows\n ? { fsCache: getTranspileCacheDir() }\n : {}),\n ...(isSingleFileBuild ? { virtualModules: await getVirtualModules() } : { alias: getAliases() }),\n });\n const module = await jiti.import(extensionImportSpecifier(extensionPath, cacheToken), { default: true });\n if (isWindows && !forceTransformedImports) {\n nativelyImportedPaths.add(extensionPath);\n }\n const factory = module as ExtensionFactory;\n if (typeof factory !== \"function\") return undefined;\n if (isCurrentCacheToken(cacheToken)) {\n extensionCache.set(extensionPath, factory);\n }\n return factory;\n}\n"]}
1
+ {"version":3,"file":"loader-virtual-modules.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader-virtual-modules.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAMnD,iBAAe,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqCnE;AAoDD,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAOrE;AAiBD;;;;;;;;GAQG;AACH,iBAAS,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ5E;AAED;;;GAGG;AACH,iBAAS,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoD5C;AAED,kEAAkE;AAClE,eAAO,MAAM,wBAAwB;;;;CAIpC,CAAC;AAUF,wBAAsB,mBAAmB,CACvC,aAAa,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,mBAAmB,GAC/B,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmCvC","sourcesContent":["import * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { createJiti } from \"jiti/static\";\nimport { getExtensionTranspileCacheDir, isBunBinary, isBundledBuild } from \"../../config.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport type { ExtensionFactory } from \"./types.ts\";\n\nconst require = createRequire(import.meta.url);\nlet _virtualModules: Record<string, object> | null = null;\nlet _virtualModulesPromise: Promise<Record<string, object>> | null = null;\n\nasync function loadVirtualModules(): Promise<Record<string, object>> {\n const [typebox, typeboxCompile, typeboxValue, piAgentCore, piTui, piAi, piAiOauth, piCodingAgent] = await Promise.all([\n import(\"typebox\"),\n import(\"typebox/compile\"),\n import(\"typebox/value\"),\n import(\"@earendil-works/pi-agent-core\"),\n import(\"@earendil-works/pi-tui\"),\n // pi 0.80.2: the old global pi-ai API moved off the root entrypoint onto\n // `/compat` (a strict superset). Extensions still `import ... from\n // \"@earendil-works/pi-ai\"`, so we load the compat module here and key it\n // under the root specifier below to keep every extension working unchanged.\n import(\"@earendil-works/pi-ai/compat\"),\n import(\"@earendil-works/pi-ai/oauth\"),\n // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n // avoiding a circular dependency while preserving the package-name extension import path.\n import(\"../../index.ts\"),\n ]);\n\n return {\n typebox,\n \"typebox/compile\": typeboxCompile,\n \"typebox/value\": typeboxValue,\n \"@sinclair/typebox\": typebox,\n \"@sinclair/typebox/compile\": typeboxCompile,\n \"@sinclair/typebox/value\": typeboxValue,\n \"@earendil-works/pi-agent-core\": piAgentCore,\n \"@earendil-works/pi-tui\": piTui,\n \"@earendil-works/pi-ai\": piAi,\n \"@earendil-works/pi-ai/compat\": piAi,\n \"@earendil-works/pi-ai/oauth\": piAiOauth,\n \"@bastani/atomic\": piCodingAgent,\n \"@mariozechner/pi-agent-core\": piAgentCore,\n \"@mariozechner/pi-tui\": piTui,\n \"@mariozechner/pi-ai\": piAi,\n \"@mariozechner/pi-ai/compat\": piAi,\n \"@mariozechner/pi-ai/oauth\": piAiOauth,\n };\n}\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary). */\nasync function getVirtualModules(): Promise<Record<string, object>> {\n if (_virtualModules) return _virtualModules;\n _virtualModulesPromise ??= loadVirtualModules().then(\n (virtualModules) => {\n _virtualModules = virtualModules;\n return virtualModules;\n },\n (error: Error) => {\n _virtualModulesPromise = null;\n throw error;\n },\n );\n return _virtualModulesPromise;\n}\nlet _aliases: Record<string, string> | null = null;\nlet _transpileCacheDir: string | null = null;\n\n/**\n * Persistent on-disk cache for jiti-transpiled extension modules.\n * jiti keys cache entries by source-content hash, so entries self-invalidate\n * when extension sources change; stale sibling version dirs are pruned\n * in the background.\n */\nfunction getTranspileCacheDir(): string {\n if (_transpileCacheDir) return _transpileCacheDir;\n _transpileCacheDir = getExtensionTranspileCacheDir();\n pruneStaleTranspileCaches(_transpileCacheDir);\n return _transpileCacheDir;\n}\n\nfunction pruneStaleTranspileCaches(currentDir: string): void {\n const parent = path.dirname(currentDir);\n const keep = path.basename(currentDir);\n void fs.promises\n .readdir(parent)\n .then((entries) =>\n Promise.all(\n entries\n .filter((entry) => entry !== keep)\n .map((entry) => fs.promises.rm(path.join(parent, entry), { recursive: true, force: true })),\n ),\n )\n .catch(() => {});\n}\n\nlet extensionCacheCwd: string | undefined;\nlet extensionCacheGeneration = 0;\nconst extensionCache = new Map<string, ExtensionFactory>();\n\nexport interface ExtensionCacheToken {\n cwd: string;\n generation: number;\n}\n\nexport function clearExtensionCache(): void {\n extensionCache.clear();\n extensionCacheCwd = undefined;\n extensionCacheGeneration++;\n}\n\nexport function useExtensionCacheCwd(cwd: string): ExtensionCacheToken {\n const resolvedCwd = resolvePath(cwd);\n if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {\n clearExtensionCache();\n }\n extensionCacheCwd = resolvedCwd;\n return { cwd: resolvedCwd, generation: extensionCacheGeneration };\n}\n\nfunction isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {\n return (\n cacheToken !== undefined &&\n extensionCacheCwd === cacheToken.cwd &&\n extensionCacheGeneration === cacheToken.generation\n );\n}\n\nfunction extensionImportSpecifier(extensionPath: string, cacheToken: ExtensionCacheToken | undefined): string {\n const url = pathToFileURL(extensionPath);\n const cacheKey = cacheToken ? `${cacheToken.generation}:${cacheToken.cwd}` : `${Date.now()}:${Math.random()}`;\n url.searchParams.set(\"atomicExtensionCache\", cacheKey);\n return url.href;\n}\n\n/**\n * Locate an installed package's root directory without consulting its\n * \"exports\" map: require.resolve(\"<pkg>/package.json\") throws\n * ERR_PACKAGE_PATH_NOT_EXPORTED under Node for packages that do not export\n * \"./package.json\" (pi-ai does not), and import.meta.resolve() cannot be\n * used because its mere presence silently disables bytecode generation for\n * the compiled binary (CJS bundle). Scanning require.resolve.paths() walks\n * the same node_modules chain Node would, without exports-map encapsulation.\n */\nfunction findPackageRoot(packageName: string, searchPaths?: string[]): string {\n for (const base of searchPaths ?? require.resolve.paths(packageName) ?? []) {\n const candidate = path.join(base, packageName);\n if (fs.existsSync(path.join(candidate, \"package.json\"))) {\n return candidate;\n }\n }\n throw new Error(`Cannot locate package directory for \"${packageName}\"`);\n}\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nfunction getAliases(): Record<string, string> {\n if (_aliases) return _aliases;\n\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n const typeboxEntry = require.resolve(\"typebox\");\n const typeboxCompileEntry = require.resolve(\"typebox/compile\");\n const typeboxValueEntry = require.resolve(\"typebox/value\");\n\n const packagesRoot = path.resolve(__dirname, \"../../../../\");\n const resolveWorkspaceOrImport = (workspaceRelativePath: string, packageName: string): string => {\n const workspacePath = path.join(packagesRoot, workspaceRelativePath);\n if (fs.existsSync(workspacePath)) {\n return workspacePath;\n }\n const packageRoot = findPackageRoot(packageName);\n const entryRelativePath = workspaceRelativePath.split(\"/\").slice(1).join(\"/\");\n return path.join(packageRoot, entryRelativePath);\n };\n\n const piCodingAgentEntry = packageIndex;\n const piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/dist/index.js\", \"@earendil-works/pi-agent-core\");\n const piTuiEntry = resolveWorkspaceOrImport(\"tui/dist/index.js\", \"@earendil-works/pi-tui\");\n // The workspace path mirrors pi-ai 0.80.x's built dist layout. If an\n // upstream layout change moves these files, this join needs updating to\n // match the package's real dist paths.\n const piAiEntry = resolveWorkspaceOrImport(\"ai/dist/compat.js\", \"@earendil-works/pi-ai\");\n const piAiOauthEntry = resolveWorkspaceOrImport(\"ai/dist/oauth.js\", \"@earendil-works/pi-ai\");\n\n _aliases = {\n \"@bastani/atomic\": piCodingAgentEntry,\n \"@earendil-works/pi-coding-agent\": piCodingAgentEntry,\n \"@earendil-works/pi-agent-core\": piAgentCoreEntry,\n \"@earendil-works/pi-tui\": piTuiEntry,\n \"@earendil-works/pi-ai\": piAiEntry,\n \"@earendil-works/pi-ai/compat\": piAiEntry,\n \"@earendil-works/pi-ai/oauth\": piAiOauthEntry,\n \"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n \"@mariozechner/pi-tui\": piTuiEntry,\n \"@mariozechner/pi-ai\": piAiEntry,\n \"@mariozechner/pi-ai/compat\": piAiEntry,\n \"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n typebox: typeboxEntry,\n \"typebox/compile\": typeboxCompileEntry,\n \"typebox/value\": typeboxValueEntry,\n \"@sinclair/typebox\": typeboxEntry,\n \"@sinclair/typebox/compile\": typeboxCompileEntry,\n \"@sinclair/typebox/value\": typeboxValueEntry,\n };\n\n return _aliases;\n}\n\n/** Internal hooks for extension-loader alias regression tests. */\nexport const extensionLoaderTestHooks = {\n loadVirtualModules,\n getAliases,\n findPackageRoot,\n};\n\n/**\n * Extension paths already evaluated via native import() in this process. Bun on\n * Windows ignores the cache-busting query on file URLs, so re-loads of these\n * paths (e.g. /reload) must go through jiti's transformed-import path to get a\n * fresh module evaluation.\n */\nconst nativelyImportedPaths = new Set<string>();\n\nexport async function loadExtensionModule(\n extensionPath: string,\n cacheToken?: ExtensionCacheToken,\n): Promise<ExtensionFactory | undefined> {\n if (isCurrentCacheToken(cacheToken)) {\n const cachedFactory = extensionCache.get(extensionPath);\n if (cachedFactory) return cachedFactory;\n }\n\n const isWindows = process.platform === \"win32\";\n // Single-file builds (compiled binary or dev bundle) cannot alias host\n // package specifiers to files on disk: extensions must share the live\n // module instances baked into the build, so virtualModules is used instead\n // (which requires jiti's transformed-import path).\n const isSingleFileBuild = isBunBinary || isBundledBuild;\n // Windows first-load fast path: native import() (jiti's default tryNative)\n // skips per-launch transpilation of the extension module graph. Re-loads of\n // the same path fall back to transformed imports for fresh evaluation.\n const forceTransformedImports = isSingleFileBuild || (isWindows && nativelyImportedPaths.has(extensionPath));\n const jiti = createJiti(import.meta.url, {\n moduleCache: false,\n ...(forceTransformedImports\n ? { fsCache: getTranspileCacheDir(), tryNative: false }\n : isWindows\n ? { fsCache: getTranspileCacheDir() }\n : {}),\n ...(isSingleFileBuild ? { virtualModules: await getVirtualModules() } : { alias: getAliases() }),\n });\n const module = await jiti.import(extensionImportSpecifier(extensionPath, cacheToken), { default: true });\n if (isWindows && !forceTransformedImports) {\n nativelyImportedPaths.add(extensionPath);\n }\n const factory = module as ExtensionFactory;\n if (typeof factory !== \"function\") return undefined;\n if (isCurrentCacheToken(cacheToken)) {\n extensionCache.set(extensionPath, factory);\n }\n return factory;\n}\n"]}
@@ -110,6 +110,24 @@ function extensionImportSpecifier(extensionPath, cacheToken) {
110
110
  url.searchParams.set("atomicExtensionCache", cacheKey);
111
111
  return url.href;
112
112
  }
113
+ /**
114
+ * Locate an installed package's root directory without consulting its
115
+ * "exports" map: require.resolve("<pkg>/package.json") throws
116
+ * ERR_PACKAGE_PATH_NOT_EXPORTED under Node for packages that do not export
117
+ * "./package.json" (pi-ai does not), and import.meta.resolve() cannot be
118
+ * used because its mere presence silently disables bytecode generation for
119
+ * the compiled binary (CJS bundle). Scanning require.resolve.paths() walks
120
+ * the same node_modules chain Node would, without exports-map encapsulation.
121
+ */
122
+ function findPackageRoot(packageName, searchPaths) {
123
+ for (const base of searchPaths ?? require.resolve.paths(packageName) ?? []) {
124
+ const candidate = path.join(base, packageName);
125
+ if (fs.existsSync(path.join(candidate, "package.json"))) {
126
+ return candidate;
127
+ }
128
+ }
129
+ throw new Error(`Cannot locate package directory for "${packageName}"`);
130
+ }
113
131
  /**
114
132
  * Get aliases for jiti (used in Node.js/development mode).
115
133
  * In Bun binary mode, virtualModules is used instead.
@@ -128,11 +146,7 @@ function getAliases() {
128
146
  if (fs.existsSync(workspacePath)) {
129
147
  return workspacePath;
130
148
  }
131
- // Resolve via the package.json export and join the built-entry path, not
132
- // import.meta.resolve: its mere presence silently breaks bytecode
133
- // generation for the compiled binary (CJS bundle), and require.resolve
134
- // fails on these ESM-only packages.
135
- const packageRoot = path.dirname(require.resolve(`${packageName}/package.json`));
149
+ const packageRoot = findPackageRoot(packageName);
136
150
  const entryRelativePath = workspaceRelativePath.split("/").slice(1).join("/");
137
151
  return path.join(packageRoot, entryRelativePath);
138
152
  };
@@ -170,6 +184,7 @@ function getAliases() {
170
184
  export const extensionLoaderTestHooks = {
171
185
  loadVirtualModules,
172
186
  getAliases,
187
+ findPackageRoot,
173
188
  };
174
189
  /**
175
190
  * Extension paths already evaluated via native import() in this process. Bun on