@gajae-code/coding-agent 0.5.1 → 0.5.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 +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
package/src/task/render.ts
CHANGED
|
@@ -525,6 +525,10 @@ function renderAgentProgress(
|
|
|
525
525
|
expanded: boolean,
|
|
526
526
|
theme: Theme,
|
|
527
527
|
spinnerFrame?: number,
|
|
528
|
+
/** When true, omit wall-clock-derived displays (current-tool elapsed, retry
|
|
529
|
+
* countdown) so the output is a pure function of `progress` — required when the
|
|
530
|
+
* caller caches these lines (the `subagent` await panel). */
|
|
531
|
+
staticTime = false,
|
|
528
532
|
): string[] {
|
|
529
533
|
const lines: string[] = [];
|
|
530
534
|
const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
|
|
@@ -587,7 +591,7 @@ function renderAgentProgress(
|
|
|
587
591
|
if (toolDetail) {
|
|
588
592
|
toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
|
|
589
593
|
}
|
|
590
|
-
if (progress.currentToolStartMs) {
|
|
594
|
+
if (!staticTime && progress.currentToolStartMs) {
|
|
591
595
|
const elapsed = Date.now() - progress.currentToolStartMs;
|
|
592
596
|
if (elapsed > 5000) {
|
|
593
597
|
toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
|
|
@@ -610,12 +614,17 @@ function renderAgentProgress(
|
|
|
610
614
|
// long until the next attempt. Without this, the parent UI would just
|
|
611
615
|
// keep spinning while a child sleeps on a 3-hour provider rate-limit.
|
|
612
616
|
if (progress.retryState && progress.status === "running") {
|
|
613
|
-
const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
|
|
614
|
-
const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
|
|
615
617
|
const attemptLabel = progress.retryState.unbounded
|
|
616
618
|
? `attempt ${progress.retryState.attempt}`
|
|
617
619
|
: `${progress.retryState.attempt}/${progress.retryState.maxAttempts}`;
|
|
618
|
-
|
|
620
|
+
// `staticTime` omits the wall-clock countdown so a cached await body stays a
|
|
621
|
+
// pure function of its key (the producer already drops time-only churn).
|
|
622
|
+
let waitLabel = "";
|
|
623
|
+
if (!staticTime) {
|
|
624
|
+
const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
|
|
625
|
+
waitLabel = remainingMs > 0 ? ` in ${formatDuration(remainingMs)}` : " now";
|
|
626
|
+
}
|
|
627
|
+
const summary = `retrying ${attemptLabel}${waitLabel}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
|
|
619
628
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
620
629
|
} else if (progress.retryFailure && progress.status !== "running") {
|
|
621
630
|
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
@@ -687,7 +696,7 @@ function renderAgentProgress(
|
|
|
687
696
|
const inflight = progress.inflightTaskDetails;
|
|
688
697
|
if (completedTaskCalls.length > 0 || inflight) {
|
|
689
698
|
const snapshots = inflight ? [...completedTaskCalls, inflight] : completedTaskCalls;
|
|
690
|
-
const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame);
|
|
699
|
+
const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame, staticTime);
|
|
691
700
|
for (const line of nestedLines) {
|
|
692
701
|
lines.push(`${continuePrefix}${line}`);
|
|
693
702
|
}
|
|
@@ -712,8 +721,9 @@ export function renderSubagentLiveProgress(
|
|
|
712
721
|
expanded: boolean,
|
|
713
722
|
theme: Theme,
|
|
714
723
|
spinnerFrame?: number,
|
|
724
|
+
staticTime = false,
|
|
715
725
|
): string[] {
|
|
716
|
-
return renderAgentProgress(progress, true, expanded, theme, spinnerFrame);
|
|
726
|
+
return renderAgentProgress(progress, true, expanded, theme, spinnerFrame, staticTime);
|
|
717
727
|
}
|
|
718
728
|
|
|
719
729
|
/**
|
|
@@ -1051,6 +1061,7 @@ function renderNestedTaskTree(
|
|
|
1051
1061
|
expanded: boolean,
|
|
1052
1062
|
theme: Theme,
|
|
1053
1063
|
spinnerFrame?: number,
|
|
1064
|
+
staticTime = false,
|
|
1054
1065
|
): string[] {
|
|
1055
1066
|
const lines: string[] = [];
|
|
1056
1067
|
for (const details of detailsList) {
|
|
@@ -1066,7 +1077,7 @@ function renderNestedTaskTree(
|
|
|
1066
1077
|
if (inflight && inflight.length > 0) {
|
|
1067
1078
|
inflight.forEach((prog, index) => {
|
|
1068
1079
|
const isLast = index === inflight.length - 1;
|
|
1069
|
-
lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame));
|
|
1080
|
+
lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame, staticTime));
|
|
1070
1081
|
});
|
|
1071
1082
|
}
|
|
1072
1083
|
}
|
|
@@ -315,7 +315,16 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
|
|
|
315
315
|
throw new ToolError(`Unsupported archive format: ${filePath}`);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
// F20: cap the compressed archive read so opening a multi-GB archive cannot buffer it
|
|
319
|
+
// whole into memory. (Zip-bomb expanded-size bounding would need a streaming inflate.)
|
|
320
|
+
const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
321
|
+
const file = Bun.file(filePath);
|
|
322
|
+
if (file.size > MAX_ARCHIVE_BYTES) {
|
|
323
|
+
throw new ToolError(
|
|
324
|
+
`Archive too large to open: ${filePath} is ${file.size} bytes (limit ${MAX_ARCHIVE_BYTES}). Extract a subset with a dedicated tool.`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const bytes = await file.bytes();
|
|
319
328
|
const entries = format === "zip" ? await readZipEntries(bytes) : await readTarEntries(bytes);
|
|
320
329
|
return new ArchiveReader(format, entries);
|
|
321
330
|
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
visibleWidth,
|
|
27
27
|
wrapTextWithAnsi,
|
|
28
28
|
} from "@gajae-code/tui";
|
|
29
|
-
import { prompt, untilAborted } from "@gajae-code/utils";
|
|
29
|
+
import { logger, prompt, untilAborted } from "@gajae-code/utils";
|
|
30
30
|
import * as z from "zod/v4";
|
|
31
31
|
import {
|
|
32
32
|
formatDeepInterviewSelectorPrompt,
|
|
@@ -43,6 +43,7 @@ import { renderStatusLine } from "../tui";
|
|
|
43
43
|
import type { ToolSession } from ".";
|
|
44
44
|
import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
|
|
45
45
|
import { ToolAbortError } from "./tool-errors";
|
|
46
|
+
import { assertUltragoalAskAllowed } from "./ultragoal-ask-guard";
|
|
46
47
|
|
|
47
48
|
// =============================================================================
|
|
48
49
|
// Types
|
|
@@ -501,7 +502,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
501
502
|
{ sessionId },
|
|
502
503
|
);
|
|
503
504
|
} catch (error) {
|
|
504
|
-
|
|
505
|
+
logger.warn(
|
|
505
506
|
`ask: deep-interview round recording failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
506
507
|
);
|
|
507
508
|
}
|
|
@@ -514,6 +515,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
514
515
|
_onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
|
|
515
516
|
context?: AgentToolContext,
|
|
516
517
|
): Promise<AgentToolResult<AskToolDetails>> {
|
|
518
|
+
await assertUltragoalAskAllowed(this.session.cwd);
|
|
517
519
|
const gateEmitter = this.session.getWorkflowGateEmitter?.();
|
|
518
520
|
const canUseWorkflowGate = gateEmitter?.isUnattended() === true;
|
|
519
521
|
|
package/src/tools/bash.ts
CHANGED
|
@@ -37,8 +37,13 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
|
37
37
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
38
38
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
39
39
|
|
|
40
|
-
async function
|
|
40
|
+
export async function saveBashOriginalArtifactForTests(
|
|
41
|
+
session: ToolSession,
|
|
42
|
+
originalText: string,
|
|
43
|
+
): Promise<string | undefined> {
|
|
41
44
|
try {
|
|
45
|
+
const manager = session.getArtifactManager?.();
|
|
46
|
+
if (manager) return await manager.save(originalText, "bash-original");
|
|
42
47
|
const alloc = await session.allocateOutputArtifact?.("bash-original");
|
|
43
48
|
if (!alloc?.path || !alloc.id) return undefined;
|
|
44
49
|
await Bun.write(alloc.path, originalText);
|
|
@@ -375,6 +380,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
375
380
|
env: options.resolvedEnv,
|
|
376
381
|
artifactPath,
|
|
377
382
|
artifactId,
|
|
383
|
+
oneShot: true,
|
|
378
384
|
onChunk: chunk => {
|
|
379
385
|
tailBuffer.append(chunk);
|
|
380
386
|
latestText = tailBuffer.text();
|
|
@@ -387,7 +393,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
387
393
|
// path above.
|
|
388
394
|
manager.appendOutput(jobId, chunk);
|
|
389
395
|
},
|
|
390
|
-
onMinimizedSave: originalText =>
|
|
396
|
+
onMinimizedSave: originalText => saveBashOriginalArtifactForTests(this.session, originalText),
|
|
391
397
|
});
|
|
392
398
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
393
399
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
@@ -675,6 +681,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
675
681
|
env: prepared.resolvedEnv,
|
|
676
682
|
artifactPath,
|
|
677
683
|
artifactId,
|
|
684
|
+
oneShot: true,
|
|
678
685
|
onChunk: chunk => {
|
|
679
686
|
tailBuffer.append(chunk);
|
|
680
687
|
void reportProgress(tailBuffer.text(), {
|
|
@@ -688,7 +695,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
688
695
|
cursorOffset = slice.nextOffset;
|
|
689
696
|
dispatchLines(slice.text);
|
|
690
697
|
},
|
|
691
|
-
onMinimizedSave: originalText =>
|
|
698
|
+
onMinimizedSave: originalText => saveBashOriginalArtifactForTests(this.session, originalText),
|
|
692
699
|
});
|
|
693
700
|
flushTrailingLine();
|
|
694
701
|
this.#buildResultText(result, prepared.timeoutSec, result.output || "(no output)");
|
|
@@ -996,7 +1003,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
996
1003
|
artifactPath,
|
|
997
1004
|
artifactId,
|
|
998
1005
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
999
|
-
onMinimizedSave: originalText =>
|
|
1006
|
+
onMinimizedSave: originalText => saveBashOriginalArtifactForTests(this.session, originalText),
|
|
1000
1007
|
});
|
|
1001
1008
|
if (result.cancelled) {
|
|
1002
1009
|
if (signal?.aborted) {
|
|
@@ -55,6 +55,8 @@ export interface TabSession {
|
|
|
55
55
|
pending: Map<string, PendingRun>;
|
|
56
56
|
dialogPolicy?: DialogPolicy;
|
|
57
57
|
kindTag: BrowserKindTag;
|
|
58
|
+
/** Session that acquired this tab; used for session-scoped teardown (F13). */
|
|
59
|
+
ownerId?: string;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export interface AcquireTabOptions {
|
|
@@ -65,6 +67,8 @@ export interface AcquireTabOptions {
|
|
|
65
67
|
signal?: AbortSignal;
|
|
66
68
|
timeoutMs: number;
|
|
67
69
|
dialogs?: DialogPolicy;
|
|
70
|
+
/** Owning session id so dispose can release only this session's tabs (F13). */
|
|
71
|
+
ownerId?: string;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export interface AcquireTabResult {
|
|
@@ -161,6 +165,7 @@ export async function acquireTab(
|
|
|
161
165
|
pending: new Map(),
|
|
162
166
|
dialogPolicy: opts.dialogs,
|
|
163
167
|
kindTag: browser.kind.kind,
|
|
168
|
+
ownerId: opts.ownerId,
|
|
164
169
|
};
|
|
165
170
|
worker.onMessage(msg => handleTabMessage(tab, msg));
|
|
166
171
|
tabs.set(name, tab);
|
|
@@ -254,6 +259,23 @@ export async function releaseAllTabs(opts: ReleaseTabOptions = {}): Promise<numb
|
|
|
254
259
|
return count;
|
|
255
260
|
}
|
|
256
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Release only the tabs owned by `ownerId` (F13 session-scoped teardown). Tabs acquired
|
|
264
|
+
* by other sessions (or with no owner) are left untouched. No-op for a null/empty owner.
|
|
265
|
+
*/
|
|
266
|
+
export async function releaseTabsForOwner(
|
|
267
|
+
ownerId: string | null | undefined,
|
|
268
|
+
opts: ReleaseTabOptions = {},
|
|
269
|
+
): Promise<number> {
|
|
270
|
+
if (!ownerId) return 0;
|
|
271
|
+
const names = [...tabs.entries()].filter(([, tab]) => tab.ownerId === ownerId).map(([name]) => name);
|
|
272
|
+
let count = 0;
|
|
273
|
+
for (const name of names) {
|
|
274
|
+
if (await releaseTab(name, opts)) count++;
|
|
275
|
+
}
|
|
276
|
+
return count;
|
|
277
|
+
}
|
|
278
|
+
|
|
257
279
|
export async function dropHeadlessTabs(): Promise<void> {
|
|
258
280
|
const names = [...tabs.values()].filter(tab => tab.kindTag === "headless").map(tab => tab.name);
|
|
259
281
|
for (const name of names) await releaseTab(name);
|
package/src/tools/browser.ts
CHANGED
|
@@ -213,6 +213,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
213
213
|
|
|
214
214
|
const result = await untilAborted(signal, () =>
|
|
215
215
|
acquireTab(name, browser, {
|
|
216
|
+
ownerId: this.session.getSessionId?.() ?? undefined,
|
|
216
217
|
url: params.url,
|
|
217
218
|
waitUntil: params.wait_until,
|
|
218
219
|
viewport: params.viewport
|
|
@@ -381,11 +382,44 @@ function sameBrowserKind(a: BrowserKind, b: BrowserKind): boolean {
|
|
|
381
382
|
return false;
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
/** Max chars of a browser return value surfaced into the tool result (F22). */
|
|
386
|
+
const MAX_BROWSER_RETURN_CHARS = 256 * 1024;
|
|
387
|
+
|
|
388
|
+
const BROWSER_RETURN_BUDGET_EXCEEDED = Symbol("browser-return-budget-exceeded");
|
|
389
|
+
|
|
390
|
+
/** Hard-cap any surfaced browser return string at the byte/char limit with a notice. */
|
|
391
|
+
function capBrowserReturn(text: string): string {
|
|
392
|
+
if (text.length <= MAX_BROWSER_RETURN_CHARS) return text;
|
|
393
|
+
return `${text.slice(0, MAX_BROWSER_RETURN_CHARS)}\n\n[Browser return value truncated: ${text.length} chars exceeds the ${MAX_BROWSER_RETURN_CHARS}-char cap.]`;
|
|
394
|
+
}
|
|
395
|
+
|
|
384
396
|
function stringifyReturnValue(value: unknown): string {
|
|
385
|
-
if (typeof value === "string") return value;
|
|
397
|
+
if (typeof value === "string") return capBrowserReturn(value);
|
|
398
|
+
// F22: bound the serialization itself — the replacer tracks running size and aborts early so a
|
|
399
|
+
// huge object/array cannot build megabytes before truncation — AND hard-cap the final string,
|
|
400
|
+
// since pretty-print structural overhead (indent/braces/commas) is not counted by the budget.
|
|
401
|
+
let budget = MAX_BROWSER_RETURN_CHARS;
|
|
386
402
|
try {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
403
|
+
const text = JSON.stringify(
|
|
404
|
+
value,
|
|
405
|
+
(_key, val) => {
|
|
406
|
+
if (typeof val === "string") budget -= val.length + 4;
|
|
407
|
+
else if (typeof val === "number" || typeof val === "boolean") budget -= 8;
|
|
408
|
+
else budget -= 2;
|
|
409
|
+
if (budget < 0) throw BROWSER_RETURN_BUDGET_EXCEEDED;
|
|
410
|
+
return val;
|
|
411
|
+
},
|
|
412
|
+
2,
|
|
413
|
+
);
|
|
414
|
+
return text === undefined ? capBrowserReturn(String(value)) : capBrowserReturn(text);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (error === BROWSER_RETURN_BUDGET_EXCEEDED) {
|
|
417
|
+
return `[Browser return value too large to serialize (exceeds the ${MAX_BROWSER_RETURN_CHARS}-char cap). Return a smaller or summarized value from the page script.]`;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
return capBrowserReturn(String(value));
|
|
421
|
+
} catch {
|
|
422
|
+
return "[unserializable browser return value]";
|
|
423
|
+
}
|
|
390
424
|
}
|
|
391
425
|
}
|
package/src/tools/cron.ts
CHANGED
|
@@ -391,7 +391,7 @@ export function calculateCronFireTimeMs(params: {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
function setCronTimeout(callback: () => void, delayMs: number): CronTimerHandle {
|
|
394
|
-
let handle:
|
|
394
|
+
let handle: NodeJS.Timeout | undefined;
|
|
395
395
|
let cleared = false;
|
|
396
396
|
const schedule = (remainingMs: number) => {
|
|
397
397
|
if (cleared) return;
|
package/src/tools/read.ts
CHANGED
|
@@ -1270,19 +1270,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1270
1270
|
}
|
|
1271
1271
|
case "raw": {
|
|
1272
1272
|
const result = executeReadQuery(db, selector.sql);
|
|
1273
|
+
const table = renderTable(result.columns, result.rows, {
|
|
1274
|
+
totalCount: result.rows.length,
|
|
1275
|
+
offset: 0,
|
|
1276
|
+
limit: result.rows.length || DEFAULT_MAX_LINES,
|
|
1277
|
+
table: "query",
|
|
1278
|
+
dbPath: resolvedSqlitePath.absolutePath,
|
|
1279
|
+
});
|
|
1280
|
+
const body = result.truncated
|
|
1281
|
+
? `${table}\n\n[Output truncated to the first ${result.rows.length} rows; add a LIMIT clause to the query to bound or page the result.]`
|
|
1282
|
+
: table;
|
|
1273
1283
|
return toolResult<ReadToolDetails>(details)
|
|
1274
|
-
.text(
|
|
1275
|
-
prependSuffixResolutionNotice(
|
|
1276
|
-
renderTable(result.columns, result.rows, {
|
|
1277
|
-
totalCount: result.rows.length,
|
|
1278
|
-
offset: 0,
|
|
1279
|
-
limit: result.rows.length || DEFAULT_MAX_LINES,
|
|
1280
|
-
table: "query",
|
|
1281
|
-
dbPath: resolvedSqlitePath.absolutePath,
|
|
1282
|
-
}),
|
|
1283
|
-
resolvedSqlitePath.suffixResolution,
|
|
1284
|
-
),
|
|
1285
|
-
)
|
|
1284
|
+
.text(prependSuffixResolutionNotice(body, resolvedSqlitePath.suffixResolution))
|
|
1286
1285
|
.sourcePath(resolvedSqlitePath.absolutePath)
|
|
1287
1286
|
.done();
|
|
1288
1287
|
}
|
|
@@ -590,15 +590,29 @@ export function getRowByRowId(db: Database, table: string, key: string): Record<
|
|
|
590
590
|
.get(binding);
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
const MAX_RAW_QUERY_ROWS = 1000;
|
|
594
|
+
|
|
595
|
+
export function executeReadQuery(
|
|
596
|
+
db: Database,
|
|
597
|
+
sql: string,
|
|
598
|
+
maxRows: number = MAX_RAW_QUERY_ROWS,
|
|
599
|
+
): { columns: string[]; rows: Record<string, unknown>[]; truncated: boolean } {
|
|
594
600
|
const statement = db.prepare<SqliteRow, []>(sql);
|
|
595
601
|
if (statement.paramsCount > 0) {
|
|
596
602
|
throw new ToolError("SQLite raw queries do not support bound parameters");
|
|
597
603
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
604
|
+
// Stream rows and stop at the cap (F20): a raw `q=SELECT ...` over a huge table
|
|
605
|
+
// must not materialize every row into memory via statement.all().
|
|
606
|
+
const rows: Record<string, unknown>[] = [];
|
|
607
|
+
let truncated = false;
|
|
608
|
+
for (const row of statement.iterate()) {
|
|
609
|
+
if (rows.length >= maxRows) {
|
|
610
|
+
truncated = true;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
rows.push(row as Record<string, unknown>);
|
|
614
|
+
}
|
|
615
|
+
return { columns: [...statement.columnNames], rows, truncated };
|
|
602
616
|
}
|
|
603
617
|
|
|
604
618
|
export function insertRow(db: Database, table: string, data: Record<string, unknown>): void {
|
|
@@ -12,7 +12,7 @@ import { Text } from "@gajae-code/tui";
|
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import type { Theme } from "../modes/theme/theme";
|
|
14
14
|
import { renderSubagentLiveProgress } from "../task/render";
|
|
15
|
-
import { Ellipsis, Hasher,
|
|
15
|
+
import { Ellipsis, Hasher, renderStatusLine } from "../tui";
|
|
16
16
|
import {
|
|
17
17
|
formatDuration,
|
|
18
18
|
formatStatusIcon,
|
|
@@ -21,12 +21,87 @@ import {
|
|
|
21
21
|
type ToolUIStatus,
|
|
22
22
|
truncateToWidth,
|
|
23
23
|
} from "./render-utils";
|
|
24
|
-
import type
|
|
24
|
+
import { type SubagentSnapshot, type SubagentToolDetails, subagentAwaitRenderedStateSignature } from "./subagent";
|
|
25
25
|
|
|
26
26
|
const PREVIEW_LINES_COLLAPSED = 1;
|
|
27
27
|
const PREVIEW_LINES_EXPANDED = 4;
|
|
28
28
|
const PREVIEW_LINE_WIDTH = 80;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Bounded, content-addressed cache for each subagent's heavy body lines (the
|
|
32
|
+
* indented receipt fields + `renderSubagentLiveProgress` -> `renderAgentProgress`
|
|
33
|
+
* output). It is module-level so it survives the built-in renderer recreating the
|
|
34
|
+
* result component on every partial update (`tool-execution.ts` clears the content
|
|
35
|
+
* box and re-invokes `renderResult`), which a per-component `let cached` cannot.
|
|
36
|
+
*
|
|
37
|
+
* The cached body is a PURE function of its key: the per-subagent rendered-state
|
|
38
|
+
* signature (reused from the producer; excludes time-derived churn), expanded
|
|
39
|
+
* state, width, and the actual Theme instance identity. `spinnerFrame` and all
|
|
40
|
+
* wall-clock displays are deliberately kept OUT of the cached body — the animated
|
|
41
|
+
* spinner and the fresh duration live in the cheap per-subagent status line, and
|
|
42
|
+
* `renderSubagentLiveProgress` is invoked with `staticTime` so current-tool elapsed
|
|
43
|
+
* and retry countdowns are never baked into cached lines.
|
|
44
|
+
*/
|
|
45
|
+
const SUBAGENT_BODY_CACHE_MAX = 128;
|
|
46
|
+
const subagentBodyCache = new Map<bigint, string[]>();
|
|
47
|
+
let subagentBodyRenderCount = 0;
|
|
48
|
+
|
|
49
|
+
// Stable identity per Theme instance so a theme change (preview, symbol preset,
|
|
50
|
+
// color-blind reload, custom-theme reload, or in-memory swap) never reuses stale
|
|
51
|
+
// ANSI/glyph strings — distinct Theme objects get distinct ids even when the theme
|
|
52
|
+
// name is unchanged (e.g. the "<in-memory>" name).
|
|
53
|
+
const themeIdentity = new WeakMap<Theme, number>();
|
|
54
|
+
let nextThemeId = 1;
|
|
55
|
+
function themeIdentityId(theme: Theme): number {
|
|
56
|
+
let id = themeIdentity.get(theme);
|
|
57
|
+
if (id === undefined) {
|
|
58
|
+
id = nextThemeId++;
|
|
59
|
+
themeIdentity.set(theme, id);
|
|
60
|
+
}
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Test-only seam (PR3 deterministic cache-hit assertions). */
|
|
65
|
+
export const subagentBodyCacheTestHooks = {
|
|
66
|
+
get bodyRenders(): number {
|
|
67
|
+
return subagentBodyRenderCount;
|
|
68
|
+
},
|
|
69
|
+
get size(): number {
|
|
70
|
+
return subagentBodyCache.size;
|
|
71
|
+
},
|
|
72
|
+
reset(): void {
|
|
73
|
+
subagentBodyRenderCount = 0;
|
|
74
|
+
subagentBodyCache.clear();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function renderCachedSubagentBody(
|
|
79
|
+
snapshot: SubagentSnapshot,
|
|
80
|
+
signature: string,
|
|
81
|
+
expanded: boolean,
|
|
82
|
+
width: number,
|
|
83
|
+
theme: Theme,
|
|
84
|
+
): string[] {
|
|
85
|
+
const key = new Hasher().str(signature).bool(expanded).u32(width).u32(themeIdentityId(theme)).digest();
|
|
86
|
+
const hit = subagentBodyCache.get(key);
|
|
87
|
+
if (hit) {
|
|
88
|
+
// Refresh LRU recency.
|
|
89
|
+
subagentBodyCache.delete(key);
|
|
90
|
+
subagentBodyCache.set(key, hit);
|
|
91
|
+
return hit;
|
|
92
|
+
}
|
|
93
|
+
const lines = renderSubagentSnapshotBody(snapshot, expanded, theme).map(line =>
|
|
94
|
+
line.length > 0 ? truncateToWidth(line, width, Ellipsis.Omit) : "",
|
|
95
|
+
);
|
|
96
|
+
subagentBodyRenderCount += 1;
|
|
97
|
+
subagentBodyCache.set(key, lines);
|
|
98
|
+
if (subagentBodyCache.size > SUBAGENT_BODY_CACHE_MAX) {
|
|
99
|
+
const oldest = subagentBodyCache.keys().next().value;
|
|
100
|
+
if (oldest !== undefined) subagentBodyCache.delete(oldest);
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
|
|
30
105
|
function statusIconKind(status: SubagentSnapshot["status"]): ToolUIStatus {
|
|
31
106
|
switch (status) {
|
|
32
107
|
case "completed":
|
|
@@ -44,13 +119,10 @@ function statusIconKind(status: SubagentSnapshot["status"]): ToolUIStatus {
|
|
|
44
119
|
}
|
|
45
120
|
}
|
|
46
121
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
spinnerFrame: number | undefined,
|
|
52
|
-
): string[] {
|
|
53
|
-
const lines: string[] = [];
|
|
122
|
+
// Cheap, dynamic per-subagent status line: the spinner may animate and the duration
|
|
123
|
+
// is the snapshot's own (fresh) value, so this line is rebuilt every frame and is
|
|
124
|
+
// NOT part of the cached body.
|
|
125
|
+
function renderSubagentStatusLine(snapshot: SubagentSnapshot, theme: Theme, spinnerFrame: number | undefined): string {
|
|
54
126
|
const icon = formatStatusIcon(
|
|
55
127
|
statusIconKind(snapshot.status),
|
|
56
128
|
theme,
|
|
@@ -59,7 +131,14 @@ function renderSubagentSnapshot(
|
|
|
59
131
|
const id = theme.fg("muted", snapshot.id);
|
|
60
132
|
const status = theme.fg("dim", snapshot.status);
|
|
61
133
|
const duration = theme.fg("dim", formatDuration(snapshot.durationMs));
|
|
62
|
-
|
|
134
|
+
return `${icon} ${id} ${status} ${duration}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Heavy, cacheable per-subagent body: a pure function of (snapshot content, expanded,
|
|
138
|
+
// theme). No spinner frame and no wall-clock displays leak in (live progress uses
|
|
139
|
+
// `staticTime`), so the module body cache can never serve stale or frozen-ticking lines.
|
|
140
|
+
function renderSubagentSnapshotBody(snapshot: SubagentSnapshot, expanded: boolean, theme: Theme): string[] {
|
|
141
|
+
const lines: string[] = [];
|
|
63
142
|
|
|
64
143
|
// Static receipt fields (parity with the markdown content for non-await actions).
|
|
65
144
|
if (snapshot.jobId !== snapshot.id) lines.push(` ${theme.fg("dim", `Job: ${snapshot.jobId}`)}`);
|
|
@@ -82,13 +161,13 @@ function renderSubagentSnapshot(
|
|
|
82
161
|
for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
|
|
83
162
|
}
|
|
84
163
|
|
|
85
|
-
// Defense in depth: the producer only attaches `progress` when a live
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
164
|
+
// Defense in depth: the producer only attaches `progress` when a live producer
|
|
165
|
+
// exists (subagent.ts #liveProgressFields), but the renderer also honors an
|
|
166
|
+
// explicit `liveProgressAvailable: false` so stale retained progress can never
|
|
167
|
+
// resurrect a live panel (AC5). `staticTime` keeps wall-clock displays out of
|
|
168
|
+
// these cached lines.
|
|
89
169
|
if (snapshot.progress && snapshot.liveProgressAvailable !== false) {
|
|
90
|
-
|
|
91
|
-
for (const pl of renderSubagentLiveProgress(snapshot.progress, expanded, theme, spinnerFrame)) {
|
|
170
|
+
for (const pl of renderSubagentLiveProgress(snapshot.progress, expanded, theme, undefined, true)) {
|
|
92
171
|
lines.push(` ${pl}`);
|
|
93
172
|
}
|
|
94
173
|
} else if (snapshot.liveProgressAvailable && (snapshot.status === "running" || snapshot.status === "queued")) {
|
|
@@ -133,14 +212,17 @@ export const subagentToolRenderer = {
|
|
|
133
212
|
|
|
134
213
|
const runningCount = subagents.filter(s => s.status === "running").length;
|
|
135
214
|
|
|
136
|
-
|
|
215
|
+
// Each snapshot's rendered-state signature is constant for this component
|
|
216
|
+
// instance, so compute them at most once; the heavy per-subagent bodies are
|
|
217
|
+
// cached module-side and keyed by that signature.
|
|
218
|
+
let snapshotSignatures: string[] | undefined;
|
|
137
219
|
return {
|
|
138
220
|
render(width: number): string[] {
|
|
139
221
|
const expanded = options.expanded;
|
|
140
|
-
const spinnerFrame = options.spinnerFrame ?? 0;
|
|
141
|
-
const key = new Hasher().bool(expanded).u32(width).u32(spinnerFrame).digest();
|
|
142
|
-
if (cached?.key === key) return cached.lines;
|
|
143
222
|
|
|
223
|
+
// Cheap dynamic header: may animate with `spinnerFrame` and is rebuilt
|
|
224
|
+
// every frame, but it is a single status line plus an optional hint, so
|
|
225
|
+
// it is never gated by the heavy body cache.
|
|
144
226
|
const header = renderStatusLine(
|
|
145
227
|
{
|
|
146
228
|
icon: runningCount > 0 ? "info" : "success",
|
|
@@ -153,23 +235,31 @@ export const subagentToolRenderer = {
|
|
|
153
235
|
},
|
|
154
236
|
theme,
|
|
155
237
|
);
|
|
156
|
-
|
|
157
|
-
const lines: string[] = [header];
|
|
238
|
+
const out: string[] = [truncateToWidth(header, width, Ellipsis.Omit)];
|
|
158
239
|
// Discoverability: the inline panel is a bounded preview; the session
|
|
159
240
|
// observer (ctrl+s) streams the full per-subagent message history.
|
|
160
241
|
if (runningCount > 0) {
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
for (const snapshot of subagents) {
|
|
164
|
-
lines.push(...renderSubagentSnapshot(snapshot, expanded, theme, options.spinnerFrame));
|
|
242
|
+
out.push(truncateToWidth(` ${theme.fg("dim", "(ctrl+s to observe sessions)")}`, width, Ellipsis.Omit));
|
|
165
243
|
}
|
|
166
244
|
|
|
167
|
-
|
|
168
|
-
|
|
245
|
+
snapshotSignatures ??= subagents.map(snapshot => subagentAwaitRenderedStateSignature([snapshot]));
|
|
246
|
+
subagents.forEach((snapshot, index) => {
|
|
247
|
+
// Fresh per-subagent status line (cheap), then the cached heavy body.
|
|
248
|
+
out.push(
|
|
249
|
+
truncateToWidth(
|
|
250
|
+
renderSubagentStatusLine(snapshot, theme, options.spinnerFrame),
|
|
251
|
+
width,
|
|
252
|
+
Ellipsis.Omit,
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
out.push(...renderCachedSubagentBody(snapshot, snapshotSignatures![index]!, expanded, width, theme));
|
|
256
|
+
});
|
|
169
257
|
return out;
|
|
170
258
|
},
|
|
171
259
|
invalidate() {
|
|
172
|
-
|
|
260
|
+
// The heavy body cache is content-addressed (keyed by the rendered-state
|
|
261
|
+
// signature, width, expanded, and theme), so there is no instance-local
|
|
262
|
+
// state to clear here.
|
|
173
263
|
},
|
|
174
264
|
};
|
|
175
265
|
},
|