@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.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.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
import { independentContextId } from './independent-context-state.mjs'
|
|
5
|
+
|
|
6
|
+
const fail = code => { throw Object.assign(new Error(code), { code }) }
|
|
7
|
+
const ref = value => typeof value === 'string' && /^[-A-Za-z0-9_.]{1,128}$/.test(value)
|
|
8
|
+
const requestRef = value => typeof value === 'string' && /^[-A-Za-z0-9_]{16,128}$/.test(value)
|
|
9
|
+
const plain = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
const owner = context => { if (context?.kind !== 'owner') fail('REMOTE_OWNER_REQUIRED') }
|
|
11
|
+
const keys = (value, allowed) => { if (!plain(value) || Object.keys(value).some(key => !allowed.includes(key))) fail('INDEPENDENT_CHAT_INVALID_REQUEST') }
|
|
12
|
+
const hash = input => createHash('sha256').update(input).digest('hex')
|
|
13
|
+
const alive = pid => {
|
|
14
|
+
try { process.kill(pid, 0); return true } catch (error) { return error.code !== 'ESRCH' }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Owner chat admission and durable request outcomes, not a second money ledger.
|
|
19
|
+
* A transport retry reads the original request. It never starts another model
|
|
20
|
+
* turn, including after restart or an unknown result. Actual messages remain in
|
|
21
|
+
* the Bot transcript; this journal retains only bounded outcome summaries.
|
|
22
|
+
*/
|
|
23
|
+
export function createIndependentChatService({ Database, file, lifecycle, runtime, now = Date.now,
|
|
24
|
+
maxRequests = 4096, maxActive = 32, isProcessAlive = alive } = {}) {
|
|
25
|
+
if (typeof Database !== 'function' || typeof file !== 'string' || !file || typeof lifecycle?.committedBot !== 'function' || typeof runtime !== 'function') fail('INDEPENDENT_CHAT_HOST_REQUIRED')
|
|
26
|
+
if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || !Number.isSafeInteger(maxActive) || maxActive < 1) fail('INDEPENDENT_CHAT_INVALID_LIMIT')
|
|
27
|
+
if (file !== ':memory:') mkdirSync(dirname(file), { recursive: true })
|
|
28
|
+
const db = new Database(file), ownerId = randomUUID(), active = new Map()
|
|
29
|
+
let closed = false
|
|
30
|
+
try {
|
|
31
|
+
db.pragma('busy_timeout=5000'); db.pragma('synchronous=FULL')
|
|
32
|
+
if (file !== ':memory:' && db.pragma('journal_mode=WAL', { simple: true }) !== 'wal') fail('INDEPENDENT_CHAT_WAL_REQUIRED')
|
|
33
|
+
db.exec(`CREATE TABLE IF NOT EXISTS bot_independent_chat_requests (
|
|
34
|
+
bot_id TEXT NOT NULL, request_key TEXT NOT NULL, schema_version INTEGER NOT NULL,
|
|
35
|
+
input_hash TEXT NOT NULL, state TEXT NOT NULL, owner_id TEXT NOT NULL, owner_pid INTEGER NOT NULL,
|
|
36
|
+
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, cancel_requested_at INTEGER,
|
|
37
|
+
result TEXT, error_code TEXT, PRIMARY KEY(bot_id, request_key));
|
|
38
|
+
CREATE UNIQUE INDEX IF NOT EXISTS bot_independent_chat_pending_bot
|
|
39
|
+
ON bot_independent_chat_requests(bot_id) WHERE state IN ('accepted','running');`)
|
|
40
|
+
if (!db.pragma('table_info(bot_independent_chat_requests)').some(column => column.name === 'input_text')) {
|
|
41
|
+
db.exec('ALTER TABLE bot_independent_chat_requests ADD COLUMN input_text TEXT')
|
|
42
|
+
}
|
|
43
|
+
for (const [name, definition] of [['operation_kind', "TEXT NOT NULL DEFAULT 'send'"], ['context_id', 'TEXT'], ['base_context_id', 'TEXT']]) {
|
|
44
|
+
if (!db.pragma('table_info(bot_independent_chat_requests)').some(column => column.name === name)) db.exec(`ALTER TABLE bot_independent_chat_requests ADD COLUMN ${name} ${definition}`)
|
|
45
|
+
}
|
|
46
|
+
if (db.prepare('SELECT 1 FROM bot_independent_chat_requests WHERE schema_version != 1').get()) fail('INDEPENDENT_CHAT_SCHEMA_UNSUPPORTED')
|
|
47
|
+
if (db.prepare("SELECT 1 FROM bot_independent_chat_requests WHERE operation_kind NOT IN ('send','new-conversation')").get()) fail('INDEPENDENT_CHAT_SCHEMA_UNSUPPORTED')
|
|
48
|
+
} catch (error) { db.close(); throw error }
|
|
49
|
+
const read = (botId, requestKey) => db.prepare('SELECT * FROM bot_independent_chat_requests WHERE bot_id=? AND request_key=?').get(botId, requestKey)
|
|
50
|
+
const assertOpen = () => { if (closed) fail('INDEPENDENT_CHAT_CLOSED') }
|
|
51
|
+
const identity = payload => {
|
|
52
|
+
if (!ref(payload.botId) || !requestRef(payload.requestKey)) fail('INDEPENDENT_CHAT_INVALID_REQUEST')
|
|
53
|
+
}
|
|
54
|
+
const entryKey = (botId, requestKey) => `${botId}\0${requestKey}`
|
|
55
|
+
const pending = row => row && ['accepted', 'running'].includes(row.state)
|
|
56
|
+
const recover = (row, readOnly = false) => {
|
|
57
|
+
// FS and SQLite are separate commits. Only the exact Host-derived context
|
|
58
|
+
// token reconciles a reset whose journal acknowledgement was lost. Reading
|
|
59
|
+
// it never retries retirement, starts a native process, or dispatches a turn.
|
|
60
|
+
if (row?.operation_kind === 'new-conversation' && ['accepted', 'running', 'unknown'].includes(row.state)) {
|
|
61
|
+
const adapter = runtime()
|
|
62
|
+
const context = typeof adapter?.contextOf === 'function' ? adapter.contextOf(row.bot_id) : null
|
|
63
|
+
if (context && context.contextId === row.context_id && !context.resetPending) {
|
|
64
|
+
const result = JSON.stringify({ contextId: context.contextId, sequence: context.sequence, createdAt: context.createdAt })
|
|
65
|
+
if (readOnly) return { ...row, state: 'succeeded', result, error_code: null }
|
|
66
|
+
db.prepare("UPDATE bot_independent_chat_requests SET state='succeeded',result=?,error_code=NULL,updated_at=? WHERE bot_id=? AND request_key=? AND operation_kind='new-conversation' AND state IN ('accepted','running','unknown')")
|
|
67
|
+
.run(result, now(), row.bot_id, row.request_key)
|
|
68
|
+
return read(row.bot_id, row.request_key)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// An OS process that is gone cannot resume its local loop. An alive or
|
|
72
|
+
// uninspectable owner stays pending; expiry alone is never stop evidence.
|
|
73
|
+
if (!readOnly && pending(row) && row.owner_id !== ownerId && !isProcessAlive(row.owner_pid)) {
|
|
74
|
+
db.prepare("UPDATE bot_independent_chat_requests SET state='unknown',error_code='INDEPENDENT_CHAT_HOST_LOST',updated_at=? WHERE bot_id=? AND request_key=? AND owner_id=? AND state IN ('accepted','running')")
|
|
75
|
+
.run(now(), row.bot_id, row.request_key, row.owner_id)
|
|
76
|
+
return read(row.bot_id, row.request_key)
|
|
77
|
+
}
|
|
78
|
+
return row
|
|
79
|
+
}
|
|
80
|
+
const project = row => {
|
|
81
|
+
if (!row) fail('INDEPENDENT_CHAT_REQUEST_NOT_FOUND')
|
|
82
|
+
const uncertain = pending(row) && (row.owner_id !== ownerId || !active.has(entryKey(row.bot_id, row.request_key)))
|
|
83
|
+
// A live preview is observation, never a transcript or a durable answer.
|
|
84
|
+
// It disappears on settlement, lost ownership or restart. Reloading the
|
|
85
|
+
// browser can inspect it without dispatching another model request.
|
|
86
|
+
const progress = !uncertain && pending(row) ? active.get(entryKey(row.bot_id, row.request_key))?.progress : null
|
|
87
|
+
return { schemaVersion: 1, botId: row.bot_id, requestKey: row.request_key,
|
|
88
|
+
operationKind: row.operation_kind ?? 'send',
|
|
89
|
+
...(row.operation_kind === 'new-conversation' ? { contextId: row.context_id } : {}),
|
|
90
|
+
state: uncertain ? 'unknown' : row.state, pending: pending(row), finished: !pending(row),
|
|
91
|
+
createdAt: row.created_at, updatedAt: row.updated_at,
|
|
92
|
+
cancelRequested: row.cancel_requested_at !== null, cancelRequestedAt: row.cancel_requested_at,
|
|
93
|
+
stopConfirmed: false, result: row.result ? JSON.parse(row.result) : null,
|
|
94
|
+
progress: progress ? { ...progress } : null,
|
|
95
|
+
durableInput: row.operation_kind !== 'new-conversation' && (row.input_text !== null || row.state === 'succeeded'),
|
|
96
|
+
...(row.operation_kind !== 'new-conversation' && ['unknown', 'refused', 'cancelled'].includes(uncertain ? 'unknown' : row.state) && row.input_text !== null ? { recoveryInput: row.input_text } : {}),
|
|
97
|
+
errorCode: uncertain ? row.owner_id === ownerId ? 'INDEPENDENT_CHAT_OUTCOME_NOT_COMMITTED' : 'INDEPENDENT_CHAT_OTHER_OWNER_PENDING' : row.error_code }
|
|
98
|
+
}
|
|
99
|
+
const current = botId => {
|
|
100
|
+
if (!lifecycle.committedBot(botId)) fail('BOT_NOT_COMMITTED')
|
|
101
|
+
const adapter = runtime()
|
|
102
|
+
if (typeof adapter?.turn !== 'function') fail('INDEPENDENT_CHAT_UNAVAILABLE')
|
|
103
|
+
const mode = typeof adapter.modeOf === 'function' ? adapter.modeOf(botId) : adapter.describe?.(botId)?.runtime
|
|
104
|
+
if (mode !== 'independent') fail('RUNTIME_MODE_MISMATCH')
|
|
105
|
+
return adapter
|
|
106
|
+
}
|
|
107
|
+
const summary = value => {
|
|
108
|
+
if (!plain(value) || typeof value.answer !== 'string') fail('INDEPENDENT_CHAT_RESULT_INVALID')
|
|
109
|
+
const calls = Array.isArray(value.toolCalls) ? value.toolCalls : []
|
|
110
|
+
return { answerChars: value.answer.length, answerPreview: value.answer.slice(0, 256),
|
|
111
|
+
...(Number.isSafeInteger(value.steps) ? { steps: value.steps } : {}),
|
|
112
|
+
...(Number.isSafeInteger(value.turns) ? { turns: value.turns } : {}),
|
|
113
|
+
toolCallCount: calls.length, toolCallsTruncated: calls.length > 32,
|
|
114
|
+
toolCalls: calls.slice(0, 32).map(call => ({ id: String(call.id ?? '').slice(0, 256), name: String(call.name ?? '').slice(0, 128) })) }
|
|
115
|
+
}
|
|
116
|
+
const assertNoPending = botId => {
|
|
117
|
+
const other = db.prepare("SELECT * FROM bot_independent_chat_requests WHERE bot_id=? AND state IN ('accepted','running')").get(botId)
|
|
118
|
+
if (pending(recover(other))) fail('INDEPENDENT_CHAT_PENDING_RECONCILIATION')
|
|
119
|
+
// An uncertain reset fences future sends across Host restarts as well as
|
|
120
|
+
// within the original runtime. Its original nonce remains inspectable.
|
|
121
|
+
const resets = db.prepare("SELECT * FROM bot_independent_chat_requests WHERE bot_id=? AND operation_kind='new-conversation' AND state='unknown'").all(botId)
|
|
122
|
+
if (resets.some(row => recover(row).state === 'unknown')) fail('INDEPENDENT_CONTEXT_PENDING_RECONCILIATION')
|
|
123
|
+
}
|
|
124
|
+
const settle = (row, state, result, errorCode) => {
|
|
125
|
+
if (closed) return
|
|
126
|
+
db.prepare("UPDATE bot_independent_chat_requests SET state=?,updated_at=?,result=?,error_code=?,input_text=CASE WHEN ?='succeeded' THEN NULL ELSE input_text END WHERE bot_id=? AND request_key=? AND owner_id=? AND state IN ('accepted','running')")
|
|
127
|
+
.run(state, now(), result ? JSON.stringify(result) : null, errorCode, state, row.bot_id, row.request_key, ownerId)
|
|
128
|
+
}
|
|
129
|
+
const run = async (row, input, entry) => {
|
|
130
|
+
let invoked = false
|
|
131
|
+
try {
|
|
132
|
+
if (closed) return
|
|
133
|
+
const adapter = current(row.bot_id)
|
|
134
|
+
entry.controller.signal.throwIfAborted()
|
|
135
|
+
db.prepare("UPDATE bot_independent_chat_requests SET state='running',updated_at=? WHERE bot_id=? AND request_key=? AND owner_id=? AND state='accepted'")
|
|
136
|
+
.run(now(), row.bot_id, row.request_key, ownerId)
|
|
137
|
+
invoked = true
|
|
138
|
+
const result = await adapter.turn({ botId: row.bot_id, input, signal: entry.controller.signal,
|
|
139
|
+
onProgress(value) {
|
|
140
|
+
if (closed || entry.controller.signal.aborted || active.get(entryKey(row.bot_id, row.request_key)) !== entry) return
|
|
141
|
+
if (!plain(value) || !Number.isSafeInteger(value.step) || value.step < 1 || value.step > 64 || typeof value.text !== 'string') fail('INDEPENDENT_CHAT_PROGRESS_INVALID')
|
|
142
|
+
if (entry.progress && value.step < entry.progress.step) fail('INDEPENDENT_CHAT_PROGRESS_INVALID')
|
|
143
|
+
let text = value.text.slice(0, 64000)
|
|
144
|
+
if (text.length < value.text.length && /[\uD800-\uDBFF]$/.test(text)) text = text.slice(0, -1)
|
|
145
|
+
entry.progress = { step: value.step, sequence: (entry.progress?.sequence ?? 0) + 1,
|
|
146
|
+
text, truncated: text.length < value.text.length }
|
|
147
|
+
} })
|
|
148
|
+
// A late cancel does not undo an already committed answer. Keep both facts.
|
|
149
|
+
settle(row, 'succeeded', summary(result), null)
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const cancelled = entry.controller.signal.aborted && (error?.name === 'AbortError' || error?.code === 'BOT_LOOP_CANCELLED')
|
|
152
|
+
const code = /^[A-Z][A-Z0-9_:-]{0,95}$/.test(error?.code ?? '') ? error.code : 'INDEPENDENT_CHAT_OUTCOME_UNKNOWN'
|
|
153
|
+
settle(row, cancelled ? 'cancelled' : invoked ? 'unknown' : 'refused', null, code)
|
|
154
|
+
} finally { active.delete(entryKey(row.bot_id, row.request_key)) }
|
|
155
|
+
}
|
|
156
|
+
const runReset = async (row, entry) => {
|
|
157
|
+
let invoked = false
|
|
158
|
+
try {
|
|
159
|
+
if (closed) return
|
|
160
|
+
const adapter = current(row.bot_id)
|
|
161
|
+
if (typeof adapter.newConversation !== 'function') fail('INDEPENDENT_CONTEXT_UNAVAILABLE')
|
|
162
|
+
entry.controller.signal.throwIfAborted()
|
|
163
|
+
db.prepare("UPDATE bot_independent_chat_requests SET state='running',updated_at=? WHERE bot_id=? AND request_key=? AND owner_id=? AND state='accepted'")
|
|
164
|
+
.run(now(), row.bot_id, row.request_key, ownerId)
|
|
165
|
+
invoked = true
|
|
166
|
+
const result = await adapter.newConversation(row.bot_id, { requestKey: row.request_key,
|
|
167
|
+
expectedContextId: row.base_context_id, signal: entry.controller.signal })
|
|
168
|
+
if (!plain(result) || result.contextId !== row.context_id || result.resetPending) fail('INDEPENDENT_CONTEXT_RESULT_INVALID')
|
|
169
|
+
settle(row, 'succeeded', { contextId: result.contextId, sequence: result.sequence, createdAt: result.createdAt }, null)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const code = /^[A-Z][A-Z0-9_:-]{0,95}$/.test(error?.code ?? '') ? error.code : 'INDEPENDENT_CONTEXT_OUTCOME_UNKNOWN'
|
|
172
|
+
settle(row, invoked && error?.outcome !== 'refused' ? 'unknown' : 'refused', null, code)
|
|
173
|
+
} finally { active.delete(entryKey(row.bot_id, row.request_key)) }
|
|
174
|
+
}
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
ready: true,
|
|
177
|
+
view(payload, context) {
|
|
178
|
+
owner(context); assertOpen(); keys(payload, ['botId'])
|
|
179
|
+
if (!ref(payload.botId)) fail('INDEPENDENT_CHAT_INVALID_REQUEST')
|
|
180
|
+
context.signal?.throwIfAborted()
|
|
181
|
+
if (!lifecycle.committedBot(payload.botId)) fail('BOT_NOT_COMMITTED')
|
|
182
|
+
const adapter = runtime()
|
|
183
|
+
const mode = typeof adapter?.modeOf === 'function' ? adapter.modeOf(payload.botId) : adapter?.describe?.(payload.botId)?.runtime ?? null
|
|
184
|
+
const readOnly = context.readOnly === true
|
|
185
|
+
const available = !readOnly && mode === 'independent' && typeof adapter?.turn === 'function' && typeof adapter?.conversationPage === 'function'
|
|
186
|
+
const pendingRow = recover(db.prepare("SELECT * FROM bot_independent_chat_requests WHERE bot_id=? AND state IN ('accepted','running') ORDER BY created_at DESC LIMIT 1").get(payload.botId), readOnly)
|
|
187
|
+
const latest = recover(db.prepare('SELECT * FROM bot_independent_chat_requests WHERE bot_id=? ORDER BY created_at DESC,request_key DESC LIMIT 1').get(payload.botId), readOnly)
|
|
188
|
+
const conversationContext = typeof adapter?.contextOf === 'function' ? adapter.contextOf(payload.botId) : null
|
|
189
|
+
return { schemaVersion: 1, botId: payload.botId, mode, available, readOnly,
|
|
190
|
+
reason: readOnly ? 'PRODUCT_READ_ONLY' : available ? null : mode === 'integration' ? 'RUNTIME_MODE_MISMATCH' : 'INDEPENDENT_CHAT_UNAVAILABLE',
|
|
191
|
+
pendingRequest: pending(pendingRow) ? project(pendingRow) : null,
|
|
192
|
+
latestRequest: latest ? project(latest) : null,
|
|
193
|
+
context: conversationContext,
|
|
194
|
+
newConversationAvailable: !readOnly && mode === 'independent' && Boolean(conversationContext) && !conversationContext.resetPending && !pending(pendingRow) &&
|
|
195
|
+
!adapter.busyBots?.().includes(payload.botId) && typeof adapter?.newConversation === 'function' }
|
|
196
|
+
},
|
|
197
|
+
messages(payload, context) {
|
|
198
|
+
owner(context); assertOpen(); keys(payload, ['botId', 'cursor', 'limit'])
|
|
199
|
+
if (!ref(payload.botId) || (payload.cursor !== undefined && (typeof payload.cursor !== 'string' || payload.cursor.length > 4096))
|
|
200
|
+
|| (payload.limit !== undefined && (!Number.isSafeInteger(payload.limit) || payload.limit < 1 || payload.limit > 100))) fail('INDEPENDENT_CHAT_INVALID_REQUEST')
|
|
201
|
+
context.signal?.throwIfAborted()
|
|
202
|
+
if (!lifecycle.committedBot(payload.botId)) fail('BOT_NOT_COMMITTED')
|
|
203
|
+
const adapter = runtime()
|
|
204
|
+
if (typeof adapter?.conversationPage !== 'function') fail('INDEPENDENT_CHAT_UNAVAILABLE')
|
|
205
|
+
// One adapter call owns one immutable transcript snapshot. Never turn a
|
|
206
|
+
// preview position into a second read of a moving transcript.
|
|
207
|
+
return adapter.conversationPage(payload.botId, { ...(payload.cursor === undefined ? {} : { cursor: payload.cursor }), limit: payload.limit ?? 40 })
|
|
208
|
+
},
|
|
209
|
+
send(payload, context) {
|
|
210
|
+
owner(context); assertOpen(); keys(payload, ['botId', 'requestKey', 'input']); identity(payload)
|
|
211
|
+
if (context.readOnly === true) fail('PRODUCT_READ_ONLY')
|
|
212
|
+
if (typeof payload.input !== 'string' || !payload.input.trim() || Buffer.byteLength(payload.input) > 64_000) fail('INDEPENDENT_CHAT_INPUT_LIMIT')
|
|
213
|
+
context.signal?.throwIfAborted()
|
|
214
|
+
const inputHash = hash(payload.input)
|
|
215
|
+
const previous = read(payload.botId, payload.requestKey)
|
|
216
|
+
if (previous) {
|
|
217
|
+
if (previous.operation_kind !== 'send' || previous.input_hash !== inputHash) fail('INDEPENDENT_CHAT_REQUEST_CONFLICT')
|
|
218
|
+
return project(recover(previous))
|
|
219
|
+
}
|
|
220
|
+
const adapter = current(payload.botId)
|
|
221
|
+
if (active.size >= maxActive) fail('INDEPENDENT_CHAT_CONCURRENCY_LIMIT')
|
|
222
|
+
if (adapter.busyBots?.().includes(payload.botId)) fail('BOT_BUSY')
|
|
223
|
+
const inserted = db.transaction(() => {
|
|
224
|
+
const raced = read(payload.botId, payload.requestKey)
|
|
225
|
+
if (raced) {
|
|
226
|
+
if (raced.operation_kind !== 'send' || raced.input_hash !== inputHash) fail('INDEPENDENT_CHAT_REQUEST_CONFLICT')
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
assertNoPending(payload.botId)
|
|
230
|
+
if (db.prepare('SELECT count(*) AS n FROM bot_independent_chat_requests').get().n >= maxRequests) fail('INDEPENDENT_CHAT_HISTORY_LIMIT')
|
|
231
|
+
context.signal?.throwIfAborted()
|
|
232
|
+
db.prepare("INSERT INTO bot_independent_chat_requests(bot_id,request_key,schema_version,input_hash,state,owner_id,owner_pid,created_at,updated_at,cancel_requested_at,result,error_code,input_text) VALUES(?,?,1,?,'accepted',?,?,?,?,NULL,NULL,NULL,?)")
|
|
233
|
+
.run(payload.botId, payload.requestKey, inputHash, ownerId, process.pid, now(), now(), payload.input)
|
|
234
|
+
return true
|
|
235
|
+
}).immediate()
|
|
236
|
+
if (!inserted) return project(recover(read(payload.botId, payload.requestKey)))
|
|
237
|
+
const row = read(payload.botId, payload.requestKey), entry = { controller: new AbortController(), done: null }
|
|
238
|
+
active.set(entryKey(payload.botId, payload.requestKey), entry)
|
|
239
|
+
entry.done = Promise.resolve().then(() => run(row, payload.input, entry))
|
|
240
|
+
entry.done.catch(() => {})
|
|
241
|
+
return project(row)
|
|
242
|
+
},
|
|
243
|
+
newConversation(payload, context) {
|
|
244
|
+
owner(context); assertOpen(); keys(payload, ['botId', 'requestKey']); identity(payload)
|
|
245
|
+
if (context.readOnly === true) fail('PRODUCT_READ_ONLY')
|
|
246
|
+
context.signal?.throwIfAborted()
|
|
247
|
+
const previous = read(payload.botId, payload.requestKey)
|
|
248
|
+
if (previous) {
|
|
249
|
+
if (previous.operation_kind !== 'new-conversation') fail('INDEPENDENT_CHAT_REQUEST_CONFLICT')
|
|
250
|
+
return project(recover(previous))
|
|
251
|
+
}
|
|
252
|
+
const adapter = current(payload.botId)
|
|
253
|
+
if (typeof adapter.newConversation !== 'function' || typeof adapter.contextOf !== 'function') fail('INDEPENDENT_CONTEXT_UNAVAILABLE')
|
|
254
|
+
if (active.size >= maxActive) fail('INDEPENDENT_CHAT_CONCURRENCY_LIMIT')
|
|
255
|
+
if (adapter.busyBots?.().includes(payload.botId)) fail('BOT_BUSY')
|
|
256
|
+
const base = adapter.contextOf(payload.botId)
|
|
257
|
+
if (base.resetPending) fail('INDEPENDENT_CONTEXT_PENDING_RECONCILIATION')
|
|
258
|
+
const contextId = independentContextId(payload.botId, payload.requestKey)
|
|
259
|
+
const inserted = db.transaction(() => {
|
|
260
|
+
const raced = read(payload.botId, payload.requestKey)
|
|
261
|
+
if (raced) {
|
|
262
|
+
if (raced.operation_kind !== 'new-conversation') fail('INDEPENDENT_CHAT_REQUEST_CONFLICT')
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
assertNoPending(payload.botId)
|
|
266
|
+
if (db.prepare('SELECT count(*) AS n FROM bot_independent_chat_requests').get().n >= maxRequests) fail('INDEPENDENT_CHAT_HISTORY_LIMIT')
|
|
267
|
+
context.signal?.throwIfAborted()
|
|
268
|
+
db.prepare("INSERT INTO bot_independent_chat_requests(bot_id,request_key,schema_version,input_hash,state,owner_id,owner_pid,created_at,updated_at,cancel_requested_at,result,error_code,input_text,operation_kind,context_id,base_context_id) VALUES(?,?,1,?,'accepted',?,?,?,?,NULL,NULL,NULL,NULL,'new-conversation',?,?)")
|
|
269
|
+
.run(payload.botId, payload.requestKey, hash('new-conversation'), ownerId, process.pid, now(), now(), contextId, base.contextId)
|
|
270
|
+
return true
|
|
271
|
+
}).immediate()
|
|
272
|
+
if (!inserted) return project(recover(read(payload.botId, payload.requestKey)))
|
|
273
|
+
const row = read(payload.botId, payload.requestKey), entry = { controller: new AbortController(), done: null }
|
|
274
|
+
active.set(entryKey(payload.botId, payload.requestKey), entry)
|
|
275
|
+
entry.done = Promise.resolve().then(() => runReset(row, entry))
|
|
276
|
+
entry.done.catch(() => {})
|
|
277
|
+
return project(row)
|
|
278
|
+
},
|
|
279
|
+
inspect(payload, context) {
|
|
280
|
+
owner(context); assertOpen(); keys(payload, ['botId', 'requestKey']); identity(payload)
|
|
281
|
+
context.signal?.throwIfAborted()
|
|
282
|
+
return project(recover(read(payload.botId, payload.requestKey), context.readOnly === true))
|
|
283
|
+
},
|
|
284
|
+
cancel(payload, context) {
|
|
285
|
+
owner(context); assertOpen(); keys(payload, ['botId', 'requestKey']); identity(payload)
|
|
286
|
+
if (context.readOnly === true) fail('PRODUCT_READ_ONLY')
|
|
287
|
+
context.signal?.throwIfAborted()
|
|
288
|
+
const row = read(payload.botId, payload.requestKey)
|
|
289
|
+
if (!row) fail('INDEPENDENT_CHAT_REQUEST_NOT_FOUND')
|
|
290
|
+
const entry = active.get(entryKey(payload.botId, payload.requestKey))
|
|
291
|
+
if (pending(row) && entry) {
|
|
292
|
+
db.prepare('UPDATE bot_independent_chat_requests SET cancel_requested_at=coalesce(cancel_requested_at,?),updated_at=? WHERE bot_id=? AND request_key=? AND owner_id=?')
|
|
293
|
+
.run(now(), now(), payload.botId, payload.requestKey, ownerId)
|
|
294
|
+
entry.controller.abort(Object.assign(new Error('INDEPENDENT_CHAT_CANCEL_REQUESTED'), { code: 'BOT_LOOP_CANCELLED' }))
|
|
295
|
+
}
|
|
296
|
+
return project(recover(read(payload.botId, payload.requestKey)))
|
|
297
|
+
},
|
|
298
|
+
async whenIdle() { await Promise.allSettled([...active.values()].map(entry => entry.done)) },
|
|
299
|
+
close() {
|
|
300
|
+
if (closed) return
|
|
301
|
+
closed = true
|
|
302
|
+
for (const entry of active.values()) entry.controller.abort(Object.assign(new Error('INDEPENDENT_CHAT_CLOSED'), { code: 'BOT_LOOP_CANCELLED' }))
|
|
303
|
+
try {
|
|
304
|
+
db.prepare("UPDATE bot_independent_chat_requests SET state='unknown',error_code='INDEPENDENT_CHAT_CLOSED',updated_at=? WHERE owner_id=? AND state IN ('accepted','running')").run(now(), ownerId)
|
|
305
|
+
} finally { db.close() }
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export const LEGACY_CONTEXT = 'legacy'
|
|
4
|
+
export const MAX_CONTEXTS = 64
|
|
5
|
+
export const contextIdOk = value => typeof value === 'string' && /^[-A-Za-z0-9_.]{1,128}$/.test(value)
|
|
6
|
+
export const contextRequestOk = value => typeof value === 'string' && /^[-A-Za-z0-9_]{16,128}$/.test(value)
|
|
7
|
+
const fail = code => { throw Object.assign(new Error(code), { code }) }
|
|
8
|
+
|
|
9
|
+
// Derived by the Host, never accepted from a model or a GenerateOptions sessionId.
|
|
10
|
+
export const independentContextId = (botId, requestKey) => `context-${createHash('sha256').update(JSON.stringify([botId, requestKey])).digest('hex')}`
|
|
11
|
+
export function contextFields(value) {
|
|
12
|
+
const contextId = value.contextId ?? LEGACY_CONTEXT
|
|
13
|
+
if (!contextIdOk(contextId)) fail('INVALID_BOT_CONTEXT')
|
|
14
|
+
const contexts = value.contexts ?? [{ contextId: LEGACY_CONTEXT, sequence: 0, createdAt: value.createdAt ?? 0 }]
|
|
15
|
+
if (!Array.isArray(contexts) || !contexts.length || contexts.length > MAX_CONTEXTS || contexts.some(row =>
|
|
16
|
+
!row || !contextIdOk(row.contextId) || !Number.isSafeInteger(row.sequence) || row.sequence < 0 ||
|
|
17
|
+
!Number.isSafeInteger(row.createdAt) || row.requestKey !== undefined && !contextRequestOk(row.requestKey) ||
|
|
18
|
+
row.previousContextId !== undefined && !contextIdOk(row.previousContextId)) ||
|
|
19
|
+
new Set(contexts.map(row => row.contextId)).size !== contexts.length || !contexts.some(row => row.contextId === contextId)) fail('INVALID_BOT_CONTEXT')
|
|
20
|
+
const contextReset = value.contextReset
|
|
21
|
+
if (contextReset !== undefined && (!contextReset || !contextRequestOk(contextReset.requestKey) ||
|
|
22
|
+
!contextIdOk(contextReset.contextId) || contextReset.contextId === contextId ||
|
|
23
|
+
contextReset.previousContextId !== contextId || !Number.isSafeInteger(contextReset.createdAt))) fail('INVALID_BOT_CONTEXT_RESET')
|
|
24
|
+
return { contextId, contexts: contexts.map(row => ({ contextId: row.contextId, sequence: row.sequence, createdAt: row.createdAt,
|
|
25
|
+
...(row.requestKey ? { requestKey: row.requestKey } : {}), ...(row.previousContextId ? { previousContextId: row.previousContextId } : {}) })),
|
|
26
|
+
...(contextReset ? { contextReset: { requestKey: contextReset.requestKey, contextId: contextReset.contextId,
|
|
27
|
+
previousContextId: contextReset.previousContextId, createdAt: contextReset.createdAt } } : {}),
|
|
28
|
+
contextsTruncated: value.contextsTruncated === true }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function contextView(session) {
|
|
32
|
+
const counts = new Map()
|
|
33
|
+
for (const row of session.messages) counts.set(row.contextId ?? LEGACY_CONTEXT, (counts.get(row.contextId ?? LEGACY_CONTEXT) ?? 0) + 1)
|
|
34
|
+
const active = session.contexts.find(row => row.contextId === session.contextId)
|
|
35
|
+
return { contextId: session.contextId, sequence: active.sequence, createdAt: active.createdAt,
|
|
36
|
+
messageCount: counts.get(session.contextId) ?? 0, resetPending: Boolean(session.contextReset),
|
|
37
|
+
contextsTruncated: session.contextsTruncated,
|
|
38
|
+
contexts: session.contexts.map(row => ({ contextId: row.contextId, sequence: row.sequence, createdAt: row.createdAt,
|
|
39
|
+
messageCount: counts.get(row.contextId) ?? 0 })) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A stop request, a timeout, or contradictory evidence cannot establish retirement.
|
|
43
|
+
export const retirementConfirmed = value => Boolean(value && value.treeStopped !== false && value.possiblyApplied !== true && value.unknown !== true &&
|
|
44
|
+
(value.treeStopped === true || value.notStarted === true && value.possiblyApplied === false))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const block = text => ({ type: 'text', text })
|
|
4
|
+
const sourceOf = value => value?.kind === 'model' && typeof value.provider === 'string' && value.provider && typeof value.model === 'string' && value.model
|
|
5
|
+
? { kind: 'model', provider: value.provider, model: value.model } : null
|
|
6
|
+
const callsOf = row => Array.isArray(row.toolCalls) ? row.toolCalls : row.toolCall ? [row.toolCall] : []
|
|
7
|
+
const idOf = (row, occurrence) => row.messageId ?? `independent-${createHash('sha256').update(JSON.stringify({ role: row.role, content: row.content, at: row.at, toolCallId: row.toolCallId, toolCalls: callsOf(row), occurrence })).digest('hex')}`
|
|
8
|
+
|
|
9
|
+
/** Current committed identity is instructions for this request, never a grant. */
|
|
10
|
+
export function independentSystemMessage(bot) {
|
|
11
|
+
const name = typeof bot.name === 'string' && bot.name.trim() ? bot.name.trim() : bot.id
|
|
12
|
+
const role = typeof bot.role === 'string' ? bot.role.trim() : ''
|
|
13
|
+
const instructions = typeof bot.instructions === 'string' ? bot.instructions.trim() : ''
|
|
14
|
+
const text = [`You are ${name}.`, ...(role ? [`Role: ${role}`] : []), ...(instructions ? [instructions] : [])].join('\n\n')
|
|
15
|
+
const id = `independent-system-${createHash('sha256').update(JSON.stringify([bot.id, text])).digest('hex')}`
|
|
16
|
+
return { id, role: 'system', source: { kind: 'system-prompt' }, content: [block(text)] }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Project the Bot-owned transcript to the installed dsh-llm message vocabulary.
|
|
21
|
+
* This creates no Harness Session and confers no authority. Model provenance comes
|
|
22
|
+
* from each saved reply, never from the Bot's currently selected model. Old rows
|
|
23
|
+
* without provenance and interrupted tool batches remain clearly labeled recall;
|
|
24
|
+
* they cannot become an invented assistant response or a replayable tool call.
|
|
25
|
+
*/
|
|
26
|
+
export function independentMessageWire(rows) {
|
|
27
|
+
if (!Array.isArray(rows)) throw Object.assign(new Error('INDEPENDENT_TRANSCRIPT_REQUIRED'), { code: 'INDEPENDENT_TRANSCRIPT_REQUIRED' })
|
|
28
|
+
const pending = new Map(), lastOwner = new Map(), results = new Map(), resultOwners = new Map(), complete = new Set(), occurrences = new Map()
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
if (row.role === 'assistant' && sourceOf(row.modelSource)) {
|
|
31
|
+
const ids = new Set()
|
|
32
|
+
results.set(row, new Map())
|
|
33
|
+
for (const call of callsOf(row)) {
|
|
34
|
+
if (typeof call?.id !== 'string' || !call.id || typeof call.name !== 'string' || !call.name || ids.has(call.id)) {
|
|
35
|
+
throw Object.assign(new Error('INDEPENDENT_TOOL_HISTORY_INVALID'), { code: 'INDEPENDENT_TOOL_HISTORY_INVALID' })
|
|
36
|
+
}
|
|
37
|
+
ids.add(call.id)
|
|
38
|
+
// Providers can reuse an id in a later response. Its nearest preceding
|
|
39
|
+
// request owns the result; an earlier unresolved batch stays recall.
|
|
40
|
+
pending.set(call.id, row)
|
|
41
|
+
lastOwner.set(call.id, row)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (row.role === 'tool' && typeof row.toolCallId === 'string') {
|
|
45
|
+
const owner = pending.get(row.toolCallId)
|
|
46
|
+
if (owner) {
|
|
47
|
+
results.get(owner).set(row.toolCallId, row)
|
|
48
|
+
resultOwners.set(row, owner)
|
|
49
|
+
pending.delete(row.toolCallId)
|
|
50
|
+
} else if (lastOwner.has(row.toolCallId)) {
|
|
51
|
+
throw Object.assign(new Error('INDEPENDENT_TOOL_HISTORY_DUPLICATE'), { code: 'INDEPENDENT_TOOL_HISTORY_DUPLICATE' })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const calls = callsOf(row)
|
|
57
|
+
if (row.role === 'assistant' && sourceOf(row.modelSource) && calls.every(call => results.get(row)?.has(call.id))) complete.add(row)
|
|
58
|
+
}
|
|
59
|
+
return rows.map(row => {
|
|
60
|
+
const key = JSON.stringify([row.role, row.at, row.content])
|
|
61
|
+
const occurrence = occurrences.get(key) ?? 0
|
|
62
|
+
occurrences.set(key, occurrence + 1)
|
|
63
|
+
const id = idOf(row, occurrence)
|
|
64
|
+
const text = typeof row.content === 'string' ? row.content : ''
|
|
65
|
+
if (row.role === 'user') return { role: 'user', content: [block(text)] }
|
|
66
|
+
if (row.role === 'system') return { id, role: 'system', source: { kind: 'system-prompt' }, content: [block(text)] }
|
|
67
|
+
const source = sourceOf(row.modelSource)
|
|
68
|
+
const calls = callsOf(row)
|
|
69
|
+
if (row.role === 'assistant' && source && complete.has(row)) return {
|
|
70
|
+
id, role: 'assistant', source,
|
|
71
|
+
content: [...(text ? [block(text)] : []), ...calls.map(call => ({ type: 'tool-call', id: call.id, name: call.name, arguments: JSON.stringify(call.args ?? {}) }))],
|
|
72
|
+
}
|
|
73
|
+
const owner = resultOwners.get(row)
|
|
74
|
+
if (owner && complete.has(owner)) return {
|
|
75
|
+
id, role: 'tool', source: { kind: 'tool', callId: row.toolCallId }, toolCallId: row.toolCallId,
|
|
76
|
+
content: [block(text)], ...(row.isError === true ? { isError: true } : {}),
|
|
77
|
+
}
|
|
78
|
+
const label = row.role === 'assistant'
|
|
79
|
+
? calls.length ? 'Earlier assistant tool request; execution outcome not fully confirmed' : 'Earlier assistant message; model provenance unrecorded'
|
|
80
|
+
: 'Earlier tool result; original call is outside this retained history'
|
|
81
|
+
return { id, role: 'user', source: { kind: 'independent-history', form: 'recall' },
|
|
82
|
+
content: [block(`[${label}]\n${text}${calls.length ? `\n${JSON.stringify(calls)}` : ''}`)] }
|
|
83
|
+
})
|
|
84
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool catalog one independent Bot may see.
|
|
3
|
+
*
|
|
4
|
+
* The model is offered exactly what this Bot's committed grants allow, re-decided from committed
|
|
5
|
+
* state on every request: no tool name, machine, root or capability comes from the request, and
|
|
6
|
+
* nothing here hands the model an owner, a subject, an executable path, an endpoint or an
|
|
7
|
+
* operation identity. Those are Host facts; the model only ever supplies business content.
|
|
8
|
+
*
|
|
9
|
+
* Three rules are load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* 1. A capability the Host cannot actually run is **not** offered. `unavailable()` names it and
|
|
12
|
+
* says why, so a Host with no durable Task backend says "no tracked write" instead of advertising
|
|
13
|
+
* a schema whose execution would be a guess.
|
|
14
|
+
* 2. Every schema is derived, never cached across a grant change: the caller asks again per step and
|
|
15
|
+
* per call, and a grant the authority no longer holds removes the tool from the request.
|
|
16
|
+
* 3. The machine and root are Host-chosen from the Bot's own live binding. A model may name one of
|
|
17
|
+
* its own bound machines or roots; the Host still re-checks both before the call runs.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Effects. A read may run beside another read; everything else stays sequential. */
|
|
21
|
+
export const READ_EFFECT = 'read'
|
|
22
|
+
export const WRITE_EFFECT = 'workspace-write'
|
|
23
|
+
|
|
24
|
+
/** Machine tools the independent path may reach, with the Host-side business arguments. */
|
|
25
|
+
const TARGET_KEYS = {
|
|
26
|
+
machineId: { type: 'string', description: '本 Bot 已绑定的其中一台机器;不填则由宿主选择。' },
|
|
27
|
+
rootId: { type: 'string', description: '该机器上已授权给本 Bot 的其中一个根目录;不填则由宿主选择。' },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pathParameter = description => ({ type: 'string', description })
|
|
31
|
+
|
|
32
|
+
const objectSchema = (properties, required = []) => Object.freeze({
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: Object.freeze(properties),
|
|
35
|
+
required: Object.freeze(required),
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One offered capability. `machineTool` is the remote agent tool; `backend` names the durable Task
|
|
41
|
+
* backend that must exist before the capability may be offered at all; `grants` are the authority
|
|
42
|
+
* capabilities a call will need, so a grant that was withdrawn takes the tool out of the request.
|
|
43
|
+
*/
|
|
44
|
+
const CAPABILITIES = Object.freeze([
|
|
45
|
+
Object.freeze({
|
|
46
|
+
name: 'bot_machine_ping', machineTool: 'machine_ping', effect: READ_EFFECT, backend: null,
|
|
47
|
+
grants: ['remote/machine_ping'],
|
|
48
|
+
description: '确认本 Bot 已绑定的远程机器在线,并读取它的能力清单。',
|
|
49
|
+
parameters: objectSchema({ ...TARGET_KEYS }),
|
|
50
|
+
}),
|
|
51
|
+
Object.freeze({
|
|
52
|
+
name: 'bot_fs_list', machineTool: 'fs_list', effect: READ_EFFECT, backend: null,
|
|
53
|
+
grants: ['remote/fs_list'],
|
|
54
|
+
description: '列出已授权根目录下的文件名。路径相对于该根目录,不能越出它。',
|
|
55
|
+
parameters: objectSchema({ ...TARGET_KEYS, relativePath: pathParameter('相对已授权根目录的目录路径,默认当前目录。') }, []),
|
|
56
|
+
}),
|
|
57
|
+
Object.freeze({
|
|
58
|
+
name: 'bot_fs_read', machineTool: 'fs_read', effect: READ_EFFECT, backend: null,
|
|
59
|
+
grants: ['remote/fs_read'],
|
|
60
|
+
description: '读取已授权根目录下的一个文本文件,有长度上限。',
|
|
61
|
+
parameters: objectSchema({ ...TARGET_KEYS, relativePath: pathParameter('相对已授权根目录的文件路径。') }, ['relativePath']),
|
|
62
|
+
}),
|
|
63
|
+
Object.freeze({
|
|
64
|
+
name: 'bot_fs_write', machineTool: 'fs_write', effect: WRITE_EFFECT, backend: 'remote-write',
|
|
65
|
+
grants: ['remote/fs_write', 'task/submit'],
|
|
66
|
+
description: '把文本内容写入已授权根目录下的一个文件。这是一次可跟踪的持久 Task:宿主保留请求键、Task、attempt 与 operation 身份,结果未知时不会重放。',
|
|
67
|
+
parameters: objectSchema({
|
|
68
|
+
...TARGET_KEYS,
|
|
69
|
+
relativePath: pathParameter('相对已授权根目录的文件路径。'),
|
|
70
|
+
content: { type: 'string', description: '要写入的文本内容。' },
|
|
71
|
+
expectedHash: { type: ['string', 'null'], description: '期望的当前文件 SHA256;新文件填 null。' },
|
|
72
|
+
}, ['relativePath', 'content', 'expectedHash']),
|
|
73
|
+
}),
|
|
74
|
+
Object.freeze({
|
|
75
|
+
name: 'bot_command', machineTool: 'container_command_v1', effect: WRITE_EFFECT, backend: 'remote-command',
|
|
76
|
+
grants: ['remote/container_command_v1', 'task/submit'],
|
|
77
|
+
description: '在已授权的远程容器 profile 中运行一条操作员预先批准的 argv 命令。不接受 shell 字符串、可执行路径或环境变量。',
|
|
78
|
+
parameters: objectSchema({
|
|
79
|
+
...TARGET_KEYS,
|
|
80
|
+
workspaceRelativePath: pathParameter('命令的工作目录,相对已授权根目录;空字符串表示该根本身。'),
|
|
81
|
+
argv: { type: 'array', items: { type: 'string' }, description: '命令参数数组,第一项必须是已批准的别名。' },
|
|
82
|
+
timeoutMs: { type: 'integer', description: '超时毫秒数,宿主另有上界。', minimum: 1000, maximum: 600000 },
|
|
83
|
+
}, ['workspaceRelativePath', 'argv']),
|
|
84
|
+
}),
|
|
85
|
+
Object.freeze({
|
|
86
|
+
name: 'bot_operation_status', machineTool: 'operation_status', effect: READ_EFFECT, backend: null,
|
|
87
|
+
grants: ['remote/operation_status'],
|
|
88
|
+
description: '读取本 Bot 早前一次操作的当前状态。operationId 必须来自宿主返回的上一次工具结果。',
|
|
89
|
+
parameters: objectSchema({ operationId: { type: 'string', description: '宿主早前返回的 operationId。' } }, ['operationId']),
|
|
90
|
+
}),
|
|
91
|
+
Object.freeze({
|
|
92
|
+
name: 'bot_operation_cancel', machineTool: 'operation_cancel', effect: WRITE_EFFECT, backend: null,
|
|
93
|
+
grants: ['remote/operation_cancel'],
|
|
94
|
+
description: '请求停止本 Bot 早前的一次操作。这只是停止请求,已生效的副作用不会回滚。operationId 必须来自宿主返回的上一次工具结果。',
|
|
95
|
+
parameters: objectSchema({ operationId: { type: 'string', description: '宿主早前返回的 operationId。' } }, ['operationId']),
|
|
96
|
+
}),
|
|
97
|
+
Object.freeze({
|
|
98
|
+
name: 'bot_artifact_stat', machineTool: 'artifact_stat', effect: READ_EFFECT, backend: null,
|
|
99
|
+
grants: ['remote/artifact_stat'],
|
|
100
|
+
description: '读取本 Bot 早前一次操作产出的附件元数据。artifactId 必须来自宿主返回的上一次工具结果。',
|
|
101
|
+
parameters: objectSchema({ artifactId: { type: 'string', description: '宿主早前返回的 artifactId。' } }, ['artifactId']),
|
|
102
|
+
}),
|
|
103
|
+
Object.freeze({
|
|
104
|
+
name: 'bot_artifact_read', machineTool: 'artifact_read', effect: READ_EFFECT, backend: null,
|
|
105
|
+
grants: ['remote/artifact_read'],
|
|
106
|
+
description: '分块读取本 Bot 早前一次操作产出的附件内容。artifactId 必须来自宿主返回的上一次工具结果。',
|
|
107
|
+
parameters: objectSchema({
|
|
108
|
+
artifactId: { type: 'string', description: '宿主早前返回的 artifactId。' },
|
|
109
|
+
offset: { type: 'integer', description: '起始字节偏移。', minimum: 0 },
|
|
110
|
+
length: { type: 'integer', description: '读取字节数,宿主另有上界。', minimum: 1 },
|
|
111
|
+
}, ['artifactId']),
|
|
112
|
+
}),
|
|
113
|
+
Object.freeze({
|
|
114
|
+
name: 'bot_task_status', machineTool: null, effect: READ_EFFECT, backend: 'task-service', taskAction: 'read',
|
|
115
|
+
grants: ['task/read'],
|
|
116
|
+
description: '读取本 Bot 自己提交的一次持久 Task 的状态。taskId 必须来自宿主返回的上一次工具结果。',
|
|
117
|
+
parameters: objectSchema({ taskId: { type: 'string', description: '宿主早前返回的 taskId。' } }, ['taskId']),
|
|
118
|
+
}),
|
|
119
|
+
Object.freeze({
|
|
120
|
+
name: 'bot_task_cancel', machineTool: null, effect: WRITE_EFFECT, backend: 'task-service', taskAction: 'cancel',
|
|
121
|
+
grants: ['task/cancel'],
|
|
122
|
+
description: '请求停止本 Bot 自己提交的一次持久 Task。这只是停止请求,attempt 保持自己的身份与未知结果证据。taskId 必须来自宿主返回的上一次工具结果。',
|
|
123
|
+
parameters: objectSchema({ taskId: { type: 'string', description: '宿主早前返回的 taskId。' } }, ['taskId']),
|
|
124
|
+
}),
|
|
125
|
+
])
|
|
126
|
+
|
|
127
|
+
export const INDEPENDENT_TOOL_NAMES = Object.freeze(CAPABILITIES.map(entry => entry.name))
|
|
128
|
+
|
|
129
|
+
const byName = new Map(CAPABILITIES.map(entry => [entry.name, entry]))
|
|
130
|
+
|
|
131
|
+
export const independentToolEntry = name => byName.get(name) ?? null
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The offered schemas for one Bot, plus what this Host cannot run.
|
|
135
|
+
*
|
|
136
|
+
* @param {{ scopes?: Array<{ machineId: string, roots: string[], tools: string[], mode: string }>,
|
|
137
|
+
* backends?: string[], granted?: (capability: string) => boolean }} facts read live
|
|
138
|
+
* @returns {{ tools: object[], unavailable: Array<{ name: string, code: string }>, machines: string[] }}
|
|
139
|
+
*/
|
|
140
|
+
export function catalogFor({ scopes = [], backends = [], granted = () => true } = {}) {
|
|
141
|
+
if (!Array.isArray(scopes)) throw new TypeError('INDEPENDENT_TOOL_SCOPES_REQUIRED')
|
|
142
|
+
const live = scopes.filter(scope => scope && typeof scope.machineId === 'string' && Array.isArray(scope.tools))
|
|
143
|
+
const available = new Set(Array.isArray(backends) ? backends : [])
|
|
144
|
+
const allowed = name => { try { return granted(name) !== false } catch { return false } }
|
|
145
|
+
const tools = []
|
|
146
|
+
const unavailable = []
|
|
147
|
+
for (const entry of CAPABILITIES) {
|
|
148
|
+
// A durable backend is a precondition, not a runtime surprise: without it the capability is
|
|
149
|
+
// named as unavailable instead of being offered and failing later.
|
|
150
|
+
if (entry.backend && !available.has(entry.backend)) {
|
|
151
|
+
unavailable.push({ name: entry.name, code: entry.backend === 'task-service' ? 'TASK_SERVICE_UNAVAILABLE' : 'TASK_BACKEND_UNAVAILABLE' })
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (!entry.grants.every(allowed)) {
|
|
155
|
+
unavailable.push({ name: entry.name, code: 'CAPABILITY_NOT_GRANTED' })
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if (entry.machineTool === null) {
|
|
159
|
+
tools.push(Object.freeze({ name: entry.name, description: entry.description, parameters: entry.parameters }))
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
const grantedScope = live.some(scope => scope.tools.includes(entry.machineTool)
|
|
163
|
+
&& (entry.effect !== WRITE_EFFECT || scope.mode === 'workspace-write'))
|
|
164
|
+
if (!grantedScope) {
|
|
165
|
+
unavailable.push({ name: entry.name, code: entry.effect === WRITE_EFFECT ? 'MACHINE_WRITE_NOT_GRANTED' : 'MACHINE_TOOL_NOT_GRANTED' })
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
tools.push(Object.freeze({ name: entry.name, description: entry.description, parameters: entry.parameters }))
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
tools: Object.freeze(tools),
|
|
172
|
+
unavailable: Object.freeze(unavailable),
|
|
173
|
+
machines: Object.freeze([...new Set(live.map(scope => scope.machineId))].sort()),
|
|
174
|
+
}
|
|
175
|
+
}
|