@ljwei-stak/dsh-model-route-bot 0.64.0 → 0.66.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.
Files changed (34) hide show
  1. package/.dsh-plugin/client.js +694 -407
  2. package/CHANGELOG.md +47 -0
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +209 -147
  5. package/README.zh.md +208 -146
  6. package/apps/integration-client/bot-attention.mjs +2 -2
  7. package/apps/integration-client/panels.jsx +239 -134
  8. package/apps/integration-client/product.css +36 -0
  9. package/apps/integration-client/routing-submissions.mjs +68 -0
  10. package/apps/integration-client/task-progress.mjs +46 -0
  11. package/apps/integration-client/tasks-panel.jsx +147 -0
  12. package/modules/bot-runtime/read-model.mjs +2 -2
  13. package/package.json +3 -1
  14. package/packages/capability-broker/src/authority.mjs +15 -1
  15. package/packages/model-router-plugin/README.md +39 -15
  16. package/packages/model-router-plugin/README.zh.md +40 -16
  17. package/packages/model-router-plugin/package.json +1 -1
  18. package/packages/orchestration/src/bot-routing-candidates.mjs +6 -6
  19. package/packages/orchestration/src/bot-routing-service.mjs +30 -5
  20. package/packages/orchestration/src/bot-routing.mjs +6 -4
  21. package/packages/orchestration/src/product-composition.mjs +65 -31
  22. package/packages/orchestration/src/sqlite-router-state.mjs +33 -8
  23. package/packages/orchestration/src/stop-proof.mjs +37 -0
  24. package/packages/orchestration/src/task-contract.mjs +42 -2
  25. package/packages/orchestration/src/task-journal.mjs +143 -37
  26. package/packages/orchestration/src/task-product.mjs +49 -0
  27. package/packages/orchestration/src/task-service.mjs +110 -18
  28. package/packages/orchestration/src/task-stage.mjs +36 -0
  29. package/packages/orchestration/src/write-retry.mjs +56 -0
  30. package/remote-computer-agent/build.mjs +48 -13
  31. package/remote-computer-agent/dist/cli.mjs +11 -1
  32. package/remote-computer-agent/dist/cli.mjs.map +2 -2
  33. package/remote-computer-agent/dist/index.mjs +11 -1
  34. package/remote-computer-agent/dist/index.mjs.map +2 -2
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Bounded retry for SQLite write contention, and the database's own clock.
3
+ *
4
+ * Why this exists: the router ledger opens the shared connection with
5
+ * `busy_timeout=0` on purpose (its CAS commits must not block an await), so a
6
+ * borrowed journal connection inherits a non-blocking handle. A journal write
7
+ * that collides with another Host then fails with a bare `SQLITE_BUSY` instead
8
+ * of waiting, and the second Host cannot start. Read-modify-write contention on
9
+ * this file is expected — several Hosts, one ledger — so the failure must be a
10
+ * bounded wait, not a crash.
11
+ *
12
+ * Retry is synchronous on purpose: journal writes run inside a synchronous
13
+ * transaction and must not yield the event loop while holding a lock.
14
+ */
15
+
16
+ const BUSY = Object.freeze(new Set(['SQLITE_BUSY', 'SQLITE_BUSY_SNAPSHOT', 'SQLITE_LOCKED']))
17
+
18
+ /** Sleep without yielding: safe inside a held write transaction. */
19
+ export function sleepSync(ms) {
20
+ if (!(ms > 0)) return
21
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(ms, 1000))
22
+ }
23
+
24
+ /** The database's own wall clock in milliseconds, independent of the caller's. */
25
+ export function readDatabaseClock(db) {
26
+ const row = db.prepare("SELECT CAST(round(julianday('now')*86400000) AS INTEGER) AS ms").get()
27
+ if (!row || !Number.isSafeInteger(row.ms) || row.ms <= 0) throw Object.assign(new Error('SQLITE_CLOCK_UNAVAILABLE'), { code: 'SQLITE_CLOCK_UNAVAILABLE' })
28
+ return row.ms
29
+ }
30
+
31
+ /**
32
+ * Run `work` inside an immediate transaction, retrying while the file is
33
+ * write-locked by another connection. Any other error, including a policy
34
+ * refusal thrown by `work`, propagates on the first attempt.
35
+ */
36
+ export function writeTransaction(db, work, { deadlineMs = 10_000, sleepMs = 10 } = {}) {
37
+ if (typeof work !== 'function') throw new TypeError('writeTransaction requires work')
38
+ const until = Date.now() + Math.max(1, deadlineMs)
39
+ let attempt = 0
40
+ for (;;) {
41
+ try {
42
+ return db.transaction(work).immediate()
43
+ } catch (error) {
44
+ if (!BUSY.has(error?.code) || Date.now() >= until) {
45
+ if (BUSY.has(error?.code)) {
46
+ throw Object.assign(new Error(`SQLITE_BUSY after ${deadlineMs}ms of retries`), {
47
+ code: 'SQLITE_BUSY', cause: error, sqliteCode: error?.code, deadlineMs, attempts: attempt + 1,
48
+ })
49
+ }
50
+ throw error
51
+ }
52
+ attempt++
53
+ sleepSync(Math.min(sleepMs * attempt, 100))
54
+ }
55
+ }
56
+ }
@@ -1,16 +1,51 @@
1
1
  import { build } from 'esbuild'
2
- import { copyFile, mkdir, lstat, rm } from 'node:fs/promises'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { mkdir, lstat, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
3
4
  import { fileURLToPath } from 'node:url'
4
- import { resolve, relative } from 'node:path'
5
+ import { basename, dirname, join, resolve, relative } from 'node:path'
6
+ import { setTimeout as sleep } from 'node:timers/promises'
5
7
  import assert from 'node:assert/strict'
6
- const root=fileURLToPath(new URL('.', import.meta.url))
7
- const output=resolve(root,'dist')
8
- assert.equal(relative(resolve(root),output),'dist','Build output must remain in the Agent dist directory')
9
- const previous=await lstat(output).catch(error=>{if(error.code==='ENOENT')return null;throw error})
10
- assert.ok(!previous?.isSymbolicLink(),'Refusing to replace a linked build output')
11
- await rm(output,{recursive:true,force:true})
12
- const options={absWorkingDir:root,bundle:true,packages:'external',platform:'node',format:'esm',target:'node22',sourcemap:true}
13
- await build({...options,entryPoints:['src/index.ts','src/cli.ts'],outbase:'src',outdir:'dist',outExtension:{'.js':'.mjs'}})
14
- await mkdir(`${root}dist`,{recursive:true})
15
- await build({...options,entryPoints:['../packages/capability-broker/src/cli-stdio-bridge.mjs'],outfile:'dist/cli-stdio-bridge.mjs'})
16
- for(const helper of ['native-peer.ps1','native-peer.py'])await copyFile(`${root}../packages/cli-runtime/src/${helper}`,`${root}dist/${helper}`)
8
+
9
+ const root = fileURLToPath(new URL('.', import.meta.url))
10
+ const output = resolve(root, 'dist')
11
+ assert.equal(relative(resolve(root), output), 'dist', 'Build output must remain in the Agent dist directory')
12
+ async function checkOutput() {
13
+ const previous = await lstat(output).catch(error => { if (error.code === 'ENOENT') return null; throw error })
14
+ assert.ok(!previous?.isSymbolicLink(), 'Refusing to replace a linked build output')
15
+ assert.ok(!previous || previous.isDirectory(), 'Agent build output must be a directory')
16
+ }
17
+ await checkOutput()
18
+
19
+ // Compile everything before touching the published build. Native tests run separate builders
20
+ // while other processes import or launch these entry points; deleting dist creates an ENOENT race.
21
+ const options = { absWorkingDir: root, bundle: true, packages: 'external', platform: 'node', format: 'esm', target: 'node22', sourcemap: true, write: false }
22
+ const main = await build({ ...options, entryPoints: ['src/index.ts', 'src/cli.ts'], outbase: 'src', outdir: 'dist', outExtension: { '.js': '.mjs' } })
23
+ const bridge = await build({ ...options, entryPoints: ['../packages/capability-broker/src/cli-stdio-bridge.mjs'], outfile: 'dist/cli-stdio-bridge.mjs' })
24
+ const files = [...main.outputFiles, ...bridge.outputFiles]
25
+ for (const helper of ['native-peer.ps1', 'native-peer.py']) {
26
+ files.push({ path: join(output, helper), contents: await readFile(resolve(root, '../packages/cli-runtime/src', helper)) })
27
+ }
28
+ for (const file of files) assert.equal(dirname(resolve(file.path)), output, 'Agent build files must remain directly inside dist')
29
+ await checkOutput()
30
+ await mkdir(output, { recursive: true })
31
+ const previousFiles = await readdir(output, { withFileTypes: true })
32
+ assert.ok(previousFiles.every(file => !file.isDirectory()), 'Unexpected directory in flat Agent build output')
33
+ for (const file of files) {
34
+ const bytes = Buffer.from(file.contents)
35
+ const previous = await readFile(file.path).catch(error => { if (error.code === 'ENOENT') return null; throw error })
36
+ if (previous?.equals(bytes)) continue
37
+ // Temporary files stay outside dist so another builder cannot prune or package them.
38
+ const temporary = join(root, `.agent-build-${randomUUID()}.tmp`)
39
+ try {
40
+ await writeFile(temporary, bytes, { flag: 'wx', mode: bytes.subarray(0, 2).toString() === '#!' ? 0o755 : 0o644 })
41
+ for (let attempt = 0; ; attempt++) {
42
+ try { await rename(temporary, file.path); break }
43
+ catch (error) {
44
+ if (attempt >= 5 || !['EPERM', 'EACCES', 'EBUSY'].includes(error.code)) throw error
45
+ await sleep(50)
46
+ }
47
+ }
48
+ } finally { await rm(temporary, { force: true }) }
49
+ }
50
+ const names = new Set(files.map(file => basename(file.path)))
51
+ for (const file of previousFiles) if (!names.has(file.name)) await rm(join(output, file.name), { force: true })
@@ -2705,10 +2705,20 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
2705
2705
 
2706
2706
  // ../packages/capability-broker/src/authority.mjs
2707
2707
  var PolicyError = class extends Error {
2708
- constructor(code) {
2708
+ /**
2709
+ * `detail.gate` names the checkpoint that refused, and `detail.rule` the rule
2710
+ * text when one matched. A refusal that only says "TOOL_DENIED" leaves the
2711
+ * reader to guess which of several gates stopped it, and support then runs on
2712
+ * pasted logs.
2713
+ */
2714
+ constructor(code, detail = {}) {
2709
2715
  super(`Capability refused: ${code}`);
2710
2716
  this.name = "PolicyError";
2711
2717
  this.code = code;
2718
+ if (detail && typeof detail === "object") {
2719
+ if (typeof detail.gate === "string" && detail.gate) this.gate = detail.gate;
2720
+ if (typeof detail.rule === "string" && detail.rule) this.rule = detail.rule;
2721
+ }
2712
2722
  }
2713
2723
  };
2714
2724