jeo-code 0.12.15 → 0.12.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.
@@ -216,7 +216,12 @@ export function messengerSessionRoute(cwd: string, message: MessengerMessage): s
216
216
  return new Bun.CryptoHasher("sha256").update(JSON.stringify([cwd, messengerConversationKey(message)])).digest("hex");
217
217
  }
218
218
 
219
- function schedulingKey(message: MessengerMessage, adapter: MessengerAdapter): string {
219
+ /**
220
+ * Admission lane. A top-level message has no thread yet, so only its raw channel can order it against its
221
+ * neighbours; anything already resolved is ordered by its own conversation. A lane is held only until the request
222
+ * is queued, never while it runs, so it cannot serialize requests that end up in different threads.
223
+ */
224
+ function admissionKey(message: MessengerMessage, adapter: MessengerAdapter): string {
220
225
  if (adapter.prepare && !message.direct && !message.threadId && !message.parentChannelId) {
221
226
  return JSON.stringify([message.platform, message.accountId, message.guildId, message.channelId, message.platform === "discord" ? null : message.senderId]);
222
227
  }
@@ -243,12 +248,15 @@ export function isMessengerMessageAllowed(message: MessengerMessage, config: Mes
243
248
  }
244
249
 
245
250
  interface PendingMessage {
251
+ /** Prepared destination: replies, deliveries and activity target it. */
246
252
  message: MessengerMessage;
247
253
  adapter: MessengerAdapter;
248
254
  signal: AbortSignal;
249
255
  resolve: () => void;
250
- /** Requests sharing a key never run at the same time. */
256
+ /** Destination conversation key; requests sharing it never run at the same time. */
251
257
  key: string;
258
+ /** Status reactions on the user's original message. */
259
+ status: (state: "processing" | "success" | "failure") => void;
252
260
  }
253
261
 
254
262
  export interface MessengerGateway {
@@ -260,8 +268,9 @@ export interface MessengerGatewayOptions {
260
268
  /** Minimum spacing between in-place activity edits for one request. */
261
269
  activityIntervalMs?: number;
262
270
  /**
263
- * Requests processed at once (default 1). Requests of one prepared conversation still answer and deliver in
264
- * order; above 1 the reply implementation must accept concurrent turns for different conversations.
271
+ * Requests processed at once (default 1). Requests of one conversation answer and deliver in arrival order and
272
+ * wait in the queue, not in a slot, behind their predecessor; above 1 the reply implementation must accept
273
+ * concurrent turns for different conversations.
265
274
  */
266
275
  concurrency?: number;
267
276
  /** Authenticated human controls bypass model queues, including busy sessions. */
@@ -286,23 +295,22 @@ export function createMessengerGateway(
286
295
  const queue: PendingMessage[] = [];
287
296
  const activeKeys = new Set<string>();
288
297
  const turns = new Set<Promise<void>>();
289
- /** Settles when the latest request of a prepared conversation has delivered its reply. */
290
- const conversationTails = new Map<string, Promise<void>>();
298
+ /** Orders admission (sandbox control, capacity check, destination preparation) per raw lane. */
291
299
  const admissionTails = new Map<string, Promise<void>>();
300
+ /** Requests holding a queue slot while the adapter prepares their destination; counted against the queue cap. */
301
+ let preparing = 0;
292
302
  const lifetime = new AbortController();
293
303
  let running = false;
294
304
  let fatalError: MessengerFatalError | undefined;
295
305
  let statusInFlight = 0;
296
306
 
297
- async function processRequest(pending: PendingMessage): Promise<void> {
298
- const { message, adapter, signal, resolve } = pending;
299
- let failed = false;
300
- let activity = NO_ACTIVITY;
301
- let release = (): void => {};
302
- let tail: Promise<void> | undefined;
303
- let conversation = "";
307
+ function createStatusReporter(
308
+ adapter: MessengerAdapter,
309
+ message: MessengerMessage,
310
+ signal: AbortSignal,
311
+ ): (state: "processing" | "success" | "failure") => void {
304
312
  let statusChain = Promise.resolve();
305
- const status = (state: "processing" | "success" | "failure"): void => {
313
+ return state => {
306
314
  if (!adapter.status) return;
307
315
  statusChain = statusChain.then(async () => {
308
316
  if (signal.aborted || statusInFlight >= MAX_STATUS_IN_FLIGHT) return;
@@ -332,22 +340,20 @@ export function createMessengerGateway(
332
340
  }
333
341
  });
334
342
  };
343
+ }
344
+
345
+ async function processRequest(pending: PendingMessage): Promise<void> {
346
+ const { message, adapter, signal, resolve, status } = pending;
347
+ let failed = false;
348
+ let activity = NO_ACTIVITY;
335
349
  try {
336
350
  if (signal.aborted) return;
337
351
  status("processing");
338
- const destination = (await adapter.prepare?.(message, signal)) ?? message;
339
- conversation = messengerConversationKey(destination);
340
- const previous = conversationTails.get(conversation);
341
- const done = Promise.withResolvers<void>();
342
- release = done.resolve;
343
- tail = done.promise;
344
- conversationTails.set(conversation, tail);
345
- await previous;
346
- activity = createActivityReporter(adapter, destination, signal, activityIntervalMs);
352
+ activity = createActivityReporter(adapter, message, signal, activityIntervalMs);
347
353
  activity.report("Starting request");
348
354
  let text: string;
349
355
  try {
350
- text = await reply(destination, signal, activity.report);
356
+ text = await reply(message, signal, activity.report);
351
357
  } catch (error) {
352
358
  failed = true;
353
359
  if (error instanceof MessengerFatalError) {
@@ -363,7 +369,7 @@ export function createMessengerGateway(
363
369
  }
364
370
  if (!signal.aborted && text.trim()) {
365
371
  // A failed send may already have delivered a chunk: never retry the entire reply.
366
- await adapter.send(destination, text, signal);
372
+ await adapter.send(message, text, signal);
367
373
  }
368
374
  if (!signal.aborted) status(failed ? "failure" : "success");
369
375
  } catch {
@@ -371,8 +377,6 @@ export function createMessengerGateway(
371
377
  if (!signal.aborted) status("failure");
372
378
  } finally {
373
379
  await activity.close(failed ? "failure" : signal.aborted ? "cancelled" : "success");
374
- release();
375
- if (tail && conversationTails.get(conversation) === tail) conversationTails.delete(conversation);
376
380
  resolve();
377
381
  }
378
382
  }
@@ -407,12 +411,12 @@ export function createMessengerGateway(
407
411
  const oldest = seen.values().next().value;
408
412
  if (oldest !== undefined) seen.delete(oldest);
409
413
  }
410
- const key = schedulingKey(message, adapter);
411
- const previous = admissionTails.get(key) ?? Promise.resolve();
414
+ const lane = admissionKey(message, adapter);
415
+ const previous = admissionTails.get(lane) ?? Promise.resolve();
412
416
  const ready = Promise.withResolvers<void>();
413
417
  const tail = previous.then(() => ready.promise);
414
- admissionTails.set(key, tail);
415
- void tail.then(() => { if (admissionTails.get(key) === tail) admissionTails.delete(key); });
418
+ admissionTails.set(lane, tail);
419
+ void tail.then(() => { if (admissionTails.get(lane) === tail) admissionTails.delete(lane); });
416
420
  try {
417
421
  if (options.sandboxControl) {
418
422
  const control = await options.sandboxControl(message, effectiveSignal);
@@ -422,9 +426,30 @@ export function createMessengerGateway(
422
426
  }
423
427
  }
424
428
  await previous;
425
- if (effectiveSignal.aborted || queue.length >= MAX_QUEUED_MESSAGES) return;
429
+ if (effectiveSignal.aborted) return;
430
+ const status = createStatusReporter(adapter, message, effectiveSignal);
431
+ // Capacity is claimed before preparing: a request that cannot be queued must never create a thread.
432
+ if (queue.length + preparing >= MAX_QUEUED_MESSAGES) {
433
+ // Never executed, so forget the id: a redelivery is accepted once capacity returns, replaying nothing.
434
+ seen.delete(id);
435
+ status("failure");
436
+ return;
437
+ }
438
+ let destination: MessengerMessage;
439
+ let key: string;
440
+ preparing++;
441
+ try {
442
+ destination = (await adapter.prepare?.(message, effectiveSignal)) ?? message;
443
+ key = messengerConversationKey(destination);
444
+ } catch {
445
+ if (!effectiveSignal.aborted) status("failure");
446
+ return;
447
+ } finally {
448
+ preparing--;
449
+ }
450
+ if (effectiveSignal.aborted) return;
426
451
  const { promise, resolve } = Promise.withResolvers<void>();
427
- queue.push({ message, adapter, signal: effectiveSignal, resolve, key });
452
+ queue.push({ message: destination, adapter, signal: effectiveSignal, resolve, key, status });
428
453
  schedule();
429
454
  return promise;
430
455
  } finally { ready.resolve(); }
@@ -50,6 +50,8 @@ export interface MessengerPlatformConfig {
50
50
  }
51
51
 
52
52
  export interface MessengerConfig {
53
+ /** Directory holding the loaded messenger.json, token file and run script; set by `loadMessengerConfig`. */
54
+ profileDir?: string;
53
55
  cwd: string;
54
56
  sessionDir: string;
55
57
  toolNames: string[];
@@ -23,7 +23,7 @@ Bundled system prompts, role definitions, and workflow skill documentation for t
23
23
  - Do not remove existing constraints without explicit reason.
24
24
 
25
25
  ### Testing Requirements
26
- - Prompt changes should be verified via end-to-end testing or `bun run run-evolution.sh`.
26
+ - Prompt changes should be verified via end-to-end testing.
27
27
 
28
28
  ### Common Patterns
29
29
  - XML-like tags for structural organization within prompts.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * POSIX tool resolution for Windows hosts.
3
+ *
4
+ * `Bun.spawn(["bash"|"grep"|"find", …])` resolves from PATH, where Windows
5
+ * ships imposters ahead of Git for Windows:
6
+ * - `C:\Windows\System32\bash.exe` launches WSL — Windows paths
7
+ * (`C:\Users\…`) do not exist inside it and caller env vars are dropped.
8
+ * - `C:\Windows\System32\find.exe` is the legacy DOS FIND (substring
9
+ * filter), not POSIX find.
10
+ *
11
+ * Resolve to real Git for Windows binaries instead, with an env override.
12
+ * Non-Windows platforms pass the bare name through unchanged.
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import path from "node:path";
16
+
17
+ export type PosixTool = "bash" | "grep" | "find";
18
+
19
+ export interface ResolveOptions {
20
+ /** Platform override for tests (default: process.platform). */
21
+ readonly platform?: NodeJS.Platform;
22
+ /** Env override for tests (default: process.env). */
23
+ readonly env?: Record<string, string | undefined>;
24
+ /** File-existence probe override for tests (default: fs.existsSync). */
25
+ readonly exists?: (p: string) => boolean;
26
+ }
27
+
28
+ /** Candidate absolute paths for a tool, in preference order. */
29
+ export function candidatePaths(
30
+ tool: PosixTool,
31
+ env: Record<string, string | undefined>
32
+ ): string[] {
33
+ const override = env[`JOC_${tool.toUpperCase()}_PATH`];
34
+ const roots = [
35
+ env["ProgramFiles"] && path.join(env["ProgramFiles"], "Git"),
36
+ env["ProgramFiles(x86)"] && path.join(env["ProgramFiles(x86)"], "Git"),
37
+ env["LOCALAPPDATA"] && path.join(env["LOCALAPPDATA"], "Programs", "Git"),
38
+ ].filter((r): r is string => Boolean(r));
39
+ // bash lives in both bin/ and usr/bin/; grep/find only in usr/bin/.
40
+ const subdirs = tool === "bash" ? ["bin", path.join("usr", "bin")] : [path.join("usr", "bin")];
41
+ return [
42
+ ...(override ? [override] : []),
43
+ ...roots.flatMap(root => subdirs.map(sub => path.join(root, sub, `${tool}.exe`))),
44
+ ];
45
+ }
46
+
47
+ const cache = new Map<PosixTool, string>();
48
+
49
+ /**
50
+ * Resolve `tool` to a Unix-semantics executable. Returns an absolute Git
51
+ * for Windows path when found, otherwise the bare name (PATH fallback).
52
+ */
53
+ export function resolvePosixTool(tool: PosixTool, opts: ResolveOptions = {}): string {
54
+ const platform = opts.platform ?? process.platform;
55
+ if (platform !== "win32") return tool;
56
+ const injected = opts.env || opts.exists;
57
+ if (!injected) {
58
+ const hit = cache.get(tool);
59
+ if (hit) return hit;
60
+ }
61
+ const env = opts.env ?? process.env;
62
+ const exists = opts.exists ?? existsSync;
63
+ const found = candidatePaths(tool, env).find(c => exists(c)) ?? tool;
64
+ if (!injected) cache.set(tool, found);
65
+ return found;
66
+ }
package/src/util/retry.ts CHANGED
@@ -64,6 +64,18 @@ export function defaultRetryable(err: unknown): boolean {
64
64
  if (isRefusalError(err)) {
65
65
  return false;
66
66
  }
67
+ // An Antigravity/Gemini 3.x `finishReason=MALFORMED_FUNCTION_CALL` on an otherwise-200
68
+ // response that already carried partial text/functionCall parts is rejected by
69
+ // antigravity.ts as "…response incomplete (finishReason=MALFORMED_FUNCTION_CALL)."
70
+ // (call AND stream). It is an intermittent model fault, not a deterministic classifier
71
+ // verdict: the adapter throws BEFORE serializing the buffered functionCalls, so a resend
72
+ // executes no unvalidated call. Retry it within the normal attempt budget. Scoped to this
73
+ // exact shape — other `response incomplete (…)` reasons (MAX_TOKENS, missing finishReason)
74
+ // stay non-retryable; the empty-candidate variant ("returned no content (…)") is already
75
+ // retried below. A stream that has yielded a chunk is never re-run by retryableStream.
76
+ if (/Antigravity Cloud Code Assist response incomplete \(finishReason=MALFORMED_FUNCTION_CALL\)/i.test(errorMessageOf(err))) {
77
+ return true;
78
+ }
67
79
  // A pre-response connection failure (refused / unresolved host) — retryable: a
68
80
  // local provider that is mid-restart, or a transient DNS blip, can clear within
69
81
  // the backoff budget. A provider that is simply never running exhausts the