dsh-lcx-codex 0.4.3 → 0.5.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/transport.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { abortError } from "./abort-error.js";
1
2
  const DEFAULT_TIMEOUT_MS = 300_000;
2
3
  const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
3
4
  const MAX_ERROR_BYTES = 512 * 1024;
@@ -6,8 +7,7 @@ function errorFacts(value) {
6
7
  }
7
8
  export function abortIfNeeded(signal) {
8
9
  if (signal?.aborted)
9
- throw (signal.reason ??
10
- Object.assign(new Error("request aborted"), { code: "LCX_ABORTED" }));
10
+ throw abortError(signal);
11
11
  }
12
12
  function makeError(message, code, extra = {}) {
13
13
  const error = new Error(message);
@@ -63,11 +63,11 @@ async function sleep(ms, signal) {
63
63
  return;
64
64
  await new Promise((resolve, reject) => {
65
65
  if (signal?.aborted)
66
- return reject(signal.reason);
66
+ return reject(abortError(signal));
67
67
  const onAbort = () => {
68
68
  clearTimeout(timer);
69
69
  signal?.removeEventListener("abort", onAbort);
70
- reject(signal?.reason);
70
+ reject(abortError(signal));
71
71
  };
72
72
  const timer = setTimeout(() => {
73
73
  signal?.removeEventListener("abort", onAbort);
@@ -161,7 +161,7 @@ export async function fetchJsonWithRetry(url, body, headers = {}, signal, timeou
161
161
  }
162
162
  catch (error) {
163
163
  if (signal?.aborted)
164
- throw signal.reason ?? error;
164
+ throw abortError(signal, error);
165
165
  const details = errorFacts(error);
166
166
  const retryable = details.retryable === true ||
167
167
  details.code === "LCX_HTTP_RETRYABLE" ||
@@ -231,7 +231,7 @@ export async function fetchSseWithRetry(url, body, headers = {}, signal, timeout
231
231
  }
232
232
  catch (error) {
233
233
  if (signal?.aborted)
234
- throw signal.reason ?? error;
234
+ throw abortError(signal, error);
235
235
  const details = errorFacts(error);
236
236
  const retryable = details.retryable === true ||
237
237
  details.code === "LCX_HTTP_RETRYABLE" ||
@@ -12,5 +12,14 @@ export type StructuredMediaTool = "web_search" | "websearch_gpt_advanced";
12
12
  /** Narrow LCX-owned tool-private media metadata without reading model-visible output. */
13
13
  export declare function structuredSearchMedia(meta: unknown, expectedTool: StructuredMediaTool): readonly SearchMediaItem[];
14
14
  export declare function mergeSearchMedia(structured: readonly SearchMediaItem[], fallback: readonly SearchMediaItem[]): readonly SearchMediaItem[];
15
- /** Extract bounded remote media linked by visible assistant prose. */
16
- export declare function extractSearchMedia(text: string): readonly SearchMediaItem[];
15
+ /**
16
+ * Extract bounded direct media links from visible assistant prose, in text order.
17
+ *
18
+ * Ownership (Issue #102): Markdown images are rendered by DSH itself, so their
19
+ * destinations are never returned, and a bare link to an image DSH already renders
20
+ * natively in the same text is skipped. Plain direct image links and direct video
21
+ * files are LCX's; everything else (pages, video sites) stays an ordinary link.
22
+ */
23
+ export declare function extractDirectMediaLinks(text: string): readonly SearchMediaItem[];
24
+ /** Direct video files only (see `extractDirectMediaLinks`). */
25
+ export declare function extractDirectVideoLinks(text: string): readonly SearchMediaItem[];
@@ -1,5 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import type { Context } from "@deepseek-ai/cordis";
2
+ import type { Context, Volatile } from "@deepseek-ai/cordis";
3
3
  import "@deepseek-ai/dsh-agent";
4
4
  import "@deepseek-ai/dsh-tools";
5
5
  import "@deepseek-ai/dsh-web";
@@ -26,56 +26,84 @@ type ConfigInput = {
26
26
  assistantRetentionTokenReserve?: unknown;
27
27
  assistantRetentionPerMessageTokenCap?: unknown;
28
28
  };
29
+ type LiveSettingsConfig = {
30
+ enabled: Volatile<boolean>;
31
+ webSearch: Volatile<boolean>;
32
+ advancedHostedSearch: Volatile<boolean>;
33
+ alphaSearch: Volatile<boolean>;
34
+ grokNativeWebSearch: Volatile<boolean>;
35
+ grokNativeXSearch: Volatile<boolean>;
36
+ searchMediaPreview: Volatile<boolean>;
37
+ };
29
38
  type HostContext = Context;
30
- export declare const Config: z<Schemastery.ObjectS<{
31
- supportsLongCacheRetention: z<boolean, boolean>;
32
- supportsExplicitPromptCacheMode: z<boolean, boolean>;
33
- alphaCapabilityPath: z<string, string>;
34
- alphaRefPath: z<string, string>;
35
- alphaProfile: z<string, string>;
36
- alphaGroup: z<string, string>;
37
- alphaMaxOutputTokens: z<number, number>;
38
- webSearchProvider: z<string, string>;
39
- webMaxResults: z<number, number>;
40
- timeoutMs: z<number, number>;
41
- maxResponseBytes: z<number, number>;
42
- maxAttempts: z<number, number>;
43
- maxRequestImageBytes: z<number, number>;
44
- requestImagePixelBudget: z<number, number>;
45
- requestImageMaxBytes: z<number, number>;
46
- portableReplayMaxChars: z<number, number>;
47
- nativeRetentionTokenBudget: z<number, number>;
48
- assistantRetentionTokenReserve: z<number, number>;
49
- assistantRetentionPerMessageTokenCap: z<number, number>;
50
- }>, Schemastery.ObjectT<{
51
- supportsLongCacheRetention: z<boolean, boolean>;
52
- supportsExplicitPromptCacheMode: z<boolean, boolean>;
53
- alphaCapabilityPath: z<string, string>;
54
- alphaRefPath: z<string, string>;
55
- alphaProfile: z<string, string>;
56
- alphaGroup: z<string, string>;
57
- alphaMaxOutputTokens: z<number, number>;
58
- webSearchProvider: z<string, string>;
59
- webMaxResults: z<number, number>;
60
- timeoutMs: z<number, number>;
61
- maxResponseBytes: z<number, number>;
62
- maxAttempts: z<number, number>;
63
- maxRequestImageBytes: z<number, number>;
64
- requestImagePixelBudget: z<number, number>;
65
- requestImageMaxBytes: z<number, number>;
66
- portableReplayMaxChars: z<number, number>;
67
- nativeRetentionTokenBudget: z<number, number>;
68
- assistantRetentionTokenReserve: z<number, number>;
69
- assistantRetentionPerMessageTokenCap: z<number, number>;
70
- }>>;
39
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
40
+ enabled: z<boolean, boolean, "volatile-defined">;
41
+ webSearch: z<boolean, boolean, "volatile-defined">;
42
+ advancedHostedSearch: z<boolean, boolean, "volatile-defined">;
43
+ alphaSearch: z<boolean, boolean, "volatile-defined">;
44
+ grokNativeWebSearch: z<boolean, boolean, "volatile-defined">;
45
+ grokNativeXSearch: z<boolean, boolean, "volatile-defined">;
46
+ searchMediaPreview: z<boolean, boolean, "volatile-defined">;
47
+ supportsLongCacheRetention: z<boolean, boolean, "defined">;
48
+ supportsExplicitPromptCacheMode: z<boolean, boolean, "defined">;
49
+ alphaCapabilityPath: z<string, string, "defined">;
50
+ alphaRefPath: z<string, string, "defined">;
51
+ alphaProfile: z<string, string, "defined">;
52
+ alphaGroup: z<string, string, "defined">;
53
+ alphaMaxOutputTokens: z<number, number, "defined">;
54
+ webSearchProvider: z<string, string, "defined">;
55
+ webMaxResults: z<number, number, "defined">;
56
+ timeoutMs: z<number, number, "defined">;
57
+ maxResponseBytes: z<number, number, "defined">;
58
+ maxAttempts: z<number, number, "defined">;
59
+ maxRequestImageBytes: z<number, number, "defined">;
60
+ requestImagePixelBudget: z<number, number, "defined">;
61
+ requestImageMaxBytes: z<number, number, "defined">;
62
+ portableReplayMaxChars: z<number, number, "defined">;
63
+ nativeRetentionTokenBudget: z<number, number, "defined">;
64
+ assistantRetentionTokenReserve: z<number, number, "defined">;
65
+ assistantRetentionPerMessageTokenCap: z<number, number, "defined">;
66
+ }>>, Schemastery.ObjectT<NoInfer<{
67
+ enabled: z<boolean, boolean, "volatile-defined">;
68
+ webSearch: z<boolean, boolean, "volatile-defined">;
69
+ advancedHostedSearch: z<boolean, boolean, "volatile-defined">;
70
+ alphaSearch: z<boolean, boolean, "volatile-defined">;
71
+ grokNativeWebSearch: z<boolean, boolean, "volatile-defined">;
72
+ grokNativeXSearch: z<boolean, boolean, "volatile-defined">;
73
+ searchMediaPreview: z<boolean, boolean, "volatile-defined">;
74
+ supportsLongCacheRetention: z<boolean, boolean, "defined">;
75
+ supportsExplicitPromptCacheMode: z<boolean, boolean, "defined">;
76
+ alphaCapabilityPath: z<string, string, "defined">;
77
+ alphaRefPath: z<string, string, "defined">;
78
+ alphaProfile: z<string, string, "defined">;
79
+ alphaGroup: z<string, string, "defined">;
80
+ alphaMaxOutputTokens: z<number, number, "defined">;
81
+ webSearchProvider: z<string, string, "defined">;
82
+ webMaxResults: z<number, number, "defined">;
83
+ timeoutMs: z<number, number, "defined">;
84
+ maxResponseBytes: z<number, number, "defined">;
85
+ maxAttempts: z<number, number, "defined">;
86
+ maxRequestImageBytes: z<number, number, "defined">;
87
+ requestImagePixelBudget: z<number, number, "defined">;
88
+ requestImageMaxBytes: z<number, number, "defined">;
89
+ portableReplayMaxChars: z<number, number, "defined">;
90
+ nativeRetentionTokenBudget: z<number, number, "defined">;
91
+ assistantRetentionTokenReserve: z<number, number, "defined">;
92
+ assistantRetentionPerMessageTokenCap: z<number, number, "defined">;
93
+ }>>, "plain">;
71
94
  export declare function compactionPressureBand(totalTokens: number, contextWindow: number, policy: {
72
95
  auto: number;
73
96
  emergency: number;
97
+ outputReserve: number;
98
+ headroomTokens: number;
74
99
  }): {
100
+ pressureBudget: number;
101
+ nativeThreshold: number;
102
+ emergencyThreshold: number;
75
103
  ratioPercent: number;
76
104
  band: string;
77
105
  };
78
- export declare function apply(ctx: HostContext, configInput?: ConfigInput): void;
106
+ export declare function apply(ctx: HostContext, configInput: ConfigInput & LiveSettingsConfig): void;
79
107
  export declare namespace apply {
80
108
  export { inject };
81
109
  export { Config };
@@ -0,0 +1,62 @@
1
+ const ASSISTANT_CONTEXT_TOKEN_LIMIT = 1_000;
2
+ const APPROX_BYTES_PER_TOKEN = 4;
3
+ function textBlocks(message) {
4
+ const type = message.role === "user" ? "input_text" : "output_text";
5
+ return message.content.flatMap((block) => block.type === "text" ? [{ type, text: block.text }] : []);
6
+ }
7
+ function approxTokens(text) {
8
+ return Math.ceil(Buffer.byteLength(text, "utf8") / APPROX_BYTES_PER_TOKEN);
9
+ }
10
+ // Codex's UTF-8 middle truncation for one over-budget assistant text block.
11
+ function truncateAssistantText(text, tokens) {
12
+ const bytes = Buffer.byteLength(text, "utf8");
13
+ const budget = tokens * APPROX_BYTES_PER_TOKEN;
14
+ if (bytes <= budget)
15
+ return text;
16
+ const leftBudget = Math.floor(budget / 2);
17
+ const rightBudget = budget - leftBudget;
18
+ let prefix = "";
19
+ let suffix = "";
20
+ let consumed = 0;
21
+ for (const char of text) {
22
+ const next = consumed + Buffer.byteLength(char, "utf8");
23
+ if (next <= leftBudget)
24
+ prefix += char;
25
+ if (consumed >= bytes - rightBudget)
26
+ suffix += char;
27
+ consumed = next;
28
+ }
29
+ return `${prefix}…${Math.ceil((bytes - budget) / APPROX_BYTES_PER_TOKEN)} tokens truncated…${suffix}`;
30
+ }
31
+ /** Codex recent_input on the canonical DSH message surface. */
32
+ export function recentAlphaInput(messages) {
33
+ const users = messages.flatMap((message, index) => message.role === "user" && message.source.kind === "user" && textBlocks(message).length ? [index] : []);
34
+ if (!users.length)
35
+ return undefined;
36
+ const start = users.at(-2) ?? users.at(-1);
37
+ const end = users.at(-1);
38
+ let assistantTokens = ASSISTANT_CONTEXT_TOKEN_LIMIT;
39
+ const items = [];
40
+ for (let index = start; index <= end; index++) {
41
+ const message = messages[index];
42
+ if (message.role === "user" && message.source.kind === "user") {
43
+ const content = textBlocks(message);
44
+ if (content.length)
45
+ items.push({ type: "message", role: "user", content });
46
+ }
47
+ else if (message.role === "assistant" && assistantTokens > 0) {
48
+ const content = [];
49
+ for (const block of textBlocks(message)) {
50
+ if (assistantTokens === 0)
51
+ break;
52
+ const cost = approxTokens(block.text);
53
+ content.push({ type: "output_text", text: cost <= assistantTokens
54
+ ? block.text : truncateAssistantText(block.text, assistantTokens) });
55
+ assistantTokens = Math.max(0, assistantTokens - cost);
56
+ }
57
+ if (content.length)
58
+ items.push({ type: "message", role: "assistant", content });
59
+ }
60
+ }
61
+ return items.length ? items : undefined;
62
+ }
@@ -31,12 +31,16 @@ const RESPONSE_LENGTHS = ["short", "medium", "long"];
31
31
  export const ALPHA_SEARCH_PARAMETERS = {
32
32
  type: "object",
33
33
  properties: {
34
- action: { type: "string", enum: ALPHA_ACTIONS },
34
+ action: {
35
+ type: "string",
36
+ enum: ALPHA_ACTIONS,
37
+ description: "Choose one action. search_query/image_query use query, domains, recency; open uses refId, lineNumber; find uses refId, pattern; click uses refId, linkId; screenshot uses refId, pageNumber; finance uses ticker, assetType, market; weather uses location, start, duration; sports uses fn, league, team, opponent, dateFrom, dateTo, numberOfGames, locale; time uses utcOffset. responseLength applies to every action. Other declared fields are ignored.",
38
+ },
35
39
  query: { type: "string" },
36
40
  domains: { type: "array", items: { type: "string" } },
37
41
  recency: { type: "integer" },
38
- refId: { type: "string" },
39
- lineNumber: { type: "integer" },
42
+ refId: { type: "string", description: "Copy only the reference after opaqueRef=, including any lcx-alpha- handle. Do not include the opaqueRef= label. A public URL is allowed for open/find." },
43
+ lineNumber: { type: "integer", description: "Omit unless opening a specific line. Zero requests line 0." },
40
44
  linkId: { type: "integer" },
41
45
  pattern: { type: "string" },
42
46
  pageNumber: { type: "integer" },
@@ -105,7 +109,9 @@ export const ALPHA_SEARCH_OUTPUT = {
105
109
  export const ALPHA_SCHEMA_FINGERPRINT = createHash("sha256")
106
110
  .update(JSON.stringify(ALPHA_SEARCH_PARAMETERS))
107
111
  .digest("hex");
108
- export const ALPHA_PROBE_VERSION = 12;
112
+ export const ALPHA_PROBE_VERSION = 15;
113
+ export const ALPHA_PROBE_QUERY = "Python official documentation docs.python.org/3/";
114
+ export const ALPHA_PROBE_MAX_OPEN_CANDIDATES = 3;
109
115
  function failure(message, code = "WEB_INVALID_REQUEST") {
110
116
  return Object.assign(new Error(message), { code });
111
117
  }
@@ -140,7 +146,7 @@ function date(value, field) {
140
146
  return normalized;
141
147
  }
142
148
  function normalizeDomains(value) {
143
- if (!Array.isArray(value) || !value.length || value.length > 100)
149
+ if (!Array.isArray(value) || value.length > 100)
144
150
  throw failure("websearch_alpha.domains is invalid");
145
151
  return value.map((item) => {
146
152
  const d = text(item, "domains", 253).toLowerCase();
@@ -149,35 +155,16 @@ function normalizeDomains(value) {
149
155
  return d;
150
156
  });
151
157
  }
152
- const FIELDS = {
153
- search_query: ["query", "domains", "recency"],
154
- image_query: ["query", "domains", "recency"],
155
- open: ["refId", "lineNumber"],
156
- find: ["refId", "pattern"],
157
- click: ["refId", "linkId"],
158
- screenshot: ["refId", "pageNumber"],
159
- finance: ["ticker", "assetType", "market"],
160
- weather: ["location", "start", "duration"],
161
- sports: [
162
- "fn",
163
- "league",
164
- "team",
165
- "opponent",
166
- "dateFrom",
167
- "dateTo",
168
- "numberOfGames",
169
- "locale",
170
- ],
171
- time: ["utcOffset"],
172
- };
173
158
  function isAlphaAction(value) {
174
159
  return typeof value === "string" && ALPHA_ACTIONS.includes(value);
175
160
  }
176
161
  export function normalizeAlphaSearchArgs(args) {
177
162
  if (!isObject(args) || !isAlphaAction(args.action))
178
163
  throw failure("websearch_alpha.action is invalid");
179
- const allowed = new Set(["action", "responseLength", ...FIELDS[args.action]]);
180
- if (Object.keys(args).some((k) => !allowed.has(k)))
164
+ // The advertised flat schema permits every declared field for every action.
165
+ // Ignore fields owned by another action; never forward them to Alpha.
166
+ const declared = new Set(Object.keys(ALPHA_SEARCH_PARAMETERS.properties));
167
+ if (Object.keys(args).some((k) => !declared.has(k)))
181
168
  throw failure(`websearch_alpha.${args.action} received an unrelated field`);
182
169
  const result = { action: args.action };
183
170
  if (args.responseLength !== undefined) {
@@ -190,7 +177,7 @@ export function normalizeAlphaSearchArgs(args) {
190
177
  case "search_query":
191
178
  case "image_query":
192
179
  result.query = text(args.query, "query", 16000);
193
- if (args.domains !== undefined)
180
+ if (args.domains !== undefined && (!Array.isArray(args.domains) || args.domains.length > 0))
194
181
  result.domains = normalizeDomains(args.domains);
195
182
  if (args.recency !== undefined)
196
183
  result.recency = integer(args.recency, "recency", 0, 3650);
@@ -326,30 +313,13 @@ export function alphaActionCommand(args) {
326
313
  ...(args.responseLength ? { response_length: args.responseLength } : {}),
327
314
  };
328
315
  }
329
- function actionInput(args) {
330
- return String(args.query ??
331
- (args.refId
332
- ? `${args.action}: ${args.refId}`
333
- : args.ticker
334
- ? `${args.action}: ${args.ticker}`
335
- : args.location
336
- ? `${args.action}: ${args.location}`
337
- : args.utcOffset
338
- ? `${args.action}: ${args.utcOffset}`
339
- : `${args.action}: ${args.league ?? ""}`.trim()));
340
- }
341
- export function buildAlphaSearchBody(args, model, sessionId, externalWebAccess = true, maxOutputTokens = 2500) {
316
+ export function buildAlphaSearchBody(args, model, sessionId, externalWebAccess = true, maxOutputTokens = 2500, input) {
342
317
  if (typeof sessionId !== "string" || !sessionId)
343
318
  throw failure("websearch_alpha requires a DSH session", "LCX_ALPHA_SESSION_REQUIRED");
344
319
  return {
345
320
  id: sessionId,
346
321
  model,
347
- input: [
348
- {
349
- role: "user",
350
- content: [{ type: "input_text", text: actionInput(args) }],
351
- },
352
- ],
322
+ ...(input?.length ? { input } : {}),
353
323
  commands: alphaActionCommand(args),
354
324
  settings: {
355
325
  allowed_callers: ["direct"],
@@ -684,8 +654,26 @@ const ALPHA_SEMANTIC_FAILURE = /^\s*(?:reference(?: id)?\s+(?:is\s+)?(?:invalid|
684
654
  const ALPHA_COMMAND_ERROR_ENVELOPE = /^\s*Internal Error\s*\([^\r\n)]*\)\s*(?:\r?\n[ \t]*)+(?:\uE200cite\uE202[^\uE201\r\n]+\uE201[ \t]*(?:\[wordlim:\s*\d+\][ \t]*)?)?Unable to resolve (open|find|click|screenshot) call\s*:/iu;
685
655
  const ALPHA_FETCH_FAILURE_ENVELOPE = /^\s*Internal Error\s*\([^\r\n)]*\)\s*(?:\r?\n[ \t]*)+(?:\uE200cite\uE202[^\uE201\r\n]+\uE201[ \t]*(?:\[wordlim:\s*\d+\][ \t]*)?)?Source:\s*(open|find|click|screenshot)\s*\([^\r\n]*\)\s*;\s*Total lines:\s*\d+\s*(?:\r?\n)L0:[ \t]*Failed to fetch \S+:[ \t]*\(\d{3}\)/iu;
686
656
  const ALPHA_REFERENCE_ERROR_ENVELOPE = /^\s*(?:\*\*Result:\*\*[ \t]*)?Unable to (?:execute[ \t]+)?`?(open|find|click|screenshot)`?(?=[\s:])([^\r\n]*)/iu;
657
+ const ALPHA_FIND_SOURCE_ERROR_ENVELOPE = /^\s*The (?:request|search) failed: `[^`\r\n]+` is not a valid or available source reference, so\b/iu;
658
+ // Recorded HTTP-200 empty error view: no page body, but a newly produced view citation.
659
+ // Anchor the entire envelope so error examples inside fetched pages never match.
660
+ const ALPHA_EMPTY_FETCH_ERROR_VIEW = /^\s*(?:Internal[ \t]+)?Error[ \t]*\([ \t]*\)[ \t]*\r?\n[ \t]*\uE200cite\uE202turn\d+view\d+\uE201[ \t]*(?:\[wordlim:\s*\d+\][ \t]*)?\s*$/u;
661
+ // A notice-only open view: every body line belongs to the fetch failure,
662
+ // never arbitrary page prose or numbered page content.
663
+ const ALPHA_FETCH_NOTICE_VIEW = /^\s*(?:(?:Internal[ \t]+)?Error[ \t]*\([ \t]*\)[ \t]*\r?\n[ \t]*)?(?:\uE200cite\uE202turn\d+view\d+\uE201[ \t]*(?:\[wordlim:\s*\d+\][ \t]*)?\r?\n[ \t]*)?Failed to fetch[ \t]+https?:\/\/\S+[^\r\n]*?(?:\r?\n[ \t]*|[ \t]+)Error fetching[ \t]+https?:\/\/\S+[^\r\n]*\s*$/u;
664
+ function alphaPageFetchFailed(output, action) {
665
+ if (action !== "open" && action !== "click")
666
+ return false;
667
+ const text = String(output ?? "");
668
+ const explicit = text.match(ALPHA_FETCH_FAILURE_ENVELOPE);
669
+ return ALPHA_EMPTY_FETCH_ERROR_VIEW.test(text) ||
670
+ (action === "open" && ALPHA_FETCH_NOTICE_VIEW.test(text)) ||
671
+ explicit?.[1].toLowerCase() === action;
672
+ }
687
673
  function alphaCommandErrorEnvelope(output, action) {
688
674
  const text = String(output ?? "");
675
+ if (action === "find" && ALPHA_FIND_SOURCE_ERROR_ENVELOPE.test(text))
676
+ return true;
689
677
  const command = text.match(ALPHA_COMMAND_ERROR_ENVELOPE);
690
678
  if (command && command[1].toLowerCase() === action)
691
679
  return true;
@@ -705,6 +693,8 @@ export function parseAlphaSearchResponse(response, options) {
705
693
  (response.results !== undefined && !Array.isArray(response.results)))
706
694
  throw failure("LCX Alpha Web Search returned an invalid response", "LCX_ALPHA_INVALID_RESPONSE");
707
695
  const action = typeof options.action === "string" ? options.action : undefined;
696
+ if (alphaPageFetchFailed(response.output, action))
697
+ throw failure("LCX Alpha Web Search: the page could not be fetched; try another result", "LCX_ALPHA_PAGE_FETCH_FAILED");
708
698
  if (ACTION_ERROR.test(response.output) ||
709
699
  (["open", "find", "click", "screenshot"].includes(action ?? "") &&
710
700
  (ALPHA_SEMANTIC_FAILURE.test(response.output) ||
@@ -818,6 +808,28 @@ export function renderAlphaSearchResult(value) {
818
808
  parts.push(`检索时间:${String(data.retrievedAt ?? "")}`);
819
809
  return [{ type: "text", text: parts.filter(Boolean).join("\n\n") }];
820
810
  }
811
+ export function remapAlphaResultRefs(value, handles) {
812
+ const changes = Object.entries(handles).filter(([raw, handle]) => raw !== handle);
813
+ if (!changes.length)
814
+ return value;
815
+ const urlFields = new Set(["url", "source_url", "source_website_url", "image_url", "thumbnail_url", "href"]);
816
+ const replace = (input, key = "") => {
817
+ if (urlFields.has(key))
818
+ return input;
819
+ if (typeof input === "string") {
820
+ let output = input;
821
+ for (const [raw, handle] of changes)
822
+ output = output.replace(new RegExp(`(?<![\\w-])${raw.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?![\\w-])`, "gu"), handle);
823
+ return output;
824
+ }
825
+ if (Array.isArray(input))
826
+ return input.map((item) => replace(item));
827
+ if (isObject(input))
828
+ return Object.fromEntries(Object.entries(input).map(([field, item]) => [field, replace(item, field)]));
829
+ return input;
830
+ };
831
+ return replace(value);
832
+ }
821
833
  export const ALPHA_STATEFUL_RETRY_MAX_ATTEMPTS = 1;
822
834
  const alphaSessionLocks = new Map();
823
835
  export function alphaSearchRetryOptions(maxResponseBytes) {
@@ -892,6 +904,8 @@ function alphaProbeStructuredBlock(block) {
892
904
  const ALPHA_PROBE_SEMANTIC_FAILURE = /\breference(?: id)?\s+(?:is\s+)?(?:invalid|unavailable)\b/iu;
893
905
  function alphaProbeResultHasEvidence(action, value) {
894
906
  const data = isObject(value) ? value : {};
907
+ if (alphaPageFetchFailed(data.content, action))
908
+ return false;
895
909
  if (alphaCommandErrorEnvelope(data.content, action))
896
910
  return false;
897
911
  if (ALPHA_PROBE_SEMANTIC_FAILURE.test(String(data.content ?? "")) &&
@@ -913,7 +927,7 @@ function alphaProbeResultHasEvidence(action, value) {
913
927
  return Boolean(refs || pdfRefs || structuredResults || structuredBlocks);
914
928
  return Boolean(refs || links || pdfRefs || structuredResults || structuredBlocks);
915
929
  }
916
- export async function probeAlphaCapabilities({ invoke, schemaFingerprint, trustedNativeProvenance = false, actionProbes = {}, clickProbeRef, screenshotProbeRef, }) {
930
+ export async function probeAlphaCapabilities({ invoke, startChain, schemaFingerprint, trustedNativeProvenance = false, actionProbes = {}, clickProbeRef, screenshotProbeRef, }) {
917
931
  const actions = Object.fromEntries(ALPHA_ACTIONS.map((action) => [action, "unknown"]));
918
932
  const result = {
919
933
  classification: "unsupported",
@@ -923,78 +937,89 @@ export async function probeAlphaCapabilities({ invoke, schemaFingerprint, truste
923
937
  probeVersion: ALPHA_PROBE_VERSION,
924
938
  provenance: trustedNativeProvenance ? "trusted-native" : "unavailable",
925
939
  };
926
- let search;
927
- try {
928
- search = await invoke({
929
- action: "search_query",
930
- query: "OpenAI official documentation",
931
- responseLength: "short",
932
- });
933
- actions.search_query = "supported";
934
- }
935
- catch (error) {
936
- actions.search_query = probeFailureState(error);
937
- result.classification =
938
- actions.search_query === "unsupported" ? "unsupported" : "unknown";
939
- return result;
940
- }
941
- const searchSources = Array.isArray(search.sources) ? search.sources : [];
942
- const searchUrl = searchSources
943
- .map((source) => isObject(source) ? httpUrl(source.url) : undefined)
944
- .find((value) => value !== undefined);
945
- const searchRefs = Array.isArray(search.refs) ? search.refs : [];
946
- const searchRef = searchRefs[0];
947
- const continuationTarget = searchUrl ?? searchRef;
948
- if (!continuationTarget) {
949
- result.classification = "emulated-search-only";
950
- actions.open = "unsupported";
951
- actions.find = "unsupported";
952
- actions.click = "unsupported";
953
- return result;
954
- }
955
940
  let opened;
956
- try {
957
- opened = await invoke({
958
- action: "open",
959
- refId: continuationTarget,
960
- responseLength: "short",
961
- });
962
- if (!alphaProbeResultHasEvidence("open", opened)) {
941
+ const chainIds = new Set();
942
+ // Two independent chains, each bounded to search + 3 candidate opens + find:
943
+ // at most 10 mandatory calls, 19 with all optional checks. Only page-fetch
944
+ // failures allow another candidate; never retry the same ref or failed find.
945
+ for (let chain = 0; chain < 2; chain++) {
946
+ try {
947
+ const chainId = await startChain(chain);
948
+ if (typeof chainId !== "string" || !chainId || chainIds.has(chainId)) {
949
+ result.classification = "unknown";
950
+ return result;
951
+ }
952
+ chainIds.add(chainId);
953
+ }
954
+ catch {
955
+ result.classification = "unknown";
956
+ return result;
957
+ }
958
+ let search;
959
+ try {
960
+ search = await invoke({ action: "search_query", query: ALPHA_PROBE_QUERY, responseLength: "short" });
961
+ actions.search_query = "supported";
962
+ }
963
+ catch (error) {
964
+ actions.search_query = probeFailureState(error);
965
+ result.classification = chain === 0 && actions.search_query === "unsupported" ? "unsupported" : "unknown";
966
+ return result;
967
+ }
968
+ const searchRefs = Array.isArray(search.refs) ? search.refs : [];
969
+ const candidates = [...new Set(searchRefs.filter((ref) => typeof ref === "string" && ref && !isAlphaHttpUrl(ref)))].slice(0, ALPHA_PROBE_MAX_OPEN_CANDIDATES);
970
+ if (!candidates.length) {
971
+ result.classification = chain === 0 ? "emulated-search-only" : "unknown";
963
972
  actions.open = "unsupported";
964
- result.classification = "emulated-search-only";
973
+ actions.find = "unsupported";
974
+ actions.click = "unsupported";
975
+ return result;
976
+ }
977
+ opened = undefined;
978
+ let continuationTarget;
979
+ for (const candidate of candidates) {
980
+ try {
981
+ const value = await invoke({ action: "open", refId: candidate, responseLength: "short" });
982
+ if (alphaPageFetchFailed(value.content, "open"))
983
+ continue;
984
+ if (!alphaProbeResultHasEvidence("open", value)) {
985
+ actions.open = "unsupported";
986
+ result.classification = chain === 0 ? "emulated-search-only" : "unknown";
987
+ return result;
988
+ }
989
+ opened = value;
990
+ continuationTarget = candidate;
991
+ actions.open = "supported";
992
+ break;
993
+ }
994
+ catch (error) {
995
+ if (isObject(error) && error.code === "LCX_ALPHA_PAGE_FETCH_FAILED")
996
+ continue;
997
+ actions.open = probeFailureState(error);
998
+ result.classification = chain === 0 && actions.open === "unsupported" ? "emulated-search-only" : "unknown";
999
+ return result;
1000
+ }
1001
+ }
1002
+ if (!opened) {
1003
+ actions.open = "unknown";
1004
+ result.classification = "unknown";
1005
+ return result;
1006
+ }
1007
+ const openedRefs = Array.isArray(opened.refs) ? opened.refs : [];
1008
+ const findTarget = openedRefs.find((ref) => typeof ref === "string" && !isAlphaHttpUrl(ref)) ?? continuationTarget;
1009
+ try {
1010
+ const found = await invoke({ action: "find", refId: findTarget, pattern: "Python", responseLength: "short" });
1011
+ actions.find = alphaProbeResultHasEvidence("find", found) ? "supported" : "unsupported";
1012
+ }
1013
+ catch (error) {
1014
+ actions.find = probeFailureState(error);
1015
+ }
1016
+ if (actions.find !== "supported") {
1017
+ result.classification = "unknown";
965
1018
  return result;
966
1019
  }
967
- actions.open = "supported";
968
- }
969
- catch (error) {
970
- actions.open = probeFailureState(error);
971
- result.classification =
972
- actions.open === "unsupported" ? "emulated-search-only" : "unknown";
973
- return result;
974
- }
975
- const openedSources = opened && Array.isArray(opened.sources) ? opened.sources : [];
976
- const openedUrl = openedSources
977
- .map((source) => isObject(source) ? httpUrl(source.url) : undefined)
978
- .find((value) => value !== undefined);
979
- const openedRefs = opened && Array.isArray(opened.refs) ? opened.refs : [];
980
- const findTarget = searchUrl ?? openedUrl ?? openedRefs[0] ?? continuationTarget;
981
- try {
982
- const found = await invoke({
983
- action: "find",
984
- refId: findTarget,
985
- pattern: "OpenAI",
986
- responseLength: "short",
987
- });
988
- actions.find = alphaProbeResultHasEvidence("find", found)
989
- ? "supported"
990
- : "unsupported";
991
- }
992
- catch (error) {
993
- actions.find = probeFailureState(error);
994
1020
  }
995
- let clickPage = opened, clickProbeUsed = false;
1021
+ let clickPage = opened;
996
1022
  if (!(clickPage && Array.isArray(clickPage.links) && clickPage.links.length > 0) && clickProbeRef) {
997
- clickProbeUsed = true;
998
1023
  try {
999
1024
  clickPage = await invoke({
1000
1025
  action: "open",
@@ -1026,15 +1051,7 @@ export async function probeAlphaCapabilities({ invoke, schemaFingerprint, truste
1026
1051
  actions.click = probeFailureState(error);
1027
1052
  }
1028
1053
  }
1029
- if (actions.find === "supported" ||
1030
- (actions.click === "supported" && !clickProbeUsed))
1031
- result.classification = trustedNativeProvenance
1032
- ? "native"
1033
- : "command-capable";
1034
- else if (actions.find === "unsupported" && actions.click === "unsupported")
1035
- result.classification = "emulated-search-only";
1036
- else
1037
- result.classification = "unknown";
1054
+ result.classification = trustedNativeProvenance ? "native" : "command-capable";
1038
1055
  if (screenshotProbeRef) {
1039
1056
  try {
1040
1057
  const pdf = await invoke({
@@ -67,7 +67,7 @@ export function alphaSearchParametersFor(record) {
67
67
  ...ALPHA_SEARCH_PARAMETERS,
68
68
  properties: {
69
69
  ...ALPHA_SEARCH_PARAMETERS.properties,
70
- action: { type: "string", enum: alphaAdvertisedActions(record) },
70
+ action: { ...ALPHA_SEARCH_PARAMETERS.properties.action, enum: alphaAdvertisedActions(record) },
71
71
  },
72
72
  };
73
73
  }