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.
- package/CHANGELOG.md +26 -1
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +11 -11
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/engine.ts +63 -8
- package/src/agent/hooks.ts +2 -1
- package/src/agent/loop-guards.ts +18 -7
- package/src/agent/sandbox/authority.ts +105 -42
- package/src/agent/sandbox/bundle.ts +5 -2
- package/src/agent/sandbox/docker.ts +13 -3
- package/src/agent/sandbox/hands-source.text +540 -540
- package/src/agent/sandbox/policy.ts +23 -6
- package/src/agent/sandbox/supervisor.ts +21 -9
- package/src/agent/shell-lease.ts +2 -1
- package/src/agent/task-tool.ts +17 -3
- package/src/agent/tools.ts +2 -1
- package/src/commands/launch/input.ts +25 -3
- package/src/commands/launch.ts +26 -5
- package/src/commands/messenger.ts +2 -0
- package/src/commands/ralplan.ts +14 -1
- package/src/messenger/config.ts +27 -1
- package/src/messenger/discord-setup.ts +97 -17
- package/src/messenger/gateway.ts +58 -33
- package/src/messenger/types.ts +2 -0
- package/src/prompts/AGENTS.md +1 -1
- package/src/util/posix-tools.ts +66 -0
- package/src/util/retry.ts +12 -0
package/src/messenger/gateway.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
|
264
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
411
|
-
const previous = admissionTails.get(
|
|
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(
|
|
415
|
-
void tail.then(() => { if (admissionTails.get(
|
|
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
|
|
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(); }
|
package/src/messenger/types.ts
CHANGED
|
@@ -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[];
|
package/src/prompts/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|