@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
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
RecoveryFsIdentity,
|
|
10
10
|
RecoveryFsRoot,
|
|
11
11
|
} from "@gajae-code/natives";
|
|
12
|
+
import { isEnoent, logger } from "@gajae-code/utils";
|
|
12
13
|
import type { SessionStorageRangeSnapshot, SessionStorageStat } from "../session-storage";
|
|
13
14
|
import {
|
|
14
15
|
classifyNativePublishOutcome,
|
|
@@ -25,8 +26,10 @@ type NativeManagedSessionStorage = Pick<
|
|
|
25
26
|
| "exactReplacePath"
|
|
26
27
|
| "exactUnlink"
|
|
27
28
|
| "linkNoReplacePath"
|
|
29
|
+
| "linkNoReplacePathAsync"
|
|
28
30
|
| "openRecoveryFsRoot"
|
|
29
31
|
| "renameNoReplacePath"
|
|
32
|
+
| "renameNoReplacePathAsync"
|
|
30
33
|
| "repairOwnerOnlyPathSecurityExpected"
|
|
31
34
|
| "snapshotDirectoryTree"
|
|
32
35
|
| "verifyOwnerOnlyFdSecurity"
|
|
@@ -497,6 +500,22 @@ const SCRUBBED_REMNANT_PREFIXES = [
|
|
|
497
500
|
/** In-flight protocol steps complete in milliseconds; anything older is abandoned. */
|
|
498
501
|
const SCRUBBED_REMNANT_MIN_AGE_MS = 15 * 60 * 1000;
|
|
499
502
|
|
|
503
|
+
/** Minimum interval between best-effort remnant reaps of one bound store directory. */
|
|
504
|
+
const SCRUBBED_REMNANT_REAP_INTERVAL_MS = 60_000;
|
|
505
|
+
|
|
506
|
+
export interface ScrubbedProtocolRemnantReapResult {
|
|
507
|
+
readonly reaped: number;
|
|
508
|
+
readonly failures: number;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function reportScrubbedProtocolRemnantReap(reaped: number, failures: number): ScrubbedProtocolRemnantReapResult {
|
|
512
|
+
if (failures > 0)
|
|
513
|
+
logger.warn("Managed session remnant reaping completed with failures", {
|
|
514
|
+
failureCount: failures,
|
|
515
|
+
reapedCount: reaped,
|
|
516
|
+
});
|
|
517
|
+
return { reaped, failures };
|
|
518
|
+
}
|
|
500
519
|
/**
|
|
501
520
|
* Best-effort removal of scrubbed write-protocol remnants from one managed
|
|
502
521
|
* directory. Only zero-length, single-link, non-symlink regular files whose
|
|
@@ -535,6 +554,48 @@ export function reapScrubbedProtocolRemnantsSync(
|
|
|
535
554
|
return reaped;
|
|
536
555
|
}
|
|
537
556
|
|
|
557
|
+
/** Number of candidate remnants inspected between event-loop yields. */
|
|
558
|
+
const SCRUBBED_REMNANT_REAP_BATCH_SIZE = 256;
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Async twin of {@link reapScrubbedProtocolRemnantsSync} with the same safety
|
|
562
|
+
* filters (terminal remnant prefix, zero-length, single-link, non-symlink,
|
|
563
|
+
* older than the age gate), yielding between bounded batches. Long-lived
|
|
564
|
+
* processes reap per-session descendant directories through this path so a
|
|
565
|
+
* legacy oversized directory cannot starve timers or sibling subagents while
|
|
566
|
+
* it is being drained (issue #4394).
|
|
567
|
+
*/
|
|
568
|
+
export async function reapScrubbedProtocolRemnants(
|
|
569
|
+
directory: string,
|
|
570
|
+
minAgeMs: number = SCRUBBED_REMNANT_MIN_AGE_MS,
|
|
571
|
+
): Promise<ScrubbedProtocolRemnantReapResult> {
|
|
572
|
+
let names: string[];
|
|
573
|
+
try {
|
|
574
|
+
names = await fsp.readdir(directory);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
return reportScrubbedProtocolRemnantReap(0, isEnoent(error) ? 0 : 1);
|
|
577
|
+
}
|
|
578
|
+
const cutoff = Date.now() - minAgeMs;
|
|
579
|
+
let reaped = 0;
|
|
580
|
+
let failures = 0;
|
|
581
|
+
let scanned = 0;
|
|
582
|
+
for (const name of names) {
|
|
583
|
+
if (!SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix))) continue;
|
|
584
|
+
if (++scanned % SCRUBBED_REMNANT_REAP_BATCH_SIZE === 0) await Bun.sleep(0);
|
|
585
|
+
const pathname = path.join(directory, name);
|
|
586
|
+
try {
|
|
587
|
+
const named = await fsp.lstat(pathname);
|
|
588
|
+
if (!named.isFile() || named.isSymbolicLink() || named.nlink !== 1 || named.size !== 0) continue;
|
|
589
|
+
if (named.mtimeMs > cutoff) continue;
|
|
590
|
+
await fsp.unlink(pathname);
|
|
591
|
+
reaped += 1;
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (!isEnoent(error)) failures += 1;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return reportScrubbedProtocolRemnantReap(reaped, failures);
|
|
597
|
+
}
|
|
598
|
+
|
|
538
599
|
const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
|
|
539
600
|
const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
|
|
540
601
|
const GENERAL_FAILURE_CODES = new Set([
|
|
@@ -895,6 +956,8 @@ export class ManagedSessionDescendantStore {
|
|
|
895
956
|
#ownsAuthority = false;
|
|
896
957
|
#closed = false;
|
|
897
958
|
#reconcilingReplacementCleanup = false;
|
|
959
|
+
#remnantReapInFlight = false;
|
|
960
|
+
#lastRemnantReapAttempt = 0;
|
|
898
961
|
readonly #authorityBaseDir: string;
|
|
899
962
|
/** Logical profile root inherited by nested managed session destinations. */
|
|
900
963
|
readonly #profileAgentDir: string;
|
|
@@ -1340,6 +1403,30 @@ export class ManagedSessionDescendantStore {
|
|
|
1340
1403
|
this.#assertBound();
|
|
1341
1404
|
this.#reconcileReplacementCleanupReceipts();
|
|
1342
1405
|
this.#assertBound();
|
|
1406
|
+
this.#scheduleRemnantReap();
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Best-effort asynchronous reaping of scrubbed write-protocol remnants in
|
|
1411
|
+
* this store's bound directory. Replacements leak zero-byte remnants on
|
|
1412
|
+
* platforms without retained authority (macOS), and scope-resolution reaping
|
|
1413
|
+
* never visits per-session descendant directories, so unbounded remnant
|
|
1414
|
+
* growth there degraded every namespace mutation and let one publication
|
|
1415
|
+
* stall the whole process (issue #4394). Reaping is throttled, serialized
|
|
1416
|
+
* per store, and never fails the triggering mutation.
|
|
1417
|
+
*/
|
|
1418
|
+
#scheduleRemnantReap(): void {
|
|
1419
|
+
const now = Date.now();
|
|
1420
|
+
if (this.#remnantReapInFlight || now - this.#lastRemnantReapAttempt < SCRUBBED_REMNANT_REAP_INTERVAL_MS) return;
|
|
1421
|
+
this.#lastRemnantReapAttempt = now;
|
|
1422
|
+
this.#remnantReapInFlight = true;
|
|
1423
|
+
void reapScrubbedProtocolRemnants(this.#baseDir)
|
|
1424
|
+
.catch((error: unknown) => {
|
|
1425
|
+
logger.warn("Managed session remnant reaping failed", { error: String(error) });
|
|
1426
|
+
})
|
|
1427
|
+
.finally(() => {
|
|
1428
|
+
this.#remnantReapInFlight = false;
|
|
1429
|
+
});
|
|
1343
1430
|
}
|
|
1344
1431
|
|
|
1345
1432
|
ensureDirectory(relativePath = ""): ManagedDirectoryRoot {
|
|
@@ -2255,6 +2342,17 @@ function fsyncDirectory(pathname: string): void {
|
|
|
2255
2342
|
}
|
|
2256
2343
|
}
|
|
2257
2344
|
|
|
2345
|
+
/** Async twin of {@link fsyncDirectory} for the off-loop publication path. */
|
|
2346
|
+
async function fsyncDirectoryAsync(pathname: string): Promise<void> {
|
|
2347
|
+
if (!shouldFsyncManagedDirectory()) return;
|
|
2348
|
+
const handle = await fsp.open(pathname, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
|
|
2349
|
+
try {
|
|
2350
|
+
await handle.sync();
|
|
2351
|
+
} finally {
|
|
2352
|
+
await handle.close();
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2258
2356
|
function bootId(): string | undefined {
|
|
2259
2357
|
try {
|
|
2260
2358
|
return fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
@@ -2536,7 +2634,7 @@ export async function publishManagedFileNoReplace(
|
|
|
2536
2634
|
const parent = path.dirname(destination);
|
|
2537
2635
|
ensureManagedDirectory(parent, root, policy);
|
|
2538
2636
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
2539
|
-
let
|
|
2637
|
+
let handle: fsp.FileHandle | undefined;
|
|
2540
2638
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
2541
2639
|
let failure: unknown;
|
|
2542
2640
|
let outcome: NativePublishOutcome | undefined;
|
|
@@ -2545,36 +2643,47 @@ export async function publishManagedFileNoReplace(
|
|
|
2545
2643
|
|
|
2546
2644
|
try {
|
|
2547
2645
|
assertOwned?.();
|
|
2548
|
-
|
|
2646
|
+
// The whole staging + publication chain is awaited off the resident event
|
|
2647
|
+
// loop: FileHandle operations run on the libuv pool and the no-replace
|
|
2648
|
+
// namespace publication crosses the async native boundary. A rename that
|
|
2649
|
+
// stalls in the kernel — e.g. into an oversized APFS directory namespace,
|
|
2650
|
+
// issue #4394 — blocks one pool thread instead of freezing the process:
|
|
2651
|
+
// await timeouts, sibling subagents, and watchdogs keep running, and a
|
|
2652
|
+
// hung publication degrades to one unresolved receipt.
|
|
2653
|
+
handle = await fsp.open(
|
|
2549
2654
|
staging,
|
|
2550
2655
|
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
|
|
2551
2656
|
0o600,
|
|
2552
2657
|
);
|
|
2553
|
-
secureFileDescriptor(staging, fd, "apply");
|
|
2658
|
+
secureFileDescriptor(staging, handle.fd, "apply");
|
|
2554
2659
|
let offset = 0;
|
|
2555
|
-
while (offset < bytes.byteLength)
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2660
|
+
while (offset < bytes.byteLength) {
|
|
2661
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.byteLength - offset);
|
|
2662
|
+
if (bytesWritten <= 0) throw new Error("managed_publish_failed");
|
|
2663
|
+
offset += bytesWritten;
|
|
2664
|
+
}
|
|
2665
|
+
await handle.sync();
|
|
2666
|
+
secureFileDescriptor(staging, handle.fd, "verify");
|
|
2667
|
+
const staged = await handle.stat({ bigint: true });
|
|
2559
2668
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
2560
2669
|
assertOwned?.();
|
|
2561
2670
|
|
|
2562
2671
|
renameAttempted = true;
|
|
2563
|
-
outcome = classifyNativePublishOutcome(
|
|
2672
|
+
outcome = classifyNativePublishOutcome(
|
|
2673
|
+
await nativeSessionStorage().renameNoReplacePathAsync(staging, destination),
|
|
2674
|
+
);
|
|
2564
2675
|
if (renameFlagsUnsupported(outcome)) {
|
|
2565
|
-
//
|
|
2566
|
-
//
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
// the published inode.
|
|
2571
|
-
outcome = classifyNativePublishOutcome(nativeSessionStorage().linkNoReplacePath(staging, destination));
|
|
2676
|
+
// See publishManagedFileNoReplaceSync: the staging link outlives this
|
|
2677
|
+
// publication and is removed only after the secured descriptor is closed.
|
|
2678
|
+
outcome = classifyNativePublishOutcome(
|
|
2679
|
+
await nativeSessionStorage().linkNoReplacePathAsync(staging, destination),
|
|
2680
|
+
);
|
|
2572
2681
|
linkPublished = outcome.ok;
|
|
2573
2682
|
}
|
|
2574
2683
|
|
|
2575
2684
|
if (!outcome.ok) throw publishFailure(outcome);
|
|
2576
2685
|
|
|
2577
|
-
const named =
|
|
2686
|
+
const named = await fsp.lstat(destination, { bigint: true });
|
|
2578
2687
|
if (
|
|
2579
2688
|
!named.isFile() ||
|
|
2580
2689
|
named.isSymbolicLink() ||
|
|
@@ -2583,15 +2692,21 @@ export async function publishManagedFileNoReplace(
|
|
|
2583
2692
|
) {
|
|
2584
2693
|
throw new Error("destination_identity_changed");
|
|
2585
2694
|
}
|
|
2586
|
-
secureFileDescriptor(destination, fd, "verify");
|
|
2587
|
-
|
|
2588
|
-
|
|
2695
|
+
secureFileDescriptor(destination, handle.fd, "verify");
|
|
2696
|
+
await handle.close();
|
|
2697
|
+
handle = undefined;
|
|
2589
2698
|
|
|
2590
|
-
|
|
2699
|
+
await fsyncDirectoryAsync(parent);
|
|
2591
2700
|
} catch (error) {
|
|
2592
2701
|
failure = error;
|
|
2593
2702
|
} finally {
|
|
2594
|
-
if (
|
|
2703
|
+
if (handle !== undefined) {
|
|
2704
|
+
try {
|
|
2705
|
+
await handle.close();
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
failure ??= error;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2595
2710
|
|
|
2596
2711
|
if (stagingIdentity && (linkPublished || !renameAttempted || (outcome && mayCleanCurrentStaging(outcome)))) {
|
|
2597
2712
|
await fsp
|