dsh-wsl-desktop 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/client.js +21 -0
- package/lib/host/http.js +110 -0
- package/lib/host/presets.js +97 -0
- package/lib/host/state.js +58 -0
- package/lib/index.js +100 -276
- package/lib/wsl/distros.js +190 -0
- package/lib/wsl/preset.js +7 -0
- package/lib/wsl/proc.js +182 -0
- package/lib/wsl/shell.js +2 -1
- package/lib/wsl/subprocess.js +2 -1
- package/lib/wsl/world.js +4 -341
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -13,19 +13,25 @@
|
|
|
13
13
|
* @module dsh-wsl-desktop
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
19
|
-
import { homedir } from 'node:os'
|
|
16
|
+
import { watch as nativeFsWatch } from 'node:fs'
|
|
17
|
+
import { mkdir, readFile } from 'node:fs/promises'
|
|
20
18
|
import { dirname, join } from 'node:path'
|
|
21
19
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
22
20
|
import z from '@deepseek-ai/schemastery'
|
|
23
21
|
import {
|
|
24
22
|
MAX_BODY_BYTES, DEV_TOKEN_HEADER, bodyAdmission, dispatchAdmission, fenceRejection, methodAdmission, preflight, tokenMatches,
|
|
25
23
|
} from './http-admission.js'
|
|
24
|
+
import {
|
|
25
|
+
connectionOf, devToken, isDeveloperCaller, readJsonBody, sendJson, sendRejection,
|
|
26
|
+
} from './host/http.js'
|
|
27
|
+
import {
|
|
28
|
+
GENERATED_PREFIX, basePluginsOf, runShellSpec, sourceDirFor, variantIdFor,
|
|
29
|
+
} from './host/presets.js'
|
|
30
|
+
import { bindingTape, hostSession, presetRun } from './host/state.js'
|
|
26
31
|
import { buildVariantPlugins } from './wsl/preset.js'
|
|
27
32
|
import { setLocalSubprocess } from './wsl/host-refs.js'
|
|
28
|
-
import { checkLinuxPath,
|
|
33
|
+
import { checkLinuxPath, defaultWorkspaceUnc, listLinuxDir, resolveDistroHome, runWslShell } from './wsl/world.js'
|
|
34
|
+
import { defaultDistro, listDistros, wslMajorVersion } from './wsl/distros.js'
|
|
29
35
|
import { joinWslUnc, parseWslUnc, shellQuote, windowsToMntPath } from './wsl/paths.js'
|
|
30
36
|
import { WslRouteError, browserFailure, routeErrorFor } from './route-errors.js'
|
|
31
37
|
import { CHECK_IDS, CHECK_LABELS, CHECK_REMEDIES, DOCS, ERROR_CODES, FEATURE_IDS, FEATURE_LABELS } from './protocol.js'
|
|
@@ -87,23 +93,6 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
87
93
|
*/
|
|
88
94
|
const SELF_MODULE = fileURLToPath(import.meta.url)
|
|
89
95
|
|
|
90
|
-
/** Prefix of every preset this plugin owns; its own outputs are never sources. */
|
|
91
|
-
const GENERATED_PREFIX = 'wsl-'
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The generated variant id for a resolved base preset.
|
|
95
|
-
*
|
|
96
|
-
* A preset that is already a variant keeps its id; anything else gets the prefix. The
|
|
97
|
-
* selftest and createWorkspace both derived it this way — the second site's own comment
|
|
98
|
-
* admitted it resolved exactly like the first — so a change to the prefix rule had two
|
|
99
|
-
* homes.
|
|
100
|
-
* @param {{ id: string }} preset - the resolved base preset.
|
|
101
|
-
* @returns {string} the variant id.
|
|
102
|
-
*/
|
|
103
|
-
function variantIdFor(preset) {
|
|
104
|
-
return preset.id.startsWith(GENERATED_PREFIX) ? preset.id : `${GENERATED_PREFIX}${preset.id}`
|
|
105
|
-
}
|
|
106
|
-
|
|
107
96
|
/**
|
|
108
97
|
* Module specifiers of the WSL providers, as the preset rows must name them.
|
|
109
98
|
* The 0.1.7 loader imports row names via `new URL(name, baseUrl)` or a bare
|
|
@@ -132,30 +121,6 @@ const SANDBOX_MODULE = pathToFileURL(join(HERE, 'wsl', 'sandbox.js')).href
|
|
|
132
121
|
*/
|
|
133
122
|
const SEARCH_MODULE = pathToFileURL(join(HERE, 'wsl', 'search-tool.js')).href
|
|
134
123
|
|
|
135
|
-
/** Last preset materialization outcome, reported through the route. */
|
|
136
|
-
let presetState = { status: 'pending' }
|
|
137
|
-
|
|
138
|
-
/** Recent preset-binding decisions, reported through the route. */
|
|
139
|
-
const bindingLog = []
|
|
140
|
-
|
|
141
|
-
/** Most binding decisions kept for inspection. */
|
|
142
|
-
const BINDING_LOG_LIMIT = 50
|
|
143
|
-
|
|
144
|
-
/** The plugin context, captured so route methods can reach host services. */
|
|
145
|
-
let hostCtx
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The configured `wsl.exe` path, captured where the route methods can read it.
|
|
149
|
-
*
|
|
150
|
-
* The methods table is module-level (the route dispatcher calls into it without
|
|
151
|
-
* carrying the plugin's config), so the configured path is held beside the
|
|
152
|
-
* captured context rather than threaded through every signature.
|
|
153
|
-
*/
|
|
154
|
-
let routeWslPath = 'wsl.exe'
|
|
155
|
-
|
|
156
|
-
/** Whether the preset registry has been reached; `bootstrap` waits briefly for it. */
|
|
157
|
-
let hostReady = false
|
|
158
|
-
|
|
159
124
|
/** The plugin version, read from the shipped package.json once. */
|
|
160
125
|
let pluginVersionValue = null
|
|
161
126
|
|
|
@@ -184,21 +149,21 @@ async function pluginVersion() {
|
|
|
184
149
|
* @returns {Promise<object>} the probe, or a pending marker.
|
|
185
150
|
*/
|
|
186
151
|
async function probeHostWhenReady() {
|
|
187
|
-
if (
|
|
152
|
+
if (hostSession.ctx === undefined) {
|
|
188
153
|
return { status: 'pending', features: {}, missing: [], checkedAt: new Date().toISOString() }
|
|
189
154
|
}
|
|
190
155
|
const deadline = Date.now() + 5_000
|
|
191
|
-
while (!
|
|
156
|
+
while (!hostSession.ready && Date.now() < deadline) {
|
|
192
157
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
193
158
|
}
|
|
194
|
-
if (!
|
|
159
|
+
if (!hostSession.ready) {
|
|
195
160
|
// The registry never appeared. Probing the context anyway reports the missing
|
|
196
161
|
// feature as degraded; returning `pending` here would leave the picker
|
|
197
162
|
// proceeding against a world it cannot name a preset in.
|
|
198
|
-
if (
|
|
163
|
+
if (hostSession.ctx.get('agentPresets') === undefined) return probeHost(hostSession.ctx)
|
|
199
164
|
return { status: 'pending', features: {}, missing: [], checkedAt: new Date().toISOString() }
|
|
200
165
|
}
|
|
201
|
-
return probeHost(
|
|
166
|
+
return probeHost(hostSession.ctx)
|
|
202
167
|
}
|
|
203
168
|
|
|
204
169
|
/**
|
|
@@ -209,14 +174,6 @@ const DISCOVERY_METHODS = new Set([
|
|
|
209
174
|
'listDistros', 'defaultDistro', 'listDir', 'checkPath', 'resolve', 'resolveHome', 'wslPresetFor',
|
|
210
175
|
])
|
|
211
176
|
|
|
212
|
-
/**
|
|
213
|
-
* The harness home that holds the user preset root.
|
|
214
|
-
* @returns {string} the resolved `$DSH_HOME`.
|
|
215
|
-
*/
|
|
216
|
-
function dshHome() {
|
|
217
|
-
return process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
177
|
/**
|
|
221
178
|
* Resolve a location the caller supplied into one distribution plus Linux path.
|
|
222
179
|
* @param {unknown} location - UNC path, absolute Linux path, or Windows drive path.
|
|
@@ -235,53 +192,6 @@ async function resolveLocation(location, distroHint) {
|
|
|
235
192
|
return { distro, linuxPath, uncPath: joinWslUnc(distro, linuxPath) }
|
|
236
193
|
}
|
|
237
194
|
|
|
238
|
-
/**
|
|
239
|
-
* Locate the directory a preset was discovered under.
|
|
240
|
-
*
|
|
241
|
-
* A preset may name a row by a path relative to its own directory, so the
|
|
242
|
-
* variant needs that base to rewrite the name to something that still resolves
|
|
243
|
-
* from where the variant lives.
|
|
244
|
-
*
|
|
245
|
-
* Both spellings are historical: the package is `@deepseek-ai/dsh-agent-preset`
|
|
246
|
-
* (singular — the plural name never existed, so this resolve always threw), and
|
|
247
|
-
* since the presets moved into profile YAML declarations the package ships no
|
|
248
|
-
* `presets/<id>/agent.cordis.yml` tree at all. Neither branch is expected to
|
|
249
|
-
* hit on a current desktop; a base preset that DOES use relative row names
|
|
250
|
-
* therefore has no resolvable source directory, and `buildVariantPlugins`
|
|
251
|
-
* leaves the name as it found it. This stays a lookup rather than a guess so
|
|
252
|
-
* the failure mode is unchanged if a deployment ever ships that tree again.
|
|
253
|
-
* @param {string} id - preset id.
|
|
254
|
-
* @returns {string | undefined} the directory, or undefined when it cannot be found.
|
|
255
|
-
*/
|
|
256
|
-
function sourceDirFor(id) {
|
|
257
|
-
const local = join(dshHome(), '.agent-presets', id)
|
|
258
|
-
if (existsSync(join(local, 'agent.cordis.yml'))) return local
|
|
259
|
-
try {
|
|
260
|
-
const manifest = fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-agent-preset/package.json'))
|
|
261
|
-
const shipped = join(dirname(manifest), 'presets', id)
|
|
262
|
-
if (existsSync(join(shipped, 'agent.cordis.yml'))) return shipped
|
|
263
|
-
} catch {
|
|
264
|
-
// A deployment without the preset package leaves relative names as they are.
|
|
265
|
-
}
|
|
266
|
-
return undefined
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Read a base preset's declared plugin rows as OBJECTS.
|
|
271
|
-
*
|
|
272
|
-
* 0.1.7 keeps each declaration's `PresetDefinition` — with `plugins` as entry
|
|
273
|
-
* objects — in a registry map that is TS-private but reachable in-process
|
|
274
|
-
* (ctx.get returns the service instance). No YAML parser is needed on this
|
|
275
|
-
* path; the public readDocument only exposes a re-dumped text form.
|
|
276
|
-
* @param {object} presets - the agentPresets service.
|
|
277
|
-
* @param {string} id - base preset id.
|
|
278
|
-
* @returns {readonly object[] | null} the declared rows, or null when unreachable.
|
|
279
|
-
*/
|
|
280
|
-
function basePluginsOf(presets, id) {
|
|
281
|
-
const plugins = presets.definitions?.get?.(id)?.config?.plugins
|
|
282
|
-
return Array.isArray(plugins) ? plugins : null
|
|
283
|
-
}
|
|
284
|
-
|
|
285
195
|
/**
|
|
286
196
|
* Derive and register the WSL variant presets (0.1.7-native).
|
|
287
197
|
*
|
|
@@ -303,24 +213,6 @@ function basePluginsOf(presets, id) {
|
|
|
303
213
|
* refuses duplicates).
|
|
304
214
|
* @returns {Promise<object>} the registration outcome for the route.
|
|
305
215
|
*/
|
|
306
|
-
/**
|
|
307
|
-
* Run a shell spec through whichever seam the host exposes, and always hand back a
|
|
308
|
-
* handle whose `result()` is awaitable.
|
|
309
|
-
*
|
|
310
|
-
* 0.1.7 hosts expose `execute`, which resolves with a live handle; older ones have
|
|
311
|
-
* only `run`, whose settled result carries no `result()` projection. Both selftest
|
|
312
|
-
* steps used to spell that dual dispatch themselves, and the second one called
|
|
313
|
-
* `.result()` on `run`'s settled result — a TypeError on exactly the hosts that
|
|
314
|
-
* branch exists to support. One owner, so the two steps cannot drift again.
|
|
315
|
-
* @param {object} shell - the realm's shell service.
|
|
316
|
-
* @param {object} spec - the resolved spec to run.
|
|
317
|
-
* @returns {Promise<{ result: () => Promise<object> }>} a result()-able handle.
|
|
318
|
-
*/
|
|
319
|
-
function runShellSpec(shell, spec) {
|
|
320
|
-
if (typeof shell.execute === 'function') return shell.execute(spec)
|
|
321
|
-
return shell.run(spec).then((settled) => ({ result: async () => settled }))
|
|
322
|
-
}
|
|
323
|
-
|
|
324
216
|
async function materializePresets(ctx, distro, generation, previous) {
|
|
325
217
|
const presets = ctx.get('agentPresets')
|
|
326
218
|
if (presets === undefined) throw new Error('agentPresets 服务不可用')
|
|
@@ -415,12 +307,6 @@ async function materializePresets(ctx, distro, generation, previous) {
|
|
|
415
307
|
return { written, skipped, hostCompat }
|
|
416
308
|
}
|
|
417
309
|
|
|
418
|
-
/** Serialises refreshPresets so boot-time and developer-token calls cannot interleave. */
|
|
419
|
-
let refreshMutex = Promise.resolve()
|
|
420
|
-
|
|
421
|
-
/** The live registration generation; teardown flips its disposed flag. */
|
|
422
|
-
let currentGeneration = null
|
|
423
|
-
|
|
424
310
|
/**
|
|
425
311
|
* Refresh the presets, recording the outcome for the route.
|
|
426
312
|
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin context.
|
|
@@ -429,7 +315,7 @@ let currentGeneration = null
|
|
|
429
315
|
* generation's lifecycle token (see materializePresets).
|
|
430
316
|
* @returns {Promise<object>} the new preset state.
|
|
431
317
|
*/
|
|
432
|
-
async function refreshPresets(ctx, distro, generation =
|
|
318
|
+
async function refreshPresets(ctx, distro, generation = presetRun.generation) {
|
|
433
319
|
const run = async () => {
|
|
434
320
|
// A caller may omit the generation (developer-token regenerate before the
|
|
435
321
|
// inject ran): mint a transient one so every disposer has an owner.
|
|
@@ -437,19 +323,19 @@ async function refreshPresets(ctx, distro, generation = currentGeneration) {
|
|
|
437
323
|
// Withdrawal targets the PREVIOUS generation's disposers; a self-refresh
|
|
438
324
|
// (developer-token regenerate) withdraws its own — the exact behaviour of
|
|
439
325
|
// the historical shared map, minus the cross-generation teardown theft.
|
|
440
|
-
const previous =
|
|
441
|
-
|
|
442
|
-
|
|
326
|
+
const previous = presetRun.generation === gen ? gen : presetRun.generation
|
|
327
|
+
presetRun.generation = gen
|
|
328
|
+
presetRun.state = { status: 'running' }
|
|
443
329
|
try {
|
|
444
330
|
const result = await materializePresets(ctx, distro, gen, previous)
|
|
445
|
-
|
|
331
|
+
presetRun.state = { status: 'ready', ...result }
|
|
446
332
|
} catch (error) {
|
|
447
|
-
|
|
333
|
+
presetRun.state = { status: 'failed', error: error instanceof Error ? error.message : String(error) }
|
|
448
334
|
}
|
|
449
|
-
return
|
|
335
|
+
return presetRun.state
|
|
450
336
|
}
|
|
451
|
-
const result =
|
|
452
|
-
|
|
337
|
+
const result = presetRun.mutex.then(run, run)
|
|
338
|
+
presetRun.mutex = result.then(() => {}, () => {})
|
|
453
339
|
return result
|
|
454
340
|
}
|
|
455
341
|
|
|
@@ -510,8 +396,8 @@ const METHODS = {
|
|
|
510
396
|
return healthFor({
|
|
511
397
|
distro,
|
|
512
398
|
username,
|
|
513
|
-
wslPath:
|
|
514
|
-
wslVersionProbe: ({ distro: name }) => wslMajorVersion(name, { wslPath:
|
|
399
|
+
wslPath: hostSession.wslPath,
|
|
400
|
+
wslVersionProbe: ({ distro: name }) => wslMajorVersion(name, { wslPath: hostSession.wslPath }),
|
|
515
401
|
})
|
|
516
402
|
},
|
|
517
403
|
|
|
@@ -519,7 +405,7 @@ const METHODS = {
|
|
|
519
405
|
* List the installed distributions.
|
|
520
406
|
* @returns {Promise<string[]>} distribution names.
|
|
521
407
|
*/
|
|
522
|
-
listDistros: () => listDistros({ wslPath:
|
|
408
|
+
listDistros: () => listDistros({ wslPath: hostSession.wslPath }),
|
|
523
409
|
|
|
524
410
|
/**
|
|
525
411
|
* Report the default distribution.
|
|
@@ -536,7 +422,7 @@ const METHODS = {
|
|
|
536
422
|
assertDistro(distro)
|
|
537
423
|
assertLinuxPath(path ?? '/')
|
|
538
424
|
try {
|
|
539
|
-
return await listLinuxDir(distro, path ?? '/', { wslPath:
|
|
425
|
+
return await listLinuxDir(distro, path ?? '/', { wslPath: hostSession.wslPath })
|
|
540
426
|
} catch (error) {
|
|
541
427
|
throw routeErrorFor(error, { distro, path })
|
|
542
428
|
}
|
|
@@ -554,7 +440,7 @@ const METHODS = {
|
|
|
554
440
|
const uncPath = joinWslUnc(distro, path)
|
|
555
441
|
let facts
|
|
556
442
|
try {
|
|
557
|
-
facts = await checkLinuxPath(distro, path, { wslPath:
|
|
443
|
+
facts = await checkLinuxPath(distro, path, { wslPath: hostSession.wslPath })
|
|
558
444
|
} catch (error) {
|
|
559
445
|
throw routeErrorFor(error, { distro, path })
|
|
560
446
|
}
|
|
@@ -601,7 +487,7 @@ const METHODS = {
|
|
|
601
487
|
assertDistro(distro)
|
|
602
488
|
assertUser(username)
|
|
603
489
|
try {
|
|
604
|
-
return await resolveDistroHome(distro, username, { wslPath:
|
|
490
|
+
return await resolveDistroHome(distro, username, { wslPath: hostSession.wslPath })
|
|
605
491
|
} catch (error) {
|
|
606
492
|
throw routeErrorFor(error, { distro, user: username })
|
|
607
493
|
}
|
|
@@ -617,7 +503,7 @@ const METHODS = {
|
|
|
617
503
|
* @returns {Promise<{ agentPreset: string, base: string }>} the variant to request.
|
|
618
504
|
*/
|
|
619
505
|
wslPresetFor: async ({ base } = {}) => {
|
|
620
|
-
const presets =
|
|
506
|
+
const presets = hostSession.ctx?.get('agentPresets')
|
|
621
507
|
if (presets === undefined) throw new WslRouteError('host-capability-missing', { feature: 'preset-generation' })
|
|
622
508
|
let resolved
|
|
623
509
|
try {
|
|
@@ -669,7 +555,7 @@ const METHODS = {
|
|
|
669
555
|
...(username !== undefined ? { username } : {}),
|
|
670
556
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
671
557
|
...(raw === true ? { raw: true } : {}),
|
|
672
|
-
wslPath:
|
|
558
|
+
wslPath: hostSession.wslPath,
|
|
673
559
|
})
|
|
674
560
|
return { ...result, target }
|
|
675
561
|
},
|
|
@@ -679,7 +565,7 @@ const METHODS = {
|
|
|
679
565
|
* @returns {Promise<object>} the state, plus `deployed` — the path the running host loaded.
|
|
680
566
|
*/
|
|
681
567
|
presetStatus: () => Promise.resolve({
|
|
682
|
-
...
|
|
568
|
+
...presetRun.state,
|
|
683
569
|
deployed: { modulePath: SELF_MODULE },
|
|
684
570
|
}),
|
|
685
571
|
|
|
@@ -708,8 +594,8 @@ const METHODS = {
|
|
|
708
594
|
let handle
|
|
709
595
|
let agent
|
|
710
596
|
try {
|
|
711
|
-
const agents =
|
|
712
|
-
const presets =
|
|
597
|
+
const agents = hostSession.ctx.get('agents')
|
|
598
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
713
599
|
if (agents === undefined || presets === undefined) throw new Error('agents / agentPresets 服务不可用')
|
|
714
600
|
// NOT /tmp: the fence refuses a workspace root under /tmp by design (the
|
|
715
601
|
// confinement mounts a private tmpfs over /tmp AFTER binding the workspace,
|
|
@@ -721,7 +607,7 @@ const METHODS = {
|
|
|
721
607
|
// throws before any probe — and then handed its {user, home} result to a
|
|
722
608
|
// function that requires a path string. Nothing reddened: every acceptance
|
|
723
609
|
// path passes cwd. The default is now exercised by verify-world/check-2.
|
|
724
|
-
const workspaceCwd = cwd ?? (await defaultWorkspaceUnc({ wslPath:
|
|
610
|
+
const workspaceCwd = cwd ?? (await defaultWorkspaceUnc({ wslPath: hostSession.wslPath })).uncPath
|
|
725
611
|
const sessionId = `wsl-selftest-${Date.now()}`
|
|
726
612
|
handle = await agents.create({ sessionId, meta: { cwd: workspaceCwd, agentPreset: preset } })
|
|
727
613
|
agent = handle?.agent ?? handle
|
|
@@ -739,7 +625,7 @@ const METHODS = {
|
|
|
739
625
|
if (presets.composedPreset(agent.ctx) !== preset) {
|
|
740
626
|
try {
|
|
741
627
|
await presets.select(agent, preset)
|
|
742
|
-
record('select', { ok: true, composed: presets.composedPreset(agent.ctx) ?? null, bindings:
|
|
628
|
+
record('select', { ok: true, composed: presets.composedPreset(agent.ctx) ?? null, bindings: bindingTape.log.length })
|
|
743
629
|
} catch (error) {
|
|
744
630
|
record('select', { ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
745
631
|
}
|
|
@@ -958,7 +844,7 @@ const METHODS = {
|
|
|
958
844
|
|
|
959
845
|
// The model calls tools, not providers. Driving the registry is what makes
|
|
960
846
|
// the acceptance cover the layer a session actually uses.
|
|
961
|
-
const tools =
|
|
847
|
+
const tools = hostSession.ctx.get('tools')
|
|
962
848
|
if (tools !== undefined && agent !== undefined) {
|
|
963
849
|
const controller = new AbortController()
|
|
964
850
|
/**
|
|
@@ -969,14 +855,38 @@ const METHODS = {
|
|
|
969
855
|
*/
|
|
970
856
|
const invoke = async (toolName, args) => {
|
|
971
857
|
try {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
858
|
+
// Host 0.2.1-rc.3 changed `tools.execute` from ONE call to a BATCH: it now
|
|
859
|
+
// takes `{ calls: ToolCallBlock[], signal, agent? }` and answers
|
|
860
|
+
// `{ results, additionalContexts, concluded }`. The previous single-call
|
|
861
|
+
// object is rejected with `ToolBatchExecutionError` ("Tool batch execution
|
|
862
|
+
// failed"), which every tool row below reported as a null result.
|
|
863
|
+
//
|
|
864
|
+
// `ToolCallBlock.arguments` is the RAW JSON STRING the model produces, not an
|
|
865
|
+
// object — the block is the wire shape, not a convenience wrapper.
|
|
866
|
+
const batch = await tools.execute({
|
|
867
|
+
calls: [{
|
|
868
|
+
type: 'tool-call',
|
|
869
|
+
id: `wsl-selftest-${toolName}-${Date.now()}`,
|
|
870
|
+
name: toolName,
|
|
871
|
+
arguments: JSON.stringify(args ?? {}),
|
|
872
|
+
}],
|
|
976
873
|
agent,
|
|
977
874
|
signal: controller.signal,
|
|
978
875
|
})
|
|
979
|
-
|
|
876
|
+
const outcome = batch?.results?.[0]
|
|
877
|
+
if (outcome === undefined) return { ok: false, error: 'the batch returned no result for this call' }
|
|
878
|
+
// `ToolCallResult` is `{ callId, result: ToolExecutionResult }`, and the
|
|
879
|
+
// execution result carries `value` (often a serialized JSON string). The
|
|
880
|
+
// rows below read `.text`, so the text is what this normalises: a string
|
|
881
|
+
// value is used as-is, anything else is serialized.
|
|
882
|
+
const execution = outcome.result ?? {}
|
|
883
|
+
const isError = execution.isError === true || execution.error !== undefined
|
|
884
|
+
const text = typeof execution.value === 'string'
|
|
885
|
+
? execution.value
|
|
886
|
+
: JSON.stringify(execution.value ?? execution ?? null)
|
|
887
|
+
return isError
|
|
888
|
+
? { ok: false, error: String(execution.error?.message ?? execution.error ?? 'the tool reported an error'), text }
|
|
889
|
+
: { ok: true, result: JSON.parse(JSON.stringify(execution ?? null)), text }
|
|
980
890
|
} catch (error) {
|
|
981
891
|
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
982
892
|
}
|
|
@@ -985,7 +895,7 @@ const METHODS = {
|
|
|
985
895
|
record('tools.bash', {
|
|
986
896
|
ok: bashTool.ok,
|
|
987
897
|
error: bashTool.error ?? null,
|
|
988
|
-
text: JSON.stringify(bashTool.result ?? null).slice(0, 400),
|
|
898
|
+
text: (bashTool.text ?? JSON.stringify(bashTool.result ?? null)).slice(0, 400),
|
|
989
899
|
})
|
|
990
900
|
// Derived from the workspace, not hardcoded: the fixture used to live at
|
|
991
901
|
// /tmp/dsh-wsl-selftest.txt, which only worked while the workspace was
|
|
@@ -1003,13 +913,13 @@ const METHODS = {
|
|
|
1003
913
|
record('tools.read', {
|
|
1004
914
|
ok: readTool.ok,
|
|
1005
915
|
error: readTool.error ?? null,
|
|
1006
|
-
text: JSON.stringify(readTool.result ?? null).slice(0, 400),
|
|
916
|
+
text: (readTool.text ?? JSON.stringify(readTool.result ?? null)).slice(0, 400),
|
|
1007
917
|
})
|
|
1008
918
|
const pwshTool = await invoke('pwsh', { command: 'echo host-side', description: 'selftest: this tool must not exist in a WSL session' })
|
|
1009
919
|
record('tools.pwsh', {
|
|
1010
920
|
ok: pwshTool.ok,
|
|
1011
921
|
error: pwshTool.error ?? null,
|
|
1012
|
-
text: JSON.stringify(pwshTool.result ?? null).slice(0, 200),
|
|
922
|
+
text: (pwshTool.text ?? JSON.stringify(pwshTool.result ?? null)).slice(0, 200),
|
|
1013
923
|
})
|
|
1014
924
|
|
|
1015
925
|
// The realm's own `grep`/`glob`: the host removes both names from a WSL world
|
|
@@ -1085,13 +995,13 @@ const METHODS = {
|
|
|
1085
995
|
record('tool.search.grep', {
|
|
1086
996
|
ok: grepSearch.ok && searchFixtureError === null,
|
|
1087
997
|
error: grepSearch.error ?? searchFixtureError,
|
|
1088
|
-
text: JSON.stringify(grepSearch.result ?? null).slice(0, 800),
|
|
998
|
+
text: (grepSearch.text ?? JSON.stringify(grepSearch.result ?? null)).slice(0, 800),
|
|
1089
999
|
})
|
|
1090
1000
|
const globSearch = await invoke('glob', { pattern: 'src/**/*.ts' })
|
|
1091
1001
|
record('tool.search.glob', {
|
|
1092
1002
|
ok: globSearch.ok && searchFixtureError === null,
|
|
1093
1003
|
error: globSearch.error ?? searchFixtureError,
|
|
1094
|
-
text: JSON.stringify(globSearch.result ?? null).slice(0, 800),
|
|
1004
|
+
text: (globSearch.text ?? JSON.stringify(globSearch.result ?? null)).slice(0, 800),
|
|
1095
1005
|
})
|
|
1096
1006
|
|
|
1097
1007
|
// A tool resolves its policy from the session's logged permission state
|
|
@@ -1148,7 +1058,7 @@ const METHODS = {
|
|
|
1148
1058
|
record('tools.bashPersist.a', {
|
|
1149
1059
|
ok: persistA.ok,
|
|
1150
1060
|
error: persistA.error ?? null,
|
|
1151
|
-
text: JSON.stringify(persistA.result ?? null).slice(0, 400),
|
|
1061
|
+
text: (persistA.text ?? JSON.stringify(persistA.result ?? null)).slice(0, 400),
|
|
1152
1062
|
})
|
|
1153
1063
|
const persistB = await invoke('bash', {
|
|
1154
1064
|
command: `pwd; echo "$DSH_WSL_PERSIST_PROBE"; echo probe > "$HOME/.dsh-wsl-persist-outside"; echo write-exit=$?`,
|
|
@@ -1157,7 +1067,7 @@ const METHODS = {
|
|
|
1157
1067
|
record('tools.bashPersist.b', {
|
|
1158
1068
|
ok: persistB.ok,
|
|
1159
1069
|
error: persistB.error ?? null,
|
|
1160
|
-
text: JSON.stringify(persistB.result ?? null).slice(0, 800),
|
|
1070
|
+
text: (persistB.text ?? JSON.stringify(persistB.result ?? null)).slice(0, 800),
|
|
1161
1071
|
})
|
|
1162
1072
|
|
|
1163
1073
|
// F8's guard, observed from the realm: once a persistent terminal session exists, a
|
|
@@ -1216,7 +1126,7 @@ const METHODS = {
|
|
|
1216
1126
|
// through this runner; that pin now covers THIS form structurally as well
|
|
1217
1127
|
// (`insideCall` + `carrier` in the structural row beside the count table), so dropping
|
|
1218
1128
|
// the spread reddens it instead of silently reverting to the default `wsl.exe`.
|
|
1219
|
-
const sweepSelect = { wslPath:
|
|
1129
|
+
const sweepSelect = { wslPath: hostSession.wslPath }
|
|
1220
1130
|
const sweepShell = (command) => runWslShell({ distro: sweepDistro, linuxCwd: '/', loginShell: false, command, ...sweepSelect })
|
|
1221
1131
|
const sweepList = async () => String((await sweepShell('ls -1 /tmp/dsh-pty-*.fifo 2>/dev/null; echo END')).stdout ?? '')
|
|
1222
1132
|
.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('/tmp/dsh-pty-'))
|
|
@@ -1260,7 +1170,7 @@ const METHODS = {
|
|
|
1260
1170
|
const [distroSecondsText, distroIso] = String(clock.stdout ?? '').trim().split('\n')
|
|
1261
1171
|
await sweepPlant()
|
|
1262
1172
|
const planted = await sweepList()
|
|
1263
|
-
const removedFirst = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath:
|
|
1173
|
+
const removedFirst = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath: hostSession.wslPath })
|
|
1264
1174
|
const afterFirst = await sweepList()
|
|
1265
1175
|
// The host-side shift. `Date.now` is the only host clock this process offers the
|
|
1266
1176
|
// sweep, so patching it is the whole of "the host's reading changed".
|
|
@@ -1280,7 +1190,7 @@ const METHODS = {
|
|
|
1280
1190
|
let removedShifted
|
|
1281
1191
|
try {
|
|
1282
1192
|
Date.now = () => realNow() + HOST_SHIFT_MS
|
|
1283
|
-
removedShifted = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath:
|
|
1193
|
+
removedShifted = await sweepOrphanFifos({ distro: sweepDistro }, { wslPath: hostSession.wslPath })
|
|
1284
1194
|
} finally {
|
|
1285
1195
|
Date.now = realNow
|
|
1286
1196
|
}
|
|
@@ -1375,17 +1285,17 @@ const METHODS = {
|
|
|
1375
1285
|
* @returns {Promise<object>} what each step observed.
|
|
1376
1286
|
*/
|
|
1377
1287
|
workspaceFlow: async ({ distro, linuxPath }) => {
|
|
1378
|
-
const registry =
|
|
1288
|
+
const registry = hostSession.ctx.get('workspaceRegistry')
|
|
1379
1289
|
if (registry === undefined) throw new Error('workspaceRegistry 服务不可用')
|
|
1380
1290
|
// Default the same way the selftest does — the distribution home, never /tmp:
|
|
1381
1291
|
// the fence refuses a workspace root under /tmp (confinement mounts a private
|
|
1382
1292
|
// tmpfs over it after binding the workspace), so a workspace registered there
|
|
1383
1293
|
// could never be used as a writable root. This method's old default handed the
|
|
1384
1294
|
// registry exactly such a path.
|
|
1385
|
-
const location = linuxPath ?? (await defaultWorkspaceUnc({ wslPath:
|
|
1295
|
+
const location = linuxPath ?? (await defaultWorkspaceUnc({ wslPath: hostSession.wslPath })).uncPath
|
|
1386
1296
|
const target = await resolveLocation(location, distro)
|
|
1387
|
-
const listing = await listLinuxDir(target.distro, target.linuxPath, { wslPath:
|
|
1388
|
-
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath:
|
|
1297
|
+
const listing = await listLinuxDir(target.distro, target.linuxPath, { wslPath: hostSession.wslPath })
|
|
1298
|
+
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath: hostSession.wslPath })
|
|
1389
1299
|
const existing = await registry.resolveByPath(target.uncPath)
|
|
1390
1300
|
const workspace = existing ?? await registry.create(target.uncPath, `wsl-selftest ${target.linuxPath}`)
|
|
1391
1301
|
const created = existing === undefined
|
|
@@ -1408,7 +1318,7 @@ const METHODS = {
|
|
|
1408
1318
|
* Report recent preset-binding decisions.
|
|
1409
1319
|
* @returns {Promise<object>} the decisions, newest last.
|
|
1410
1320
|
*/
|
|
1411
|
-
bindingLog: () => Promise.resolve({ entries:
|
|
1321
|
+
bindingLog: () => Promise.resolve({ entries: bindingTape.log }),
|
|
1412
1322
|
|
|
1413
1323
|
/**
|
|
1414
1324
|
* Read the harness's own view of every preset's composition.
|
|
@@ -1418,7 +1328,7 @@ const METHODS = {
|
|
|
1418
1328
|
* @returns {Promise<object>} the composition inventory.
|
|
1419
1329
|
*/
|
|
1420
1330
|
presetInventory: async () => {
|
|
1421
|
-
const presets =
|
|
1331
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
1422
1332
|
if (presets === undefined) throw new Error('agentPresets 服务不可用')
|
|
1423
1333
|
return { inventory: await presets.compositionInventory() }
|
|
1424
1334
|
},
|
|
@@ -1428,7 +1338,7 @@ const METHODS = {
|
|
|
1428
1338
|
* @returns {Promise<object>} the roster.
|
|
1429
1339
|
*/
|
|
1430
1340
|
presetRoster: async () => {
|
|
1431
|
-
const presets =
|
|
1341
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
1432
1342
|
if (presets === undefined) throw new Error('agentPresets 服务不可用')
|
|
1433
1343
|
const roster = await presets.list()
|
|
1434
1344
|
return { roster: roster.map((entry) => ({ id: entry.id, name: entry.name, broken: entry.broken ?? null, isDefault: entry.isDefault ?? false })) }
|
|
@@ -1439,7 +1349,7 @@ const METHODS = {
|
|
|
1439
1349
|
* @param {{ distro?: string }} params - optional distribution to pin.
|
|
1440
1350
|
* @returns {Promise<object>} the new state.
|
|
1441
1351
|
*/
|
|
1442
|
-
regeneratePresets: ({ distro }) => refreshPresets(
|
|
1352
|
+
regeneratePresets: ({ distro }) => refreshPresets(hostSession.ctx, distro),
|
|
1443
1353
|
|
|
1444
1354
|
/**
|
|
1445
1355
|
* Create (or find) the workspace for a WSL directory.
|
|
@@ -1451,9 +1361,9 @@ const METHODS = {
|
|
|
1451
1361
|
*/
|
|
1452
1362
|
createWorkspace: async ({ distro, path, title }) => {
|
|
1453
1363
|
const target = await resolveLocation(path, distro)
|
|
1454
|
-
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath:
|
|
1364
|
+
const facts = await checkLinuxPath(target.distro, target.linuxPath, { wslPath: hostSession.wslPath })
|
|
1455
1365
|
if (!facts.isDirectory) throw new Error(`${target.linuxPath} 不是一个存在的目录`)
|
|
1456
|
-
const registry =
|
|
1366
|
+
const registry = hostSession.ctx.get('workspaceRegistry')
|
|
1457
1367
|
if (registry === undefined) throw new Error('workspaceRegistry 服务不可用')
|
|
1458
1368
|
const existing = await registry.resolveByPath(target.uncPath)
|
|
1459
1369
|
const workspace = existing ?? await registry.create(target.uncPath, title ?? target.linuxPath)
|
|
@@ -1463,7 +1373,7 @@ const METHODS = {
|
|
|
1463
1373
|
// degrades to the default spelling instead of failing the creation.
|
|
1464
1374
|
let presetId = 'wsl-standard'
|
|
1465
1375
|
try {
|
|
1466
|
-
const presets =
|
|
1376
|
+
const presets = hostSession.ctx.get('agentPresets')
|
|
1467
1377
|
if (presets !== undefined) {
|
|
1468
1378
|
const resolved = await presets.resolve()
|
|
1469
1379
|
const candidate = variantIdFor(resolved)
|
|
@@ -1492,7 +1402,7 @@ const METHODS = {
|
|
|
1492
1402
|
listPresetDirectories: async () => {
|
|
1493
1403
|
// 0.1.6 read generated directories off disk; the registry is the source of
|
|
1494
1404
|
// truth now. The method name is kept for the developer surface.
|
|
1495
|
-
const presets =
|
|
1405
|
+
const presets = hostSession.ctx?.get('agentPresets')
|
|
1496
1406
|
if (presets === undefined) return { root: null, names: [] }
|
|
1497
1407
|
const roster = await presets.list()
|
|
1498
1408
|
const names = roster
|
|
@@ -1511,7 +1421,7 @@ const METHODS = {
|
|
|
1511
1421
|
if (typeof id !== 'string' || !/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
1512
1422
|
throw new Error(`readPreset: 非法的预设 id ${JSON.stringify(id)}`)
|
|
1513
1423
|
}
|
|
1514
|
-
const presets =
|
|
1424
|
+
const presets = hostSession.ctx?.get('agentPresets')
|
|
1515
1425
|
if (presets === undefined) throw new Error('readPreset: agentPresets 服务不可用')
|
|
1516
1426
|
// 0.1.7: readDocument returns the declaration re-dumped as YAML content.
|
|
1517
1427
|
const doc = await presets.readDocument(id)
|
|
@@ -1519,102 +1429,17 @@ const METHODS = {
|
|
|
1519
1429
|
},
|
|
1520
1430
|
}
|
|
1521
1431
|
|
|
1522
|
-
/**
|
|
1523
|
-
* Read one JSON request body, bounded.
|
|
1524
|
-
* @param {import('node:http').IncomingMessage} req - the request to drain.
|
|
1525
|
-
* @returns {Promise<{ text: string | null, byteLength: number }>} the body, or null past the ceiling.
|
|
1526
|
-
*/
|
|
1527
|
-
async function readJsonBody(req) {
|
|
1528
|
-
const chunks = []
|
|
1529
|
-
let size = 0
|
|
1530
|
-
for await (const chunk of req) {
|
|
1531
|
-
size += chunk.byteLength
|
|
1532
|
-
if (size > MAX_BODY_BYTES) {
|
|
1533
|
-
// Drain the remainder so the refusal is a readable response, not a socket cut.
|
|
1534
|
-
req.resume()
|
|
1535
|
-
return { text: null, byteLength: size }
|
|
1536
|
-
}
|
|
1537
|
-
chunks.push(chunk)
|
|
1538
|
-
}
|
|
1539
|
-
return { text: Buffer.concat(chunks, size).toString('utf8'), byteLength: size }
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
/**
|
|
1543
|
-
* Answer one request with a JSON envelope and an explicit status.
|
|
1544
|
-
* @param {import('node:http').ServerResponse} res - the response to complete.
|
|
1545
|
-
* @param {number} status - the HTTP status.
|
|
1546
|
-
* @param {{ ok: boolean }} payload - the envelope, success or failure.
|
|
1547
|
-
*/
|
|
1548
|
-
function sendJson(res, status, payload) {
|
|
1549
|
-
const body = JSON.stringify(payload)
|
|
1550
|
-
res.writeHead(status, {
|
|
1551
|
-
'content-type': 'application/json; charset=utf-8',
|
|
1552
|
-
'content-length': Buffer.byteLength(body),
|
|
1553
|
-
'cache-control': 'no-store',
|
|
1554
|
-
})
|
|
1555
|
-
res.end(body)
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
/**
|
|
1559
|
-
* Answer with one admission rejection.
|
|
1560
|
-
* @param {import('node:http').ServerResponse} res - the response to complete.
|
|
1561
|
-
* @param {import('./http-admission.js').Rejection} rejection - the refusal.
|
|
1562
|
-
*/
|
|
1563
|
-
function sendRejection(res, rejection) {
|
|
1564
|
-
sendJson(res, rejection.status, { ok: false, code: rejection.code, error: rejection.message })
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
/** Trust surface consumed here; the browser-side connection package owns the full type. */
|
|
1568
|
-
function connectionOf(ctx) {
|
|
1569
|
-
return Reflect.get(ctx, 'connection')
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
/**
|
|
1573
|
-
* This installation's development token, minted on first use.
|
|
1574
|
-
*
|
|
1575
|
-
* The file is owner-only and lives outside the repository. A web page cannot
|
|
1576
|
-
* read it, which is what keeps the acceptance surface unreachable from a
|
|
1577
|
-
* cross-site request even though the route itself is on loopback.
|
|
1578
|
-
* @returns {Promise<string>} the token.
|
|
1579
|
-
*/
|
|
1580
|
-
async function devToken() {
|
|
1581
|
-
const path = join(dshHome(), 'wsl-desktop-dev-token')
|
|
1582
|
-
try {
|
|
1583
|
-
const existing = (await readFile(path, 'utf8')).trim()
|
|
1584
|
-
if (existing.length > 0) return existing
|
|
1585
|
-
// An empty file is unusual — treat it like absent and re-mint below.
|
|
1586
|
-
} catch (error) {
|
|
1587
|
-
// ENOENT = first use. Any other read failure (ACL, AV lock, disk) must
|
|
1588
|
-
// fail loud rather than silently rotate the credential and revoke every
|
|
1589
|
-
// scripted caller.
|
|
1590
|
-
if (error?.code !== 'ENOENT') throw error
|
|
1591
|
-
}
|
|
1592
|
-
const minted = randomBytes(32).toString('hex')
|
|
1593
|
-
await mkdir(dirname(path), { recursive: true })
|
|
1594
|
-
await writeFile(path, `${minted}\n`, { mode: 0o600 })
|
|
1595
|
-
return minted
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
/**
|
|
1599
|
-
* Whether one request may use the acceptance surface.
|
|
1600
|
-
* @param {import('node:http').IncomingMessage} req - the request.
|
|
1601
|
-
* @param {{ developerTools: boolean }} config - resolved plugin config.
|
|
1602
|
-
* @returns {Promise<boolean>} true for a caller presenting this installation's token.
|
|
1603
|
-
*/
|
|
1604
|
-
async function isDeveloperCaller(req, config) {
|
|
1605
|
-
if (config.developerTools !== true) return false
|
|
1606
|
-
return tokenMatches(req.headers[DEV_TOKEN_HEADER], await devToken(), timingSafeEqual)
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
1432
|
/**
|
|
1610
1433
|
* Register the WSL route and generate the WSL presets.
|
|
1611
1434
|
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin context.
|
|
1612
1435
|
* @param {{ routePath: string, developerTools: boolean, wslPath?: string }} config - resolved plugin config.
|
|
1613
1436
|
*/
|
|
1614
1437
|
export function apply(ctx, config) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1438
|
+
hostSession.ctx = ctx
|
|
1439
|
+
// The declared default, not `undefined`: every route method passes this to
|
|
1440
|
+
// wsl.exe, and an undefined path is not the same as the default one.
|
|
1441
|
+
hostSession.wslPath = config.wslPath ?? 'wsl.exe'
|
|
1442
|
+
hostSession.ready = false
|
|
1618
1443
|
// Bound concurrent route methods: each discovery call spawns wsl.exe, and a
|
|
1619
1444
|
// misbehaving page could fan out without limit. Named because a bare "8" here
|
|
1620
1445
|
// was once mis-read as a wsl.exe SPAWN cap — it is not: one route method may
|
|
@@ -1744,10 +1569,10 @@ export function apply(ctx, config) {
|
|
|
1744
1569
|
// The probe is allowed to run even after a failed materialization: the result
|
|
1745
1570
|
// then says which capability is missing, instead of leaving the browser on
|
|
1746
1571
|
// "pending" forever.
|
|
1747
|
-
|
|
1572
|
+
hostSession.ready = true
|
|
1748
1573
|
// One startup line, as the spec asks: the post-restart gate and a bug report
|
|
1749
1574
|
// both need the capability state without opening the dialog.
|
|
1750
|
-
void probeHost(
|
|
1575
|
+
void probeHost(hostSession.ctx).then((status) => {
|
|
1751
1576
|
ctx.logger?.info?.(`wsl-desktop: capabilities ${JSON.stringify(status)}`)
|
|
1752
1577
|
}, () => {})
|
|
1753
1578
|
})
|
|
@@ -1758,7 +1583,7 @@ export function apply(ctx, config) {
|
|
|
1758
1583
|
// This listener is the LAST RESORT for a WSL-workspace session created
|
|
1759
1584
|
// without a preset (some other entry point): it tries a post-hoc select,
|
|
1760
1585
|
// which is refused for any session that has already taken a turn, and both
|
|
1761
|
-
// outcomes land in `
|
|
1586
|
+
// outcomes land in `bindingTape.log` as an anomaly signal.
|
|
1762
1587
|
//
|
|
1763
1588
|
// ONE listener, not two. `agent/created` is a scoped event, but a listener
|
|
1764
1589
|
// registered without a scope is admitted globally, so registering both paths
|
|
@@ -1776,8 +1601,7 @@ export function apply(ctx, config) {
|
|
|
1776
1601
|
* @param {object} entry - the decision to record.
|
|
1777
1602
|
*/
|
|
1778
1603
|
function recordBinding(entry) {
|
|
1779
|
-
|
|
1780
|
-
if (bindingLog.length > BINDING_LOG_LIMIT) bindingLog.splice(0, bindingLog.length - BINDING_LOG_LIMIT)
|
|
1604
|
+
bindingTape.record(entry)
|
|
1781
1605
|
}
|
|
1782
1606
|
|
|
1783
1607
|
/**
|