@gajae-code/coding-agent 0.16.0 → 0.16.3
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 +52 -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 +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -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 +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -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 +156 -32
- 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 +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- 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 +6 -1
- 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 +118 -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 +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- 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 +497 -31
- package/src/sdk/host/session-runtime.ts +387 -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 +210 -94
- package/src/sdk/session.ts +59 -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 +158 -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 +4 -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 +86 -1
package/src/tools/bash.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Component } from "@gajae-code/tui";
|
|
|
5
5
|
import { getKeybindings, ImageProtocol, TERMINAL, Text, visibleWidth } from "@gajae-code/tui";
|
|
6
6
|
import { getProjectDir, isEnoent, logger, prompt } from "@gajae-code/utils";
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
|
-
import { AsyncJobManager } from "../async";
|
|
8
|
+
import { AsyncJobManager, type FoldReason } from "../async";
|
|
9
9
|
import { type BashArtifactSaveResult, type BashResult, executeBash } from "../exec/bash-executor";
|
|
10
10
|
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
ClientBridgeTerminalHandle,
|
|
26
26
|
ClientBridgeTerminalOutput,
|
|
27
27
|
} from "../session/client-bridge";
|
|
28
|
+
import type { FoldAdapter } from "../session/fold-coordinator";
|
|
28
29
|
import {
|
|
29
30
|
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
30
31
|
OutputSink,
|
|
@@ -85,6 +86,18 @@ function sliceTextAfterUtf8ByteOffset(text: string, offsetBytes: number): string
|
|
|
85
86
|
import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
|
|
86
87
|
|
|
87
88
|
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
89
|
+
/**
|
|
90
|
+
* A user steer folds a running foreground bash call only after it has been
|
|
91
|
+
* running at least this long. Shorter commands finish normally and the steer is
|
|
92
|
+
* consumed at the ordinary tool boundary, so a quick `git status` never turns
|
|
93
|
+
* into a background job plus a wake-up turn.
|
|
94
|
+
*/
|
|
95
|
+
export const STEER_FOLD_GRACE_MS = 2_000;
|
|
96
|
+
|
|
97
|
+
/** Model-facing line appended to a steer-folded background-start result. */
|
|
98
|
+
export function steerFoldReasonLine(jobId: string): string {
|
|
99
|
+
return `Folded into background job ${jobId} because a user steer arrived; the command keeps running with its original timeout and its result will wake a later turn.`;
|
|
100
|
+
}
|
|
88
101
|
|
|
89
102
|
const BASH_ERROR_MAX_BYTES = 4096;
|
|
90
103
|
const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
@@ -488,6 +501,8 @@ export interface BashToolDetails {
|
|
|
488
501
|
timeoutSeconds?: number;
|
|
489
502
|
requestedTimeoutSeconds?: number;
|
|
490
503
|
terminalId?: string;
|
|
504
|
+
/** Why the foreground wait was folded, when this result is a background-start after a fold. */
|
|
505
|
+
foldReason?: FoldReason;
|
|
491
506
|
async?: {
|
|
492
507
|
state: "running" | "completed" | "failed";
|
|
493
508
|
jobId: string;
|
|
@@ -934,7 +949,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
934
949
|
label: string,
|
|
935
950
|
previewText: string,
|
|
936
951
|
timeoutSec: number,
|
|
937
|
-
options: {
|
|
952
|
+
options: {
|
|
953
|
+
requestedTimeoutSec?: number;
|
|
954
|
+
notices?: readonly string[];
|
|
955
|
+
terminalId?: string;
|
|
956
|
+
foldReason?: FoldReason;
|
|
957
|
+
} = {},
|
|
938
958
|
): AgentToolResult<BashToolDetails> {
|
|
939
959
|
const details: BashToolDetails = {
|
|
940
960
|
timeoutSeconds: timeoutSec,
|
|
@@ -946,6 +966,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
946
966
|
if (options.requestedTimeoutSec !== undefined && options.requestedTimeoutSec !== timeoutSec) {
|
|
947
967
|
details.requestedTimeoutSeconds = options.requestedTimeoutSec;
|
|
948
968
|
}
|
|
969
|
+
if (options.foldReason !== undefined) details.foldReason = options.foldReason;
|
|
949
970
|
const lines: string[] = [];
|
|
950
971
|
const trimmedPreview = previewText.trimEnd();
|
|
951
972
|
if (trimmedPreview.length > 0) {
|
|
@@ -955,6 +976,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
955
976
|
lines.push(...options.notices, "");
|
|
956
977
|
}
|
|
957
978
|
lines.push(`Background job ${jobId} started: ${label}`);
|
|
979
|
+
if (options.foldReason === "steer") lines.push(steerFoldReasonLine(jobId));
|
|
958
980
|
lines.push("Result will be delivered automatically when complete.");
|
|
959
981
|
lines.push(
|
|
960
982
|
`You can use \`job\` to poll until complete, but prefer to continue with another task in the meanwhile if it's not blocking.`,
|
|
@@ -1115,24 +1137,109 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1115
1137
|
return AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance();
|
|
1116
1138
|
}
|
|
1117
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Whether a queued user steer may fold the running foreground wait. Mirrors
|
|
1142
|
+
* the loop's steer admission: `busyPromptMode=queue` never admits a steer
|
|
1143
|
+
* into the busy run, so it never folds. `toolInterruptPolicy` is
|
|
1144
|
+
* deliberately NOT a gate: `finish_tools` means "do not kill the batch to
|
|
1145
|
+
* deliver a steer", and a fold kills nothing. It is the one way to deliver
|
|
1146
|
+
* the steer now AND let the command finish, so it applies under both
|
|
1147
|
+
* policies; the policy only decides what the loop does with sibling tools
|
|
1148
|
+
* after the fold returns. The auto-background setting is likewise not
|
|
1149
|
+
* consulted: like the chord, a steer fold is a user action.
|
|
1150
|
+
*
|
|
1151
|
+
* Fails closed: a session that cannot report newly admitted steering or
|
|
1152
|
+
* accept a fold request is not steer-foldable.
|
|
1153
|
+
*/
|
|
1154
|
+
#steerFoldEnabled(): boolean {
|
|
1155
|
+
const { waitForUserSteering, requestForegroundBashBackground } = this.session;
|
|
1156
|
+
if (!waitForUserSteering || !requestForegroundBashBackground) return false;
|
|
1157
|
+
return this.session.settings.get("busyPromptMode") === "steer";
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Fold `adapter` on the first user steer that ARRIVES after the wait has run
|
|
1162
|
+
* for {@link STEER_FOLD_GRACE_MS} since `startedAt`. A steer already queued
|
|
1163
|
+
* when the wait starts, or one that arrives inside the grace window, never
|
|
1164
|
+
* folds: the command finishes normally and that steer is consumed at the
|
|
1165
|
+
* ordinary tool boundary. The watcher keeps observing so a later qualifying
|
|
1166
|
+
* steer still folds, and re-checks the gate at that moment so a
|
|
1167
|
+
* `busyPromptMode` change during the command is honored.
|
|
1168
|
+
* Returns a stop function; call it when the wait settles. No-op when steer
|
|
1169
|
+
* folding is gated off at start.
|
|
1170
|
+
*/
|
|
1171
|
+
#watchSteerForFold(adapter: FoldAdapter, startedAt: number): () => void {
|
|
1172
|
+
if (!this.#steerFoldEnabled()) return () => {};
|
|
1173
|
+
const waitForSteer = this.session.waitForUserSteering;
|
|
1174
|
+
const requestFold = this.session.requestForegroundBashBackground;
|
|
1175
|
+
if (!waitForSteer || !requestFold) return () => {};
|
|
1176
|
+
const watch = new AbortController();
|
|
1177
|
+
const observe = async (): Promise<void> => {
|
|
1178
|
+
while (!watch.signal.aborted) {
|
|
1179
|
+
await waitForSteer(watch.signal);
|
|
1180
|
+
if (watch.signal.aborted) return;
|
|
1181
|
+
if (Date.now() - startedAt < STEER_FOLD_GRACE_MS) continue;
|
|
1182
|
+
if (!this.#steerFoldEnabled()) continue;
|
|
1183
|
+
await requestFold("steer", adapter);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
observe().catch((error: unknown) => {
|
|
1188
|
+
logger.warn("Steer-triggered fold failed", {
|
|
1189
|
+
jobId: adapter.jobId,
|
|
1190
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
return () => watch.abort();
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Race a managed job against the auto-background threshold, an explicit fold
|
|
1198
|
+
* (chord / SDK control / steer), and abort. A steer that arrives once the
|
|
1199
|
+
* command has run for {@link STEER_FOLD_GRACE_MS} requests a `steer` fold,
|
|
1200
|
+
* which settles this wait through the same `backgroundRequest` path as the
|
|
1201
|
+
* chord; a steer inside the grace window is ignored here and consumed at the
|
|
1202
|
+
* ordinary tool boundary once the command finishes.
|
|
1203
|
+
*/
|
|
1118
1204
|
async #waitForManagedBashJob(
|
|
1119
1205
|
job: ManagedBashJobHandle,
|
|
1120
1206
|
thresholdMs: number,
|
|
1121
1207
|
signal?: AbortSignal,
|
|
1122
|
-
backgroundRequest?: Promise<
|
|
1123
|
-
|
|
1208
|
+
backgroundRequest?: Promise<FoldReason>,
|
|
1209
|
+
foldAdapter?: FoldAdapter,
|
|
1210
|
+
): Promise<ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" }> {
|
|
1124
1211
|
if (signal?.aborted) {
|
|
1125
1212
|
return { kind: "aborted" };
|
|
1126
1213
|
}
|
|
1127
1214
|
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1215
|
+
const startedAt = Date.now();
|
|
1216
|
+
const threshold = Promise.withResolvers<{ kind: "running"; reason: FoldReason }>();
|
|
1217
|
+
const thresholdTimer = setTimeout(
|
|
1218
|
+
() => {
|
|
1219
|
+
const requestFold = this.session.requestForegroundBashBackground;
|
|
1220
|
+
if (!foldAdapter || !requestFold) {
|
|
1221
|
+
threshold.resolve({ kind: "running", reason: "timer" });
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
void requestFold("timer", foldAdapter)
|
|
1225
|
+
.then(folded => {
|
|
1226
|
+
if (!folded) threshold.resolve({ kind: "running", reason: "timer" });
|
|
1227
|
+
})
|
|
1228
|
+
.catch(error => {
|
|
1229
|
+
logger.warn("Timer-triggered fold failed", {
|
|
1230
|
+
jobId: foldAdapter.jobId,
|
|
1231
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1232
|
+
});
|
|
1233
|
+
threshold.resolve({ kind: "running", reason: "timer" });
|
|
1234
|
+
});
|
|
1235
|
+
},
|
|
1236
|
+
Math.max(0, thresholdMs),
|
|
1237
|
+
);
|
|
1238
|
+
const waiters: Array<
|
|
1239
|
+
Promise<ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" }>
|
|
1240
|
+
> = [job.completion, threshold.promise];
|
|
1134
1241
|
if (backgroundRequest) {
|
|
1135
|
-
waiters.push(backgroundRequest.then(
|
|
1242
|
+
waiters.push(backgroundRequest.then(reason => ({ kind: "running" as const, reason })));
|
|
1136
1243
|
}
|
|
1137
1244
|
|
|
1138
1245
|
let onAbort: (() => void) | undefined;
|
|
@@ -1142,9 +1249,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1142
1249
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1143
1250
|
waiters.push(aborted.promise);
|
|
1144
1251
|
}
|
|
1252
|
+
|
|
1253
|
+
const stopSteerWatch =
|
|
1254
|
+
backgroundRequest && foldAdapter ? this.#watchSteerForFold(foldAdapter, startedAt) : () => {};
|
|
1145
1255
|
try {
|
|
1146
1256
|
return await Promise.race(waiters);
|
|
1147
1257
|
} finally {
|
|
1258
|
+
stopSteerWatch();
|
|
1148
1259
|
clearTimeout(thresholdTimer);
|
|
1149
1260
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
1150
1261
|
}
|
|
@@ -1657,7 +1768,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1657
1768
|
});
|
|
1658
1769
|
const jobGeneration = asyncManager.getJob(job.jobId)?.generation ?? job.jobId;
|
|
1659
1770
|
job.setBackgrounded(true);
|
|
1660
|
-
asyncManager.
|
|
1771
|
+
asyncManager.markStartedInBackground(job.jobId, jobGeneration);
|
|
1661
1772
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
1662
1773
|
requestedTimeoutSec,
|
|
1663
1774
|
notices: pendingNotices,
|
|
@@ -1711,13 +1822,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1711
1822
|
const jobGeneration = ownedManager.getJob(job.jobId)?.generation ?? job.jobId;
|
|
1712
1823
|
if (startBackgrounded) {
|
|
1713
1824
|
job.setBackgrounded(true);
|
|
1714
|
-
ownedManager.
|
|
1825
|
+
ownedManager.markStartedInBackground(job.jobId, jobGeneration);
|
|
1715
1826
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
1716
1827
|
requestedTimeoutSec,
|
|
1717
1828
|
notices: pendingNotices,
|
|
1718
1829
|
});
|
|
1719
1830
|
}
|
|
1720
|
-
const backgroundRequest = Promise.withResolvers<
|
|
1831
|
+
const backgroundRequest = Promise.withResolvers<FoldReason>();
|
|
1721
1832
|
// Exactly one party may settle the foreground caller. detachObserver (the
|
|
1722
1833
|
// fold won) and resolveForegroundObserver (a completion was handed back
|
|
1723
1834
|
// after a failed fold) share this flag, so a race cannot double-settle.
|
|
@@ -1727,7 +1838,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1727
1838
|
foregroundSettled = true;
|
|
1728
1839
|
return "resolved";
|
|
1729
1840
|
};
|
|
1730
|
-
const
|
|
1841
|
+
const managedFoldAdapter: FoldAdapter = {
|
|
1731
1842
|
kind: "bash-managed",
|
|
1732
1843
|
jobId: job.jobId,
|
|
1733
1844
|
jobGeneration,
|
|
@@ -1746,24 +1857,26 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1746
1857
|
const current = ownedManager.getJob(job.jobId);
|
|
1747
1858
|
return current?.generation === jobGeneration ? current : undefined;
|
|
1748
1859
|
},
|
|
1749
|
-
detachObserver:
|
|
1860
|
+
detachObserver: receipt => {
|
|
1750
1861
|
const outcome = settleForeground();
|
|
1751
1862
|
if (outcome === "resolved") {
|
|
1752
1863
|
job.setBackgrounded(true);
|
|
1753
|
-
ownedManager.markBackgrounded(job.jobId, jobGeneration);
|
|
1754
|
-
backgroundRequest.resolve();
|
|
1864
|
+
ownedManager.markBackgrounded(job.jobId, jobGeneration, receipt.reason);
|
|
1865
|
+
backgroundRequest.resolve(receipt.reason);
|
|
1755
1866
|
}
|
|
1756
1867
|
return outcome;
|
|
1757
1868
|
},
|
|
1758
1869
|
resolveForegroundObserver: () => settleForeground(),
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1870
|
+
};
|
|
1871
|
+
const unregisterBackgroundRequest = this.session.registerForegroundFoldParticipant?.(managedFoldAdapter);
|
|
1872
|
+
let waitResult: ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" };
|
|
1761
1873
|
try {
|
|
1762
1874
|
waitResult = await this.#waitForManagedBashJob(
|
|
1763
1875
|
job,
|
|
1764
1876
|
autoBackgroundWaitMs,
|
|
1765
1877
|
signal,
|
|
1766
1878
|
backgroundRequest.promise,
|
|
1879
|
+
managedFoldAdapter,
|
|
1767
1880
|
);
|
|
1768
1881
|
} finally {
|
|
1769
1882
|
unregisterBackgroundRequest?.();
|
|
@@ -1791,10 +1904,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1791
1904
|
throw new ToolAbortError(formatManagedAbortFailure(undefined, undefined, job.getLatestText()));
|
|
1792
1905
|
}
|
|
1793
1906
|
job.setBackgrounded(true);
|
|
1794
|
-
ownedManager.markBackgrounded(job.jobId, jobGeneration);
|
|
1907
|
+
ownedManager.markBackgrounded(job.jobId, jobGeneration, waitResult.reason);
|
|
1795
1908
|
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
1796
1909
|
requestedTimeoutSec,
|
|
1797
1910
|
notices: pendingNotices,
|
|
1911
|
+
foldReason: waitResult.reason,
|
|
1798
1912
|
});
|
|
1799
1913
|
}
|
|
1800
1914
|
|
|
@@ -2323,9 +2437,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2323
2437
|
void bridgeHandle.completion.finally(unregisterOwnerCleanup);
|
|
2324
2438
|
registerOwnedIfLineaged(bridgeManager, toolCallId, bridgeJobId, bridgeEndpointId);
|
|
2325
2439
|
|
|
2326
|
-
const bridgeFoldRequest = Promise.withResolvers<
|
|
2440
|
+
const bridgeFoldRequest = Promise.withResolvers<FoldReason>();
|
|
2327
2441
|
const bridgeOriginatingTurn = ctx?.attemptScope !== undefined;
|
|
2328
|
-
const
|
|
2442
|
+
const bridgeFoldAdapter: FoldAdapter = {
|
|
2329
2443
|
kind: "client-terminal",
|
|
2330
2444
|
jobId: bridgeJobId,
|
|
2331
2445
|
jobGeneration: bridgeGeneration,
|
|
@@ -2342,25 +2456,27 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2342
2456
|
const current = bridgeManager.getJob(bridgeJobId);
|
|
2343
2457
|
return current?.generation === bridgeGeneration ? current : undefined;
|
|
2344
2458
|
},
|
|
2345
|
-
detachObserver:
|
|
2459
|
+
detachObserver: receipt => {
|
|
2346
2460
|
const outcome = settleBridgeForeground();
|
|
2347
2461
|
if (outcome === "resolved") {
|
|
2348
2462
|
bridgeHandle.setBackgrounded(true);
|
|
2349
|
-
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration);
|
|
2350
|
-
bridgeFoldRequest.resolve();
|
|
2463
|
+
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration, receipt.reason);
|
|
2464
|
+
bridgeFoldRequest.resolve(receipt.reason);
|
|
2351
2465
|
}
|
|
2352
2466
|
return outcome;
|
|
2353
2467
|
},
|
|
2354
2468
|
resolveForegroundObserver: () => settleBridgeForeground(),
|
|
2355
|
-
}
|
|
2469
|
+
};
|
|
2470
|
+
const unregisterBridgeFold = this.session.registerForegroundFoldParticipant?.(bridgeFoldAdapter);
|
|
2356
2471
|
|
|
2357
|
-
let bridgeWait: ManagedBashJobCompletion | { kind: "running" } | { kind: "aborted" };
|
|
2472
|
+
let bridgeWait: ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" };
|
|
2358
2473
|
try {
|
|
2359
2474
|
bridgeWait = await this.#waitForManagedBashJob(
|
|
2360
2475
|
bridgeHandle,
|
|
2361
2476
|
this.#autoBackgroundEnabled ? this.#resolveAutoBackgroundWaitMs(timeoutMs) : timeoutMs + 1_000,
|
|
2362
2477
|
signal,
|
|
2363
2478
|
bridgeFoldRequest.promise,
|
|
2479
|
+
bridgeFoldAdapter,
|
|
2364
2480
|
);
|
|
2365
2481
|
} finally {
|
|
2366
2482
|
unregisterBridgeFold?.();
|
|
@@ -2395,11 +2511,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2395
2511
|
// Folded (or auto-backgrounded): the remote terminal keeps running and its
|
|
2396
2512
|
// result arrives as a background completion.
|
|
2397
2513
|
bridgeHandle.setBackgrounded(true);
|
|
2398
|
-
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration);
|
|
2514
|
+
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration, bridgeWait.reason);
|
|
2399
2515
|
return this.#buildBackgroundStartResult(bridgeJobId, bridgeLabel, bridgeHandle.getLatestText(), timeoutSec, {
|
|
2400
2516
|
requestedTimeoutSec,
|
|
2401
2517
|
notices: pendingNotices,
|
|
2402
2518
|
terminalId: handle.terminalId,
|
|
2519
|
+
foldReason: bridgeWait.reason,
|
|
2403
2520
|
});
|
|
2404
2521
|
}
|
|
2405
2522
|
|
|
@@ -2521,7 +2638,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2521
2638
|
},
|
|
2522
2639
|
);
|
|
2523
2640
|
void controls.terminalCompletion.finally(unregisterPtyOwnerCleanup);
|
|
2524
|
-
|
|
2641
|
+
const ptyStartedAt = Date.now();
|
|
2642
|
+
const ptyFoldAdapter: FoldAdapter = {
|
|
2525
2643
|
kind: "bash-pty",
|
|
2526
2644
|
jobId: ptyJobId,
|
|
2527
2645
|
jobGeneration: ptyGeneration,
|
|
@@ -2538,12 +2656,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2538
2656
|
const current = ptyManager.getJob(ptyJobId);
|
|
2539
2657
|
return current?.generation === ptyGeneration ? current : undefined;
|
|
2540
2658
|
},
|
|
2541
|
-
detachObserver:
|
|
2659
|
+
detachObserver: receipt => {
|
|
2542
2660
|
// Output-only continuation: stdin forwarding ends here and the
|
|
2543
2661
|
// process is never killed or restarted by folding.
|
|
2544
2662
|
const started = this.#buildBackgroundStartResult(ptyJobId, ptyLabel, "", timeoutSec, {
|
|
2545
2663
|
requestedTimeoutSec,
|
|
2546
2664
|
notices: pendingNotices,
|
|
2665
|
+
foldReason: receipt.reason,
|
|
2547
2666
|
});
|
|
2548
2667
|
const outcome = controls.detachObserver(
|
|
2549
2668
|
interactiveResultFromText(this.#extractTextResult(started)),
|
|
@@ -2551,13 +2670,19 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2551
2670
|
if (outcome === "resolved") {
|
|
2552
2671
|
controls.detachForegroundCancellation();
|
|
2553
2672
|
ptyBackgrounded = true;
|
|
2554
|
-
ptyManager.markBackgrounded(ptyJobId, ptyGeneration);
|
|
2673
|
+
ptyManager.markBackgrounded(ptyJobId, ptyGeneration, receipt.reason);
|
|
2555
2674
|
ptyFoldResult = started;
|
|
2556
2675
|
}
|
|
2557
2676
|
return outcome;
|
|
2558
2677
|
},
|
|
2559
2678
|
resolveForegroundObserver: () => "already-settled",
|
|
2560
|
-
}
|
|
2679
|
+
};
|
|
2680
|
+
const unregisterPtyFold = this.session.registerForegroundFoldParticipant?.(ptyFoldAdapter);
|
|
2681
|
+
const stopPtySteerWatch = this.#watchSteerForFold(ptyFoldAdapter, ptyStartedAt);
|
|
2682
|
+
ptyFoldUnregister = () => {
|
|
2683
|
+
stopPtySteerWatch();
|
|
2684
|
+
unregisterPtyFold?.();
|
|
2685
|
+
};
|
|
2561
2686
|
},
|
|
2562
2687
|
onFoldKey: () => {
|
|
2563
2688
|
const now = Date.now();
|
|
@@ -293,6 +293,43 @@ function resolveSystemChromium(): string | undefined {
|
|
|
293
293
|
return found;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
export function resolveSystemChromiumForTest(): string | undefined {
|
|
297
|
+
return resolveSystemChromium();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function resetSystemChromiumForTest(): void {
|
|
301
|
+
resolvedChromium = undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function headlessExecutableForProfileReuse(): string | undefined {
|
|
305
|
+
return resolveSystemChromium() ?? trustedBrowserEnv("PUPPETEER_EXECUTABLE_PATH");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* On macOS the Cookies DB is encrypted with a Keychain key whose service name is
|
|
310
|
+
* brand-specific ("Chrome Safe Storage" vs "Chromium Safe Storage"). A Chrome profile
|
|
311
|
+
* copied into a bundled Chromium therefore decrypts to garbage with no warning.
|
|
312
|
+
* Return true only when the headless executable can actually decrypt the discovered profile.
|
|
313
|
+
*/
|
|
314
|
+
export function canDecryptProfileCookies(
|
|
315
|
+
executablePath: string | undefined,
|
|
316
|
+
discoveredUserDataDir: string | undefined,
|
|
317
|
+
platform: NodeJS.Platform = process.platform,
|
|
318
|
+
): boolean {
|
|
319
|
+
if (platform !== "darwin") return true;
|
|
320
|
+
if (!discoveredUserDataDir) return true;
|
|
321
|
+
if (!executablePath) return false;
|
|
322
|
+
const lowerExe = executablePath.toLowerCase();
|
|
323
|
+
const lowerDir = discoveredUserDataDir.toLowerCase();
|
|
324
|
+
const isChromiumExe = lowerExe.includes("chromium");
|
|
325
|
+
const isChromeExe = lowerExe.includes("google chrome") || lowerExe.includes("/google/chrome");
|
|
326
|
+
const isChromiumProfile = lowerDir.includes("chromium");
|
|
327
|
+
const isChromeProfile = lowerDir.includes("google") && lowerDir.includes("chrome");
|
|
328
|
+
if (isChromeProfile && isChromiumExe && !isChromeExe) return false;
|
|
329
|
+
if (isChromiumProfile && isChromeExe) return false;
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
296
333
|
/** Edge is Chromium-based but keeps its own profile format under its own user data root. */
|
|
297
334
|
const EDGE_EXECUTABLE_PATTERN =
|
|
298
335
|
/(?:^|[/\\])(?:msedge(?:\.exe)?|microsoft-edge(?:-(?:stable|beta|dev|canary))?|com\.microsoft\.Edge|Microsoft Edge(?: Beta| Dev| Canary)?)$/i;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { logger } from "@gajae-code/utils";
|
|
4
5
|
import type { Subprocess } from "bun";
|
|
@@ -12,7 +13,14 @@ import {
|
|
|
12
13
|
killExistingByPath,
|
|
13
14
|
waitForCdp,
|
|
14
15
|
} from "./attach";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
BROWSER_PROTOCOL_TIMEOUT_MS,
|
|
18
|
+
canDecryptProfileCookies,
|
|
19
|
+
headlessExecutableForProfileReuse,
|
|
20
|
+
launchHeadlessBrowser,
|
|
21
|
+
loadPuppeteer,
|
|
22
|
+
type UserAgentOverride,
|
|
23
|
+
} from "./launch";
|
|
16
24
|
import { defaultDiscoveryEnv } from "./profile-discovery";
|
|
17
25
|
import type { ProfileReusePosture } from "./profile-posture";
|
|
18
26
|
import { resolveProfileReuse } from "./profile-reuse";
|
|
@@ -55,6 +63,17 @@ type SpawnedChromeProfileKind = Extract<BrowserKind, { kind: "chrome-profile" }>
|
|
|
55
63
|
const browsers = new Map<string, BrowserHandle>();
|
|
56
64
|
const browserOpenPromises = new Map<string, Promise<BrowserHandle>>();
|
|
57
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Chrome profile warm-up dirs this module created, keyed by the handle that owns one.
|
|
68
|
+
*
|
|
69
|
+
* Ownership is a registry-internal fact, not a property of the handle: `BrowserHandle`
|
|
70
|
+
* is exported and mutable, so a field would let any caller point recursive deletion at
|
|
71
|
+
* an arbitrary path. Recording the dir here — only on the launch path that created it —
|
|
72
|
+
* means disposal can delete exactly what this module made and nothing else. A path
|
|
73
|
+
* prefix check would not prove ownership; an entry in this map does.
|
|
74
|
+
*/
|
|
75
|
+
const ownedWarmupDirs = new WeakMap<BrowserHandle, string>();
|
|
76
|
+
|
|
58
77
|
/**
|
|
59
78
|
* Upper bound on the CDP `browser.close()` round-trip during a forced (signal-path)
|
|
60
79
|
* teardown before we fall back to killing the Chrome process tree. Only applies when
|
|
@@ -110,6 +129,19 @@ export function browserKeyForTest(
|
|
|
110
129
|
return browserKey(kind, geo, opts.profileReuse);
|
|
111
130
|
}
|
|
112
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Create and register a warm-up dir as registry-owned for `handle`.
|
|
134
|
+
*
|
|
135
|
+
* Exists so tests can exercise disposal without spawning Chrome. The caller cannot
|
|
136
|
+
* supply the deletion target: this module creates the directory itself before recording
|
|
137
|
+
* ownership, preserving the same invariant as the production launch path.
|
|
138
|
+
*/
|
|
139
|
+
export function createOwnedWarmupDirForTest(handle: BrowserHandle): string {
|
|
140
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gjc-profile-warmup-"));
|
|
141
|
+
ownedWarmupDirs.set(handle, dir);
|
|
142
|
+
return dir;
|
|
143
|
+
}
|
|
144
|
+
|
|
113
145
|
export async function acquireBrowser(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {
|
|
114
146
|
const geo = kind.kind === "headless" ? normalizeGeo(opts.geo) : undefined;
|
|
115
147
|
const key = browserKey(kind, geo, opts.profileReuse);
|
|
@@ -148,17 +180,38 @@ async function openBrowserHandle(
|
|
|
148
180
|
discoveryEnv: defaultDiscoveryEnv(fs.existsSync),
|
|
149
181
|
});
|
|
150
182
|
if (reuse.warning) logger.warn(reuse.warning);
|
|
151
|
-
if (reuse.mode === "real" && reuse.warmupDir) {
|
|
183
|
+
if (reuse.mode === "real" && reuse.warmupDir && reuse.discovered) {
|
|
184
|
+
const executablePath = headlessExecutableForProfileReuse();
|
|
185
|
+
if (!canDecryptProfileCookies(executablePath, reuse.discovered.userDataDir)) {
|
|
186
|
+
try {
|
|
187
|
+
fs.rmSync(reuse.warmupDir, { recursive: true, force: true });
|
|
188
|
+
} catch {}
|
|
189
|
+
const hint = executablePath
|
|
190
|
+
? `Real Chrome profile at ${reuse.discovered.userDataDir} cannot be decrypted by ${executablePath} on macOS (Chrome Safe Storage vs Chromium Safe Storage Keychain mismatch). Using synthetic browser state instead. Set PUPPETEER_EXECUTABLE_PATH to the matching Chrome/Chromium binary.`
|
|
191
|
+
: `Real Chrome profile at ${reuse.discovered.userDataDir} found but no compatible browser executable is available on macOS (bundled Chromium cannot decrypt Chrome's cookies — Chrome Safe Storage vs Chromium Safe Storage). Using synthetic browser state instead. Set PUPPETEER_EXECUTABLE_PATH to /Applications/Google Chrome.app/Contents/MacOS/Google Chrome or install Chrome.`;
|
|
192
|
+
logger.warn(hint);
|
|
193
|
+
} else {
|
|
194
|
+
profileWarmupDir = reuse.warmupDir;
|
|
195
|
+
}
|
|
196
|
+
} else if (reuse.mode === "real" && reuse.warmupDir) {
|
|
152
197
|
profileWarmupDir = reuse.warmupDir;
|
|
153
198
|
}
|
|
154
199
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
200
|
+
let browser: Browser;
|
|
201
|
+
try {
|
|
202
|
+
browser = await launchHeadlessBrowser({
|
|
203
|
+
headless: kind.headless,
|
|
204
|
+
viewport: opts.viewport,
|
|
205
|
+
profileWarmupDir,
|
|
206
|
+
...(geo ? { geo } : {}),
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// Launch failed after the warm-up copy landed: drop the temp dir here,
|
|
210
|
+
// because no handle will exist to carry it into releaseBrowser().
|
|
211
|
+
removeWarmupDir(profileWarmupDir);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
const handle: BrowserHandle = {
|
|
162
215
|
key,
|
|
163
216
|
|
|
164
217
|
kind,
|
|
@@ -168,6 +221,11 @@ async function openBrowserHandle(
|
|
|
168
221
|
refCount: 0,
|
|
169
222
|
stealth: { browserSession: null, override: null },
|
|
170
223
|
};
|
|
224
|
+
// Record ownership only here, where this module created the dir. Disposal
|
|
225
|
+
// deletes solely what this map holds, so a caller-supplied path is never
|
|
226
|
+
// a deletion target.
|
|
227
|
+
if (profileWarmupDir) ownedWarmupDirs.set(handle, profileWarmupDir);
|
|
228
|
+
return handle;
|
|
171
229
|
}
|
|
172
230
|
if (kind.kind === "connected") {
|
|
173
231
|
const cdpUrl = kind.cdpUrl.replace(/\/+$/, "");
|
|
@@ -418,6 +476,35 @@ export async function releaseBrowser(handle: BrowserHandle, opts: { kill: boolea
|
|
|
418
476
|
}
|
|
419
477
|
}
|
|
420
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Delete the warm-up dir this module created for `handle`, if any.
|
|
481
|
+
*
|
|
482
|
+
* The path comes from `ownedWarmupDirs`, never from the caller, so recursive deletion
|
|
483
|
+
* can only ever target a dir this registry made. The entry is consumed first, making a
|
|
484
|
+
* second call a no-op even if disposal runs twice.
|
|
485
|
+
*
|
|
486
|
+
* Best-effort: a leftover temp dir is preferable to a teardown that throws.
|
|
487
|
+
*/
|
|
488
|
+
function removeOwnedWarmupDir(handle: BrowserHandle): void {
|
|
489
|
+
const dir = ownedWarmupDirs.get(handle);
|
|
490
|
+
if (dir === undefined) return;
|
|
491
|
+
ownedWarmupDirs.delete(handle);
|
|
492
|
+
removeWarmupDir(dir);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Delete a dir this module created. Callers must have proven ownership first. */
|
|
496
|
+
function removeWarmupDir(dir: string | undefined): void {
|
|
497
|
+
if (!dir) return;
|
|
498
|
+
try {
|
|
499
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
500
|
+
} catch (err) {
|
|
501
|
+
logger.debug("Failed to remove Chrome profile warm-up dir", {
|
|
502
|
+
dir,
|
|
503
|
+
error: (err as Error).message,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
421
508
|
async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean }): Promise<void> {
|
|
422
509
|
if (handle.kind.kind === "headless") {
|
|
423
510
|
// Capture the launched Chrome process before close() so a forced (signal-path)
|
|
@@ -436,6 +523,9 @@ async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean
|
|
|
436
523
|
}
|
|
437
524
|
}
|
|
438
525
|
if (opts.kill && proc?.pid !== undefined) await gracefulKillTreeOnce(proc.pid);
|
|
526
|
+
// Chrome owns files under the warm-up dir until the process is gone, so this
|
|
527
|
+
// runs after close()/kill rather than alongside them.
|
|
528
|
+
removeOwnedWarmupDir(handle);
|
|
439
529
|
return;
|
|
440
530
|
}
|
|
441
531
|
if (handle.kind.kind === "connected") {
|
package/src/tools/cron.ts
CHANGED
|
@@ -453,13 +453,15 @@ function deliverCronFire(record: CronScheduleRecord): Promise<void> | undefined
|
|
|
453
453
|
cron_expression: record.snapshot.cron_expression,
|
|
454
454
|
recurring: record.snapshot.recurring,
|
|
455
455
|
};
|
|
456
|
-
|
|
456
|
+
if (!record.session.sendCustomMessage) {
|
|
457
|
+
// The custom-message router is the only delivery path; treating its absence
|
|
458
|
+
// as a successful fire would silently retire a one-shot schedule.
|
|
459
|
+
return Promise.reject(new Error("Cron delivery requires a session custom-message router."));
|
|
460
|
+
}
|
|
461
|
+
return record.session.sendCustomMessage(
|
|
457
462
|
{ customType: "cron-fire", content, display: false, attribution: "agent", details },
|
|
458
463
|
{ triggerTurn: true, deliverAs: "followUp" },
|
|
459
464
|
);
|
|
460
|
-
if (sendPromise) return sendPromise;
|
|
461
|
-
record.session.steer?.({ customType: "cron-fire", content, details });
|
|
462
|
-
return undefined;
|
|
463
465
|
}
|
|
464
466
|
|
|
465
467
|
function scheduleRecord(ownerId: string | undefined, state: OwnerScheduleState, record: CronScheduleRecord): void {
|
|
@@ -39,6 +39,7 @@ function makeSession(overrides: Record<string, unknown> = {}): any {
|
|
|
39
39
|
"calc.enabled": true,
|
|
40
40
|
"skill.enabled": true,
|
|
41
41
|
"browser.enabled": true,
|
|
42
|
+
"browser.backend": "native",
|
|
42
43
|
"computer.enabled": true,
|
|
43
44
|
"checkpoint.enabled": true,
|
|
44
45
|
"irc.enabled": true,
|
|
@@ -371,6 +372,20 @@ describe("tool descriptor compatibility gate", () => {
|
|
|
371
372
|
}
|
|
372
373
|
});
|
|
373
374
|
|
|
375
|
+
test("hides the built-in browser tool when Aside is the selected backend", () => {
|
|
376
|
+
const context = availabilityContext();
|
|
377
|
+
|
|
378
|
+
expect(BUILTIN_TOOL_DESCRIPTORS.browser.isAvailable(makeSession({ "browser.backend": "aside" }), context)).toBe(
|
|
379
|
+
false,
|
|
380
|
+
);
|
|
381
|
+
expect(BUILTIN_TOOL_DESCRIPTORS.browser.isAvailable(makeSession({ "browser.backend": "native" }), context)).toBe(
|
|
382
|
+
true,
|
|
383
|
+
);
|
|
384
|
+
expect(BUILTIN_TOOL_DESCRIPTORS.browser.isAvailable(makeSession({ "browser.backend": undefined }), context)).toBe(
|
|
385
|
+
true,
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
374
389
|
test("descriptor creation is side-effect free; materialization registers cleanup exactly once", () => {
|
|
375
390
|
const cleanupCalls: Array<() => void> = [];
|
|
376
391
|
let constructed = 0;
|
package/src/tools/descriptors.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "@gajae-code/agent-core";
|
|
2
2
|
import type { RawArgumentValidationResult, TSchema } from "@gajae-code/ai/types";
|
|
3
3
|
import { $which } from "@gajae-code/utils";
|
|
4
|
+
import { resolveBrowserBackend } from "../browser-backend";
|
|
4
5
|
import type { ToolFactory, ToolSession } from ".";
|
|
5
6
|
import { selectAskParameters } from "./ask-contract";
|
|
6
7
|
import { isComputerCallable, isComputerLoadablePlatform } from "./computer-policy";
|
|
@@ -260,7 +261,10 @@ function availableFor(name: string, session: ToolSession, context = defaultAvail
|
|
|
260
261
|
if (name === "search_tool_bm25") return context.discoveryActive ?? resolveDiscoveryActive(session);
|
|
261
262
|
if (name === "calc") return Boolean(session.settings.get("calc.enabled"));
|
|
262
263
|
if (name === "skill" || name === "skill_discovery") return Boolean(session.settings.get("skill.enabled"));
|
|
263
|
-
if (name === "browser")
|
|
264
|
+
if (name === "browser")
|
|
265
|
+
return (
|
|
266
|
+
Boolean(session.settings.get("browser.enabled")) && resolveBrowserBackend(session.settings).exposesBuiltinTool
|
|
267
|
+
);
|
|
264
268
|
if (name === "computer") return isComputerCallable(session);
|
|
265
269
|
if (name === "checkpoint" || name === "rewind")
|
|
266
270
|
return Boolean(session.settings.get("checkpoint.enabled")) && (session.taskDepth ?? 0) === 0;
|