@sayknow-cli/coding-agent 0.3.5 → 0.3.7
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/README.md +76 -0
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/telegram-cli.d.ts +13 -0
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/commands/telegram.d.ts +22 -0
- package/dist/types/config/keybindings.d.ts +12 -6
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +284 -4
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/config/telegram-autostart.d.ts +14 -0
- package/dist/types/config/telegram-env-bridge.d.ts +18 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/defaults/skc-defaults.d.ts +7 -0
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -1
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +5 -1
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +15 -3
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +6 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +4 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/skc-runtime/state-writer.d.ts +1 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/utils/cmux-workspace.d.ts +52 -0
- package/dist/types/web/search/index.d.ts +4 -2
- package/dist/types/web/search/provider.d.ts +14 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/web/search/render.d.ts +1 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +0 -5
- package/src/capability/mcp.ts +2 -0
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/telegram-cli.ts +156 -0
- package/src/cli/update-cli.ts +179 -13
- package/src/cli/web-search-cli.ts +5 -7
- package/src/cli.ts +20 -1
- package/src/commands/launch.ts +47 -2
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/commands/telegram.ts +47 -0
- package/src/config/keybindings.ts +15 -6
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +256 -4
- package/src/config/settings.ts +11 -0
- package/src/config/telegram-autostart.ts +107 -0
- package/src/config/telegram-env-bridge.ts +92 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
- package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
- package/src/defaults/skc-defaults.ts +16 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +30 -2
- package/src/hooks/skill-state.ts +33 -73
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.settings.ts +28 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.settings.ts +28 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.settings.ts +28 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -7
- package/src/main.ts +5 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/agent-dashboard.ts +3 -35
- package/src/modes/components/custom-editor.ts +26 -0
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +246 -11
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +551 -11
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +16 -19
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +281 -117
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +165 -30
- package/src/modes/controllers/command-controller.ts +26 -2
- package/src/modes/controllers/event-controller.ts +102 -4
- package/src/modes/controllers/extension-ui-controller.ts +21 -2
- package/src/modes/controllers/input-controller.ts +214 -47
- package/src/modes/controllers/selector-controller.ts +35 -33
- package/src/modes/interactive-mode.ts +36 -30
- package/src/modes/prompt-action-autocomplete.ts +48 -8
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/theme/theme.ts +5 -1
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +32 -2
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +125 -13
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +88 -46
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/system/system-prompt.md +20 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +10 -11
- package/src/session/agent-session.ts +103 -13
- package/src/session/session-manager.ts +58 -18
- package/src/skc-runtime/launch-tmux.ts +34 -8
- package/src/skc-runtime/launch-worktree.ts +16 -2
- package/src/skc-runtime/ralplan-runtime.ts +22 -6
- package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/skc-runtime/session-state-sidecar.ts +167 -24
- package/src/skc-runtime/state-runtime.ts +8 -5
- package/src/skc-runtime/state-writer.ts +5 -5
- package/src/skc-runtime/team-runtime.ts +21 -6
- package/src/skc-runtime/tmux-common.ts +16 -0
- package/src/skc-runtime/tmux-sessions.ts +13 -9
- package/src/skc-runtime/ultragoal-guard.ts +150 -24
- package/src/skc-runtime/ultragoal-runtime.ts +16 -124
- package/src/skc-runtime/workflow-manifest.generated.json +0 -8
- package/src/skc-runtime/workflow-manifest.ts +0 -1
- package/src/slash-commands/builtin-registry.ts +136 -23
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +39 -14
- package/src/tools/index.ts +8 -1
- package/src/tools/skill.ts +1 -1
- package/src/utils/cmux-workspace.ts +233 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +137 -52
- package/src/web/search/provider.ts +80 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
- package/src/web/search/render.ts +3 -0
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/vendor/insane-search/MANIFEST.json +1 -3
- package/vendor/insane-search/engine/tests/test_u1.py +0 -28
- package/vendor/insane-search/engine/transport.py +2 -3
- package/vendor/insane-search/engine/validators.py +0 -14
- package/src/defaults/skc/rules/ponytail.md +0 -68
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { logger } from "@sayknow-cli/utils";
|
|
2
|
+
|
|
3
|
+
const CMUX_COMMAND = "cmux";
|
|
4
|
+
const CMUX_WORKSPACE_ID_ENV = "CMUX_WORKSPACE_ID";
|
|
5
|
+
const CMUX_NO_RENAME_ENV = "SKC_NO_CMUX_RENAME";
|
|
6
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
7
|
+
const CMUX_WORKSPACE_TITLE_PREFIX = "SKC: ";
|
|
8
|
+
const CMUX_WORKSPACE_RENAME_TIMEOUT_MS = 1500;
|
|
9
|
+
const CMUX_WORKSPACE_LIST_TIMEOUT_MS = 1500;
|
|
10
|
+
|
|
11
|
+
export interface CmuxWorkspaceRenameCommand {
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Current ownership state of a cmux workspace, read back from `cmux workspace list`. */
|
|
17
|
+
export interface CmuxWorkspaceOwnership {
|
|
18
|
+
/** cmux marks a workspace `has_custom_title` once an explicit title is set (by the user or by SKC). */
|
|
19
|
+
hasCustomTitle: boolean;
|
|
20
|
+
/** The workspace's current display title. */
|
|
21
|
+
title: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CmuxWorkspaceRenameProcess {
|
|
25
|
+
exited: Promise<number>;
|
|
26
|
+
kill(): void;
|
|
27
|
+
unref(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CmuxWorkspaceTitleSyncOptions {
|
|
31
|
+
env?: NodeJS.ProcessEnv;
|
|
32
|
+
isTty?: boolean;
|
|
33
|
+
which?: (command: string) => string | null;
|
|
34
|
+
spawn?: (
|
|
35
|
+
command: string[],
|
|
36
|
+
options: { env: NodeJS.ProcessEnv; stdin: "ignore"; stdout: "ignore"; stderr: "ignore" },
|
|
37
|
+
) => CmuxWorkspaceRenameProcess;
|
|
38
|
+
/** Reads the current ownership state of `workspaceId`. Injectable for tests. */
|
|
39
|
+
readOwnership?: (
|
|
40
|
+
cmuxCommand: string,
|
|
41
|
+
workspaceId: string,
|
|
42
|
+
env: NodeJS.ProcessEnv,
|
|
43
|
+
) => Promise<CmuxWorkspaceOwnership | null>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function defaultSpawn(
|
|
47
|
+
command: string[],
|
|
48
|
+
options: { env: NodeJS.ProcessEnv; stdin: "ignore"; stdout: "ignore"; stderr: "ignore" },
|
|
49
|
+
): CmuxWorkspaceRenameProcess {
|
|
50
|
+
return Bun.spawn(command, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isEnvSet(value: string | undefined): boolean {
|
|
54
|
+
if (!value) return false;
|
|
55
|
+
const normalized = value.trim().toLowerCase();
|
|
56
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function sanitizeCmuxWorkspaceTitle(title: string | undefined): string | undefined {
|
|
60
|
+
if (!title) return undefined;
|
|
61
|
+
const sanitized = title.replace(CONTROL_CHARS, " ").replace(/\s+/g, " ").trim();
|
|
62
|
+
return sanitized || undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function formatCmuxWorkspaceTitle(title: string | undefined): string | undefined {
|
|
66
|
+
const sanitized = sanitizeCmuxWorkspaceTitle(title);
|
|
67
|
+
if (!sanitized) return undefined;
|
|
68
|
+
return sanitized.startsWith(CMUX_WORKSPACE_TITLE_PREFIX) ? sanitized : `${CMUX_WORKSPACE_TITLE_PREFIX}${sanitized}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildCmuxWorkspaceRenameCommand(
|
|
72
|
+
sessionName: string | undefined,
|
|
73
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
74
|
+
): CmuxWorkspaceRenameCommand | null {
|
|
75
|
+
const workspaceId = env[CMUX_WORKSPACE_ID_ENV]?.trim();
|
|
76
|
+
if (!workspaceId) return null;
|
|
77
|
+
|
|
78
|
+
const title = formatCmuxWorkspaceTitle(sessionName);
|
|
79
|
+
if (!title) return null;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
command: CMUX_COMMAND,
|
|
83
|
+
args: ["workspace", "rename", workspaceId, "--title", title],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Parse `cmux workspace list --json --id-format both` output and return the
|
|
88
|
+
* ownership state of the workspace matching `workspaceId` (by UUID `id` or `ref`). */
|
|
89
|
+
export function parseCmuxWorkspaceOwnership(jsonText: string, workspaceId: string): CmuxWorkspaceOwnership | null {
|
|
90
|
+
const target = workspaceId.trim().toLowerCase();
|
|
91
|
+
if (!target) return null;
|
|
92
|
+
|
|
93
|
+
let parsed: unknown;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(jsonText);
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const workspaces = (parsed as { workspaces?: unknown }).workspaces;
|
|
101
|
+
if (!Array.isArray(workspaces)) return null;
|
|
102
|
+
|
|
103
|
+
for (const entry of workspaces) {
|
|
104
|
+
if (!entry || typeof entry !== "object") continue;
|
|
105
|
+
const record = entry as Record<string, unknown>;
|
|
106
|
+
const id = typeof record.id === "string" ? record.id.toLowerCase() : "";
|
|
107
|
+
const ref = typeof record.ref === "string" ? record.ref.toLowerCase() : "";
|
|
108
|
+
if (id !== target && ref !== target) continue;
|
|
109
|
+
return {
|
|
110
|
+
hasCustomTitle: record.has_custom_title === true,
|
|
111
|
+
title: typeof record.title === "string" ? record.title : "",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Only rename when SKC owns the name:
|
|
118
|
+
* - unknown ownership (read failed) → skip (fail safe, never clobber)
|
|
119
|
+
* - already the desired title → skip (no-op)
|
|
120
|
+
* - workspace still on its default title → rename
|
|
121
|
+
* - any custom title (user- or peer-set) → skip
|
|
122
|
+
* This makes SKC name a fresh workspace once and then leave it alone, so it
|
|
123
|
+
* never overwrites a user-pinned name and multiple sessions sharing one
|
|
124
|
+
* CMUX_WORKSPACE_ID do not thrash the workspace title. */
|
|
125
|
+
export function shouldRenameCmuxWorkspace(ownership: CmuxWorkspaceOwnership | null, desiredTitle: string): boolean {
|
|
126
|
+
if (!ownership) return false;
|
|
127
|
+
if ((sanitizeCmuxWorkspaceTitle(ownership.title) ?? "") === desiredTitle) return false;
|
|
128
|
+
return !ownership.hasCustomTitle;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function defaultReadOwnership(
|
|
132
|
+
cmuxCommand: string,
|
|
133
|
+
workspaceId: string,
|
|
134
|
+
env: NodeJS.ProcessEnv,
|
|
135
|
+
): Promise<CmuxWorkspaceOwnership | null> {
|
|
136
|
+
try {
|
|
137
|
+
const proc = Bun.spawn([cmuxCommand, "workspace", "list", "--json", "--id-format", "both"], {
|
|
138
|
+
env: { ...env, CMUX_QUIET: "1" },
|
|
139
|
+
stdin: "ignore",
|
|
140
|
+
stdout: "pipe",
|
|
141
|
+
stderr: "ignore",
|
|
142
|
+
});
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
try {
|
|
145
|
+
proc.kill();
|
|
146
|
+
} catch {}
|
|
147
|
+
}, CMUX_WORKSPACE_LIST_TIMEOUT_MS);
|
|
148
|
+
timer.unref?.();
|
|
149
|
+
const text = await new Response(proc.stdout).text();
|
|
150
|
+
await proc.exited;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
return parseCmuxWorkspaceOwnership(text, workspaceId);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.debug("cmux workspace list failed", { error: String(error) });
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Best-effort sync of the containing cmux workspace title to the current SKC
|
|
161
|
+
* session name. Ownership-guarded: SKC reads the current workspace title and
|
|
162
|
+
* only renames a workspace that still has its default title, so it never
|
|
163
|
+
* overwrites a name the user pinned or a name a peer session (sharing the same
|
|
164
|
+
* CMUX_WORKSPACE_ID) set. Opt out with SKC_NO_CMUX_RENAME.
|
|
165
|
+
*/
|
|
166
|
+
export async function syncCmuxWorkspaceTitle(
|
|
167
|
+
sessionName: string | undefined,
|
|
168
|
+
options: CmuxWorkspaceTitleSyncOptions = {},
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const env = options.env ?? process.env;
|
|
171
|
+
if (isEnvSet(env[CMUX_NO_RENAME_ENV])) return;
|
|
172
|
+
|
|
173
|
+
const isTty = options.isTty ?? process.stdout.isTTY === true;
|
|
174
|
+
if (!isTty) return;
|
|
175
|
+
|
|
176
|
+
const workspaceId = env[CMUX_WORKSPACE_ID_ENV]?.trim();
|
|
177
|
+
if (!workspaceId) return;
|
|
178
|
+
|
|
179
|
+
const desired = formatCmuxWorkspaceTitle(sessionName);
|
|
180
|
+
if (!desired) return;
|
|
181
|
+
|
|
182
|
+
const which = options.which ?? Bun.which;
|
|
183
|
+
let resolvedCommand: string | null;
|
|
184
|
+
try {
|
|
185
|
+
resolvedCommand = which(CMUX_COMMAND);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.debug("cmux workspace rename command lookup failed", { error: String(error) });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!resolvedCommand) return;
|
|
191
|
+
|
|
192
|
+
let ownership: CmuxWorkspaceOwnership | null;
|
|
193
|
+
try {
|
|
194
|
+
const readOwnership = options.readOwnership ?? defaultReadOwnership;
|
|
195
|
+
ownership = await readOwnership(resolvedCommand, workspaceId, env);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.debug("cmux workspace ownership read failed", { error: String(error) });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!shouldRenameCmuxWorkspace(ownership, desired)) return;
|
|
202
|
+
|
|
203
|
+
const plan = buildCmuxWorkspaceRenameCommand(sessionName, env);
|
|
204
|
+
if (!plan) return;
|
|
205
|
+
|
|
206
|
+
const spawn = options.spawn ?? defaultSpawn;
|
|
207
|
+
try {
|
|
208
|
+
const proc = spawn([resolvedCommand, ...plan.args], {
|
|
209
|
+
env,
|
|
210
|
+
stdin: "ignore",
|
|
211
|
+
stdout: "ignore",
|
|
212
|
+
stderr: "ignore",
|
|
213
|
+
});
|
|
214
|
+
proc.unref();
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
try {
|
|
217
|
+
proc.kill();
|
|
218
|
+
} catch {}
|
|
219
|
+
}, CMUX_WORKSPACE_RENAME_TIMEOUT_MS);
|
|
220
|
+
timer.unref?.();
|
|
221
|
+
void proc.exited
|
|
222
|
+
.then(exitCode => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
if (exitCode !== 0) logger.debug("cmux workspace rename exited non-zero", { exitCode });
|
|
225
|
+
})
|
|
226
|
+
.catch(error => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
logger.debug("cmux workspace rename failed", { error: String(error) });
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.debug("cmux workspace rename failed to start", { error: String(error) });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { ModelRegistry } from "../config/model-registry";
|
|
|
9
9
|
import { resolveRoleSelection } from "../config/model-resolver";
|
|
10
10
|
import type { Settings } from "../config/settings";
|
|
11
11
|
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
|
|
12
|
+
import { syncCmuxWorkspaceTitle } from "./cmux-workspace";
|
|
12
13
|
|
|
13
14
|
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
14
15
|
|
|
@@ -218,6 +219,7 @@ export function setTerminalTitle(title: string): void {
|
|
|
218
219
|
|
|
219
220
|
export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
|
|
220
221
|
setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd));
|
|
222
|
+
void syncCmuxWorkspaceTitle(sessionName);
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
/**
|
package/src/web/kagi.ts
CHANGED
|
@@ -147,7 +147,7 @@ export async function searchWithKagi(
|
|
|
147
147
|
|
|
148
148
|
const response = await fetch(requestUrl, {
|
|
149
149
|
headers: getAuthHeaders(apiKey),
|
|
150
|
-
signal: withHardTimeout(options.signal),
|
|
150
|
+
signal: withHardTimeout(options.signal, "api"),
|
|
151
151
|
});
|
|
152
152
|
if (!response.ok) {
|
|
153
153
|
throw parseKagiErrorResponse(response.status, await response.text());
|
package/src/web/parallel.ts
CHANGED
|
@@ -306,7 +306,7 @@ export async function searchWithParallel(
|
|
|
306
306
|
max_chars_per_result: options.maxCharsPerResult ?? 10_000,
|
|
307
307
|
},
|
|
308
308
|
}),
|
|
309
|
-
signal: withHardTimeout(options.signal),
|
|
309
|
+
signal: withHardTimeout(options.signal, "api"),
|
|
310
310
|
});
|
|
311
311
|
if (!response.ok) {
|
|
312
312
|
throw parseParallelErrorResponse(response.status, await response.text());
|
|
@@ -338,7 +338,7 @@ export async function extractWithParallel(
|
|
|
338
338
|
excerpts: options.excerpts ?? true,
|
|
339
339
|
full_content: options.fullContent ?? false,
|
|
340
340
|
}),
|
|
341
|
-
signal: withHardTimeout(options.signal),
|
|
341
|
+
signal: withHardTimeout(options.signal, "api"),
|
|
342
342
|
});
|
|
343
343
|
if (!response.ok) {
|
|
344
344
|
throw parseParallelErrorResponse(response.status, await response.text());
|
package/src/web/search/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { discoverAuthStorage } from "../../sdk";
|
|
|
16
16
|
import type { ToolSession } from "../../tools";
|
|
17
17
|
import { formatAge } from "../../tools/render-utils";
|
|
18
18
|
import { throwIfAborted } from "../../tools/tool-errors";
|
|
19
|
-
import { getSearchProviderLabel, resolveProviderChain, type SearchProvider } from "./provider";
|
|
19
|
+
import { getSearchProviderLabel, prewarmSearchProviders, resolveProviderChain, type SearchProvider } from "./provider";
|
|
20
20
|
import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
|
|
21
21
|
import type { ActiveSearchModelContext, SearchProviderId, SearchResponse } from "./types";
|
|
22
22
|
import { SearchProviderError } from "./types";
|
|
@@ -68,6 +68,18 @@ function formatProviderError(error: unknown, provider: SearchProvider): string {
|
|
|
68
68
|
return `Unknown error from ${provider.label}`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function formatProviderFailure(error: unknown, provider: SearchProvider): string {
|
|
72
|
+
if (error instanceof SearchProviderError) return error.message;
|
|
73
|
+
return `${provider.id}: ${formatProviderError(error, provider)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatFallbackWarning(
|
|
77
|
+
failures: Array<{ provider: SearchProvider; error: unknown }>,
|
|
78
|
+
provider: SearchProvider,
|
|
79
|
+
): string {
|
|
80
|
+
return `Web search provider fallback: ${failures.map(f => formatProviderFailure(f.error, f.provider)).join("; ")}; using ${provider.label}.`;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
/** Truncate text for tool output */
|
|
72
84
|
function truncateText(text: string, maxLen: number): string {
|
|
73
85
|
if (text.length <= maxLen) return text;
|
|
@@ -136,6 +148,22 @@ interface ExecuteSearchOptions {
|
|
|
136
148
|
activeModelContext?: ActiveSearchModelContext;
|
|
137
149
|
}
|
|
138
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Delay before the keyless DuckDuckGo hedge fires while a slower primary is
|
|
153
|
+
* still in flight. Chosen to be comfortably above DDG's typical ~1-2s
|
|
154
|
+
* round-trip cost budget yet far below LLM-mediated primary latency, so a
|
|
155
|
+
* failing primary can fall back to an already-completed hedge result instead
|
|
156
|
+
* of paying primary-failure time plus DDG time sequentially.
|
|
157
|
+
*/
|
|
158
|
+
const DDG_HEDGE_DELAY_MS = 3_000;
|
|
159
|
+
|
|
160
|
+
let ddgHedgeDelayMs = DDG_HEDGE_DELAY_MS;
|
|
161
|
+
|
|
162
|
+
/** Override the hedge delay (tests). Non-finite/non-positive resets to default. */
|
|
163
|
+
export function setDdgHedgeDelayMs(ms: number | undefined): void {
|
|
164
|
+
ddgHedgeDelayMs = typeof ms === "number" && Number.isFinite(ms) && ms > 0 ? ms : DDG_HEDGE_DELAY_MS;
|
|
165
|
+
}
|
|
166
|
+
|
|
139
167
|
/** Execute web search */
|
|
140
168
|
async function executeSearch(
|
|
141
169
|
_toolCallId: string,
|
|
@@ -154,51 +182,91 @@ async function executeSearch(
|
|
|
154
182
|
activeModelContext,
|
|
155
183
|
});
|
|
156
184
|
|
|
185
|
+
const baseSearchParams = {
|
|
186
|
+
query: params.query.replace(/202\d/g, String(new Date().getFullYear())), // LUL
|
|
187
|
+
limit: params.limit,
|
|
188
|
+
recency: params.recency,
|
|
189
|
+
systemPrompt: webSearchSystemPrompt,
|
|
190
|
+
maxOutputTokens: params.max_tokens,
|
|
191
|
+
numSearchResults: params.num_search_results,
|
|
192
|
+
temperature: params.temperature,
|
|
193
|
+
xaiSearchMode: params.xai_search_mode,
|
|
194
|
+
allowedDomains: params.allowed_domains,
|
|
195
|
+
excludedDomains: params.excluded_domains,
|
|
196
|
+
allowedXHandles: params.allowed_x_handles,
|
|
197
|
+
excludedXHandles: params.excluded_x_handles,
|
|
198
|
+
fromDate: params.from_date,
|
|
199
|
+
toDate: params.to_date,
|
|
200
|
+
enableImageUnderstanding: params.enable_image_understanding,
|
|
201
|
+
enableImageSearch: params.enable_image_search,
|
|
202
|
+
enableVideoUnderstanding: params.enable_video_understanding,
|
|
203
|
+
noInlineCitations: params.no_inline_citations,
|
|
204
|
+
authStorage,
|
|
205
|
+
sessionId,
|
|
206
|
+
activeModelContext,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Hedged fallback: when DuckDuckGo (keyless, cheap) is a non-primary member
|
|
210
|
+
// of the chain, start it in the background after a short delay. If the
|
|
211
|
+
// primary succeeds first the hedge is aborted; if the primary fails, the
|
|
212
|
+
// hedge result is typically already available, collapsing the fallback
|
|
213
|
+
// latency from `t(primary failure) + t(ddg)` to `max(t(primary failure), t(ddg))`.
|
|
214
|
+
const ddgIndex = providers.findIndex(p => p.id === "duckduckgo");
|
|
215
|
+
const hedgeCtl = new AbortController();
|
|
216
|
+
const onUpstreamAbort = () => hedgeCtl.abort();
|
|
217
|
+
signal?.addEventListener("abort", onUpstreamAbort, { once: true });
|
|
218
|
+
let hedgePromise: Promise<SearchResponse> | undefined;
|
|
219
|
+
let hedgeTimer: NodeJS.Timeout | undefined;
|
|
220
|
+
if (ddgIndex > 0) {
|
|
221
|
+
const ddg = providers[ddgIndex]!;
|
|
222
|
+
hedgeTimer = setTimeout(() => {
|
|
223
|
+
hedgePromise = ddg.search({ ...baseSearchParams, signal: hedgeCtl.signal });
|
|
224
|
+
// Failures are consumed when (and if) the loop reaches DuckDuckGo;
|
|
225
|
+
// never let a losing hedge become an unhandled rejection.
|
|
226
|
+
hedgePromise.catch(() => {});
|
|
227
|
+
}, ddgHedgeDelayMs);
|
|
228
|
+
}
|
|
229
|
+
const cancelHedge = () => {
|
|
230
|
+
if (hedgeTimer !== undefined) clearTimeout(hedgeTimer);
|
|
231
|
+
hedgeCtl.abort();
|
|
232
|
+
signal?.removeEventListener("abort", onUpstreamAbort);
|
|
233
|
+
};
|
|
234
|
+
|
|
157
235
|
const failures: Array<{ provider: SearchProvider; error: unknown }> = [];
|
|
158
236
|
let lastProvider = providers[0];
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
content: [{ type: "text" as const, text }],
|
|
191
|
-
details: { response },
|
|
192
|
-
};
|
|
193
|
-
} catch (error) {
|
|
194
|
-
// Surface user-initiated cancellation immediately so the session sees
|
|
195
|
-
// a clean abort instead of a generic "all providers failed" message.
|
|
196
|
-
// Without this, an AbortError from `fetch()` is treated as a provider
|
|
197
|
-
// failure and the loop falls through to the next provider (or to the
|
|
198
|
-
// summary error), masking the cancellation.
|
|
199
|
-
throwIfAborted(signal);
|
|
200
|
-
failures.push({ provider, error });
|
|
237
|
+
try {
|
|
238
|
+
for (const provider of providers) {
|
|
239
|
+
lastProvider = provider;
|
|
240
|
+
try {
|
|
241
|
+
let response: SearchResponse;
|
|
242
|
+
if (provider.id === "duckduckgo" && hedgePromise) {
|
|
243
|
+
// Reuse the in-flight (often already-settled) hedge request.
|
|
244
|
+
if (hedgeTimer !== undefined) clearTimeout(hedgeTimer);
|
|
245
|
+
response = await hedgePromise;
|
|
246
|
+
} else {
|
|
247
|
+
if (provider.id === "duckduckgo" && hedgeTimer !== undefined) clearTimeout(hedgeTimer);
|
|
248
|
+
response = await provider.search({ ...baseSearchParams, signal });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const text = formatForLLM(response);
|
|
252
|
+
const warning = failures.length > 0 ? formatFallbackWarning(failures, provider) : undefined;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text" as const, text: warning ? `Warning: ${warning}\n\n${text}` : text }],
|
|
256
|
+
details: { response, ...(warning ? { warning } : {}) },
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// Surface user-initiated cancellation immediately so the session sees
|
|
260
|
+
// a clean abort instead of a generic "all providers failed" message.
|
|
261
|
+
// Without this, an AbortError from `fetch()` is treated as a provider
|
|
262
|
+
// failure and the loop falls through to the next provider (or to the
|
|
263
|
+
// summary error), masking the cancellation.
|
|
264
|
+
throwIfAborted(signal);
|
|
265
|
+
failures.push({ provider, error });
|
|
266
|
+
}
|
|
201
267
|
}
|
|
268
|
+
} finally {
|
|
269
|
+
cancelHedge();
|
|
202
270
|
}
|
|
203
271
|
|
|
204
272
|
const lastFailure = failures[failures.length - 1];
|
|
@@ -207,13 +275,7 @@ async function executeSearch(
|
|
|
207
275
|
: `Unknown error from ${lastProvider.label}`;
|
|
208
276
|
const message =
|
|
209
277
|
providers.length > 1
|
|
210
|
-
? `All web search providers failed: ${failures
|
|
211
|
-
.map(f =>
|
|
212
|
-
f.error instanceof SearchProviderError
|
|
213
|
-
? f.error.message
|
|
214
|
-
: `${f.provider.id}: ${formatProviderError(f.error, f.provider)}`,
|
|
215
|
-
)
|
|
216
|
-
.join("; ")}`
|
|
278
|
+
? `All web search providers failed: ${failures.map(f => formatProviderFailure(f.error, f.provider)).join("; ")}`
|
|
217
279
|
: baseMessage;
|
|
218
280
|
|
|
219
281
|
return {
|
|
@@ -266,6 +328,24 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
|
|
|
266
328
|
constructor(session: ToolSession) {
|
|
267
329
|
this.#session = session;
|
|
268
330
|
this.description = prompt.render(webSearchDescription);
|
|
331
|
+
// Prewarm: resolve the provider chain once in the background so the
|
|
332
|
+
// provider modules are imported and the chain cache is primed before
|
|
333
|
+
// the first user-visible search. Best-effort only.
|
|
334
|
+
const authStorage = session.authStorage;
|
|
335
|
+
if (authStorage) {
|
|
336
|
+
try {
|
|
337
|
+
const activeModelContext = session.model
|
|
338
|
+
? session.modelRegistry?.getActiveSearchModelContext(session.model)
|
|
339
|
+
: undefined;
|
|
340
|
+
prewarmSearchProviders({
|
|
341
|
+
authStorage,
|
|
342
|
+
sessionId: session.getSessionId?.() ?? undefined,
|
|
343
|
+
activeModelContext,
|
|
344
|
+
});
|
|
345
|
+
} catch {
|
|
346
|
+
// Never let prewarming break tool construction.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
269
349
|
}
|
|
270
350
|
|
|
271
351
|
async execute(
|
|
@@ -321,7 +401,12 @@ export function getSearchTools(): CustomTool<any, any>[] {
|
|
|
321
401
|
return [webSearchCustomTool];
|
|
322
402
|
}
|
|
323
403
|
|
|
324
|
-
export {
|
|
325
|
-
|
|
404
|
+
export {
|
|
405
|
+
getConfiguredSearchProviderPreference,
|
|
406
|
+
getSearchProvider,
|
|
407
|
+
setPreferredSearchProvider,
|
|
408
|
+
setSearchFallbackProviders,
|
|
409
|
+
} from "./provider";
|
|
410
|
+
export { applyConfiguredSearchTimeout, setSearchHardTimeoutMs } from "./providers/utils";
|
|
326
411
|
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
|
327
412
|
export { isConfigurableSearchProviderId, isSearchProviderPreference } from "./types";
|
|
@@ -141,12 +141,78 @@ const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderId> = {
|
|
|
141
141
|
let preferredProvId: SearchProviderId | "auto" = "auto";
|
|
142
142
|
let fallbackProvIds: SearchProviderId[] = [];
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolved-chain cache. Chain resolution probes provider availability
|
|
146
|
+
* (credential lookups, potentially an OAuth refresh through the broker) on
|
|
147
|
+
* every web_search call even though the result only changes when the active
|
|
148
|
+
* model, provider preference, or credentials change. Cache the resolved
|
|
149
|
+
* provider-id list per AuthStorage instance for a short TTL.
|
|
150
|
+
*
|
|
151
|
+
* Keyed by AuthStorage identity (WeakMap) so tests and short-lived CLI calls
|
|
152
|
+
* with fresh storage handles never see another handle's chain. The TTL keeps
|
|
153
|
+
* a login/logout mid-session from being masked for more than a minute.
|
|
154
|
+
*/
|
|
155
|
+
const CHAIN_CACHE_TTL_MS = 60_000;
|
|
156
|
+
let chainCache = new WeakMap<AuthStorage, Map<string, { ids: SearchProviderId[]; expires: number }>>();
|
|
157
|
+
|
|
158
|
+
/** Drop every cached resolved chain (settings changed, tests). */
|
|
159
|
+
export function clearResolvedChainCache(): void {
|
|
160
|
+
chainCache = new WeakMap();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function chainCacheKey(
|
|
164
|
+
preferred: SearchProviderId | "auto" | undefined,
|
|
165
|
+
fallbacks: readonly SearchProviderId[],
|
|
166
|
+
ctx: ActiveSearchModelContext | undefined,
|
|
167
|
+
authStorage: AuthStorage,
|
|
168
|
+
): string {
|
|
169
|
+
// Include the AuthStorage generation so credential changes (login/logout,
|
|
170
|
+
// key rotation) invalidate cached chains immediately instead of being
|
|
171
|
+
// masked for up to the TTL. Stubs without getGeneration() key as 0.
|
|
172
|
+
const generation = (authStorage as { getGeneration?: () => number }).getGeneration?.() ?? 0;
|
|
173
|
+
return JSON.stringify([
|
|
174
|
+
generation,
|
|
175
|
+
preferred ?? null,
|
|
176
|
+
fallbacks,
|
|
177
|
+
ctx
|
|
178
|
+
? [ctx.provider, ctx.modelId, ctx.wireModelId ?? null, ctx.api, ctx.baseUrl ?? null, ctx.webSearch ?? null]
|
|
179
|
+
: null,
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
export function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void {
|
|
145
184
|
preferredProvId = provider;
|
|
185
|
+
clearResolvedChainCache();
|
|
146
186
|
}
|
|
147
187
|
|
|
148
188
|
export function setSearchFallbackProviders(ids: readonly string[]): void {
|
|
149
189
|
fallbackProvIds = ids.filter(isConfigurableSearchProviderId);
|
|
190
|
+
clearResolvedChainCache();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Fire-and-forget warm-up: resolve the provider chain once so the provider
|
|
195
|
+
* modules are imported and the chain cache is primed before the first
|
|
196
|
+
* user-visible search. Errors are intentionally swallowed — prewarming must
|
|
197
|
+
* never surface a failure the real search path would handle itself.
|
|
198
|
+
*/
|
|
199
|
+
export function prewarmSearchProviders(options: ResolveProviderChainOptions): void {
|
|
200
|
+
void resolveProviderChain(options).catch(() => {});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface SearchProviderPreferenceSource {
|
|
204
|
+
get(path: "providers.webSearch" | "web_search.provider"): SearchProviderId | "auto";
|
|
205
|
+
has(path: "providers.webSearch" | "web_search.provider"): boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function getConfiguredSearchProviderPreference(
|
|
209
|
+
settings: SearchProviderPreferenceSource,
|
|
210
|
+
): SearchProviderId | "auto" {
|
|
211
|
+
const providerPreference = settings.get("providers.webSearch");
|
|
212
|
+
if (settings.has("providers.webSearch") || providerPreference !== "auto") return providerPreference;
|
|
213
|
+
|
|
214
|
+
const legacyProviderPreference = settings.get("web_search.provider");
|
|
215
|
+
return legacyProviderPreference;
|
|
150
216
|
}
|
|
151
217
|
|
|
152
218
|
export interface ResolveProviderChainOptions {
|
|
@@ -342,6 +408,16 @@ export async function resolveProviderChain(options: ResolveProviderChainOptions)
|
|
|
342
408
|
activeModelContext,
|
|
343
409
|
fallbackProviders = fallbackProvIds,
|
|
344
410
|
} = options;
|
|
411
|
+
|
|
412
|
+
const cacheKey = chainCacheKey(preferredProvider, fallbackProviders, activeModelContext, authStorage);
|
|
413
|
+
const perStorage = chainCache.get(authStorage);
|
|
414
|
+
const cached = perStorage?.get(cacheKey);
|
|
415
|
+
if (cached && cached.expires > Date.now()) {
|
|
416
|
+
const cachedProviders: SearchProvider[] = [];
|
|
417
|
+
for (const id of cached.ids) cachedProviders.push(await getSearchProvider(id));
|
|
418
|
+
return cachedProviders;
|
|
419
|
+
}
|
|
420
|
+
|
|
345
421
|
const chain: SearchProviderId[] = [];
|
|
346
422
|
|
|
347
423
|
// A forced primary is honored only when it is a user-configurable provider.
|
|
@@ -377,6 +453,10 @@ export async function resolveProviderChain(options: ResolveProviderChainOptions)
|
|
|
377
453
|
}
|
|
378
454
|
appendDeduped(chain, "duckduckgo");
|
|
379
455
|
|
|
456
|
+
const storageEntry = chainCache.get(authStorage) ?? new Map<string, { ids: SearchProviderId[]; expires: number }>();
|
|
457
|
+
storageEntry.set(cacheKey, { ids: [...chain], expires: Date.now() + CHAIN_CACHE_TTL_MS });
|
|
458
|
+
chainCache.set(authStorage, storageEntry);
|
|
459
|
+
|
|
380
460
|
const providers: SearchProvider[] = [];
|
|
381
461
|
for (const id of chain) providers.push(await getSearchProvider(id));
|
|
382
462
|
return providers;
|
|
@@ -196,7 +196,7 @@ async function fetchAndParse(
|
|
|
196
196
|
"Accept-Language": "en-US,en;q=0.9",
|
|
197
197
|
},
|
|
198
198
|
body,
|
|
199
|
-
signal: withHardTimeout(signal),
|
|
199
|
+
signal: withHardTimeout(signal, "api"),
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
// DuckDuckGo signals soft blocks with 202 (which is still response.ok).
|
|
@@ -115,7 +115,7 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
|
|
|
115
115
|
"x-api-key": apiKey,
|
|
116
116
|
},
|
|
117
117
|
body: JSON.stringify(body),
|
|
118
|
-
signal: withHardTimeout(params.signal),
|
|
118
|
+
signal: withHardTimeout(params.signal, "api"),
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
if (!response.ok) {
|