@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/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,107 @@ 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 own steer admission: `busyPromptMode=queue` never admits a
|
|
1143
|
+
* steer into the busy run, and `toolInterruptPolicy=finish_tools` explicitly
|
|
1144
|
+
* lets the tool finish, so neither may fold. The
|
|
1145
|
+
* auto-background setting is deliberately not consulted: like the chord,
|
|
1146
|
+
* a steer fold is a user action.
|
|
1147
|
+
*
|
|
1148
|
+
* Fails closed: a session that cannot prove its tool interrupt policy or
|
|
1149
|
+
* report newly admitted steering is not steer-foldable.
|
|
1150
|
+
*/
|
|
1151
|
+
#steerFoldEnabled(): boolean {
|
|
1152
|
+
const { waitForUserSteering, getToolInterruptPolicy, requestForegroundBashBackground } = this.session;
|
|
1153
|
+
if (!waitForUserSteering || !getToolInterruptPolicy || !requestForegroundBashBackground) return false;
|
|
1154
|
+
if (this.session.settings.get("busyPromptMode") !== "steer") return false;
|
|
1155
|
+
return getToolInterruptPolicy() === "abort_tools";
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Fold `adapter` on the first user steer that ARRIVES after the wait has run
|
|
1160
|
+
* for {@link STEER_FOLD_GRACE_MS} since `startedAt`. A steer already queued
|
|
1161
|
+
* when the wait starts, or one that arrives inside the grace window, never
|
|
1162
|
+
* folds: the command finishes normally and that steer is consumed at the
|
|
1163
|
+
* ordinary tool boundary. The watcher keeps observing so a later qualifying
|
|
1164
|
+
* steer still folds, and re-checks the gates at that moment so a
|
|
1165
|
+
* `busyPromptMode`/`toolInterruptPolicy` change during the command is honored.
|
|
1166
|
+
* Returns a stop function; call it when the wait settles. No-op when steer
|
|
1167
|
+
* folding is gated off at start.
|
|
1168
|
+
*/
|
|
1169
|
+
#watchSteerForFold(adapter: FoldAdapter, startedAt: number): () => void {
|
|
1170
|
+
if (!this.#steerFoldEnabled()) return () => {};
|
|
1171
|
+
const waitForSteer = this.session.waitForUserSteering;
|
|
1172
|
+
const requestFold = this.session.requestForegroundBashBackground;
|
|
1173
|
+
if (!waitForSteer || !requestFold) return () => {};
|
|
1174
|
+
const watch = new AbortController();
|
|
1175
|
+
const observe = async (): Promise<void> => {
|
|
1176
|
+
while (!watch.signal.aborted) {
|
|
1177
|
+
await waitForSteer(watch.signal);
|
|
1178
|
+
if (watch.signal.aborted) return;
|
|
1179
|
+
if (Date.now() - startedAt < STEER_FOLD_GRACE_MS) continue;
|
|
1180
|
+
if (!this.#steerFoldEnabled()) continue;
|
|
1181
|
+
await requestFold("steer", adapter);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
observe().catch((error: unknown) => {
|
|
1186
|
+
logger.warn("Steer-triggered fold failed", {
|
|
1187
|
+
jobId: adapter.jobId,
|
|
1188
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
return () => watch.abort();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Race a managed job against the auto-background threshold, an explicit fold
|
|
1196
|
+
* (chord / SDK control / steer), and abort. A steer that arrives once the
|
|
1197
|
+
* command has run for {@link STEER_FOLD_GRACE_MS} requests a `steer` fold,
|
|
1198
|
+
* which settles this wait through the same `backgroundRequest` path as the
|
|
1199
|
+
* chord; a steer inside the grace window is ignored here and consumed at the
|
|
1200
|
+
* ordinary tool boundary once the command finishes.
|
|
1201
|
+
*/
|
|
1118
1202
|
async #waitForManagedBashJob(
|
|
1119
1203
|
job: ManagedBashJobHandle,
|
|
1120
1204
|
thresholdMs: number,
|
|
1121
1205
|
signal?: AbortSignal,
|
|
1122
|
-
backgroundRequest?: Promise<
|
|
1123
|
-
|
|
1206
|
+
backgroundRequest?: Promise<FoldReason>,
|
|
1207
|
+
foldAdapter?: FoldAdapter,
|
|
1208
|
+
): Promise<ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" }> {
|
|
1124
1209
|
if (signal?.aborted) {
|
|
1125
1210
|
return { kind: "aborted" };
|
|
1126
1211
|
}
|
|
1127
1212
|
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1213
|
+
const startedAt = Date.now();
|
|
1214
|
+
const threshold = Promise.withResolvers<{ kind: "running"; reason: FoldReason }>();
|
|
1215
|
+
const thresholdTimer = setTimeout(
|
|
1216
|
+
() => {
|
|
1217
|
+
const requestFold = this.session.requestForegroundBashBackground;
|
|
1218
|
+
if (!foldAdapter || !requestFold) {
|
|
1219
|
+
threshold.resolve({ kind: "running", reason: "timer" });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
void requestFold("timer", foldAdapter)
|
|
1223
|
+
.then(folded => {
|
|
1224
|
+
if (!folded) threshold.resolve({ kind: "running", reason: "timer" });
|
|
1225
|
+
})
|
|
1226
|
+
.catch(error => {
|
|
1227
|
+
logger.warn("Timer-triggered fold failed", {
|
|
1228
|
+
jobId: foldAdapter.jobId,
|
|
1229
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1230
|
+
});
|
|
1231
|
+
threshold.resolve({ kind: "running", reason: "timer" });
|
|
1232
|
+
});
|
|
1233
|
+
},
|
|
1234
|
+
Math.max(0, thresholdMs),
|
|
1235
|
+
);
|
|
1236
|
+
const waiters: Array<
|
|
1237
|
+
Promise<ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" }>
|
|
1238
|
+
> = [job.completion, threshold.promise];
|
|
1134
1239
|
if (backgroundRequest) {
|
|
1135
|
-
waiters.push(backgroundRequest.then(
|
|
1240
|
+
waiters.push(backgroundRequest.then(reason => ({ kind: "running" as const, reason })));
|
|
1136
1241
|
}
|
|
1137
1242
|
|
|
1138
1243
|
let onAbort: (() => void) | undefined;
|
|
@@ -1142,9 +1247,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1142
1247
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1143
1248
|
waiters.push(aborted.promise);
|
|
1144
1249
|
}
|
|
1250
|
+
|
|
1251
|
+
const stopSteerWatch =
|
|
1252
|
+
backgroundRequest && foldAdapter ? this.#watchSteerForFold(foldAdapter, startedAt) : () => {};
|
|
1145
1253
|
try {
|
|
1146
1254
|
return await Promise.race(waiters);
|
|
1147
1255
|
} finally {
|
|
1256
|
+
stopSteerWatch();
|
|
1148
1257
|
clearTimeout(thresholdTimer);
|
|
1149
1258
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
1150
1259
|
}
|
|
@@ -1657,7 +1766,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1657
1766
|
});
|
|
1658
1767
|
const jobGeneration = asyncManager.getJob(job.jobId)?.generation ?? job.jobId;
|
|
1659
1768
|
job.setBackgrounded(true);
|
|
1660
|
-
asyncManager.
|
|
1769
|
+
asyncManager.markStartedInBackground(job.jobId, jobGeneration);
|
|
1661
1770
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
1662
1771
|
requestedTimeoutSec,
|
|
1663
1772
|
notices: pendingNotices,
|
|
@@ -1711,13 +1820,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1711
1820
|
const jobGeneration = ownedManager.getJob(job.jobId)?.generation ?? job.jobId;
|
|
1712
1821
|
if (startBackgrounded) {
|
|
1713
1822
|
job.setBackgrounded(true);
|
|
1714
|
-
ownedManager.
|
|
1823
|
+
ownedManager.markStartedInBackground(job.jobId, jobGeneration);
|
|
1715
1824
|
return this.#buildBackgroundStartResult(job.jobId, job.label, "", timeoutSec, {
|
|
1716
1825
|
requestedTimeoutSec,
|
|
1717
1826
|
notices: pendingNotices,
|
|
1718
1827
|
});
|
|
1719
1828
|
}
|
|
1720
|
-
const backgroundRequest = Promise.withResolvers<
|
|
1829
|
+
const backgroundRequest = Promise.withResolvers<FoldReason>();
|
|
1721
1830
|
// Exactly one party may settle the foreground caller. detachObserver (the
|
|
1722
1831
|
// fold won) and resolveForegroundObserver (a completion was handed back
|
|
1723
1832
|
// after a failed fold) share this flag, so a race cannot double-settle.
|
|
@@ -1727,7 +1836,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1727
1836
|
foregroundSettled = true;
|
|
1728
1837
|
return "resolved";
|
|
1729
1838
|
};
|
|
1730
|
-
const
|
|
1839
|
+
const managedFoldAdapter: FoldAdapter = {
|
|
1731
1840
|
kind: "bash-managed",
|
|
1732
1841
|
jobId: job.jobId,
|
|
1733
1842
|
jobGeneration,
|
|
@@ -1746,24 +1855,26 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1746
1855
|
const current = ownedManager.getJob(job.jobId);
|
|
1747
1856
|
return current?.generation === jobGeneration ? current : undefined;
|
|
1748
1857
|
},
|
|
1749
|
-
detachObserver:
|
|
1858
|
+
detachObserver: receipt => {
|
|
1750
1859
|
const outcome = settleForeground();
|
|
1751
1860
|
if (outcome === "resolved") {
|
|
1752
1861
|
job.setBackgrounded(true);
|
|
1753
|
-
ownedManager.markBackgrounded(job.jobId, jobGeneration);
|
|
1754
|
-
backgroundRequest.resolve();
|
|
1862
|
+
ownedManager.markBackgrounded(job.jobId, jobGeneration, receipt.reason);
|
|
1863
|
+
backgroundRequest.resolve(receipt.reason);
|
|
1755
1864
|
}
|
|
1756
1865
|
return outcome;
|
|
1757
1866
|
},
|
|
1758
1867
|
resolveForegroundObserver: () => settleForeground(),
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1868
|
+
};
|
|
1869
|
+
const unregisterBackgroundRequest = this.session.registerForegroundFoldParticipant?.(managedFoldAdapter);
|
|
1870
|
+
let waitResult: ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" };
|
|
1761
1871
|
try {
|
|
1762
1872
|
waitResult = await this.#waitForManagedBashJob(
|
|
1763
1873
|
job,
|
|
1764
1874
|
autoBackgroundWaitMs,
|
|
1765
1875
|
signal,
|
|
1766
1876
|
backgroundRequest.promise,
|
|
1877
|
+
managedFoldAdapter,
|
|
1767
1878
|
);
|
|
1768
1879
|
} finally {
|
|
1769
1880
|
unregisterBackgroundRequest?.();
|
|
@@ -1791,10 +1902,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1791
1902
|
throw new ToolAbortError(formatManagedAbortFailure(undefined, undefined, job.getLatestText()));
|
|
1792
1903
|
}
|
|
1793
1904
|
job.setBackgrounded(true);
|
|
1794
|
-
ownedManager.markBackgrounded(job.jobId, jobGeneration);
|
|
1905
|
+
ownedManager.markBackgrounded(job.jobId, jobGeneration, waitResult.reason);
|
|
1795
1906
|
return this.#buildBackgroundStartResult(job.jobId, job.label, job.getLatestText(), timeoutSec, {
|
|
1796
1907
|
requestedTimeoutSec,
|
|
1797
1908
|
notices: pendingNotices,
|
|
1909
|
+
foldReason: waitResult.reason,
|
|
1798
1910
|
});
|
|
1799
1911
|
}
|
|
1800
1912
|
|
|
@@ -2323,9 +2435,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2323
2435
|
void bridgeHandle.completion.finally(unregisterOwnerCleanup);
|
|
2324
2436
|
registerOwnedIfLineaged(bridgeManager, toolCallId, bridgeJobId, bridgeEndpointId);
|
|
2325
2437
|
|
|
2326
|
-
const bridgeFoldRequest = Promise.withResolvers<
|
|
2438
|
+
const bridgeFoldRequest = Promise.withResolvers<FoldReason>();
|
|
2327
2439
|
const bridgeOriginatingTurn = ctx?.attemptScope !== undefined;
|
|
2328
|
-
const
|
|
2440
|
+
const bridgeFoldAdapter: FoldAdapter = {
|
|
2329
2441
|
kind: "client-terminal",
|
|
2330
2442
|
jobId: bridgeJobId,
|
|
2331
2443
|
jobGeneration: bridgeGeneration,
|
|
@@ -2342,25 +2454,27 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2342
2454
|
const current = bridgeManager.getJob(bridgeJobId);
|
|
2343
2455
|
return current?.generation === bridgeGeneration ? current : undefined;
|
|
2344
2456
|
},
|
|
2345
|
-
detachObserver:
|
|
2457
|
+
detachObserver: receipt => {
|
|
2346
2458
|
const outcome = settleBridgeForeground();
|
|
2347
2459
|
if (outcome === "resolved") {
|
|
2348
2460
|
bridgeHandle.setBackgrounded(true);
|
|
2349
|
-
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration);
|
|
2350
|
-
bridgeFoldRequest.resolve();
|
|
2461
|
+
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration, receipt.reason);
|
|
2462
|
+
bridgeFoldRequest.resolve(receipt.reason);
|
|
2351
2463
|
}
|
|
2352
2464
|
return outcome;
|
|
2353
2465
|
},
|
|
2354
2466
|
resolveForegroundObserver: () => settleBridgeForeground(),
|
|
2355
|
-
}
|
|
2467
|
+
};
|
|
2468
|
+
const unregisterBridgeFold = this.session.registerForegroundFoldParticipant?.(bridgeFoldAdapter);
|
|
2356
2469
|
|
|
2357
|
-
let bridgeWait: ManagedBashJobCompletion | { kind: "running" } | { kind: "aborted" };
|
|
2470
|
+
let bridgeWait: ManagedBashJobCompletion | { kind: "running"; reason: FoldReason } | { kind: "aborted" };
|
|
2358
2471
|
try {
|
|
2359
2472
|
bridgeWait = await this.#waitForManagedBashJob(
|
|
2360
2473
|
bridgeHandle,
|
|
2361
2474
|
this.#autoBackgroundEnabled ? this.#resolveAutoBackgroundWaitMs(timeoutMs) : timeoutMs + 1_000,
|
|
2362
2475
|
signal,
|
|
2363
2476
|
bridgeFoldRequest.promise,
|
|
2477
|
+
bridgeFoldAdapter,
|
|
2364
2478
|
);
|
|
2365
2479
|
} finally {
|
|
2366
2480
|
unregisterBridgeFold?.();
|
|
@@ -2395,11 +2509,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2395
2509
|
// Folded (or auto-backgrounded): the remote terminal keeps running and its
|
|
2396
2510
|
// result arrives as a background completion.
|
|
2397
2511
|
bridgeHandle.setBackgrounded(true);
|
|
2398
|
-
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration);
|
|
2512
|
+
bridgeManager.markBackgrounded(bridgeJobId, bridgeGeneration, bridgeWait.reason);
|
|
2399
2513
|
return this.#buildBackgroundStartResult(bridgeJobId, bridgeLabel, bridgeHandle.getLatestText(), timeoutSec, {
|
|
2400
2514
|
requestedTimeoutSec,
|
|
2401
2515
|
notices: pendingNotices,
|
|
2402
2516
|
terminalId: handle.terminalId,
|
|
2517
|
+
foldReason: bridgeWait.reason,
|
|
2403
2518
|
});
|
|
2404
2519
|
}
|
|
2405
2520
|
|
|
@@ -2521,7 +2636,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2521
2636
|
},
|
|
2522
2637
|
);
|
|
2523
2638
|
void controls.terminalCompletion.finally(unregisterPtyOwnerCleanup);
|
|
2524
|
-
|
|
2639
|
+
const ptyStartedAt = Date.now();
|
|
2640
|
+
const ptyFoldAdapter: FoldAdapter = {
|
|
2525
2641
|
kind: "bash-pty",
|
|
2526
2642
|
jobId: ptyJobId,
|
|
2527
2643
|
jobGeneration: ptyGeneration,
|
|
@@ -2538,12 +2654,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2538
2654
|
const current = ptyManager.getJob(ptyJobId);
|
|
2539
2655
|
return current?.generation === ptyGeneration ? current : undefined;
|
|
2540
2656
|
},
|
|
2541
|
-
detachObserver:
|
|
2657
|
+
detachObserver: receipt => {
|
|
2542
2658
|
// Output-only continuation: stdin forwarding ends here and the
|
|
2543
2659
|
// process is never killed or restarted by folding.
|
|
2544
2660
|
const started = this.#buildBackgroundStartResult(ptyJobId, ptyLabel, "", timeoutSec, {
|
|
2545
2661
|
requestedTimeoutSec,
|
|
2546
2662
|
notices: pendingNotices,
|
|
2663
|
+
foldReason: receipt.reason,
|
|
2547
2664
|
});
|
|
2548
2665
|
const outcome = controls.detachObserver(
|
|
2549
2666
|
interactiveResultFromText(this.#extractTextResult(started)),
|
|
@@ -2551,13 +2668,19 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2551
2668
|
if (outcome === "resolved") {
|
|
2552
2669
|
controls.detachForegroundCancellation();
|
|
2553
2670
|
ptyBackgrounded = true;
|
|
2554
|
-
ptyManager.markBackgrounded(ptyJobId, ptyGeneration);
|
|
2671
|
+
ptyManager.markBackgrounded(ptyJobId, ptyGeneration, receipt.reason);
|
|
2555
2672
|
ptyFoldResult = started;
|
|
2556
2673
|
}
|
|
2557
2674
|
return outcome;
|
|
2558
2675
|
},
|
|
2559
2676
|
resolveForegroundObserver: () => "already-settled",
|
|
2560
|
-
}
|
|
2677
|
+
};
|
|
2678
|
+
const unregisterPtyFold = this.session.registerForegroundFoldParticipant?.(ptyFoldAdapter);
|
|
2679
|
+
const stopPtySteerWatch = this.#watchSteerForFold(ptyFoldAdapter, ptyStartedAt);
|
|
2680
|
+
ptyFoldUnregister = () => {
|
|
2681
|
+
stopPtySteerWatch();
|
|
2682
|
+
unregisterPtyFold?.();
|
|
2683
|
+
};
|
|
2561
2684
|
},
|
|
2562
2685
|
onFoldKey: () => {
|
|
2563
2686
|
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;
|