@gajae-code/coding-agent 0.13.0 → 0.13.2
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 +34 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +87 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/modes/interactive-mode.ts +4 -0
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/process-incarnation.ts +6 -1
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
- package/src/sdk/bus/telegram-daemon.ts +151 -41
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/internal/managed-session-storage.ts +136 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/builtin-registry.ts +43 -5
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +5 -4
- package/src/tools/bash.ts +11 -9
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
package/src/eval/py/runtime.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import { $env, $which, getPythonEnvDir } from "@gajae-code/utils";
|
|
10
|
+
import { $env, $which, getPythonEnvDir, WhichCachePolicy } from "@gajae-code/utils";
|
|
11
|
+
import { withFileLock } from "../../config/file-lock";
|
|
11
12
|
|
|
12
13
|
export const RLM_MANAGED_PYTHON_PACKAGES: readonly string[] = ["numpy", "pandas", "matplotlib", "polars"];
|
|
13
14
|
|
|
@@ -18,6 +19,14 @@ export interface PythonRuntimeOptions {
|
|
|
18
19
|
seedPackages?: readonly string[];
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export interface PythonRuntimeLifecycleOptions {
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
deadlineMs?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const RUNTIME_CANCEL_GRACE_MS = 1_000;
|
|
28
|
+
const RUNTIME_PROCESS_TREE_EXIT_TIMEOUT_MS = 5_000;
|
|
29
|
+
|
|
21
30
|
const DEFAULT_ENV_ALLOWLIST = new Set([
|
|
22
31
|
"PATH",
|
|
23
32
|
"HOME",
|
|
@@ -195,11 +204,56 @@ export function resolveVenvPath(cwd: string): string | undefined {
|
|
|
195
204
|
return undefined;
|
|
196
205
|
}
|
|
197
206
|
|
|
207
|
+
function createRuntimeCancellationError(timedOut: boolean): Error {
|
|
208
|
+
const error = new Error(timedOut ? "Python runtime startup timed out" : "Python runtime startup aborted");
|
|
209
|
+
error.name = timedOut ? "TimeoutError" : "AbortError";
|
|
210
|
+
return error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function waitForRuntimeLifecycle<T>(
|
|
214
|
+
value: Promise<T>,
|
|
215
|
+
lifecycle: PythonRuntimeLifecycleOptions | undefined,
|
|
216
|
+
settleAfterCancellation?: () => boolean,
|
|
217
|
+
): Promise<T> {
|
|
218
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
219
|
+
const cleanups: Array<() => void> = [];
|
|
220
|
+
const finish = (callback: () => void): void => {
|
|
221
|
+
while (cleanups.length > 0) cleanups.pop()?.();
|
|
222
|
+
callback();
|
|
223
|
+
};
|
|
224
|
+
const cancel = (timedOut: boolean): void => {
|
|
225
|
+
if (settleAfterCancellation?.()) return;
|
|
226
|
+
finish(() => reject(createRuntimeCancellationError(timedOut)));
|
|
227
|
+
};
|
|
228
|
+
if (lifecycle?.signal?.aborted) {
|
|
229
|
+
if (!settleAfterCancellation?.()) throw lifecycle.signal.reason ?? createRuntimeCancellationError(false);
|
|
230
|
+
} else if (lifecycle?.signal) {
|
|
231
|
+
const onAbort = (): void => cancel(false);
|
|
232
|
+
lifecycle.signal.addEventListener("abort", onAbort, { once: true });
|
|
233
|
+
cleanups.push(() => lifecycle.signal?.removeEventListener("abort", onAbort));
|
|
234
|
+
}
|
|
235
|
+
const remainingMs = lifecycle?.deadlineMs === undefined ? undefined : lifecycle.deadlineMs - Date.now();
|
|
236
|
+
if (remainingMs !== undefined) {
|
|
237
|
+
if (remainingMs <= 0) cancel(true);
|
|
238
|
+
else {
|
|
239
|
+
const timer = setTimeout(() => cancel(true), remainingMs);
|
|
240
|
+
timer.unref();
|
|
241
|
+
cleanups.push(() => clearTimeout(timer));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
void value.then(
|
|
245
|
+
result => finish(() => resolve(result)),
|
|
246
|
+
error => finish(() => reject(error)),
|
|
247
|
+
);
|
|
248
|
+
return await promise;
|
|
249
|
+
}
|
|
250
|
+
|
|
198
251
|
async function runRuntimeCommand(
|
|
199
252
|
cmd: string[],
|
|
200
253
|
cwd: string,
|
|
201
254
|
env: Record<string, string | undefined>,
|
|
202
255
|
description: string,
|
|
256
|
+
lifecycle?: PythonRuntimeLifecycleOptions,
|
|
203
257
|
): Promise<void> {
|
|
204
258
|
const spawnEnv: Record<string, string> = {};
|
|
205
259
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -211,15 +265,106 @@ async function runRuntimeCommand(
|
|
|
211
265
|
stdout: "pipe",
|
|
212
266
|
stderr: "pipe",
|
|
213
267
|
windowsHide: true,
|
|
268
|
+
detached: process.platform !== "win32",
|
|
214
269
|
});
|
|
215
|
-
const [stdout, stderr,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
270
|
+
const output = Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
|
|
271
|
+
const { promise, resolve, reject } = Promise.withResolvers<number>();
|
|
272
|
+
const cleanups: Array<() => void> = [];
|
|
273
|
+
const finish = (callback: () => void): void => {
|
|
274
|
+
while (cleanups.length > 0) cleanups.pop()?.();
|
|
275
|
+
callback();
|
|
276
|
+
};
|
|
277
|
+
let cancellation: { timedOut: boolean } | undefined;
|
|
278
|
+
let windowsTermination: Promise<void> | undefined;
|
|
279
|
+
const terminateWindowsProcessTree = (): void => {
|
|
280
|
+
if (windowsTermination) return;
|
|
281
|
+
const taskkill = Bun.spawn(["taskkill", "/pid", String(proc.pid), "/t", "/f"], {
|
|
282
|
+
stdout: "ignore",
|
|
283
|
+
stderr: "ignore",
|
|
284
|
+
windowsHide: true,
|
|
285
|
+
});
|
|
286
|
+
windowsTermination = taskkill.exited.then(
|
|
287
|
+
() => undefined,
|
|
288
|
+
() => undefined,
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
const waitForProcessTreeExit = async (): Promise<boolean> => {
|
|
292
|
+
if (process.platform === "win32") {
|
|
293
|
+
await windowsTermination;
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
const deadline = Date.now() + RUNTIME_PROCESS_TREE_EXIT_TIMEOUT_MS;
|
|
297
|
+
while (Date.now() < deadline) {
|
|
298
|
+
try {
|
|
299
|
+
process.kill(-proc.pid, 0);
|
|
300
|
+
await Bun.sleep(10);
|
|
301
|
+
} catch {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
};
|
|
307
|
+
const cancel = (timedOut: boolean): void => {
|
|
308
|
+
if (cancellation) return;
|
|
309
|
+
cancellation = { timedOut };
|
|
310
|
+
const signalProcessTree = (signal: NodeJS.Signals): void => {
|
|
311
|
+
try {
|
|
312
|
+
if (process.platform === "win32") {
|
|
313
|
+
terminateWindowsProcessTree();
|
|
314
|
+
} else {
|
|
315
|
+
process.kill(-proc.pid, signal);
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// The process may have exited between the cancellation check and signal.
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
signalProcessTree("SIGTERM");
|
|
322
|
+
const escalation = setTimeout(() => signalProcessTree("SIGKILL"), RUNTIME_CANCEL_GRACE_MS);
|
|
323
|
+
escalation.unref();
|
|
324
|
+
cleanups.push(() => clearTimeout(escalation));
|
|
325
|
+
};
|
|
326
|
+
if (lifecycle?.signal?.aborted) {
|
|
327
|
+
cancel(false);
|
|
328
|
+
} else if (lifecycle?.signal) {
|
|
329
|
+
const onAbort = (): void => cancel(false);
|
|
330
|
+
lifecycle.signal.addEventListener("abort", onAbort, { once: true });
|
|
331
|
+
cleanups.push(() => lifecycle.signal?.removeEventListener("abort", onAbort));
|
|
332
|
+
}
|
|
333
|
+
const remainingMs = lifecycle?.deadlineMs === undefined ? undefined : lifecycle.deadlineMs - Date.now();
|
|
334
|
+
if (remainingMs !== undefined) {
|
|
335
|
+
if (remainingMs <= 0) {
|
|
336
|
+
cancel(true);
|
|
337
|
+
} else {
|
|
338
|
+
const timer = setTimeout(() => cancel(true), remainingMs);
|
|
339
|
+
timer.unref();
|
|
340
|
+
cleanups.push(() => clearTimeout(timer));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
void proc.exited.then(async code => {
|
|
344
|
+
const cancelled = cancellation;
|
|
345
|
+
if (cancelled) {
|
|
346
|
+
const processTreeExited = await waitForProcessTreeExit();
|
|
347
|
+
finish(() => {
|
|
348
|
+
const error = createRuntimeCancellationError(cancelled.timedOut);
|
|
349
|
+
if (!processTreeExited)
|
|
350
|
+
error.message += "; Python provisioner process tree did not exit after cancellation";
|
|
351
|
+
reject(error);
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
finish(() => resolve(code));
|
|
356
|
+
});
|
|
357
|
+
let exitCode: number;
|
|
358
|
+
try {
|
|
359
|
+
exitCode = await promise;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
void output.catch(() => undefined);
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
const [stdout, stderr] = await output;
|
|
220
365
|
if (exitCode !== 0) {
|
|
221
|
-
const
|
|
222
|
-
throw new Error(`${description} failed with exit code ${exitCode}${
|
|
366
|
+
const text = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
|
|
367
|
+
throw new Error(`${description} failed with exit code ${exitCode}${text ? `: ${text}` : ""}`);
|
|
223
368
|
}
|
|
224
369
|
}
|
|
225
370
|
|
|
@@ -227,18 +372,54 @@ async function ensureWorkspaceManagedVenv(
|
|
|
227
372
|
cwd: string,
|
|
228
373
|
baseEnv: Record<string, string | undefined>,
|
|
229
374
|
seedPackages: readonly string[],
|
|
375
|
+
lifecycle?: PythonRuntimeLifecycleOptions,
|
|
376
|
+
): Promise<void> {
|
|
377
|
+
const venvPath = resolveWorkspaceManagedPythonCandidate(cwd).venvPath;
|
|
378
|
+
const lockSignal =
|
|
379
|
+
lifecycle?.deadlineMs === undefined
|
|
380
|
+
? lifecycle?.signal
|
|
381
|
+
: lifecycle.signal
|
|
382
|
+
? AbortSignal.any([lifecycle.signal, AbortSignal.timeout(Math.max(0, lifecycle.deadlineMs - Date.now()))])
|
|
383
|
+
: AbortSignal.timeout(Math.max(0, lifecycle.deadlineMs - Date.now()));
|
|
384
|
+
let lockAcquired = false;
|
|
385
|
+
const provisioning = withFileLock(
|
|
386
|
+
venvPath,
|
|
387
|
+
async () => {
|
|
388
|
+
if (lifecycle?.signal?.aborted) throw lifecycle.signal.reason ?? createRuntimeCancellationError(false);
|
|
389
|
+
if (lifecycle?.deadlineMs !== undefined && lifecycle.deadlineMs <= Date.now())
|
|
390
|
+
throw createRuntimeCancellationError(true);
|
|
391
|
+
await ensureWorkspaceManagedVenvInner(cwd, baseEnv, seedPackages, lifecycle);
|
|
392
|
+
},
|
|
393
|
+
{ retries: Number.POSITIVE_INFINITY, signal: lockSignal, onAcquired: () => (lockAcquired = true) },
|
|
394
|
+
);
|
|
395
|
+
return await waitForRuntimeLifecycle(provisioning, lifecycle, () => lockAcquired);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function ensureWorkspaceManagedVenvInner(
|
|
399
|
+
cwd: string,
|
|
400
|
+
baseEnv: Record<string, string | undefined>,
|
|
401
|
+
seedPackages: readonly string[],
|
|
402
|
+
lifecycle?: PythonRuntimeLifecycleOptions,
|
|
230
403
|
): Promise<void> {
|
|
231
404
|
const managed = resolveWorkspaceManagedPythonCandidate(cwd);
|
|
232
405
|
if (!fs.existsSync(managed.pythonPath)) {
|
|
233
|
-
const basePython =
|
|
406
|
+
const basePython =
|
|
407
|
+
$which("python3", { PATH: baseEnv.PATH, cache: WhichCachePolicy.Bypass }) ??
|
|
408
|
+
$which("python", { PATH: baseEnv.PATH, cache: WhichCachePolicy.Bypass });
|
|
234
409
|
if (!basePython) throw new Error("Python executable not found on PATH");
|
|
235
410
|
await fs.promises.mkdir(path.dirname(managed.venvPath), { recursive: true });
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
411
|
+
try {
|
|
412
|
+
await runRuntimeCommand(
|
|
413
|
+
[basePython, "-m", "venv", managed.venvPath],
|
|
414
|
+
cwd,
|
|
415
|
+
baseEnv,
|
|
416
|
+
"Managed Python venv creation",
|
|
417
|
+
lifecycle,
|
|
418
|
+
);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
await fs.promises.rm(managed.venvPath, { recursive: true, force: true });
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
242
423
|
}
|
|
243
424
|
if (seedPackages.length === 0) return;
|
|
244
425
|
const markerPath = path.join(managed.venvPath, ".gjc-seeded.json");
|
|
@@ -257,12 +438,14 @@ async function ensureWorkspaceManagedVenv(
|
|
|
257
438
|
cwd,
|
|
258
439
|
runtimeEnv,
|
|
259
440
|
"Managed Python pip bootstrap",
|
|
441
|
+
lifecycle,
|
|
260
442
|
);
|
|
261
443
|
await runRuntimeCommand(
|
|
262
444
|
[managed.pythonPath, "-m", "pip", "install", ...seedPackages],
|
|
263
445
|
cwd,
|
|
264
446
|
runtimeEnv,
|
|
265
447
|
"Managed Python package seed",
|
|
448
|
+
lifecycle,
|
|
266
449
|
);
|
|
267
450
|
await fs.promises.writeFile(
|
|
268
451
|
markerPath,
|
|
@@ -315,6 +498,7 @@ export async function ensurePythonRuntime(
|
|
|
315
498
|
cwd: string,
|
|
316
499
|
baseEnv: Record<string, string | undefined>,
|
|
317
500
|
options: PythonRuntimeOptions = {},
|
|
501
|
+
lifecycle?: PythonRuntimeLifecycleOptions,
|
|
318
502
|
): Promise<PythonRuntime> {
|
|
319
503
|
if (options.managedWorkspaceVenv) {
|
|
320
504
|
const env = { ...baseEnv };
|
|
@@ -325,7 +509,7 @@ export async function ensurePythonRuntime(
|
|
|
325
509
|
return runtimeFromVenv(candidate.venvPath, candidate.pythonPath, candidate.binDir, env);
|
|
326
510
|
}
|
|
327
511
|
}
|
|
328
|
-
await ensureWorkspaceManagedVenv(cwd, baseEnv, options.seedPackages ?? RLM_MANAGED_PYTHON_PACKAGES);
|
|
512
|
+
await ensureWorkspaceManagedVenv(cwd, baseEnv, options.seedPackages ?? RLM_MANAGED_PYTHON_PACKAGES, lifecycle);
|
|
329
513
|
}
|
|
330
514
|
return resolvePythonRuntime(cwd, baseEnv, options);
|
|
331
515
|
}
|
|
@@ -19,7 +19,7 @@ import { formatArtifactReference, resolveOutputMaxColumns, resolveOutputSinkHead
|
|
|
19
19
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
20
20
|
import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
|
|
21
21
|
|
|
22
|
-
type NativeShellBindings = Pick<typeof import("@gajae-code/natives"), "Shell"
|
|
22
|
+
type NativeShellBindings = Pick<typeof import("@gajae-code/natives"), "Shell">;
|
|
23
23
|
let nativeShellBindingsLoad: Promise<NativeShellBindings> | undefined;
|
|
24
24
|
|
|
25
25
|
async function shellNatives(): Promise<NativeShellBindings> {
|
|
@@ -288,7 +288,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
288
288
|
...(await sink.dump("Command cancelled")),
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
|
-
const { Shell
|
|
291
|
+
const { Shell } = await shellNatives();
|
|
292
292
|
|
|
293
293
|
const usePersistentShell = options?.oneShot !== true;
|
|
294
294
|
const sessionKey = buildSessionKey(shell, configuredPrefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
|
|
@@ -303,14 +303,26 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
303
303
|
});
|
|
304
304
|
shellSessions.set(sessionKey, shellSession);
|
|
305
305
|
}
|
|
306
|
+
// Non-persistent invocations still need an owned native Shell so its lifetime
|
|
307
|
+
// can be ended explicitly. executeShell creates a native shell outside the
|
|
308
|
+
// persistent registry, leaving its cleanup untrackable by the host process.
|
|
309
|
+
const oneShotShell =
|
|
310
|
+
!usePersistentShell || persistentSessionBroken
|
|
311
|
+
? new Shell({
|
|
312
|
+
sessionEnv: shellEnv,
|
|
313
|
+
snapshotPath: snapshotPath ?? undefined,
|
|
314
|
+
minimizer,
|
|
315
|
+
})
|
|
316
|
+
: undefined;
|
|
317
|
+
const activeShell = shellSession ?? oneShotShell;
|
|
306
318
|
const userSignal = options?.signal;
|
|
307
319
|
const runAbortController = new AbortController();
|
|
308
320
|
const abortCurrentExecution = () => {
|
|
309
321
|
if (!runAbortController.signal.aborted) {
|
|
310
322
|
runAbortController.abort();
|
|
311
323
|
}
|
|
312
|
-
if (
|
|
313
|
-
abortPromise =
|
|
324
|
+
if (activeShell && !abortPromise) {
|
|
325
|
+
abortPromise = activeShell.abort();
|
|
314
326
|
}
|
|
315
327
|
};
|
|
316
328
|
const abortDeferred = Promise.withResolvers<"abort">();
|
|
@@ -351,38 +363,20 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
351
363
|
let runSettled = false;
|
|
352
364
|
|
|
353
365
|
try {
|
|
354
|
-
const runPromise =
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
369
|
-
: executeShell(
|
|
370
|
-
{
|
|
371
|
-
command: finalCommand,
|
|
372
|
-
cwd: commandCwd,
|
|
373
|
-
env: commandEnv,
|
|
374
|
-
sessionEnv: shellEnv,
|
|
375
|
-
snapshotPath: snapshotPath ?? undefined,
|
|
376
|
-
minimizer,
|
|
377
|
-
timeoutMs: executionTimeoutMs,
|
|
378
|
-
signal: runAbortController.signal,
|
|
379
|
-
},
|
|
380
|
-
(err, chunk) => {
|
|
381
|
-
if (!err) {
|
|
382
|
-
enqueueChunk(chunk);
|
|
383
|
-
}
|
|
384
|
-
},
|
|
385
|
-
);
|
|
366
|
+
const runPromise = activeShell!.run(
|
|
367
|
+
{
|
|
368
|
+
command: finalCommand,
|
|
369
|
+
cwd: commandCwd,
|
|
370
|
+
env: commandEnv,
|
|
371
|
+
timeoutMs: executionTimeoutMs,
|
|
372
|
+
signal: runAbortController.signal,
|
|
373
|
+
},
|
|
374
|
+
(err, chunk) => {
|
|
375
|
+
if (!err) {
|
|
376
|
+
enqueueChunk(chunk);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
);
|
|
386
380
|
|
|
387
381
|
const winner = await Promise.race([
|
|
388
382
|
runPromise.then(result => ({ kind: "result" as const, result })),
|
|
@@ -413,7 +407,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
413
407
|
.catch(() => undefined);
|
|
414
408
|
}
|
|
415
409
|
} else {
|
|
416
|
-
|
|
410
|
+
runSettled = await awaitAbortCleanup(runPromise);
|
|
411
|
+
if (!runSettled) void runPromise.catch(() => undefined);
|
|
417
412
|
}
|
|
418
413
|
return {
|
|
419
414
|
exitCode: undefined,
|
|
@@ -512,6 +507,10 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
512
507
|
if (resetSession && runSettled && shellSessions.get(sessionKey) === shellSession) {
|
|
513
508
|
shellSessions.delete(sessionKey);
|
|
514
509
|
}
|
|
510
|
+
if (oneShotShell) {
|
|
511
|
+
const disposePromise = abortPromise ?? oneShotShell.abort();
|
|
512
|
+
await Promise.race([disposePromise.catch(() => undefined), Bun.sleep(CANCEL_CLEANUP_WAIT_MS)]);
|
|
513
|
+
}
|
|
515
514
|
}
|
|
516
515
|
}
|
|
517
516
|
|
|
@@ -1194,6 +1194,9 @@ async function markGcDiskBlobReferences(transcriptPath: string, into: Set<string
|
|
|
1194
1194
|
interface GcDiskMarkedTranscript {
|
|
1195
1195
|
size: number;
|
|
1196
1196
|
mtimeMs: number;
|
|
1197
|
+
ctimeMs: number;
|
|
1198
|
+
dev: number;
|
|
1199
|
+
ino: number;
|
|
1197
1200
|
}
|
|
1198
1201
|
|
|
1199
1202
|
/**
|
|
@@ -1233,14 +1236,26 @@ async function markGcDiskTranscript(
|
|
|
1233
1236
|
errors.push({ surface: "blobs", scope: transcriptPath, message: gcDiskErrorText(error) });
|
|
1234
1237
|
return;
|
|
1235
1238
|
}
|
|
1236
|
-
if (
|
|
1239
|
+
if (
|
|
1240
|
+
before.size !== after.size ||
|
|
1241
|
+
before.mtimeMs !== after.mtimeMs ||
|
|
1242
|
+
before.ctimeMs !== after.ctimeMs ||
|
|
1243
|
+
before.dev !== after.dev ||
|
|
1244
|
+
before.ino !== after.ino
|
|
1245
|
+
) {
|
|
1237
1246
|
// The transcript changed while its references were being read: the
|
|
1238
1247
|
// reference set is not bound to a stable snapshot, so it cannot vouch
|
|
1239
1248
|
// for any blob. Same posture as the drift rounds exhausting below.
|
|
1240
1249
|
markGcDiskEvidenceIncomplete(evidence, "sessions_changed_during_mark");
|
|
1241
1250
|
return;
|
|
1242
1251
|
}
|
|
1243
|
-
marked.set(transcriptPath, {
|
|
1252
|
+
marked.set(transcriptPath, {
|
|
1253
|
+
size: after.size,
|
|
1254
|
+
mtimeMs: after.mtimeMs,
|
|
1255
|
+
ctimeMs: after.ctimeMs,
|
|
1256
|
+
dev: after.dev,
|
|
1257
|
+
ino: after.ino,
|
|
1258
|
+
});
|
|
1244
1259
|
}
|
|
1245
1260
|
|
|
1246
1261
|
/**
|
|
@@ -1269,7 +1284,14 @@ async function findGcDiskMarkDrift(input: {
|
|
|
1269
1284
|
visit: (transcriptPath, _directory, stat) => {
|
|
1270
1285
|
const mark = input.marked.get(transcriptPath);
|
|
1271
1286
|
if (mark) {
|
|
1272
|
-
if (
|
|
1287
|
+
if (
|
|
1288
|
+
mark.size !== stat.size ||
|
|
1289
|
+
mark.mtimeMs !== stat.mtimeMs ||
|
|
1290
|
+
mark.ctimeMs !== stat.ctimeMs ||
|
|
1291
|
+
mark.dev !== stat.dev ||
|
|
1292
|
+
mark.ino !== stat.ino
|
|
1293
|
+
)
|
|
1294
|
+
drifted.push(transcriptPath);
|
|
1273
1295
|
return;
|
|
1274
1296
|
}
|
|
1275
1297
|
if (!input.accounted.has(transcriptPath)) drifted.push(transcriptPath);
|
|
@@ -164,7 +164,33 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
|
|
|
164
164
|
const childStartTime = await managedOwnerProcessProvenance(child.pid);
|
|
165
165
|
if (!childStartTime) throw new Error("managed_owner_child_start_time_unavailable");
|
|
166
166
|
const childProcess = nativeProcessBindings().Process.fromPid(child.pid);
|
|
167
|
-
if (!childProcess)
|
|
167
|
+
if (!childProcess) {
|
|
168
|
+
const exitCode = await child.exited;
|
|
169
|
+
if (child.signalCode === "SIGABRT") {
|
|
170
|
+
const receipt: ManagedOwnerSigabrtReceipt = {
|
|
171
|
+
schema_version: 2,
|
|
172
|
+
generation,
|
|
173
|
+
session_id: sessionId,
|
|
174
|
+
run_id: runId,
|
|
175
|
+
endpoint_incarnation: incarnation,
|
|
176
|
+
child_token: childToken,
|
|
177
|
+
command_sha256: binding.command_sha256,
|
|
178
|
+
supervisor_pid: process.pid,
|
|
179
|
+
supervisor_start_time: supervisorStartTime,
|
|
180
|
+
child_pid: child.pid,
|
|
181
|
+
child_start_time: childStartTime,
|
|
182
|
+
signal: "SIGABRT",
|
|
183
|
+
signal_number: 6,
|
|
184
|
+
exit_code: exitCode,
|
|
185
|
+
received_at: new Date().toISOString(),
|
|
186
|
+
};
|
|
187
|
+
await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
|
|
188
|
+
process.exitCode = 134;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
process.exitCode = exitCode;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
168
194
|
if (process.platform === "linux" && childProcess.incarnation !== `linux:${childStartTime}`)
|
|
169
195
|
throw new Error("managed_owner_child_incarnation_mismatch");
|
|
170
196
|
let childExited = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as fsSync from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
@@ -45,6 +45,8 @@ export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" |
|
|
|
45
45
|
type FinalResponseSource = "agent_end" | "launch_error";
|
|
46
46
|
const MAX_PUBLIC_ERROR_MESSAGE_LENGTH = 2000;
|
|
47
47
|
const HEARTBEAT_MS = 1000;
|
|
48
|
+
const MAX_PUBLIC_ACTIVE_TOOLS = 8;
|
|
49
|
+
const SAFE_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
48
50
|
|
|
49
51
|
type LastPayloadCacheEntry = { mtimeMs: number; size: number; payload: Record<string, unknown> };
|
|
50
52
|
const lastPayloadByStateFile = new Map<string, LastPayloadCacheEntry>();
|
|
@@ -61,6 +63,9 @@ export const __sessionStateSidecarPerfCounters = {
|
|
|
61
63
|
interface RuntimeStateEvent {
|
|
62
64
|
type: string;
|
|
63
65
|
messages?: unknown[];
|
|
66
|
+
toolCallId?: unknown;
|
|
67
|
+
toolName?: unknown;
|
|
68
|
+
isError?: unknown;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
export interface OwnerTerminalContext {
|
|
@@ -369,7 +374,95 @@ export function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
|
|
|
369
374
|
}
|
|
370
375
|
|
|
371
376
|
export function eventAffectsCoordinatorRuntimeState(event: RuntimeStateEvent): boolean {
|
|
372
|
-
return stateForEvent(event) !== null;
|
|
377
|
+
return stateForEvent(event) !== null || toolActivityForEvent(event) !== null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function safeToolName(value: unknown): string {
|
|
381
|
+
return typeof value === "string" && SAFE_TOOL_NAME.test(value) ? value : "custom";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function toolActivityForEvent(
|
|
385
|
+
event: RuntimeStateEvent,
|
|
386
|
+
):
|
|
387
|
+
| { phase: "started"; toolCallKey: string; toolName: string }
|
|
388
|
+
| { phase: "finished"; toolCallKey: string; toolName: string; outcome: "succeeded" | "failed" }
|
|
389
|
+
| null {
|
|
390
|
+
if (event.type !== "tool_execution_start" && event.type !== "tool_execution_end") return null;
|
|
391
|
+
if (typeof event.toolCallId !== "string") return null;
|
|
392
|
+
const toolCallKey = createHash("sha256").update(event.toolCallId).digest("hex");
|
|
393
|
+
if (event.type === "tool_execution_start")
|
|
394
|
+
return { phase: "started", toolCallKey, toolName: safeToolName(event.toolName) };
|
|
395
|
+
return {
|
|
396
|
+
phase: "finished",
|
|
397
|
+
toolCallKey,
|
|
398
|
+
toolName: safeToolName(event.toolName),
|
|
399
|
+
outcome: event.isError === true ? "failed" : "succeeded",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function runtimeStateFromPrevious(value: unknown): RuntimeState {
|
|
404
|
+
return value === "ready_for_input" ||
|
|
405
|
+
value === "running" ||
|
|
406
|
+
value === "needs_user_input" ||
|
|
407
|
+
value === "completed" ||
|
|
408
|
+
value === "errored"
|
|
409
|
+
? value
|
|
410
|
+
: "running";
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function activityFieldsForEvent(
|
|
414
|
+
previous: Record<string, unknown>,
|
|
415
|
+
activityEvent: NonNullable<ReturnType<typeof toolActivityForEvent>>,
|
|
416
|
+
now: string,
|
|
417
|
+
nowMs: number,
|
|
418
|
+
): Record<string, unknown> {
|
|
419
|
+
const activeToolCalls: Record<string, { tool_name: string; started_at: string }> = {};
|
|
420
|
+
const existing = previous.active_tool_calls;
|
|
421
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
422
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
423
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
424
|
+
const entry = value as Record<string, unknown>;
|
|
425
|
+
if (typeof entry.tool_name !== "string" || typeof entry.started_at !== "string") continue;
|
|
426
|
+
if (!Number.isFinite(Date.parse(entry.started_at))) continue;
|
|
427
|
+
activeToolCalls[key] = { tool_name: safeToolName(entry.tool_name), started_at: entry.started_at };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const prior = activeToolCalls[activityEvent.toolCallKey];
|
|
431
|
+
if (activityEvent.phase === "started")
|
|
432
|
+
activeToolCalls[activityEvent.toolCallKey] = { tool_name: activityEvent.toolName, started_at: now };
|
|
433
|
+
else delete activeToolCalls[activityEvent.toolCallKey];
|
|
434
|
+
const previousActivity =
|
|
435
|
+
previous.activity && typeof previous.activity === "object" && !Array.isArray(previous.activity)
|
|
436
|
+
? (previous.activity as Record<string, unknown>)
|
|
437
|
+
: null;
|
|
438
|
+
const sequence =
|
|
439
|
+
previousActivity &&
|
|
440
|
+
typeof previousActivity.sequence === "number" &&
|
|
441
|
+
Number.isSafeInteger(previousActivity.sequence) &&
|
|
442
|
+
previousActivity.sequence >= 0
|
|
443
|
+
? Math.min(Number.MAX_SAFE_INTEGER, previousActivity.sequence + 1)
|
|
444
|
+
: 1;
|
|
445
|
+
const activeTools = Object.values(activeToolCalls)
|
|
446
|
+
.sort((left, right) => left.started_at.localeCompare(right.started_at))
|
|
447
|
+
.slice(0, MAX_PUBLIC_ACTIVE_TOOLS);
|
|
448
|
+
const startedAt = activityEvent.phase === "started" ? now : prior?.started_at;
|
|
449
|
+
const elapsedMs = startedAt ? Math.max(0, nowMs - Date.parse(startedAt)) : undefined;
|
|
450
|
+
return {
|
|
451
|
+
last_activity_at: now,
|
|
452
|
+
activity: {
|
|
453
|
+
sequence,
|
|
454
|
+
kind: "tool_execution",
|
|
455
|
+
phase: activityEvent.phase,
|
|
456
|
+
tool_name: activityEvent.toolName,
|
|
457
|
+
observed_at: now,
|
|
458
|
+
...(startedAt ? { started_at: startedAt } : {}),
|
|
459
|
+
...(elapsedMs === undefined ? {} : { elapsed_ms: elapsedMs }),
|
|
460
|
+
...(activityEvent.phase === "finished" ? { outcome: activityEvent.outcome } : {}),
|
|
461
|
+
active_tool_count: Object.keys(activeToolCalls).length,
|
|
462
|
+
},
|
|
463
|
+
active_tools: activeTools,
|
|
464
|
+
active_tool_calls: activeToolCalls,
|
|
465
|
+
};
|
|
373
466
|
}
|
|
374
467
|
|
|
375
468
|
class PreviousRuntimeStateReadError extends Error {
|
|
@@ -584,6 +677,16 @@ function basePayload(input: {
|
|
|
584
677
|
workdir: identity.workdir,
|
|
585
678
|
branch: branchForContext(input.context),
|
|
586
679
|
session_file: identity.sessionFile,
|
|
680
|
+
...(typeof input.previous.last_activity_at === "string"
|
|
681
|
+
? { last_activity_at: input.previous.last_activity_at }
|
|
682
|
+
: {}),
|
|
683
|
+
...(input.previous.activity && typeof input.previous.activity === "object"
|
|
684
|
+
? { activity: input.previous.activity }
|
|
685
|
+
: {}),
|
|
686
|
+
...(Array.isArray(input.previous.active_tools) ? { active_tools: input.previous.active_tools } : {}),
|
|
687
|
+
...(input.previous.active_tool_calls && typeof input.previous.active_tool_calls === "object"
|
|
688
|
+
? { active_tool_calls: input.previous.active_tool_calls }
|
|
689
|
+
: {}),
|
|
587
690
|
...(input.context.ownerTerminal ? { owner_generation: input.context.ownerTerminal.generation } : {}),
|
|
588
691
|
};
|
|
589
692
|
}
|
|
@@ -784,8 +887,9 @@ export async function persistCoordinatorRuntimeStateFromEvent(
|
|
|
784
887
|
): Promise<void> {
|
|
785
888
|
__sessionStateSidecarPerfCounters.persistFromEventCalls += 1;
|
|
786
889
|
const stateFile = runtimeStateFileForContext(context);
|
|
787
|
-
const
|
|
788
|
-
|
|
890
|
+
const eventState = stateForEvent(event);
|
|
891
|
+
const activityEvent = toolActivityForEvent(event);
|
|
892
|
+
if (!stateFile || (!eventState && !activityEvent)) return;
|
|
789
893
|
context = contextWithManagedOwnerGeneration(context);
|
|
790
894
|
const identity = normalizedIdentity(context);
|
|
791
895
|
await serializeStateFileWrite(
|
|
@@ -799,6 +903,7 @@ export async function persistCoordinatorRuntimeStateFromEvent(
|
|
|
799
903
|
const now = new Date(nowMs).toISOString();
|
|
800
904
|
const previous = await readPreviousPayloadForEvent(stateFile);
|
|
801
905
|
assertPreviousRuntimeStateIdentity(previous, identity);
|
|
906
|
+
const state = eventState ?? runtimeStateFromPrevious(previous.state);
|
|
802
907
|
const payload = {
|
|
803
908
|
...basePayload({
|
|
804
909
|
context,
|
|
@@ -815,6 +920,7 @@ export async function persistCoordinatorRuntimeStateFromEvent(
|
|
|
815
920
|
...(state === "errored"
|
|
816
921
|
? { error: { code: "agent_error", message: "GJC agent reported an error", recoverable: true } }
|
|
817
922
|
: {}),
|
|
923
|
+
...(activityEvent ? activityFieldsForEvent(previous, activityEvent, now, nowMs) : {}),
|
|
818
924
|
};
|
|
819
925
|
if (shouldSkipRuntimeStateWrite(previous, payload, nowMs)) return;
|
|
820
926
|
await writeStateFile(stateFile, payload);
|