@gajae-code/coding-agent 0.12.19 → 0.12.20
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 +20 -9
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +66 -0
- package/dist/types/config/settings-schema.d.ts +34 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
- package/dist/types/modes/acp/acp-agent.d.ts +33 -1
- package/dist/types/modes/components/bash-execution.d.ts +7 -0
- package/dist/types/modes/components/eval-execution.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
- package/dist/types/sdk/acp/adapter.d.ts +9 -17
- package/dist/types/sdk/acp/index.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +93 -1
- package/dist/types/sdk/broker/ensure.d.ts +17 -0
- package/dist/types/sdk/broker/index.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
- package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +20 -9
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
- package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
- package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/lifecycle-session.d.ts +13 -2
- package/dist/types/sdk/prompt-failure.d.ts +13 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
- package/dist/types/sdk/session-reconnect.d.ts +20 -0
- package/dist/types/sdk/startup-capability.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +32 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
- package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
- package/dist/types/session/session-manager.d.ts +301 -27
- package/dist/types/session/session-storage.d.ts +170 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tui/output-block.d.ts +6 -0
- package/package.json +8 -8
- package/src/cli/args.ts +75 -34
- package/src/commands/acp.ts +7 -1
- package/src/commands/gc.ts +31 -2
- package/src/commands/sdk.ts +141 -7
- package/src/config/settings-schema.ts +43 -0
- package/src/coordinator-mcp/server.ts +6 -1
- package/src/export/html/index.ts +83 -39
- package/src/gjc-runtime/gc-runtime.ts +1283 -4
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/local-protocol.ts +14 -0
- package/src/main.ts +40 -9
- package/src/modes/acp/acp-agent.ts +564 -37
- package/src/modes/acp/terminal-auth.ts +2 -2
- package/src/modes/components/bash-execution.ts +14 -0
- package/src/modes/components/eval-execution.ts +14 -0
- package/src/modes/controllers/command-controller.ts +63 -8
- package/src/modes/utils/ui-helpers.ts +81 -11
- package/src/sdk/acp/adapter.ts +46 -45
- package/src/sdk/acp/index.ts +1 -1
- package/src/sdk/broker/broker.ts +392 -11
- package/src/sdk/broker/ensure.ts +131 -61
- package/src/sdk/broker/index.ts +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +59 -5
- package/src/sdk/broker/lifecycle.ts +278 -63
- package/src/sdk/broker/startup-budget.ts +103 -0
- package/src/sdk/bus/chat-daemon-control.ts +9 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
- package/src/sdk/bus/conversation-store.ts +1 -1
- package/src/sdk/bus/discord-daemon.ts +2 -1
- package/src/sdk/bus/discord-live-provider.ts +34 -6
- package/src/sdk/bus/index.ts +118 -20
- package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
- package/src/sdk/bus/prompt-reconciliation.ts +4 -9
- package/src/sdk/bus/slack-daemon.ts +91 -18
- package/src/sdk/cli/session-cli.ts +8 -1
- package/src/sdk/host/query/handlers.ts +1 -1
- package/src/sdk/host/query/revision-store.ts +48 -12
- package/src/sdk/host/session-runtime.test.ts +50 -0
- package/src/sdk/host/session-runtime.ts +12 -2
- package/src/sdk/lifecycle-session.ts +26 -3
- package/src/sdk/mcp/server.ts +6 -1
- package/src/sdk/prompt-failure.ts +42 -0
- package/src/sdk/prompt-watchdog.ts +162 -0
- package/src/sdk/session-reconnect.ts +43 -0
- package/src/sdk/startup-capability.ts +13 -4
- package/src/session/agent-session.ts +525 -222
- package/src/session/blob-store.ts +89 -0
- package/src/session/internal/managed-session-scope.ts +36 -26
- package/src/session/internal/managed-session-storage.ts +303 -32
- package/src/session/internal/session-memory-sidecar.ts +2160 -0
- package/src/session/session-manager.ts +7027 -394
- package/src/session/session-storage.ts +1134 -11
- package/src/task/executor.ts +17 -4
- package/src/tools/bash.ts +109 -7
- package/src/tools/find.ts +55 -26
- package/src/tools/todo-contract.ts +40 -8
- package/src/tui/output-block.ts +11 -1
package/src/task/executor.ts
CHANGED
|
@@ -41,7 +41,10 @@ import type { ArtifactManager } from "../session/artifacts";
|
|
|
41
41
|
import type { AuthStorage } from "../session/auth-storage";
|
|
42
42
|
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
43
43
|
import { SessionManager } from "../session/session-manager";
|
|
44
|
+
import { FileSessionStorage } from "../session/session-storage";
|
|
44
45
|
import { truncateTail } from "../session/streaming-output";
|
|
46
|
+
// Ensure mandatory subagent result extraction is available even when a session is mocked.
|
|
47
|
+
import "../tools/yield";
|
|
45
48
|
import type { ContextFileEntry } from "../tools";
|
|
46
49
|
import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
|
|
47
50
|
import type { ReportFindingDetails } from "../tools/review";
|
|
@@ -285,7 +288,7 @@ export class ManagedTaskPersistence {
|
|
|
285
288
|
throw new Error("Managed task persistence authority is unavailable");
|
|
286
289
|
}
|
|
287
290
|
|
|
288
|
-
async openSession(cwd: string): Promise<SessionManager> {
|
|
291
|
+
async openSession(cwd: string, sessionMemoryMode: "off" | "shadow" | "enabled" = "shadow"): Promise<SessionManager> {
|
|
289
292
|
const store = this.#artifacts.getManagedStore();
|
|
290
293
|
if (!store) throw new Error("Managed task persistence authority is unavailable");
|
|
291
294
|
this.#artifacts.assertManagedBinding();
|
|
@@ -296,6 +299,7 @@ export class ManagedTaskPersistence {
|
|
|
296
299
|
store,
|
|
297
300
|
undefined,
|
|
298
301
|
cwd,
|
|
302
|
+
sessionMemoryMode,
|
|
299
303
|
);
|
|
300
304
|
this.#artifacts.assertManagedBinding();
|
|
301
305
|
return session;
|
|
@@ -1509,7 +1513,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1509
1513
|
options.parentActiveModelPattern,
|
|
1510
1514
|
modelRegistry,
|
|
1511
1515
|
settings,
|
|
1512
|
-
|
|
1516
|
+
canonicalChildScope,
|
|
1513
1517
|
{ managedFallback: true },
|
|
1514
1518
|
canonicalChildScope,
|
|
1515
1519
|
),
|
|
@@ -1555,10 +1559,18 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1555
1559
|
effectiveThinkingLevelForWarning = effectiveThinkingLevel;
|
|
1556
1560
|
|
|
1557
1561
|
const sessionManager = options.managedPersistence
|
|
1558
|
-
? await awaitAbortable(
|
|
1562
|
+
? await awaitAbortable(
|
|
1563
|
+
options.managedPersistence.openSession(worktree ?? cwd, subagentSettings.get("sessionMemory.mode")),
|
|
1564
|
+
)
|
|
1559
1565
|
: sessionFile
|
|
1560
1566
|
? await awaitAbortable(
|
|
1561
|
-
SessionManager.open(
|
|
1567
|
+
SessionManager.open(
|
|
1568
|
+
sessionFile,
|
|
1569
|
+
SessionManager.explicitDestination(path.dirname(sessionFile)),
|
|
1570
|
+
new FileSessionStorage(),
|
|
1571
|
+
subagentSettings.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain",
|
|
1572
|
+
subagentSettings.get("sessionMemory.mode"),
|
|
1573
|
+
),
|
|
1562
1574
|
)
|
|
1563
1575
|
: SessionManager.inMemory(worktree ?? cwd);
|
|
1564
1576
|
if (options.parentArtifactManager) {
|
|
@@ -1649,6 +1661,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1649
1661
|
authStorage,
|
|
1650
1662
|
modelRegistry,
|
|
1651
1663
|
settings: subagentSettings,
|
|
1664
|
+
providerSessionId: canonicalChildScope,
|
|
1652
1665
|
model,
|
|
1653
1666
|
thinkingLevel: effectiveThinkingLevel,
|
|
1654
1667
|
modelSubstitution:
|
package/src/tools/bash.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
3
3
|
import type { Component } from "@gajae-code/tui";
|
|
4
|
-
import { ImageProtocol, TERMINAL, Text } from "@gajae-code/tui";
|
|
4
|
+
import { getKeybindings, ImageProtocol, TERMINAL, Text, visibleWidth } from "@gajae-code/tui";
|
|
5
5
|
import { getProjectDir, isEnoent, logger, prompt } from "@gajae-code/utils";
|
|
6
6
|
import * as z from "zod/v4";
|
|
7
7
|
import { AsyncJobManager } from "../async";
|
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
} from "../session/streaming-output";
|
|
33
33
|
|
|
34
34
|
import { renderStatusLine } from "../tui";
|
|
35
|
-
import { CachedOutputBlock } from "../tui/output-block";
|
|
35
|
+
import { CachedOutputBlock, getOutputBlockContentWidth } from "../tui/output-block";
|
|
36
|
+
import { truncateToWidth } from "../tui/utils";
|
|
36
37
|
import { getSixelLineMask } from "../utils/sixel";
|
|
37
38
|
import type { ToolSession } from ".";
|
|
38
39
|
import { checkBashAllowedPrefixes, normalizeReadOnlyBashCommand } from "./bash-allowed-prefixes";
|
|
@@ -1635,6 +1636,7 @@ export interface ShellRendererConfig<TArgs> {
|
|
|
1635
1636
|
resolveCommand?: (args: TArgs | undefined) => string | undefined;
|
|
1636
1637
|
resolveCwd?: (args: TArgs | undefined) => string | undefined;
|
|
1637
1638
|
resolveEnv?: (args: TArgs | undefined) => Record<string, string> | undefined;
|
|
1639
|
+
compactCommand?: boolean;
|
|
1638
1640
|
}
|
|
1639
1641
|
|
|
1640
1642
|
function getPartialJson<TArgs>(args: TArgs | undefined): string | undefined {
|
|
@@ -1682,6 +1684,85 @@ export function formatBashCommandLines(args: BashRenderArgs, uiTheme: Theme): st
|
|
|
1682
1684
|
return highlightedLines.map((line, i) => (i === 0 ? `${prefix}${line}` : line));
|
|
1683
1685
|
}
|
|
1684
1686
|
|
|
1687
|
+
const BASH_COLLAPSED_COMMAND_MAX_VISUAL_ROWS = 5;
|
|
1688
|
+
const BASH_COMMAND_SENTINEL_MAX_VISUAL_ROWS = 3;
|
|
1689
|
+
|
|
1690
|
+
interface BashCommandProjection {
|
|
1691
|
+
lines: string[];
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function resolveBashExpandKeyLabel(): string | undefined {
|
|
1695
|
+
return getKeybindings().getKeys("app.tools.expand")[0];
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function formatBashExpandActionHint(maxWidth?: number): string {
|
|
1699
|
+
const keyLabel = resolveBashExpandKeyLabel();
|
|
1700
|
+
const actionHint = keyLabel ? `${keyLabel} to expand` : "expand tools";
|
|
1701
|
+
return maxWidth !== undefined && visibleWidth(actionHint) > Math.max(1, maxWidth) ? "expand tools" : actionHint;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function renderBashCommandProjection(
|
|
1705
|
+
commandLines: string[],
|
|
1706
|
+
width: number,
|
|
1707
|
+
expanded: boolean,
|
|
1708
|
+
actionHint: string,
|
|
1709
|
+
uiTheme: Theme,
|
|
1710
|
+
): BashCommandProjection {
|
|
1711
|
+
const renderWidth = Math.max(1, width);
|
|
1712
|
+
const visualLines = new Text(commandLines.join("\n"), 0, 0).render(renderWidth);
|
|
1713
|
+
if (expanded || visualLines.length <= BASH_COLLAPSED_COMMAND_MAX_VISUAL_ROWS) {
|
|
1714
|
+
return { lines: visualLines };
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
const retained = visualLines.slice(0, BASH_COLLAPSED_COMMAND_MAX_VISUAL_ROWS);
|
|
1718
|
+
const omittedVisualRows = visualLines.length - retained.length;
|
|
1719
|
+
let sentinel = new Text(
|
|
1720
|
+
uiTheme.fg("dim", `… ${omittedVisualRows} command rows omitted\n${actionHint}`),
|
|
1721
|
+
0,
|
|
1722
|
+
0,
|
|
1723
|
+
).render(renderWidth);
|
|
1724
|
+
|
|
1725
|
+
if (sentinel.length > BASH_COMMAND_SENTINEL_MAX_VISUAL_ROWS) {
|
|
1726
|
+
sentinel = new Text(uiTheme.fg("dim", `… ${omittedVisualRows} rows omitted\nexpand tools`), 0, 0).render(
|
|
1727
|
+
renderWidth,
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
if (sentinel.length > BASH_COMMAND_SENTINEL_MAX_VISUAL_ROWS) {
|
|
1731
|
+
sentinel = [
|
|
1732
|
+
uiTheme.fg("dim", truncateToWidth(`… +${omittedVisualRows}`, renderWidth)),
|
|
1733
|
+
uiTheme.fg("dim", truncateToWidth("expand tools", renderWidth)),
|
|
1734
|
+
];
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
return { lines: [...retained, ...sentinel] };
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function createBashCommandProjector(commandLines: string[], uiTheme: Theme) {
|
|
1741
|
+
let cached:
|
|
1742
|
+
| {
|
|
1743
|
+
width: number;
|
|
1744
|
+
expanded: boolean;
|
|
1745
|
+
actionHint: string;
|
|
1746
|
+
projection: BashCommandProjection;
|
|
1747
|
+
}
|
|
1748
|
+
| undefined;
|
|
1749
|
+
|
|
1750
|
+
return {
|
|
1751
|
+
render(width: number, expanded: boolean): BashCommandProjection {
|
|
1752
|
+
const actionHint = formatBashExpandActionHint(width);
|
|
1753
|
+
if (cached?.width === width && cached.expanded === expanded && cached.actionHint === actionHint) {
|
|
1754
|
+
return cached.projection;
|
|
1755
|
+
}
|
|
1756
|
+
const projection = renderBashCommandProjection(commandLines, width, expanded, actionHint, uiTheme);
|
|
1757
|
+
cached = { width, expanded, actionHint, projection };
|
|
1758
|
+
return projection;
|
|
1759
|
+
},
|
|
1760
|
+
invalidate(): void {
|
|
1761
|
+
cached = undefined;
|
|
1762
|
+
},
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1685
1766
|
function toBashRenderArgs<TArgs>(args: TArgs | undefined, config: ShellRendererConfig<TArgs>): BashRenderArgs {
|
|
1686
1767
|
return {
|
|
1687
1768
|
command: config.resolveCommand?.(args),
|
|
@@ -1695,10 +1776,23 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1695
1776
|
return {
|
|
1696
1777
|
renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
1697
1778
|
const renderArgs = toBashRenderArgs(args, config);
|
|
1698
|
-
const cmdText = formatBashCommand(renderArgs);
|
|
1699
1779
|
const title = config.resolveTitle(args, options);
|
|
1700
|
-
|
|
1701
|
-
|
|
1780
|
+
if (!config.compactCommand) {
|
|
1781
|
+
const cmdText = formatBashCommand(renderArgs);
|
|
1782
|
+
const text = renderStatusLine({ icon: "pending", title, description: cmdText }, uiTheme);
|
|
1783
|
+
return new Text(text, 0, 0);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const header = renderStatusLine({ icon: "pending", title }, uiTheme);
|
|
1787
|
+
const projectCommand = createBashCommandProjector(formatBashCommandLines(renderArgs, uiTheme), uiTheme);
|
|
1788
|
+
const renderOptions = options as RenderResultOptions & { renderContext?: BashRenderContext };
|
|
1789
|
+
return {
|
|
1790
|
+
render: (width: number): string[] => {
|
|
1791
|
+
const expanded = renderOptions.renderContext?.expanded ?? renderOptions.expanded;
|
|
1792
|
+
return [header, ...projectCommand.render(width, expanded).lines];
|
|
1793
|
+
},
|
|
1794
|
+
invalidate: () => projectCommand.invalidate(),
|
|
1795
|
+
};
|
|
1702
1796
|
},
|
|
1703
1797
|
|
|
1704
1798
|
renderResult(
|
|
@@ -1713,6 +1807,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1713
1807
|
): Component {
|
|
1714
1808
|
const renderArgs = toBashRenderArgs(args, config);
|
|
1715
1809
|
const cmdLines = args ? formatBashCommandLines(renderArgs, uiTheme) : undefined;
|
|
1810
|
+
const projectCommand =
|
|
1811
|
+
config.compactCommand && cmdLines ? createBashCommandProjector(cmdLines, uiTheme) : undefined;
|
|
1716
1812
|
const isError = result.isError === true;
|
|
1717
1813
|
const icon = options.isPartial ? "pending" : isError ? "error" : "success";
|
|
1718
1814
|
const title = config.resolveTitle(args, options);
|
|
@@ -1726,6 +1822,10 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1726
1822
|
const { renderContext } = options;
|
|
1727
1823
|
const expanded = renderContext?.expanded ?? options.expanded;
|
|
1728
1824
|
const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
|
|
1825
|
+
const commandLines =
|
|
1826
|
+
cmdLines && projectCommand
|
|
1827
|
+
? projectCommand.render(getOutputBlockContentWidth(width, uiTheme), expanded).lines
|
|
1828
|
+
: (cmdLines ?? []);
|
|
1729
1829
|
|
|
1730
1830
|
// Get output from context (preferred) or fall back to result content.
|
|
1731
1831
|
// Strip the LLM-facing notice appended by wrappedExecute so we don't
|
|
@@ -1778,7 +1878,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1778
1878
|
outputLines.push(
|
|
1779
1879
|
uiTheme.fg(
|
|
1780
1880
|
"dim",
|
|
1781
|
-
`… (${result.skippedCount} earlier lines, showing ${result.visualLines.length} of ${result.skippedCount + result.visualLines.length}) (
|
|
1881
|
+
`… (${result.skippedCount} earlier lines, showing ${result.visualLines.length} of ${result.skippedCount + result.visualLines.length}) (${formatBashExpandActionHint(getOutputBlockContentWidth(width, uiTheme))})`,
|
|
1782
1882
|
),
|
|
1783
1883
|
);
|
|
1784
1884
|
}
|
|
@@ -1793,7 +1893,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1793
1893
|
header,
|
|
1794
1894
|
state: options.isPartial ? "pending" : isError ? "error" : "success",
|
|
1795
1895
|
sections: [
|
|
1796
|
-
{ lines:
|
|
1896
|
+
{ lines: commandLines },
|
|
1797
1897
|
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
1798
1898
|
],
|
|
1799
1899
|
width,
|
|
@@ -1803,6 +1903,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1803
1903
|
},
|
|
1804
1904
|
invalidate: () => {
|
|
1805
1905
|
outputBlock.invalidate();
|
|
1906
|
+
projectCommand?.invalidate();
|
|
1806
1907
|
},
|
|
1807
1908
|
};
|
|
1808
1909
|
},
|
|
@@ -1816,4 +1917,5 @@ export const bashToolRenderer = createShellRenderer<BashRenderArgs>({
|
|
|
1816
1917
|
resolveCommand: args => args?.command,
|
|
1817
1918
|
resolveCwd: args => args?.cwd,
|
|
1818
1919
|
resolveEnv: args => args?.env,
|
|
1920
|
+
compactCommand: true,
|
|
1819
1921
|
});
|
package/src/tools/find.ts
CHANGED
|
@@ -59,6 +59,25 @@ const DEFAULT_GLOB_TIMEOUT_MS = 5000;
|
|
|
59
59
|
const MIN_GLOB_TIMEOUT_MS = 500;
|
|
60
60
|
const MAX_GLOB_TIMEOUT_MS = 60_000;
|
|
61
61
|
|
|
62
|
+
function comparePaths(left: string, right: string): number {
|
|
63
|
+
let leftIndex = 0;
|
|
64
|
+
let rightIndex = 0;
|
|
65
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
66
|
+
const leftCodePoint = left.codePointAt(leftIndex);
|
|
67
|
+
const rightCodePoint = right.codePointAt(rightIndex);
|
|
68
|
+
if (leftCodePoint === undefined || rightCodePoint === undefined) break;
|
|
69
|
+
if (leftCodePoint !== rightCodePoint) return leftCodePoint - rightCodePoint;
|
|
70
|
+
leftIndex += leftCodePoint > 0xffff ? 2 : 1;
|
|
71
|
+
rightIndex += rightCodePoint > 0xffff ? 2 : 1;
|
|
72
|
+
}
|
|
73
|
+
return left.length - right.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function compareMtimePath(left: { path: string; mtime?: number }, right: { path: string; mtime?: number }): number {
|
|
77
|
+
const mtimeOrder = (right.mtime ?? 0) - (left.mtime ?? 0);
|
|
78
|
+
return mtimeOrder || comparePaths(left.path, right.path);
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/**
|
|
63
82
|
* Reject comma-separated path lists packed into a single array element
|
|
64
83
|
* (`["a.py,b.py"]`). The schema is array-of-string; agents that pass a
|
|
@@ -317,29 +336,43 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
317
336
|
}
|
|
318
337
|
|
|
319
338
|
let matches: natives.GlobMatch[];
|
|
320
|
-
const onUpdateMatches
|
|
339
|
+
const onUpdateMatches = new Map<string, number>();
|
|
340
|
+
const rankedUpdateMatches = (): Array<{ path: string; mtime: number }> =>
|
|
341
|
+
Array.from(onUpdateMatches, ([matchPath, mtime]) => ({ path: matchPath, mtime })).sort(compareMtimePath);
|
|
342
|
+
const retainUpdateMatch = (matchPath: string, mtime: number) => {
|
|
343
|
+
onUpdateMatches.set(matchPath, mtime);
|
|
344
|
+
if (onUpdateMatches.size <= effectiveLimit) return;
|
|
345
|
+
let worst: { path: string; mtime: number } | undefined;
|
|
346
|
+
for (const [candidatePath, candidateMtime] of onUpdateMatches) {
|
|
347
|
+
const candidate = { path: candidatePath, mtime: candidateMtime };
|
|
348
|
+
if (!worst || compareMtimePath(candidate, worst) > 0) worst = candidate;
|
|
349
|
+
}
|
|
350
|
+
if (worst) onUpdateMatches.delete(worst.path);
|
|
351
|
+
};
|
|
321
352
|
const updateIntervalMs = 200;
|
|
322
353
|
let lastUpdate = 0;
|
|
354
|
+
let terminal = false;
|
|
323
355
|
const emitUpdate = () => {
|
|
324
356
|
if (!onUpdate) return;
|
|
325
357
|
const now = Date.now();
|
|
326
358
|
if (now - lastUpdate < updateIntervalMs) return;
|
|
327
359
|
lastUpdate = now;
|
|
360
|
+
const ranked = rankedUpdateMatches().map(entry => entry.path);
|
|
328
361
|
const details: FindToolDetails = {
|
|
329
362
|
scopePath,
|
|
330
|
-
fileCount:
|
|
331
|
-
files:
|
|
363
|
+
fileCount: ranked.length,
|
|
364
|
+
files: ranked,
|
|
332
365
|
truncated: false,
|
|
333
366
|
};
|
|
334
367
|
onUpdate({
|
|
335
|
-
content: [{ type: "text", text:
|
|
368
|
+
content: [{ type: "text", text: ranked.join("\n") }],
|
|
336
369
|
details,
|
|
337
370
|
});
|
|
338
371
|
};
|
|
339
372
|
const onMatch = (err: Error | null, match: natives.GlobMatch | null) => {
|
|
340
|
-
if (err ||
|
|
373
|
+
if (err || terminal || combinedSignal.aborted || !match?.path) return;
|
|
341
374
|
const relativePath = formatMatchPath(match.path, match.fileType);
|
|
342
|
-
|
|
375
|
+
retainUpdateMatch(relativePath, match.mtime ?? 0);
|
|
343
376
|
emitUpdate();
|
|
344
377
|
};
|
|
345
378
|
|
|
@@ -355,6 +388,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
355
388
|
sortByMtime: true,
|
|
356
389
|
gitignore: useGitignore,
|
|
357
390
|
signal: combinedSignal,
|
|
391
|
+
timeoutMs,
|
|
358
392
|
},
|
|
359
393
|
onMatch,
|
|
360
394
|
),
|
|
@@ -363,34 +397,29 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
363
397
|
let timedOut = false;
|
|
364
398
|
try {
|
|
365
399
|
const result = await doGlob(useGitignore);
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
result.matches.sort((a, b) => (b.mtime ?? 0) - (a.mtime ?? 0));
|
|
400
|
+
// Native uncached glob retains a bounded newest set; keep the same tie-break after callback delivery.
|
|
401
|
+
result.matches.sort(compareMtimePath);
|
|
369
402
|
matches = result.matches;
|
|
370
403
|
} catch (error) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
404
|
+
const nativeTimedOut = error instanceof Error && error.message === "Aborted: Timeout";
|
|
405
|
+
if (
|
|
406
|
+
(error instanceof Error && error.name === "AbortError" && timeoutSignal.aborted && !signal?.aborted) ||
|
|
407
|
+
(nativeTimedOut && !signal?.aborted)
|
|
408
|
+
) {
|
|
409
|
+
timedOut = true;
|
|
410
|
+
matches = [];
|
|
411
|
+
} else if (error instanceof Error && error.name === "AbortError") {
|
|
412
|
+
throw new ToolAbortError();
|
|
378
413
|
} else {
|
|
379
414
|
throw error;
|
|
380
415
|
}
|
|
416
|
+
} finally {
|
|
417
|
+
terminal = true;
|
|
381
418
|
}
|
|
382
419
|
|
|
383
420
|
if (timedOut) {
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
// caller to retry blind, which is the worst possible outcome.
|
|
387
|
-
const seen = new Set<string>();
|
|
388
|
-
const partial: string[] = [];
|
|
389
|
-
for (const entry of onUpdateMatches) {
|
|
390
|
-
if (seen.has(entry)) continue;
|
|
391
|
-
seen.add(entry);
|
|
392
|
-
partial.push(entry);
|
|
393
|
-
}
|
|
421
|
+
// Return the bounded, deterministically ranked candidates observed before timeout.
|
|
422
|
+
const partial = rankedUpdateMatches().map(entry => entry.path);
|
|
394
423
|
const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
|
|
395
424
|
const notice = `find timed out after ${seconds}s; returning ${partial.length} partial matches — increase timeout or narrow pattern`;
|
|
396
425
|
return buildResult(partial, { notice, forceTruncated: true });
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* tool ever loaded, with no correction hint to recover from.
|
|
9
9
|
*/
|
|
10
10
|
import { INTENT_FIELD } from "@gajae-code/agent-core";
|
|
11
|
-
import type { RawArgumentValidationResult } from "@gajae-code/ai/types";
|
|
11
|
+
import type { RawArgumentRejectionDetail, RawArgumentValidationResult } from "@gajae-code/ai/types";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Models reliably reach for `complete`/`completed` because the status an op
|
|
@@ -34,8 +34,27 @@ const TODO_INIT_ENTRY_KEYS = new Set(["phase", "items"]);
|
|
|
34
34
|
/** `content` is accepted only as a synonym the schema rewrites to `task`. */
|
|
35
35
|
const TODO_OP_KEYS_WITH_ALIASES = new Set([...TODO_OP_KEYS, "content"]);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Exact key corrections. `note` is the one repeatable confusion: it is an `op`
|
|
39
|
+
* value, and the body of a note op goes in `text`, so a caller who wrote
|
|
40
|
+
* `note: "..."` on an entry can be told exactly which fields a note op takes.
|
|
41
|
+
* The correction names both required fields rather than a literal rewrite,
|
|
42
|
+
* because a rewrite that omits `task` is rejected again on the retry. Only add
|
|
43
|
+
* an entry here when the replacement is unambiguous — never from fuzzy or
|
|
44
|
+
* edit-distance matching, because a wrong suggestion costs more turns than no
|
|
45
|
+
* suggestion.
|
|
46
|
+
*/
|
|
47
|
+
const TODO_KEY_CORRECTIONS = new Map<string, string>([
|
|
48
|
+
["note", 'note is an op, not a key; note operations require both "task" and "text"'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function unknownKeys(value: object, allowed: Set<string>): string[] {
|
|
52
|
+
return Object.keys(value).filter(key => !allowed.has(key));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function keyRejectionDetail(keys: readonly string[]): RawArgumentRejectionDetail {
|
|
56
|
+
const hints = keys.map(key => TODO_KEY_CORRECTIONS.get(key)).filter((hint): hint is string => hint !== undefined);
|
|
57
|
+
return hints.length > 0 ? { rejectedKeys: keys, hint: hints.join("; ") } : { rejectedKeys: keys };
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -48,14 +67,21 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
48
67
|
* instead of a bare "raw arguments rejected" message.
|
|
49
68
|
*/
|
|
50
69
|
export function validateRawTodoArguments(arguments_: Record<string, unknown>): RawArgumentValidationResult {
|
|
51
|
-
|
|
70
|
+
const unknownRootKeys = unknownKeys(arguments_, TODO_WRITE_KEYS);
|
|
71
|
+
if (unknownRootKeys.length > 0)
|
|
72
|
+
return { outcome: "reject", code: "todo-write-unknown-root-key", detail: keyRejectionDetail(unknownRootKeys) };
|
|
52
73
|
if (!Array.isArray(arguments_.ops)) return { outcome: "passthrough" };
|
|
53
74
|
for (const entry of arguments_.ops) {
|
|
54
75
|
if (!isPlainRecord(entry)) continue;
|
|
55
76
|
// `content` is normalized to `task` by the schema preprocessor, so it must
|
|
56
77
|
// not be rejected here as an unknown key first.
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
const unknownEntryKeys = unknownKeys(entry, TODO_OP_KEYS_WITH_ALIASES);
|
|
79
|
+
if (unknownEntryKeys.length > 0)
|
|
80
|
+
return {
|
|
81
|
+
outcome: "reject",
|
|
82
|
+
code: "todo-write-unknown-op-entry-key",
|
|
83
|
+
detail: keyRejectionDetail(unknownEntryKeys),
|
|
84
|
+
};
|
|
59
85
|
const op = typeof entry.op === "string" && isDoneAlias(entry.op) ? "done" : entry.op;
|
|
60
86
|
const target = entry.task ?? entry.content;
|
|
61
87
|
if ((op === "done" || op === "drop") && !target && !entry.phase) {
|
|
@@ -64,8 +90,14 @@ export function validateRawTodoArguments(arguments_: Record<string, unknown>): R
|
|
|
64
90
|
const list = entry.list;
|
|
65
91
|
if (!Array.isArray(list)) continue;
|
|
66
92
|
for (const item of list) {
|
|
67
|
-
if (isPlainRecord(item)
|
|
68
|
-
|
|
93
|
+
if (!isPlainRecord(item)) continue;
|
|
94
|
+
const unknownItemKeys = unknownKeys(item, TODO_INIT_ENTRY_KEYS);
|
|
95
|
+
if (unknownItemKeys.length > 0)
|
|
96
|
+
return {
|
|
97
|
+
outcome: "reject",
|
|
98
|
+
code: "todo-write-unknown-init-entry-key",
|
|
99
|
+
detail: keyRejectionDetail(unknownItemKeys),
|
|
100
|
+
};
|
|
69
101
|
}
|
|
70
102
|
}
|
|
71
103
|
return { outcome: "passthrough" };
|
package/src/tui/output-block.ts
CHANGED
|
@@ -23,6 +23,16 @@ export interface OutputBlockOptions {
|
|
|
23
23
|
width: number;
|
|
24
24
|
applyBg?: boolean;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Return the width available to section content after the output block's
|
|
28
|
+
* visible left and right borders. Keep callers on the same layout contract as
|
|
29
|
+
* renderOutputBlock so width-aware projections are not wrapped a second time.
|
|
30
|
+
*/
|
|
31
|
+
export function getOutputBlockContentWidth(width: number, theme: Theme): number {
|
|
32
|
+
const lineWidth = Math.max(0, width);
|
|
33
|
+
const borderWidth = visibleWidth(`${theme.boxSharp.vertical} `) + visibleWidth(theme.boxSharp.vertical);
|
|
34
|
+
return Math.max(0, lineWidth - borderWidth);
|
|
35
|
+
}
|
|
26
36
|
|
|
27
37
|
export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
|
|
28
38
|
const { header, headerMeta, state, sections = [], width, applyBg = true } = options;
|
|
@@ -70,7 +80,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
70
80
|
|
|
71
81
|
const contentPrefix = border(`${v} `);
|
|
72
82
|
const contentSuffix = border(v);
|
|
73
|
-
const contentWidth =
|
|
83
|
+
const contentWidth = getOutputBlockContentWidth(lineWidth, theme);
|
|
74
84
|
const lines: string[] = [];
|
|
75
85
|
|
|
76
86
|
lines.push(
|