jeopi-cli 16.6.14 → 16.6.17

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.
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { logger } from "jeopi-utils";
2
3
  import {
3
4
  createWorkerHandle,
4
5
  createWorkerSubprocess,
@@ -106,6 +107,7 @@ interface Lane {
106
107
  /** Turns queued or running; idle retirement waits for zero. */
107
108
  turns: number;
108
109
  live?: LiveSession;
110
+ retiring?: Promise<void>;
109
111
  idle?: NodeJS.Timeout;
110
112
  /** Already chosen to free a slot, so concurrent starts pick different victims. */
111
113
  evicting?: boolean;
@@ -152,27 +154,52 @@ export function createMessengerSessionPool(
152
154
  };
153
155
 
154
156
  /** Let the process flush and dispose its session, then kill it; the session file stays for resumption. */
155
- const retire = async (lane: Lane): Promise<void> => {
157
+ const retire = (lane: Lane): Promise<void> => {
158
+ if (lane.retiring) return lane.retiring;
156
159
  const live = lane.live;
157
- if (!live) return;
160
+ if (!live) return Promise.resolve();
158
161
  lane.live = undefined;
159
- const id = String(++nextId);
160
- const acknowledged = Promise.withResolvers<void>();
161
- const unsubscribe = live.process.onMessage(event => {
162
- if (event.type === "closed" && event.id === id) acknowledged.resolve();
163
- });
164
- const timer = setTimeout(acknowledged.resolve, SESSION_CLOSE_TIMEOUT_MS);
165
- try {
166
- live.process.send({ type: "close", id });
167
- await Promise.race([acknowledged.promise, live.process.exited]);
168
- } catch {
169
- // The process is already gone; killing it below is still safe.
170
- } finally {
171
- clearTimeout(timer);
172
- unsubscribe();
162
+ const retirement = (async () => {
163
+ const id = String(++nextId);
164
+ const acknowledged = Promise.withResolvers<void>();
165
+ const unsubscribe = live.process.onMessage(event => {
166
+ if (event.type === "closed" && event.id === id) acknowledged.resolve();
167
+ });
168
+ const timer = setTimeout(acknowledged.resolve, SESSION_CLOSE_TIMEOUT_MS);
169
+ try {
170
+ live.process.send({ type: "close", id });
171
+ await Promise.race([acknowledged.promise, live.process.exited]);
172
+ } catch {
173
+ // Still try to terminate a process with a broken IPC channel.
174
+ } finally {
175
+ clearTimeout(timer);
176
+ unsubscribe();
177
+ }
173
178
  live.process.kill();
174
- release(live);
175
- }
179
+ const timeout = Promise.withResolvers<void>();
180
+ const exitTimer = setTimeout(
181
+ () => timeout.reject(new Error("Messenger session process did not exit")),
182
+ SESSION_CLOSE_TIMEOUT_MS,
183
+ );
184
+ try {
185
+ await Promise.race([
186
+ live.process.exited.then(
187
+ () => {},
188
+ () => {},
189
+ ),
190
+ timeout.promise,
191
+ ]);
192
+ } finally {
193
+ clearTimeout(exitTimer);
194
+ }
195
+ })();
196
+ lane.retiring = retirement;
197
+ const clearRetirement = (): void => {
198
+ if (lane.retiring === retirement) lane.retiring = undefined;
199
+ };
200
+ void live.process.exited.then(clearRetirement, clearRetirement);
201
+ void retirement.catch(() => {});
202
+ return retirement;
176
203
  };
177
204
 
178
205
  const retireIfIdle = async (lane: Lane): Promise<void> => {
@@ -261,6 +288,7 @@ export function createMessengerSessionPool(
261
288
  result.reject(new Error("Messenger request aborted"));
262
289
  };
263
290
  try {
291
+ if (lane.retiring) await lane.retiring;
264
292
  live ??= await start(lane, signal);
265
293
  const current = live;
266
294
  current.failTurn = result.reject;
@@ -271,7 +299,7 @@ export function createMessengerSessionPool(
271
299
  else if (event.type === "failed") result.reject(new Error("Messenger session turn failed"));
272
300
  });
273
301
  signal.addEventListener("abort", onAbort, { once: true });
274
- if (signal.aborted) onAbort();
302
+ if (signal.aborted) throw new Error("Messenger request aborted");
275
303
  try {
276
304
  current.process.send({ type: "turn", id, message });
277
305
  } catch (error) {
@@ -309,7 +337,11 @@ export function createMessengerSessionPool(
309
337
  } finally {
310
338
  current.turns--;
311
339
  if (current.turns === 0 && !closed) {
312
- current.idle = setTimeout(() => void enqueue(current, () => retireIfIdle(current)), idleMs);
340
+ current.idle = setTimeout(() => {
341
+ void enqueue(current, () => retireIfIdle(current)).catch(() =>
342
+ logger.warn("Messenger session retirement failed"),
343
+ );
344
+ }, idleMs);
313
345
  current.idle.unref?.();
314
346
  capacityChanged();
315
347
  }
@@ -323,7 +355,9 @@ export function createMessengerSessionPool(
323
355
  const all = [...lanes.values()];
324
356
  lanes.clear();
325
357
  for (const lane of all) clearTimeout(lane.idle);
326
- await Promise.allSettled(all.map(retire));
358
+ const results = await Promise.allSettled(all.map(retire));
359
+ const failure = results.find(result => result.status === "rejected");
360
+ if (failure?.status === "rejected") throw failure.reason;
327
361
  })();
328
362
  return closing;
329
363
  };
@@ -83,6 +83,9 @@ export function createMessengerSessionBridge(
83
83
  }
84
84
 
85
85
  async function initialize(route: string, file: string): Promise<void> {
86
+ const userSettings = await Settings.loadReadOnly({ cwd, agentDir: dependencies?.agentDir });
87
+ const defaultModel = userSettings.getModelRole("default");
88
+ if (defaultModel) settings.override("modelRoles", { default: defaultModel });
86
89
  const sessionManager = await SessionManager.open(file, sessionDir, undefined, {
87
90
  initialCwd: cwd,
88
91
  suppressBreadcrumb: true,
@@ -27,11 +27,15 @@ export interface MessengerAdapter {
27
27
  status: "processing" | "success" | "failure",
28
28
  signal?: AbortSignal,
29
29
  ): Promise<void>;
30
+ /** Remove an in-progress status after cancellation without adding a terminal status. */
31
+ clearStatus?(message: MessengerMessage, signal?: AbortSignal): Promise<void>;
30
32
  /**
31
33
  * Show `text` (one sanitized line) as the request's single in-progress status, editing it in place on
32
34
  * later calls; `undefined` removes it. The gateway serializes and throttles calls per request.
33
35
  */
34
36
  activity?(message: MessengerMessage, text: string | undefined, signal?: AbortSignal): Promise<void>;
37
+ /** Forget request-local cosmetic activity state after the gateway finishes cleanup retries. */
38
+ releaseActivity?(message: MessengerMessage): void;
35
39
  }
36
40
 
37
41
  export interface MessengerPlatformConfig {