dsh-lcx-codex 0.3.0

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.
@@ -0,0 +1,451 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { outputDomains, outputLineRange, outputLinks, outputPdfRefs, parseWebRunOutput } from './web-run-output.js'
3
+
4
+ export const ALPHA_ACTIONS = ['search_query', 'image_query', 'open', 'find', 'click', 'screenshot', 'finance', 'weather', 'sports', 'time']
5
+ const LEAGUES = ['nba', 'wnba', 'nfl', 'nhl', 'mlb', 'epl', 'ncaamb', 'ncaawb', 'ipl']
6
+ const ASSET_TYPES = ['equity', 'fund', 'crypto', 'index']
7
+ const RESPONSE_LENGTHS = ['short', 'medium', 'long']
8
+
9
+ export const ALPHA_SEARCH_PARAMETERS = {
10
+ type: 'object',
11
+ properties: {
12
+ action: { type: 'string', enum: ALPHA_ACTIONS },
13
+ query: { type: 'string' },
14
+ domains: { type: 'array', items: { type: 'string' } },
15
+ recency: { type: 'integer' },
16
+ refId: { type: 'string' },
17
+ lineNumber: { type: 'integer' },
18
+ linkId: { type: 'integer' },
19
+ pattern: { type: 'string' },
20
+ pageNumber: { type: 'integer' },
21
+ ticker: { type: 'string' },
22
+ assetType: { type: 'string', enum: ASSET_TYPES },
23
+ market: { type: 'string' },
24
+ location: { type: 'string' },
25
+ start: { type: 'string' },
26
+ duration: { type: 'integer' },
27
+ fn: { type: 'string', enum: ['schedule', 'standings'] },
28
+ league: { type: 'string', enum: LEAGUES },
29
+ team: { type: 'string' },
30
+ opponent: { type: 'string' },
31
+ dateFrom: { type: 'string' },
32
+ dateTo: { type: 'string' },
33
+ numberOfGames: { type: 'integer' },
34
+ locale: { type: 'string' },
35
+ utcOffset: { type: 'string' },
36
+ responseLength: { type: 'string', enum: RESPONSE_LENGTHS },
37
+ },
38
+ required: ['action'],
39
+ additionalProperties: false,
40
+ }
41
+
42
+ export const ALPHA_SEARCH_OUTPUT = {
43
+ type: 'object',
44
+ properties: {
45
+ mode: { type: 'string', enum: ['alpha'] },
46
+ action: { type: 'string' },
47
+ capability: { type: 'string' },
48
+ emulation: { type: 'string', enum: ['native', 'unknown'] },
49
+ content: { type: 'string' },
50
+ results: { type: 'array', items: { type: 'object' } },
51
+ refs: { type: 'array', items: { type: 'string' } },
52
+ sources: { type: 'array', items: { type: 'object' } },
53
+ citations: { type: 'array', items: { type: 'object' } },
54
+ outputBlocks: { type: 'array', items: { type: 'object' } },
55
+ links: { type: 'array', items: { type: 'object' } },
56
+ pdfRefs: { type: 'array', items: { type: 'string' } },
57
+ domains: { type: 'array', items: { type: 'string' } },
58
+ lineRange: { type: 'object' },
59
+ requestId: { type: 'string' },
60
+ responseId: { type: 'string' },
61
+ retrievedAt: { type: 'string' },
62
+ warnings: { type: 'array', items: { type: 'string' } },
63
+ },
64
+ required: ['mode', 'action', 'capability', 'emulation', 'content', 'results', 'refs', 'sources', 'citations', 'outputBlocks', 'links', 'pdfRefs', 'domains', 'requestId', 'retrievedAt', 'warnings'],
65
+ additionalProperties: false,
66
+ }
67
+
68
+ export const ALPHA_SCHEMA_FINGERPRINT = createHash('sha256').update(JSON.stringify(ALPHA_SEARCH_PARAMETERS), 'utf8').digest('hex')
69
+
70
+ function failure(message, code = 'WEB_INVALID_REQUEST') {
71
+ const error = new Error(message)
72
+ error.code = code
73
+ return error
74
+ }
75
+
76
+ function isRecord(value) {
77
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
78
+ }
79
+
80
+ function text(value, field, max = 1000) {
81
+ if (typeof value !== 'string' || value.trim().length === 0 || value.trim().length > max) throw failure(`websearch_alpha.${field} is invalid`)
82
+ return value.trim()
83
+ }
84
+
85
+ function integer(value, field, minimum = 0, maximum = 10_000) {
86
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw failure(`websearch_alpha.${field} is invalid`)
87
+ return value
88
+ }
89
+
90
+ function optionalText(args, result, field, wireField = field, max = 1000) {
91
+ if (args[field] !== undefined) result[wireField] = text(args[field], field, max)
92
+ }
93
+
94
+ function date(value, field) {
95
+ const normalized = text(value, field, 10)
96
+ if (!/^\d{4}-\d{2}-\d{2}$/u.test(normalized) || Number.isNaN(Date.parse(`${normalized}T00:00:00Z`))) throw failure(`websearch_alpha.${field} must use YYYY-MM-DD`)
97
+ return normalized
98
+ }
99
+
100
+ function domains(value) {
101
+ if (!Array.isArray(value) || value.length === 0 || value.length > 100) throw failure('websearch_alpha.domains is invalid')
102
+ const result = value.map((item) => {
103
+ const domain = text(item, 'domains', 253).toLowerCase()
104
+ if (domain.includes('/') || domain.includes(':') || !domain.includes('.')) throw failure('websearch_alpha.domains contains an invalid domain')
105
+ return domain
106
+ })
107
+ if (new Set(result).size !== result.length) throw failure('websearch_alpha.domains contains duplicates')
108
+ return result
109
+ }
110
+
111
+ const FIELDS = {
112
+ search_query: ['query', 'domains', 'recency'],
113
+ image_query: ['query', 'domains', 'recency'],
114
+ open: ['refId', 'lineNumber'],
115
+ find: ['refId', 'pattern'],
116
+ click: ['refId', 'linkId'],
117
+ screenshot: ['refId', 'pageNumber'],
118
+ finance: ['ticker', 'assetType', 'market'],
119
+ weather: ['location', 'start', 'duration'],
120
+ sports: ['fn', 'league', 'team', 'opponent', 'dateFrom', 'dateTo', 'numberOfGames', 'locale'],
121
+ time: ['utcOffset'],
122
+ }
123
+
124
+ export function normalizeAlphaSearchArgs(args) {
125
+ if (!isRecord(args) || !ALPHA_ACTIONS.includes(args.action)) throw failure('websearch_alpha.action is invalid')
126
+ const allowed = new Set(['action', 'responseLength', ...FIELDS[args.action]])
127
+ if (Object.keys(args).some((key) => !allowed.has(key))) throw failure(`websearch_alpha.${args.action} received an unrelated field`)
128
+ const result = { action: args.action }
129
+ if (args.responseLength !== undefined) {
130
+ if (!RESPONSE_LENGTHS.includes(args.responseLength)) throw failure('websearch_alpha.responseLength is invalid')
131
+ result.responseLength = args.responseLength
132
+ }
133
+ switch (args.action) {
134
+ case 'search_query':
135
+ case 'image_query':
136
+ result.query = text(args.query, 'query', 16_000)
137
+ if (args.domains !== undefined) result.domains = domains(args.domains)
138
+ if (args.recency !== undefined) result.recency = integer(args.recency, 'recency', 0, 3650)
139
+ break
140
+ case 'open':
141
+ result.refId = text(args.refId, 'refId')
142
+ if (args.lineNumber !== undefined) result.lineNumber = integer(args.lineNumber, 'lineNumber')
143
+ break
144
+ case 'find':
145
+ result.refId = text(args.refId, 'refId')
146
+ result.pattern = text(args.pattern, 'pattern')
147
+ break
148
+ case 'click':
149
+ result.refId = text(args.refId, 'refId')
150
+ result.linkId = integer(args.linkId, 'linkId')
151
+ break
152
+ case 'screenshot':
153
+ result.refId = text(args.refId, 'refId')
154
+ result.pageNumber = integer(args.pageNumber, 'pageNumber')
155
+ break
156
+ case 'finance':
157
+ result.ticker = text(args.ticker, 'ticker', 40)
158
+ if (!ASSET_TYPES.includes(args.assetType)) throw failure('websearch_alpha.assetType is invalid')
159
+ result.assetType = args.assetType
160
+ optionalText(args, result, 'market', 'market', 20)
161
+ break
162
+ case 'weather':
163
+ result.location = text(args.location, 'location', 500)
164
+ if (args.start !== undefined) result.start = date(args.start, 'start')
165
+ if (args.duration !== undefined) result.duration = integer(args.duration, 'duration', 1, 14)
166
+ break
167
+ case 'sports':
168
+ if (!['schedule', 'standings'].includes(args.fn)) throw failure('websearch_alpha.fn is invalid')
169
+ if (!LEAGUES.includes(args.league)) throw failure('websearch_alpha.league is invalid')
170
+ result.fn = args.fn
171
+ result.league = args.league
172
+ for (const field of ['team', 'opponent', 'locale']) optionalText(args, result, field, field, 100)
173
+ if (args.dateFrom !== undefined) result.dateFrom = date(args.dateFrom, 'dateFrom')
174
+ if (args.dateTo !== undefined) result.dateTo = date(args.dateTo, 'dateTo')
175
+ if (args.numberOfGames !== undefined) result.numberOfGames = integer(args.numberOfGames, 'numberOfGames', 1, 100)
176
+ break
177
+ case 'time':
178
+ result.utcOffset = text(args.utcOffset, 'utcOffset', 6)
179
+ if (!/^[+-](?:0\d|1\d|2[0-3]):[0-5]\d$/u.test(result.utcOffset)) throw failure('websearch_alpha.utcOffset is invalid')
180
+ break
181
+ }
182
+ return result
183
+ }
184
+
185
+ export function alphaActionCommand(args) {
186
+ let value
187
+ switch (args.action) {
188
+ case 'search_query':
189
+ case 'image_query':
190
+ value = { q: args.query, ...(args.recency !== undefined ? { recency: args.recency } : {}), ...(args.domains ? { domains: args.domains } : {}) }
191
+ break
192
+ case 'open': value = { ref_id: args.refId, ...(args.lineNumber !== undefined ? { lineno: args.lineNumber } : {}) }; break
193
+ case 'find': value = { ref_id: args.refId, pattern: args.pattern }; break
194
+ case 'click': value = { ref_id: args.refId, id: args.linkId }; break
195
+ case 'screenshot': value = { ref_id: args.refId, pageno: args.pageNumber }; break
196
+ case 'finance': value = { ticker: args.ticker, type: args.assetType, ...(args.market ? { market: args.market } : {}) }; break
197
+ case 'weather': value = { location: args.location, ...(args.start ? { start: args.start } : {}), ...(args.duration !== undefined ? { duration: args.duration } : {}) }; break
198
+ case 'sports':
199
+ value = {
200
+ tool: 'sports',
201
+ fn: args.fn,
202
+ league: args.league,
203
+ ...(args.team ? { team: args.team } : {}),
204
+ ...(args.opponent ? { opponent: args.opponent } : {}),
205
+ ...(args.dateFrom ? { date_from: args.dateFrom } : {}),
206
+ ...(args.dateTo ? { date_to: args.dateTo } : {}),
207
+ ...(args.numberOfGames !== undefined ? { num_games: args.numberOfGames } : {}),
208
+ ...(args.locale ? { locale: args.locale } : {}),
209
+ }
210
+ break
211
+ case 'time': value = { utc_offset: args.utcOffset }; break
212
+ default: throw failure('websearch_alpha.action is invalid')
213
+ }
214
+ return { [args.action]: [value], ...(args.responseLength ? { response_length: args.responseLength } : {}) }
215
+ }
216
+
217
+ function actionInput(args) {
218
+ if (args.query) return args.query
219
+ if (args.refId) return `${args.action}: ${args.refId}`
220
+ if (args.ticker) return `${args.action}: ${args.ticker}`
221
+ if (args.location) return `${args.action}: ${args.location}`
222
+ if (args.utcOffset) return `${args.action}: ${args.utcOffset}`
223
+ return `${args.action}: ${args.league ?? ''}`.trim()
224
+ }
225
+
226
+ export function buildAlphaSearchBody(args, model, sessionId, externalWebAccess = true, maxOutputTokens = 2500) {
227
+ if (typeof sessionId !== 'string' || sessionId.length === 0) throw failure('websearch_alpha requires a DSH session', 'LCX_ALPHA_SESSION_REQUIRED')
228
+ return {
229
+ id: sessionId,
230
+ model,
231
+ input: [{ role: 'user', content: [{ type: 'input_text', text: actionInput(args) }] }],
232
+ commands: alphaActionCommand(args),
233
+ settings: { allowed_callers: ['direct'], external_web_access: externalWebAccess },
234
+ max_output_tokens: maxOutputTokens,
235
+ }
236
+ }
237
+
238
+ function httpUrl(value) {
239
+ try {
240
+ const url = new URL(value)
241
+ return ['http:', 'https:'].includes(url.protocol) ? url.toString() : undefined
242
+ } catch {
243
+ return undefined
244
+ }
245
+ }
246
+
247
+ function safeResult(value, seen = new Set()) {
248
+ if (value === null || typeof value !== 'object') return value
249
+ if (seen.has(value)) return undefined
250
+ seen.add(value)
251
+ if (Array.isArray(value)) return value.map((item) => safeResult(item, seen)).filter((item) => item !== undefined)
252
+ const result = {}
253
+ for (const [key, item] of Object.entries(value)) {
254
+ if (key === 'encrypted_output' || key === 'encrypted_content') continue
255
+ if (['url', 'source_url', 'source_website_url', 'image_url', 'thumbnail_url'].includes(key)) {
256
+ const url = httpUrl(item)
257
+ if (url) result[key] = url
258
+ continue
259
+ }
260
+ const safe = safeResult(item, seen)
261
+ if (safe !== undefined) result[key] = safe
262
+ }
263
+ return result
264
+ }
265
+
266
+ function responseArtifacts(results) {
267
+ const refs = []
268
+ const refRecords = []
269
+ const sources = []
270
+ const seenRefs = new Set()
271
+ const seenSources = new Set()
272
+ const visit = (value) => {
273
+ if (!value || typeof value !== 'object') return
274
+ if (Array.isArray(value)) {
275
+ for (const item of value) visit(item)
276
+ return
277
+ }
278
+ const refId = typeof value.ref_id === 'string' && value.ref_id.length > 0
279
+ ? value.ref_id
280
+ : typeof value.id === 'string' && /^turn[\w-]+$/u.test(value.id) ? value.id : undefined
281
+ const url = httpUrl(value.url ?? value.source_url ?? value.source_website_url)
282
+ if (refId && !seenRefs.has(refId)) {
283
+ seenRefs.add(refId)
284
+ refs.push(refId)
285
+ refRecords.push({ refId, ...(url ? { url } : {}) })
286
+ }
287
+ if (url && !seenSources.has(url)) {
288
+ seenSources.add(url)
289
+ sources.push({
290
+ url,
291
+ ...(typeof value.title === 'string' && value.title ? { title: value.title } : {}),
292
+ ...(typeof value.snippet === 'string' && value.snippet ? { snippet: value.snippet } : {}),
293
+ ...(refId ? { refId } : {}),
294
+ })
295
+ }
296
+ for (const item of Object.values(value)) visit(item)
297
+ }
298
+ visit(results)
299
+ return { refs, refRecords, sources }
300
+ }
301
+
302
+ const ALPHA_ACTION_ERROR_PATTERN = /^\s*(?:Error parsing function call\b|Invalid function_name=|Invalid function call\b)/iu
303
+
304
+ export function parseAlphaSearchResponse(response, options) {
305
+ if (!isRecord(response) || typeof response.output !== 'string' || (response.results !== undefined && !Array.isArray(response.results))) {
306
+ throw failure('LCX Alpha Web Search returned an invalid response', 'LCX_ALPHA_INVALID_RESPONSE')
307
+ }
308
+ if (ALPHA_ACTION_ERROR_PATTERN.test(response.output)) {
309
+ throw failure('LCX Alpha Web Search could not execute the requested action', 'LCX_ALPHA_ACTION_FAILED')
310
+ }
311
+ const results = safeResult(response.results ?? [])
312
+ const artifacts = responseArtifacts(results)
313
+ const outputBlocks = parseWebRunOutput(response.output)
314
+ for (const refId of outputBlocks.flatMap((block) => block.references ?? [])) {
315
+ if (!artifacts.refs.includes(refId)) artifacts.refs.push(refId)
316
+ }
317
+ const blockSources = outputBlocks.flatMap((block) => block.url ? [{ url: block.url, ...(block.title ? { title: block.title } : {}) }] : [])
318
+ for (const source of blockSources) {
319
+ const canonical = httpUrl(source.url)
320
+ if (canonical && !artifacts.sources.some((item) => httpUrl(item.url) === canonical)) artifacts.sources.push(source)
321
+ }
322
+ return {
323
+ mode: 'alpha',
324
+ action: options.action,
325
+ capability: options.capability,
326
+ emulation: options.capability === 'native' ? 'native' : 'unknown',
327
+ content: response.output,
328
+ results,
329
+ refs: artifacts.refs,
330
+ sources: artifacts.sources,
331
+ citations: artifacts.sources.map((source) => ({ ...source })),
332
+ outputBlocks,
333
+ links: outputLinks(outputBlocks),
334
+ pdfRefs: outputPdfRefs(outputBlocks),
335
+ domains: outputDomains(outputBlocks),
336
+ ...(outputLineRange(outputBlocks) ? { lineRange: outputLineRange(outputBlocks) } : {}),
337
+ requestId: options.requestId,
338
+ ...(typeof response.id === 'string' && response.id ? { responseId: response.id } : {}),
339
+ retrievedAt: options.retrievedAt ?? new Date().toISOString(),
340
+ warnings: options.capability === 'command-capable' ? ['Alpha command behavior is verified, but trusted native backend provenance is unavailable.'] : [],
341
+ }
342
+ }
343
+
344
+ function probeFailureState(error) {
345
+ if ([404, 405].includes(error?.status) || error?.code === 'LCX_ALPHA_ACTION_UNAVAILABLE' || /channel does not support|unsupported action|not implemented/iu.test(String(error?.message ?? ''))) {
346
+ return 'unsupported'
347
+ }
348
+ return 'unknown'
349
+ }
350
+
351
+ export async function probeAlphaCapabilities({
352
+ invoke,
353
+ schemaFingerprint,
354
+ trustedNativeProvenance = false,
355
+ actionProbes = {},
356
+ clickProbeRef,
357
+ screenshotProbeRef,
358
+ }) {
359
+ const actions = Object.fromEntries(ALPHA_ACTIONS.map((action) => [action, 'unknown']))
360
+ const result = {
361
+ classification: 'unsupported',
362
+ actions,
363
+ probedAt: new Date().toISOString(),
364
+ schemaFingerprint,
365
+ provenance: trustedNativeProvenance ? 'trusted-native' : 'unavailable',
366
+ }
367
+ let search
368
+ try {
369
+ search = await invoke({ action: 'search_query', query: 'OpenAI official documentation', responseLength: 'short' })
370
+ actions.search_query = 'supported'
371
+ } catch (error) {
372
+ actions.search_query = probeFailureState(error)
373
+ result.classification = actions.search_query === 'unsupported' ? 'unsupported' : 'unknown'
374
+ return result
375
+ }
376
+ const searchRef = search?.refs?.[0]
377
+ if (!searchRef) {
378
+ result.classification = 'emulated-search-only'
379
+ for (const action of ['open', 'find', 'click']) actions[action] = 'unsupported'
380
+ return result
381
+ }
382
+ let opened
383
+ try {
384
+ opened = await invoke({ action: 'open', refId: searchRef, responseLength: 'short' })
385
+ actions.open = 'supported'
386
+ } catch (error) {
387
+ actions.open = probeFailureState(error)
388
+ result.classification = actions.open === 'unsupported' ? 'emulated-search-only' : 'unknown'
389
+ return result
390
+ }
391
+ try {
392
+ await invoke({ action: 'find', refId: opened?.refs?.[0] ?? searchRef, pattern: 'OpenAI', responseLength: 'short' })
393
+ actions.find = 'supported'
394
+ } catch (error) {
395
+ actions.find = probeFailureState(error)
396
+ }
397
+ let clickPage = opened
398
+ if (!(clickPage?.links?.length > 0) && clickProbeRef) {
399
+ try {
400
+ clickPage = await invoke({ action: 'open', refId: clickProbeRef, responseLength: 'short' })
401
+ } catch (error) {
402
+ actions.click = probeFailureState(error)
403
+ }
404
+ }
405
+ const clickRef = clickPage?.refs?.[0]
406
+ const linkId = clickPage?.links?.[0]?.id
407
+ if (clickRef && Number.isSafeInteger(linkId)) {
408
+ try {
409
+ await invoke({ action: 'click', refId: clickRef, linkId, responseLength: 'short' })
410
+ actions.click = 'supported'
411
+ } catch (error) {
412
+ actions.click = probeFailureState(error)
413
+ }
414
+ }
415
+ if (actions.find === 'supported' || actions.click === 'supported') {
416
+ result.classification = trustedNativeProvenance ? 'native' : 'command-capable'
417
+ } else if (actions.find === 'unsupported' && actions.click === 'unsupported') {
418
+ result.classification = 'emulated-search-only'
419
+ } else {
420
+ result.classification = 'unknown'
421
+ }
422
+ if (screenshotProbeRef) {
423
+ try {
424
+ const pdf = await invoke({ action: 'open', refId: screenshotProbeRef, responseLength: 'short' })
425
+ const pdfRef = pdf?.pdfRefs?.[0]
426
+ if (pdfRef) {
427
+ await invoke({ action: 'screenshot', refId: pdfRef, pageNumber: 0, responseLength: 'short' })
428
+ actions.screenshot = 'supported'
429
+ }
430
+ } catch (error) {
431
+ actions.screenshot = probeFailureState(error)
432
+ }
433
+ }
434
+ for (const [action, args] of Object.entries(actionProbes)) {
435
+ try {
436
+ await invoke({ action, ...args, responseLength: 'short' })
437
+ actions[action] = 'supported'
438
+ } catch (error) {
439
+ actions[action] = probeFailureState(error)
440
+ }
441
+ }
442
+ return result
443
+ }
444
+
445
+ export function renderAlphaSearchResult(value) {
446
+ const parts = [value.content]
447
+ if (value.sources?.length) parts.push(`来源:\n${value.sources.map((source) => `- [${source.title ?? source.url}](${source.url})`).join('\n')}`)
448
+ if (value.warnings?.length) parts.push(value.warnings.map((warning) => `警告:${warning}`).join('\n'))
449
+ parts.push(`检索时间:${value.retrievedAt}`)
450
+ return [{ type: 'text', text: parts.filter(Boolean).join('\n\n') }]
451
+ }
@@ -0,0 +1,78 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { AtomicWebSearchStore } from './web-search-store.js'
3
+
4
+ const VERSION = 1
5
+ const CLASSIFICATIONS = new Set(['native', 'command-capable', 'emulated-search-only', 'unsupported', 'unknown'])
6
+ const ACTION_STATES = new Set(['supported', 'unsupported', 'unknown'])
7
+
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
10
+ }
11
+
12
+ function validRecord(record) {
13
+ return isRecord(record) && CLASSIFICATIONS.has(record.classification) &&
14
+ isRecord(record.actions) && Object.values(record.actions).every((value) => ACTION_STATES.has(value)) &&
15
+ typeof record.probedAt === 'string' && !Number.isNaN(Date.parse(record.probedAt)) &&
16
+ typeof record.schemaFingerprint === 'string' && record.schemaFingerprint.length > 0 &&
17
+ (record.provenance === undefined || ['trusted-native', 'unavailable'].includes(record.provenance)) &&
18
+ (record.classification !== 'native' || record.provenance === 'trusted-native')
19
+ }
20
+
21
+ function validData(data) {
22
+ return data?.version === VERSION && isRecord(data.capabilities) &&
23
+ Object.entries(data.capabilities).every(([key, value]) => /^[a-f0-9]{64}$/u.test(key) && validRecord(value))
24
+ }
25
+
26
+ export function alphaCapabilityFingerprint(config) {
27
+ const rawBaseURL = String(config.baseURL ?? '').replace(/\/+$/u, '')
28
+ let baseURL = rawBaseURL
29
+ try {
30
+ const url = new URL(rawBaseURL)
31
+ url.hash = ''
32
+ baseURL = url.toString().replace(/\/+$/u, '')
33
+ } catch {
34
+ // Keep invalid configuration distinct; request validation reports it later.
35
+ }
36
+ const canonical = {
37
+ baseURL,
38
+ provider: String(config.provider ?? ''),
39
+ model: String(config.model ?? ''),
40
+ profile: String(config.profile ?? ''),
41
+ group: String(config.group ?? ''),
42
+ schemaFingerprint: String(config.schemaFingerprint ?? ''),
43
+ }
44
+ return createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')
45
+ }
46
+
47
+ export function alphaCapabilityUsable(record) {
48
+ return record?.classification === 'native' || record?.classification === 'command-capable'
49
+ }
50
+
51
+ export class AlphaCapabilityStore {
52
+ constructor(file) {
53
+ this.store = new AtomicWebSearchStore(file, {
54
+ empty: () => ({ version: VERSION, capabilities: {} }),
55
+ validate: validData,
56
+ corruptCode: 'LCX_ALPHA_CAPABILITY_STORE_CORRUPT',
57
+ writeCode: 'LCX_ALPHA_CAPABILITY_STORE_WRITE_FAILED',
58
+ })
59
+ }
60
+
61
+ get(fingerprint) {
62
+ this.store.refresh()
63
+ const value = this.store.data.capabilities[fingerprint]
64
+ return value === undefined ? undefined : structuredClone(value)
65
+ }
66
+
67
+ put(fingerprint, record) {
68
+ if (!/^[a-f0-9]{64}$/u.test(String(fingerprint)) || !validRecord(record)) {
69
+ const error = new Error('Invalid Alpha capability record')
70
+ error.code = 'LCX_ALPHA_CAPABILITY_INVALID'
71
+ throw error
72
+ }
73
+ this.store.update((current) => ({
74
+ version: VERSION,
75
+ capabilities: { ...current.capabilities, [fingerprint]: structuredClone(record) },
76
+ }))
77
+ }
78
+ }