@gajae-code/coding-agent 0.16.0 → 0.16.1
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 +41 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +8 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +6 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +154 -28
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +5 -5
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +111 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +33 -21
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +2 -0
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +463 -8
- package/src/sdk/host/session-runtime.ts +383 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +152 -59
- package/src/sdk/session.ts +60 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +156 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +6 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
package/src/async/job-manager.ts
CHANGED
|
@@ -68,9 +68,25 @@ export function jobElapsedMs(job: Pick<AsyncJob, "startTime" | "endTime">, now:
|
|
|
68
68
|
return Math.max(0, (job.endTime ?? now) - job.startTime);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Why a foreground wait left the foreground. Every fold path names its
|
|
73
|
+
* trigger so job snapshots and SDK clients can tell a user steer from the
|
|
74
|
+
* chord, the auto-background timer, or the explicit `bash.background` control.
|
|
75
|
+
*/
|
|
76
|
+
export type FoldReason = "steer" | "chord" | "timer" | "sdk_control";
|
|
77
|
+
|
|
78
|
+
/** A fold observed by the manager; published to SDK clients as `bash_folded`. */
|
|
79
|
+
export interface JobFoldEvent {
|
|
80
|
+
jobId: string;
|
|
81
|
+
generation: string;
|
|
82
|
+
reason: FoldReason;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
export interface AsyncJobMetadata {
|
|
72
86
|
/** Set once a foreground wait has been folded, so surfacing can show folded work. */
|
|
73
87
|
backgrounded?: boolean;
|
|
88
|
+
/** Why a foreground wait was folded; absent for jobs started directly in the background (`async: true`). */
|
|
89
|
+
foldReason?: FoldReason;
|
|
74
90
|
subagent?: {
|
|
75
91
|
id: string;
|
|
76
92
|
agent: string;
|
|
@@ -271,6 +287,7 @@ interface AsyncJobDelivery {
|
|
|
271
287
|
label: string;
|
|
272
288
|
status: AsyncJob["status"];
|
|
273
289
|
backgrounded: boolean;
|
|
290
|
+
foldReason?: FoldReason;
|
|
274
291
|
text: string;
|
|
275
292
|
originalBytes?: number;
|
|
276
293
|
truncated?: boolean;
|
|
@@ -290,6 +307,7 @@ interface AsyncJobReceiptClaim {
|
|
|
290
307
|
label: string;
|
|
291
308
|
status: AsyncJob["status"];
|
|
292
309
|
backgrounded: boolean;
|
|
310
|
+
foldReason?: FoldReason;
|
|
293
311
|
ownerId?: string;
|
|
294
312
|
}
|
|
295
313
|
|
|
@@ -331,6 +349,8 @@ export interface AsyncJobSnapshotEntry {
|
|
|
331
349
|
status: AsyncJob["status"];
|
|
332
350
|
generation: string;
|
|
333
351
|
backgrounded: boolean;
|
|
352
|
+
/** Present when `backgrounded` was set by a fold; absent for direct background starts. */
|
|
353
|
+
foldReason?: FoldReason;
|
|
334
354
|
deliveryState: JobDeliveryState;
|
|
335
355
|
}
|
|
336
356
|
|
|
@@ -630,6 +650,7 @@ export class AsyncJobManager {
|
|
|
630
650
|
}
|
|
631
651
|
|
|
632
652
|
readonly #jobs = new Map<string, AsyncJob>();
|
|
653
|
+
#retainedDisposalCompletion: Promise<void> = Promise.resolve();
|
|
633
654
|
readonly #deliveries: AsyncJobDelivery[] = [];
|
|
634
655
|
readonly #inFlightDeliveries: AsyncJobDelivery[] = [];
|
|
635
656
|
readonly #suppressedDeliveries = new Set<string>();
|
|
@@ -653,7 +674,6 @@ export class AsyncJobManager {
|
|
|
653
674
|
readonly #maxDeadLetterOverflowOwners: number;
|
|
654
675
|
#deliveryLoop: Promise<void> | undefined;
|
|
655
676
|
#disposed = false;
|
|
656
|
-
#disposing = false;
|
|
657
677
|
#runningOwnerCleanups = false;
|
|
658
678
|
readonly #subagentRecords = new Map<string, SubagentRecord>();
|
|
659
679
|
readonly #terminalEvents = new Map<string, TerminalEvent>();
|
|
@@ -996,6 +1016,8 @@ export class AsyncJobManager {
|
|
|
996
1016
|
};
|
|
997
1017
|
}
|
|
998
1018
|
|
|
1019
|
+
readonly #foldListeners = new Set<(event: JobFoldEvent) => void>();
|
|
1020
|
+
|
|
999
1021
|
#notifyChange(): void {
|
|
1000
1022
|
for (const cb of this.#changeListeners) {
|
|
1001
1023
|
try {
|
|
@@ -2307,6 +2329,7 @@ export class AsyncJobManager {
|
|
|
2307
2329
|
status: job.status,
|
|
2308
2330
|
generation: job.generation,
|
|
2309
2331
|
backgrounded: job.metadata?.backgrounded === true,
|
|
2332
|
+
foldReason: job.metadata?.foldReason,
|
|
2310
2333
|
deliveryState,
|
|
2311
2334
|
};
|
|
2312
2335
|
});
|
|
@@ -2329,6 +2352,7 @@ export class AsyncJobManager {
|
|
|
2329
2352
|
status: delivery.status,
|
|
2330
2353
|
generation: delivery.generation,
|
|
2331
2354
|
backgrounded: delivery.backgrounded,
|
|
2355
|
+
foldReason: delivery.foldReason,
|
|
2332
2356
|
deliveryState: "pending",
|
|
2333
2357
|
});
|
|
2334
2358
|
}
|
|
@@ -2344,6 +2368,7 @@ export class AsyncJobManager {
|
|
|
2344
2368
|
status: delivery.status,
|
|
2345
2369
|
generation: delivery.generation,
|
|
2346
2370
|
backgrounded: delivery.backgrounded,
|
|
2371
|
+
foldReason: delivery.foldReason,
|
|
2347
2372
|
deliveryState: "pending",
|
|
2348
2373
|
});
|
|
2349
2374
|
}
|
|
@@ -2359,6 +2384,7 @@ export class AsyncJobManager {
|
|
|
2359
2384
|
status: claim.status,
|
|
2360
2385
|
generation: claim.generation,
|
|
2361
2386
|
backgrounded: claim.backgrounded,
|
|
2387
|
+
foldReason: claim.foldReason,
|
|
2362
2388
|
deliveryState: "pending",
|
|
2363
2389
|
});
|
|
2364
2390
|
}
|
|
@@ -2408,6 +2434,7 @@ export class AsyncJobManager {
|
|
|
2408
2434
|
label: job.label,
|
|
2409
2435
|
status: job.status,
|
|
2410
2436
|
backgrounded: job.metadata?.backgrounded === true,
|
|
2437
|
+
foldReason: job.metadata?.foldReason,
|
|
2411
2438
|
text,
|
|
2412
2439
|
attempt: 0,
|
|
2413
2440
|
nextAttemptAt: Date.now(),
|
|
@@ -2426,6 +2453,7 @@ export class AsyncJobManager {
|
|
|
2426
2453
|
label: job.label,
|
|
2427
2454
|
status: job.status,
|
|
2428
2455
|
backgrounded: job.metadata?.backgrounded === true,
|
|
2456
|
+
foldReason: job.metadata?.foldReason,
|
|
2429
2457
|
ownerId: job.ownerId,
|
|
2430
2458
|
});
|
|
2431
2459
|
this.#notifyChange();
|
|
@@ -2632,12 +2660,26 @@ export class AsyncJobManager {
|
|
|
2632
2660
|
}
|
|
2633
2661
|
|
|
2634
2662
|
/**
|
|
2635
|
-
*
|
|
2636
|
-
*
|
|
2663
|
+
* Subscribe to fold events. Fires once per successful `markBackgrounded`
|
|
2664
|
+
* transition (never on the idempotent repeat), so a subscriber can publish
|
|
2665
|
+
* exactly one `bash_folded` frame per fold. Listener errors are isolated.
|
|
2637
2666
|
*/
|
|
2638
|
-
|
|
2667
|
+
onFold(cb: (event: JobFoldEvent) => void): () => void {
|
|
2668
|
+
this.#foldListeners.add(cb);
|
|
2669
|
+
return () => {
|
|
2670
|
+
this.#foldListeners.delete(cb);
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
/**
|
|
2675
|
+
* Mark a job that was launched directly in the background (`bash` with
|
|
2676
|
+
* `async: true`). It never had a foreground wait, so this is not a fold: no
|
|
2677
|
+
* `foldReason` is recorded and no fold listener fires.
|
|
2678
|
+
*/
|
|
2679
|
+
markStartedInBackground(jobId: string, generation: string): boolean {
|
|
2639
2680
|
const job = this.#jobs.get(jobId);
|
|
2640
2681
|
if (!job || job.generation !== generation) return false;
|
|
2682
|
+
if (job.metadata?.backgrounded === true) return true;
|
|
2641
2683
|
job.metadata = { ...job.metadata, backgrounded: true };
|
|
2642
2684
|
for (const delivery of [...this.#deliveries, ...this.#inFlightDeliveries]) {
|
|
2643
2685
|
if (delivery.jobId === jobId && delivery.generation === generation) delivery.backgrounded = true;
|
|
@@ -2646,6 +2688,37 @@ export class AsyncJobManager {
|
|
|
2646
2688
|
return true;
|
|
2647
2689
|
}
|
|
2648
2690
|
|
|
2691
|
+
/**
|
|
2692
|
+
* Mark a running job as backgrounded (folded out of its foreground call).
|
|
2693
|
+
* Read by getJobsSnapshot so folded work stays visible; safe to call twice.
|
|
2694
|
+
* The first transition records `reason` and notifies fold listeners; a
|
|
2695
|
+
* repeat keeps the original reason and emits nothing.
|
|
2696
|
+
*/
|
|
2697
|
+
markBackgrounded(jobId: string, generation: string, reason: FoldReason): boolean {
|
|
2698
|
+
const job = this.#jobs.get(jobId);
|
|
2699
|
+
if (!job || job.generation !== generation) return false;
|
|
2700
|
+
if (job.metadata?.backgrounded === true) return true;
|
|
2701
|
+
job.metadata = { ...job.metadata, backgrounded: true, foldReason: reason };
|
|
2702
|
+
for (const delivery of [...this.#deliveries, ...this.#inFlightDeliveries]) {
|
|
2703
|
+
if (delivery.jobId === jobId && delivery.generation === generation) {
|
|
2704
|
+
delivery.backgrounded = true;
|
|
2705
|
+
delivery.foldReason = reason;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
this.#notifyChange();
|
|
2709
|
+
const event: JobFoldEvent = { jobId, generation, reason };
|
|
2710
|
+
for (const cb of this.#foldListeners) {
|
|
2711
|
+
try {
|
|
2712
|
+
cb(event);
|
|
2713
|
+
} catch (error) {
|
|
2714
|
+
logger.warn("Async job fold listener failed", {
|
|
2715
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
return true;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2649
2722
|
async waitForAll(): Promise<void> {
|
|
2650
2723
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
2651
2724
|
}
|
|
@@ -2707,6 +2780,17 @@ export class AsyncJobManager {
|
|
|
2707
2780
|
// registration keeps the original protection (cleanups cannot register fresh
|
|
2708
2781
|
// work) without disabling delivery. Errors in cleanups are logged, never
|
|
2709
2782
|
// escalated.
|
|
2783
|
+
const unsettledJobPromises = new Set<Promise<void>>();
|
|
2784
|
+
const jobIdsByPromise = new Map<Promise<void>, string>();
|
|
2785
|
+
for (const job of this.#jobs.values()) {
|
|
2786
|
+
const promise = job.promise;
|
|
2787
|
+
unsettledJobPromises.add(promise);
|
|
2788
|
+
jobIdsByPromise.set(promise, job.id);
|
|
2789
|
+
void promise.then(
|
|
2790
|
+
() => unsettledJobPromises.delete(promise),
|
|
2791
|
+
() => unsettledJobPromises.delete(promise),
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2710
2794
|
this.#registrationClosed = true;
|
|
2711
2795
|
this.#clearEvictionTimers();
|
|
2712
2796
|
this.#runningOwnerCleanups = true;
|
|
@@ -2715,7 +2799,6 @@ export class AsyncJobManager {
|
|
|
2715
2799
|
} finally {
|
|
2716
2800
|
this.#runningOwnerCleanups = false;
|
|
2717
2801
|
}
|
|
2718
|
-
this.#disposing = true;
|
|
2719
2802
|
this.cancelAll();
|
|
2720
2803
|
for (const tombstone of this.#monitorTombstones.values()) {
|
|
2721
2804
|
try {
|
|
@@ -2734,13 +2817,22 @@ export class AsyncJobManager {
|
|
|
2734
2817
|
const remainingDeliveryMs = Math.max(0, disposalDeadline - Date.now());
|
|
2735
2818
|
const drained = await this.drainDeliveries({ timeoutMs: remainingDeliveryMs });
|
|
2736
2819
|
if (!drained) this.#projectUndeliveredDisposalFailures();
|
|
2737
|
-
const
|
|
2820
|
+
const retainedAuthority = new Set<Promise<void>>(unsettledJobPromises);
|
|
2821
|
+
for (const delivery of this.#inFlightDeliveries) {
|
|
2822
|
+
if (delivery.promise) retainedAuthority.add(delivery.promise);
|
|
2823
|
+
}
|
|
2824
|
+
this.#retainedDisposalCompletion = Promise.allSettled(retainedAuthority).then(() => {});
|
|
2825
|
+
const physicalStuckJobIds = [...unsettledJobPromises]
|
|
2826
|
+
.map(promise => jobIdsByPromise.get(promise))
|
|
2827
|
+
.filter((id): id is string => id !== undefined);
|
|
2828
|
+
const stuckJobIds = [...new Set([...waitResult.stuckJobIds, ...physicalStuckJobIds])];
|
|
2829
|
+
const disposalCompleted = waitResult.completed && drained && retainedAuthority.size === 0;
|
|
2738
2830
|
this.#lastDisposeDiagnostics = {
|
|
2739
|
-
stuckJobIds
|
|
2831
|
+
stuckJobIds,
|
|
2740
2832
|
deliveriesDrained: disposalCompleted,
|
|
2741
2833
|
};
|
|
2742
|
-
if (
|
|
2743
|
-
logger.warn("Async job manager dispose timed out waiting for jobs", { stuckJobIds
|
|
2834
|
+
if (stuckJobIds.length > 0) {
|
|
2835
|
+
logger.warn("Async job manager dispose timed out waiting for jobs", { stuckJobIds });
|
|
2744
2836
|
}
|
|
2745
2837
|
this.#clearEvictionTimers();
|
|
2746
2838
|
this.#disposed = true;
|
|
@@ -2772,9 +2864,14 @@ export class AsyncJobManager {
|
|
|
2772
2864
|
this.#ownerSubagentShutdownLeases.clear();
|
|
2773
2865
|
this.#notifyChange();
|
|
2774
2866
|
this.#changeListeners.clear();
|
|
2867
|
+
this.#foldListeners.clear();
|
|
2775
2868
|
return disposalCompleted;
|
|
2776
2869
|
}
|
|
2777
2870
|
|
|
2871
|
+
awaitRetainedDisposalCompletion(): Promise<void> {
|
|
2872
|
+
return this.#retainedDisposalCompletion;
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2778
2875
|
#projectUndeliveredDisposalFailures(): void {
|
|
2779
2876
|
const retained = [...this.#deliveries, ...this.#inFlightDeliveries];
|
|
2780
2877
|
for (const delivery of retained) {
|
|
@@ -3145,6 +3242,7 @@ export class AsyncJobManager {
|
|
|
3145
3242
|
label: job.label,
|
|
3146
3243
|
status: job.status,
|
|
3147
3244
|
backgrounded: job.metadata?.backgrounded === true,
|
|
3245
|
+
foldReason: job.metadata?.foldReason,
|
|
3148
3246
|
text: deliveryText.text,
|
|
3149
3247
|
originalBytes: deliveryText.originalBytes,
|
|
3150
3248
|
truncated: deliveryText.truncated,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import browserAsidePrompt from "./prompts/agent-fragments/browser-aside.md" with { type: "text" };
|
|
2
|
+
|
|
3
|
+
export type BrowserBackendId = "native" | "aside";
|
|
4
|
+
|
|
5
|
+
export interface BrowserBackend {
|
|
6
|
+
readonly id: BrowserBackendId;
|
|
7
|
+
readonly exposesBuiltinTool: boolean;
|
|
8
|
+
readonly developerInstructions?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface BrowserBackendSettingsSource {
|
|
12
|
+
get(key: string): unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nativeBrowserBackend: BrowserBackend = {
|
|
16
|
+
id: "native",
|
|
17
|
+
exposesBuiltinTool: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const asideBrowserBackend: BrowserBackend = {
|
|
21
|
+
id: "aside",
|
|
22
|
+
exposesBuiltinTool: false,
|
|
23
|
+
developerInstructions: browserAsidePrompt.trim(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Resolve the configured browser backend. Aside is strictly opt-in. */
|
|
27
|
+
export function resolveBrowserBackend(settings: BrowserBackendSettingsSource): BrowserBackend {
|
|
28
|
+
return settings.get("browser.backend") === "aside" ? asideBrowserBackend : nativeBrowserBackend;
|
|
29
|
+
}
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -886,11 +886,40 @@ async function handleLink(manager: PluginManager, paths: string[], flags: { json
|
|
|
886
886
|
}
|
|
887
887
|
}
|
|
888
888
|
|
|
889
|
+
async function collectMarketplaceDoctorChecks(): Promise<
|
|
890
|
+
Array<{ name: string; status: "ok" | "warning" | "error"; message: string }>
|
|
891
|
+
> {
|
|
892
|
+
const extra: Array<{ name: string; status: "ok" | "warning" | "error"; message: string }> = [];
|
|
893
|
+
try {
|
|
894
|
+
const mktMgr = await makeMarketplaceManager();
|
|
895
|
+
const installed = await mktMgr.listInstalledPlugins();
|
|
896
|
+
for (const summary of installed) {
|
|
897
|
+
for (const entry of summary.entries) {
|
|
898
|
+
let exists = false;
|
|
899
|
+
try {
|
|
900
|
+
const { statSync } = await import("node:fs");
|
|
901
|
+
statSync(entry.installPath);
|
|
902
|
+
exists = true;
|
|
903
|
+
} catch {}
|
|
904
|
+
if (!exists) {
|
|
905
|
+
extra.push({
|
|
906
|
+
name: `marketplace:${summary.id}:${entry.scope}`,
|
|
907
|
+
status: "error",
|
|
908
|
+
message: `Marketplace plugin "${summary.id}" (${entry.scope}) references missing cache dir "${entry.installPath}" (v${entry.version}); reinstall or upgrade the plugin`,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
} catch {}
|
|
914
|
+
return extra;
|
|
915
|
+
}
|
|
916
|
+
|
|
889
917
|
async function handleDoctor(
|
|
890
918
|
manager: PluginManager,
|
|
891
919
|
flags: { json?: boolean; fix?: boolean; migratePlugins?: boolean },
|
|
892
920
|
): Promise<void> {
|
|
893
921
|
const checks = await manager.doctor({ fix: flags.fix });
|
|
922
|
+
checks.push(...(await collectMarketplaceDoctorChecks()));
|
|
894
923
|
try {
|
|
895
924
|
const statuses = flags.migratePlugins
|
|
896
925
|
? await runGjcPluginMigrationPreflight(getProjectDir())
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -1150,6 +1150,67 @@ function formatRegistryProvenance(version: string, registry: string | undefined)
|
|
|
1150
1150
|
return `Version ${version} was resolved from ${registry}; GitHub release assets may not exist for a version that was never published there.`;
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
|
+
interface ProcessReportWithLibc {
|
|
1154
|
+
header?: {
|
|
1155
|
+
glibcVersionRuntime?: unknown;
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const GLIBC_VERSION_PATTERN = /^\d+\.\d+$/u;
|
|
1160
|
+
|
|
1161
|
+
function reportedGlibcVersion(value: unknown): string | undefined {
|
|
1162
|
+
if (typeof value !== "string") return undefined;
|
|
1163
|
+
const version = value.trim();
|
|
1164
|
+
return GLIBC_VERSION_PATTERN.test(version) ? version : undefined;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function runtimeGlibcVersion(): string | undefined {
|
|
1168
|
+
try {
|
|
1169
|
+
const report = process.report?.getReport?.() as ProcessReportWithLibc | undefined;
|
|
1170
|
+
return reportedGlibcVersion(report?.header?.glibcVersionRuntime);
|
|
1171
|
+
} catch {
|
|
1172
|
+
return undefined;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function hasMuslLoader(): boolean {
|
|
1177
|
+
return (
|
|
1178
|
+
fs.existsSync("/lib/ld-musl-x86_64.so.1") ||
|
|
1179
|
+
fs.existsSync("/lib/ld-musl-aarch64.so.1") ||
|
|
1180
|
+
fs.existsSync("/lib/ld-musl-armhf.so.1")
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
type LinuxLibc = "glibc" | "musl" | "unknown";
|
|
1185
|
+
|
|
1186
|
+
function detectLinuxLibcFromSignals(glibcVersionRuntime: string | undefined, muslLoaderPresent: boolean): LinuxLibc {
|
|
1187
|
+
if (reportedGlibcVersion(glibcVersionRuntime) !== undefined) return "glibc";
|
|
1188
|
+
return muslLoaderPresent ? "musl" : "unknown";
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function detectLinuxLibc(): LinuxLibc {
|
|
1192
|
+
return detectLinuxLibcFromSignals(runtimeGlibcVersion(), hasMuslLoader());
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function unsupportedLinuxLibcReason(libc: LinuxLibc): string | undefined {
|
|
1196
|
+
if (libc === "glibc") return undefined;
|
|
1197
|
+
return libc === "musl"
|
|
1198
|
+
? "Unsupported libc: musl. Prebuilt Linux binaries are glibc-only. See docs/install.md"
|
|
1199
|
+
: "Unable to verify an active glibc runtime. Prebuilt Linux binaries are glibc-only. See docs/install.md";
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function assertSupportedLinuxLibc(libc: LinuxLibc): void {
|
|
1203
|
+
const reason = unsupportedLinuxLibcReason(libc);
|
|
1204
|
+
if (reason) throw new Error(formatUnsupportedTargetMessage(reason));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
export function assertSupportedLinuxLibcForTest(
|
|
1208
|
+
glibcVersionRuntime: string | undefined,
|
|
1209
|
+
muslLoaderPresent: boolean,
|
|
1210
|
+
): void {
|
|
1211
|
+
assertSupportedLinuxLibc(detectLinuxLibcFromSignals(glibcVersionRuntime, muslLoaderPresent));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1153
1214
|
/**
|
|
1154
1215
|
* Download a release binary to a target path, replacing an existing file.
|
|
1155
1216
|
*/
|
|
@@ -1158,18 +1219,8 @@ async function updateViaBinaryAt(
|
|
|
1158
1219
|
expectedVersion: string,
|
|
1159
1220
|
registry?: string,
|
|
1160
1221
|
): Promise<InstalledVersionVerification> {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
(fs.existsSync("/lib/ld-musl-x86_64.so.1") ||
|
|
1164
|
-
fs.existsSync("/lib/ld-musl-aarch64.so.1") ||
|
|
1165
|
-
fs.existsSync("/lib/ld-musl-armhf.so.1"))
|
|
1166
|
-
) {
|
|
1167
|
-
throw new Error(
|
|
1168
|
-
formatUnsupportedTargetMessage(
|
|
1169
|
-
"Unsupported libc: musl. Prebuilt Linux binaries are glibc-only. See docs/install.md",
|
|
1170
|
-
),
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1222
|
+
const linuxLibc = process.platform === "linux" ? detectLinuxLibc() : undefined;
|
|
1223
|
+
if (linuxLibc !== undefined) assertSupportedLinuxLibc(linuxLibc);
|
|
1173
1224
|
const binaryName = getBinaryName();
|
|
1174
1225
|
const url = buildReleaseBinaryUrl(expectedVersion);
|
|
1175
1226
|
const registryNote = formatRegistryProvenance(expectedVersion, registry);
|
package/src/commands/sdk.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
SdkStartupRollbackTracker,
|
|
40
40
|
} from "../sdk/startup-capability";
|
|
41
41
|
import { runSdkServe } from "../sdk/transport/serve-cli";
|
|
42
|
+
import { isSessionDisposalIncompleteError } from "../session/agent-session";
|
|
42
43
|
import {
|
|
43
44
|
type CapturedSessionTranscriptSnapshot,
|
|
44
45
|
type ResumeSessionIdentity,
|
|
@@ -405,11 +406,13 @@ export async function runSessionHost(
|
|
|
405
406
|
sleep?: (ms: number) => Promise<void>;
|
|
406
407
|
cwd?: string;
|
|
407
408
|
processIncarnation?: (pid: number) => string | undefined;
|
|
409
|
+
applyStartupModelProfiles?: typeof applyStartupModelProfiles;
|
|
408
410
|
} = {},
|
|
409
411
|
): Promise<void> {
|
|
410
412
|
const now = timing.now ?? Date.now;
|
|
411
413
|
const sleep = timing.sleep ?? (async ms => await Bun.sleep(ms));
|
|
412
414
|
const readIncarnation = timing.processIncarnation ?? processIncarnation;
|
|
415
|
+
const applyModelProfiles = timing.applyStartupModelProfiles ?? applyStartupModelProfiles;
|
|
413
416
|
const request = readSessionLifecycleLaunchRequest(process.env.GJC_SDK_LIFECYCLE_REQUEST, now());
|
|
414
417
|
const agentDir = process.env.GJC_AGENT_DIR;
|
|
415
418
|
if (!agentDir) throw new Error("GJC_AGENT_DIR is required for sdk session-host-internal.");
|
|
@@ -599,22 +602,32 @@ export async function runSessionHost(
|
|
|
599
602
|
throw created.failure;
|
|
600
603
|
}
|
|
601
604
|
const { session, capability, rollback, startDeferredMemoryBackend } = created;
|
|
605
|
+
let lifecycleTranscriptPath: string | undefined;
|
|
606
|
+
session.registerToolSessionTransitionCleanup(async () => {
|
|
607
|
+
await session.sessionManager.ensureOnDisk();
|
|
608
|
+
lifecycleTranscriptPath = session.sessionManager.getSessionFile();
|
|
609
|
+
});
|
|
602
610
|
let sessionDisposal: Promise<void> | undefined;
|
|
603
611
|
const disposeSession = (): Promise<void> => {
|
|
604
|
-
sessionDisposal ??=
|
|
612
|
+
sessionDisposal ??= (async () => {
|
|
613
|
+
try {
|
|
614
|
+
await session.dispose();
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (!isSessionDisposalIncompleteError(error)) throw error;
|
|
617
|
+
await session.awaitDisposeCompletion();
|
|
618
|
+
}
|
|
619
|
+
})();
|
|
605
620
|
return sessionDisposal;
|
|
606
621
|
};
|
|
607
622
|
let disposal: Promise<LifecycleTranscriptEvidence | undefined> | undefined;
|
|
608
623
|
const disposeAndCapture = (): Promise<LifecycleTranscriptEvidence | undefined> => {
|
|
609
624
|
disposal ??= (async () => {
|
|
610
625
|
await disposeSession();
|
|
626
|
+
if (!lifecycleTranscriptPath) return undefined;
|
|
611
627
|
try {
|
|
612
|
-
await session.sessionManager.ensureOnDisk();
|
|
613
|
-
const transcriptPath = session.sessionManager.getSessionFile();
|
|
614
|
-
if (!transcriptPath) return undefined;
|
|
615
628
|
const [bytes, stat] = await Promise.all([
|
|
616
|
-
fs.readFile(
|
|
617
|
-
fs.stat(
|
|
629
|
+
fs.readFile(lifecycleTranscriptPath),
|
|
630
|
+
fs.stat(lifecycleTranscriptPath, { bigint: true }),
|
|
618
631
|
]);
|
|
619
632
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
620
633
|
return {
|
|
@@ -708,12 +721,14 @@ export async function runSessionHost(
|
|
|
708
721
|
const modelProfileStartup =
|
|
709
722
|
process.env.GJC_SDK_TEST_HANG_MODEL_PROFILE === cwd
|
|
710
723
|
? new Promise<void>(() => {})
|
|
711
|
-
:
|
|
724
|
+
: applyModelProfiles({
|
|
712
725
|
session,
|
|
713
726
|
settings: session.settings,
|
|
714
727
|
modelRegistry: session.modelRegistry,
|
|
715
728
|
parsedArgs: parsed,
|
|
716
729
|
startupThinkingLevel,
|
|
730
|
+
preferCachedModels: true,
|
|
731
|
+
preferCachedDefaultProfile: true,
|
|
717
732
|
});
|
|
718
733
|
await beforeCutoff(modelProfileStartup);
|
|
719
734
|
throwIfCutoff();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing path for a resolved binding's credential source — audit-reversible.
|
|
3
|
+
* Derived from (model.api, credential-provenance) where the binding actually
|
|
4
|
+
* resolves credentials, not from a provider-name allowlist.
|
|
5
|
+
*/
|
|
6
|
+
import type { Api } from "@gajae-code/ai/core";
|
|
7
|
+
import type { EffectiveProviderAuth } from "./provider-selection-policy";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Label shown in the selector row + active-model line. Only the category is
|
|
11
|
+
* disclosed (never the key material or project id).
|
|
12
|
+
*/
|
|
13
|
+
export type BillingPathKind = "metered-api" | "cloud-project" | "bundled";
|
|
14
|
+
|
|
15
|
+
export interface BillingPath {
|
|
16
|
+
kind: BillingPathKind;
|
|
17
|
+
/** Stable per-credential-source key for one-time notice dismissal. */
|
|
18
|
+
dismissKey: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Derive from the concrete binding (api + credential provenance).
|
|
24
|
+
* - oauth/keyless/unknown => bundled (plan/proxied/kNoAuth-like)
|
|
25
|
+
* - key + cloud-project api (google-vertex / bedrock) => cloud-project
|
|
26
|
+
* - key + any other api (including custom OpenAI-compatible) => metered-api
|
|
27
|
+
*/
|
|
28
|
+
export function deriveBillingPath(
|
|
29
|
+
provider: string,
|
|
30
|
+
api: Api | string | undefined,
|
|
31
|
+
effectiveAuth: EffectiveProviderAuth,
|
|
32
|
+
): BillingPath | undefined {
|
|
33
|
+
if (effectiveAuth === "oauth" || effectiveAuth === "keyless" || effectiveAuth === "unknown") return undefined;
|
|
34
|
+
if (effectiveAuth !== "key") return undefined;
|
|
35
|
+
const apiKind = typeof api === "string" ? api : undefined;
|
|
36
|
+
const cloudApis = new Set(["google-vertex", "bedrock-converse-stream"]);
|
|
37
|
+
const isCloud = apiKind ? cloudApis.has(apiKind) : false;
|
|
38
|
+
const kind: BillingPathKind = isCloud ? "cloud-project" : "metered-api";
|
|
39
|
+
const label = kind === "cloud-project" ? "cloud-project billing" : "metered API";
|
|
40
|
+
const dismissKey = `${kind}:${provider.trim().toLowerCase()}`;
|
|
41
|
+
return { kind, dismissKey, label };
|
|
42
|
+
}
|
|
@@ -7,4 +7,21 @@
|
|
|
7
7
|
* this dependency-light module so the resolver can import it without pulling in
|
|
8
8
|
* the TUI/AI-heavy settings schema.
|
|
9
9
|
*/
|
|
10
|
-
export const CONFIG_SCHEMA_VERSION =
|
|
10
|
+
export const CONFIG_SCHEMA_VERSION = 2;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize a durable `configSchemaVersion` marker into a comparable version.
|
|
14
|
+
*
|
|
15
|
+
* Every version decision (future-schema detection, migration-pending state, and
|
|
16
|
+
* the ordered migration registry) must read the SAME normalized value: a
|
|
17
|
+
* malformed marker read as a future schema by one guard and as unmigrated by
|
|
18
|
+
* the next would skip legacy migrations while still re-running v0 transforms.
|
|
19
|
+
* A finite malformed value floors to the nearest completed version; a
|
|
20
|
+
* non-finite or non-numeric one carries no version information and normalizes
|
|
21
|
+
* to 0 (fully unmigrated). `undefined` stays `undefined` (no marker written).
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeConfigSchemaVersion(value: unknown): number | undefined {
|
|
24
|
+
if (value === undefined) return undefined;
|
|
25
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
26
|
+
return Math.max(0, Math.floor(value));
|
|
27
|
+
}
|