@botcord/daemon 0.2.85 → 0.2.87
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/LICENSE +21 -0
- package/dist/cloud-daemon.js +23 -2
- package/dist/daemon-singleton.d.ts +12 -0
- package/dist/daemon-singleton.js +83 -7
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +32 -0
- package/dist/gateway/channels/botcord.js +72 -29
- package/dist/gateway/dispatcher.d.ts +16 -0
- package/dist/gateway/dispatcher.js +25 -1
- package/dist/gateway/runtimes/deepseek-tui.js +5 -1
- package/dist/index.js +27 -7
- package/dist/provision.js +37 -3
- package/dist/skill-index.d.ts +13 -0
- package/dist/skill-index.js +65 -18
- package/dist/turn-text.js +38 -1
- package/package.json +10 -11
- package/src/__tests__/cloud-daemon.test.ts +79 -0
- package/src/__tests__/daemon-singleton.test.ts +59 -1
- package/src/__tests__/dispatcher-reply-to.test.ts +61 -0
- package/src/__tests__/provision.test.ts +42 -0
- package/src/__tests__/runtime-discovery.test.ts +17 -1
- package/src/__tests__/skill-index.test.ts +130 -0
- package/src/__tests__/turn-text.test.ts +121 -0
- package/src/cloud-daemon.ts +22 -2
- package/src/daemon-singleton.ts +98 -6
- package/src/daemon.ts +37 -0
- package/src/gateway/__tests__/botcord-channel.test.ts +79 -0
- package/src/gateway/__tests__/deepseek-tui-adapter.test.ts +49 -0
- package/src/gateway/channels/botcord.ts +81 -33
- package/src/gateway/dispatcher.ts +26 -1
- package/src/gateway/runtimes/deepseek-tui.ts +7 -1
- package/src/index.ts +25 -6
- package/src/provision.ts +42 -1
- package/src/skill-index.ts +87 -19
- package/src/turn-text.ts +51 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BotLearn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cloud-daemon.js
CHANGED
|
@@ -17,7 +17,7 @@ import { ControlChannel } from "./control-channel.js";
|
|
|
17
17
|
import { toGatewayConfig } from "./daemon-config-map.js";
|
|
18
18
|
import { log as daemonLog } from "./log.js";
|
|
19
19
|
import { createProvisioner } from "./provision.js";
|
|
20
|
-
import { createDaemonChannel, pushRuntimeSnapshot } from "./daemon.js";
|
|
20
|
+
import { createDaemonChannel, pushAgentSkillSnapshot, pushRuntimeSnapshot } from "./daemon.js";
|
|
21
21
|
import { SnapshotWriter } from "./snapshot-writer.js";
|
|
22
22
|
import { createDaemonSystemContextBuilder } from "./system-context.js";
|
|
23
23
|
import { readWorkingMemorySnapshot } from "./working-memory.js";
|
|
@@ -157,7 +157,25 @@ export async function startCloudDaemon(opts) {
|
|
|
157
157
|
text: msg.text,
|
|
158
158
|
});
|
|
159
159
|
};
|
|
160
|
+
const installedAgentIds = new Set();
|
|
161
|
+
const runtimeByAgentId = new Map();
|
|
162
|
+
let controlChannel = null;
|
|
163
|
+
const pushInstalledAgentSkillSnapshot = (agentId, reason) => {
|
|
164
|
+
if (!controlChannel)
|
|
165
|
+
return;
|
|
166
|
+
const runtime = runtimeByAgentId.get(agentId) ?? opts.config.defaultRoute.adapter;
|
|
167
|
+
const pushed = pushAgentSkillSnapshot(controlChannel, agentId, { runtime });
|
|
168
|
+
logger.info("cloud control-channel: agent_skill_snapshot pushed", {
|
|
169
|
+
agentId,
|
|
170
|
+
runtime,
|
|
171
|
+
reason,
|
|
172
|
+
ok: pushed,
|
|
173
|
+
});
|
|
174
|
+
};
|
|
160
175
|
const onAgentInstalled = (info) => {
|
|
176
|
+
installedAgentIds.add(info.agentId);
|
|
177
|
+
if (info.runtime)
|
|
178
|
+
runtimeByAgentId.set(info.agentId, info.runtime);
|
|
161
179
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
162
180
|
if (info.hubUrl)
|
|
163
181
|
hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
@@ -173,6 +191,7 @@ export async function startCloudDaemon(opts) {
|
|
|
173
191
|
loopRiskBuilder: () => null,
|
|
174
192
|
}));
|
|
175
193
|
}
|
|
194
|
+
pushInstalledAgentSkillSnapshot(info.agentId, "agent_installed");
|
|
176
195
|
};
|
|
177
196
|
const gateway = new Gateway({
|
|
178
197
|
config: gwConfig,
|
|
@@ -197,7 +216,6 @@ export async function startCloudDaemon(opts) {
|
|
|
197
216
|
});
|
|
198
217
|
await gateway.start();
|
|
199
218
|
logger.info("cloud daemon gateway started (zero agents at boot)");
|
|
200
|
-
let controlChannel = null;
|
|
201
219
|
if (!opts.disableControlChannel) {
|
|
202
220
|
const auth = asUserAuthManager(new CloudAuthManager(cloudCfg));
|
|
203
221
|
const provisionerFactory = opts.provisionerFactory ?? createProvisioner;
|
|
@@ -247,6 +265,9 @@ export async function startCloudDaemon(opts) {
|
|
|
247
265
|
logger.info("cloud control-channel started; runtime_snapshot pushed", {
|
|
248
266
|
ok: pushed,
|
|
249
267
|
});
|
|
268
|
+
for (const agentId of installedAgentIds) {
|
|
269
|
+
pushInstalledAgentSkillSnapshot(agentId, "control_channel_started");
|
|
270
|
+
}
|
|
250
271
|
}
|
|
251
272
|
catch (err) {
|
|
252
273
|
logger.warn("cloud control-channel start failed; daemon will retry", {
|
|
@@ -2,6 +2,11 @@ export interface SingletonLogger {
|
|
|
2
2
|
info(message: string, meta?: Record<string, unknown>): void;
|
|
3
3
|
warn(message: string, meta?: Record<string, unknown>): void;
|
|
4
4
|
}
|
|
5
|
+
export interface DaemonSingletonLock {
|
|
6
|
+
lockPath: string;
|
|
7
|
+
release(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function defaultLockPath(pidPath?: string): string;
|
|
5
10
|
export declare function readPid(pidPath?: string): number | null;
|
|
6
11
|
export declare function pidAlive(pid: number): boolean;
|
|
7
12
|
export interface DaemonProcessInfo {
|
|
@@ -23,6 +28,13 @@ export declare function stopDaemonFromPidFileForRestart(opts?: {
|
|
|
23
28
|
currentPid?: number;
|
|
24
29
|
logger?: SingletonLogger;
|
|
25
30
|
}): Promise<void>;
|
|
31
|
+
export declare function acquireDaemonSingletonLock(opts?: {
|
|
32
|
+
lockPath?: string;
|
|
33
|
+
pidPath?: string;
|
|
34
|
+
currentPid?: number;
|
|
35
|
+
logger?: SingletonLogger;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}): Promise<DaemonSingletonLock>;
|
|
26
38
|
export declare function stopOtherDaemonProcessesForRestart(opts?: {
|
|
27
39
|
currentPid?: number;
|
|
28
40
|
logger?: SingletonLogger;
|
package/dist/daemon-singleton.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { PID_PATH } from "./config.js";
|
|
5
5
|
const noopLogger = {
|
|
@@ -10,6 +10,11 @@ const noopLogger = {
|
|
|
10
10
|
// noop
|
|
11
11
|
},
|
|
12
12
|
};
|
|
13
|
+
const DEFAULT_LOCK_WAIT_MS = 15_000;
|
|
14
|
+
const DEFAULT_LOCK_RETRY_MS = 50;
|
|
15
|
+
export function defaultLockPath(pidPath = PID_PATH) {
|
|
16
|
+
return `${pidPath}.lock`;
|
|
17
|
+
}
|
|
13
18
|
export function readPid(pidPath = PID_PATH) {
|
|
14
19
|
if (!existsSync(pidPath))
|
|
15
20
|
return null;
|
|
@@ -17,6 +22,9 @@ export function readPid(pidPath = PID_PATH) {
|
|
|
17
22
|
const pid = Number(raw);
|
|
18
23
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
19
24
|
}
|
|
25
|
+
function readLockOwner(lockPath) {
|
|
26
|
+
return readPid(path.join(lockPath, "owner.pid"));
|
|
27
|
+
}
|
|
20
28
|
export function pidAlive(pid) {
|
|
21
29
|
try {
|
|
22
30
|
process.kill(pid, 0);
|
|
@@ -98,6 +106,71 @@ export async function stopDaemonFromPidFileForRestart(opts = {}) {
|
|
|
98
106
|
await stopExistingDaemonForRestart(existing, opts);
|
|
99
107
|
}
|
|
100
108
|
}
|
|
109
|
+
export async function acquireDaemonSingletonLock(opts = {}) {
|
|
110
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
111
|
+
const lockPath = opts.lockPath ?? defaultLockPath(pidPath);
|
|
112
|
+
const currentPid = opts.currentPid ?? process.pid;
|
|
113
|
+
const logger = opts.logger ?? noopLogger;
|
|
114
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_LOCK_WAIT_MS;
|
|
115
|
+
const deadline = Date.now() + timeoutMs;
|
|
116
|
+
ensureParentDir(lockPath);
|
|
117
|
+
while (true) {
|
|
118
|
+
try {
|
|
119
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
120
|
+
writeFileSync(path.join(lockPath, "owner.pid"), String(currentPid), { mode: 0o600 });
|
|
121
|
+
return {
|
|
122
|
+
lockPath,
|
|
123
|
+
release() {
|
|
124
|
+
const owner = readLockOwner(lockPath);
|
|
125
|
+
if (owner !== null && owner !== currentPid)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const code = err.code;
|
|
138
|
+
if (code !== "EEXIST")
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
const owner = readLockOwner(lockPath);
|
|
142
|
+
if (owner === currentPid) {
|
|
143
|
+
return {
|
|
144
|
+
lockPath,
|
|
145
|
+
release() {
|
|
146
|
+
try {
|
|
147
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// ignore
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (owner !== null && pidAlive(owner)) {
|
|
156
|
+
logger.info("daemon singleton lock owner found; restarting", { pid: owner });
|
|
157
|
+
await stopExistingDaemonForRestart(owner, { pidPath, currentPid, logger });
|
|
158
|
+
}
|
|
159
|
+
const refreshedOwner = readLockOwner(lockPath);
|
|
160
|
+
if (refreshedOwner === null || !pidAlive(refreshedOwner)) {
|
|
161
|
+
try {
|
|
162
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// another starter may have removed/recreated it
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (Date.now() >= deadline) {
|
|
169
|
+
throw new Error(`timed out acquiring daemon singleton lock at ${lockPath}`);
|
|
170
|
+
}
|
|
171
|
+
await delay(DEFAULT_LOCK_RETRY_MS);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
101
174
|
export async function stopOtherDaemonProcessesForRestart(opts = {}) {
|
|
102
175
|
const currentPid = opts.currentPid ?? process.pid;
|
|
103
176
|
const logger = opts.logger ?? noopLogger;
|
|
@@ -142,12 +215,7 @@ export function writeCurrentPid(opts = {}) {
|
|
|
142
215
|
// Cloud-mode startup writes the PID file before `saveConfig` runs, so
|
|
143
216
|
// the daemon dir may not exist yet. mkdir its parent (0700) so the
|
|
144
217
|
// first write doesn't crash with ENOENT.
|
|
145
|
-
|
|
146
|
-
mkdirSync(path.dirname(pidPath), { recursive: true, mode: 0o700 });
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// best-effort — writeFileSync below will surface the real error
|
|
150
|
-
}
|
|
218
|
+
ensureParentDir(pidPath);
|
|
151
219
|
writeFileSync(pidPath, String(opts.currentPid ?? process.pid), { mode: 0o600 });
|
|
152
220
|
}
|
|
153
221
|
export function removePidFile(pidPath = PID_PATH) {
|
|
@@ -186,3 +254,11 @@ export function isBotCordDaemonStartCommand(command) {
|
|
|
186
254
|
function delay(ms) {
|
|
187
255
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
256
|
}
|
|
257
|
+
function ensureParentDir(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// best-effort — the next filesystem operation will surface real errors
|
|
263
|
+
}
|
|
264
|
+
}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -65,6 +65,9 @@ export interface RuntimeSnapshotSink {
|
|
|
65
65
|
* or wait for the next daemon restart). Exported for unit tests.
|
|
66
66
|
*/
|
|
67
67
|
export declare function pushRuntimeSnapshot(sink: RuntimeSnapshotSink, liveSnapshot?: GatewayRuntimeSnapshot): boolean;
|
|
68
|
+
export declare function pushAgentSkillSnapshot(sink: RuntimeSnapshotSink, agentId: string, opts?: {
|
|
69
|
+
runtime?: string;
|
|
70
|
+
}): boolean;
|
|
68
71
|
/** Options accepted by {@link startDaemon} — the P0.5 compatibility shim. */
|
|
69
72
|
export interface DaemonRuntimeOptions {
|
|
70
73
|
config: DaemonConfig;
|
package/dist/daemon.js
CHANGED
|
@@ -22,6 +22,7 @@ import { PolicyResolver } from "./gateway/policy-resolver.js";
|
|
|
22
22
|
import { scanMention } from "./mention-scan.js";
|
|
23
23
|
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
24
24
|
import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
|
|
25
|
+
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
25
26
|
/**
|
|
26
27
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
27
28
|
* tasks routinely exceed 10 minutes, so daemon-hosted agents get a larger
|
|
@@ -164,6 +165,22 @@ export function pushRuntimeSnapshot(sink, liveSnapshot) {
|
|
|
164
165
|
}
|
|
165
166
|
return ok;
|
|
166
167
|
}
|
|
168
|
+
export function pushAgentSkillSnapshot(sink, agentId, opts = {}) {
|
|
169
|
+
const snap = collectAgentSkillSnapshot(agentId, opts);
|
|
170
|
+
const ok = sink.send({
|
|
171
|
+
id: `skill_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
172
|
+
type: "agent_skill_snapshot",
|
|
173
|
+
params: snap,
|
|
174
|
+
ts: Date.now(),
|
|
175
|
+
});
|
|
176
|
+
if (!ok) {
|
|
177
|
+
daemonLog.warn("agent-skill-snapshot: control-channel send returned false", {
|
|
178
|
+
agentId,
|
|
179
|
+
skills: snap.skills.length,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return ok;
|
|
183
|
+
}
|
|
167
184
|
/**
|
|
168
185
|
* Adapt daemon's file-based `log` module into the gateway logger contract.
|
|
169
186
|
* Writes go to `~/.botcord/logs/daemon.log` + stderr, preserving the format
|
|
@@ -355,6 +372,12 @@ export async function startDaemon(opts) {
|
|
|
355
372
|
// next room-context fetch re-loads the BotCordClient against the new
|
|
356
373
|
// credential file.
|
|
357
374
|
credentialPathByAgentId.set(info.agentId, info.credentialsFile);
|
|
375
|
+
if (info.runtime) {
|
|
376
|
+
agentRuntimes[info.agentId] = {
|
|
377
|
+
...(agentRuntimes[info.agentId] ?? {}),
|
|
378
|
+
runtime: info.runtime,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
358
381
|
if (info.hubUrl)
|
|
359
382
|
hubUrlByAgentId.set(info.agentId, info.hubUrl);
|
|
360
383
|
if (info.displayName)
|
|
@@ -483,6 +506,15 @@ export async function startDaemon(opts) {
|
|
|
483
506
|
logger.info("control-channel: initial runtime_snapshot push", {
|
|
484
507
|
ok: pushed,
|
|
485
508
|
});
|
|
509
|
+
for (const agentId of agentIds) {
|
|
510
|
+
const runtime = agentRuntimes[agentId]?.runtime ?? opts.config.defaultRoute.adapter;
|
|
511
|
+
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId, { runtime });
|
|
512
|
+
logger.info("control-channel: initial agent_skill_snapshot push", {
|
|
513
|
+
agentId,
|
|
514
|
+
runtime,
|
|
515
|
+
ok: skillsPushed,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
486
518
|
}
|
|
487
519
|
catch (err) {
|
|
488
520
|
logger.warn("control-channel failed to start; continuing without it", {
|
|
@@ -1044,24 +1044,39 @@ function extractDeepseekToolCall(raw) {
|
|
|
1044
1044
|
const payload = raw?.payload;
|
|
1045
1045
|
if (!payload || typeof payload !== "object")
|
|
1046
1046
|
return null;
|
|
1047
|
-
|
|
1048
|
-
|
|
1047
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1048
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1049
|
+
if (event === "tool.started") {
|
|
1050
|
+
const tool = innerPayload?.tool && typeof innerPayload.tool === "object" ? innerPayload.tool : undefined;
|
|
1049
1051
|
return {
|
|
1050
|
-
name: stringField(
|
|
1051
|
-
params: parseMaybeJson(
|
|
1052
|
-
|
|
1053
|
-
|
|
1052
|
+
name: stringField(innerPayload, "name") ?? stringField(tool, "name") ?? "tool",
|
|
1053
|
+
params: parseMaybeJson(innerPayload?.input ??
|
|
1054
|
+
innerPayload?.arguments ??
|
|
1055
|
+
innerPayload?.params ??
|
|
1056
|
+
tool?.input ??
|
|
1057
|
+
tool?.rawInput ??
|
|
1058
|
+
tool?.arguments ??
|
|
1059
|
+
tool?.params),
|
|
1060
|
+
id: stringField(innerPayload, "id") ?? stringField(tool, "id"),
|
|
1061
|
+
status: stringField(innerPayload, "status") ?? stringField(tool, "status"),
|
|
1054
1062
|
};
|
|
1055
1063
|
}
|
|
1056
|
-
if (
|
|
1057
|
-
const inner =
|
|
1058
|
-
? payload
|
|
1059
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1060
|
-
? payload.payload
|
|
1061
|
-
: {};
|
|
1064
|
+
if (event === "item.started") {
|
|
1065
|
+
const inner = innerPayload ?? {};
|
|
1062
1066
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1063
1067
|
const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
|
|
1064
|
-
const
|
|
1068
|
+
const metadata = item?.metadata && typeof item.metadata === "object" ? item.metadata : undefined;
|
|
1069
|
+
const metadataCommand = metadata && (metadata.command ?? metadata.cmd)
|
|
1070
|
+
? { [metadata.command ? "command" : "cmd"]: metadata.command ?? metadata.cmd }
|
|
1071
|
+
: undefined;
|
|
1072
|
+
const itemParams = parseMaybeJson(item?.input ??
|
|
1073
|
+
item?.arguments ??
|
|
1074
|
+
item?.params ??
|
|
1075
|
+
metadata?.input ??
|
|
1076
|
+
metadata?.arguments ??
|
|
1077
|
+
metadata?.params ??
|
|
1078
|
+
metadataCommand ??
|
|
1079
|
+
item?.detail);
|
|
1065
1080
|
const detailParams = itemParams !== undefined
|
|
1066
1081
|
? itemParams
|
|
1067
1082
|
: typeof item?.detail === "string" && item.detail.trim()
|
|
@@ -1082,8 +1097,16 @@ function extractDeepseekToolCall(raw) {
|
|
|
1082
1097
|
inner.arguments ??
|
|
1083
1098
|
inner.params ??
|
|
1084
1099
|
item?.input ??
|
|
1085
|
-
item?.arguments
|
|
1086
|
-
|
|
1100
|
+
item?.arguments ??
|
|
1101
|
+
item?.params ??
|
|
1102
|
+
metadata?.input ??
|
|
1103
|
+
metadata?.arguments ??
|
|
1104
|
+
metadata?.params ??
|
|
1105
|
+
metadataCommand) ?? detailParams ?? tool ?? item,
|
|
1106
|
+
id: stringField(tool, "id") ??
|
|
1107
|
+
stringField(inner, "id") ??
|
|
1108
|
+
stringField(item, "id") ??
|
|
1109
|
+
stringField(payload, "item_id"),
|
|
1087
1110
|
status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
|
|
1088
1111
|
};
|
|
1089
1112
|
}
|
|
@@ -1093,23 +1116,23 @@ function extractDeepseekToolResult(raw) {
|
|
|
1093
1116
|
const payload = raw?.payload;
|
|
1094
1117
|
if (!payload || typeof payload !== "object")
|
|
1095
1118
|
return null;
|
|
1096
|
-
|
|
1097
|
-
|
|
1119
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1120
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1121
|
+
if (event === "tool.completed") {
|
|
1122
|
+
const result = innerPayload?.output ??
|
|
1123
|
+
innerPayload?.result ??
|
|
1124
|
+
innerPayload?.content ??
|
|
1125
|
+
innerPayload?.error ??
|
|
1126
|
+
innerPayload ??
|
|
1127
|
+
payload;
|
|
1098
1128
|
return {
|
|
1099
|
-
name: stringField(
|
|
1129
|
+
name: stringField(innerPayload, "name"),
|
|
1100
1130
|
result: stringifyToolResult(result),
|
|
1101
|
-
id: stringField(
|
|
1131
|
+
id: stringField(innerPayload, "id"),
|
|
1102
1132
|
};
|
|
1103
1133
|
}
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1106
|
-
payload.event === "item.completed" ||
|
|
1107
|
-
payload.event === "item.failed") {
|
|
1108
|
-
const inner = raw?.event === "item.completed" || raw?.event === "item.failed"
|
|
1109
|
-
? payload
|
|
1110
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1111
|
-
? payload.payload
|
|
1112
|
-
: {};
|
|
1134
|
+
if (event === "item.completed" || event === "item.failed") {
|
|
1135
|
+
const inner = innerPayload ?? {};
|
|
1113
1136
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1114
1137
|
const result = item?.output ??
|
|
1115
1138
|
item?.result ??
|
|
@@ -1128,11 +1151,31 @@ function extractDeepseekToolResult(raw) {
|
|
|
1128
1151
|
stringField(inner, "name") ??
|
|
1129
1152
|
stringField(item, "type"),
|
|
1130
1153
|
result: stringifyToolResult(result),
|
|
1131
|
-
id: stringField(item, "id") ?? stringField(inner, "id"),
|
|
1154
|
+
id: stringField(item, "id") ?? stringField(inner, "id") ?? stringField(payload, "item_id"),
|
|
1132
1155
|
};
|
|
1133
1156
|
}
|
|
1134
1157
|
return null;
|
|
1135
1158
|
}
|
|
1159
|
+
function unwrapDeepseekPayload(raw) {
|
|
1160
|
+
const payload = raw?.payload;
|
|
1161
|
+
if (!payload || typeof payload !== "object")
|
|
1162
|
+
return undefined;
|
|
1163
|
+
const nested = payload.payload;
|
|
1164
|
+
if (nested && typeof nested === "object") {
|
|
1165
|
+
const outerEvent = stringField(payload, "event");
|
|
1166
|
+
if (outerEvent ||
|
|
1167
|
+
nested.item ||
|
|
1168
|
+
nested.tool ||
|
|
1169
|
+
nested.turn ||
|
|
1170
|
+
nested.kind ||
|
|
1171
|
+
nested.output ||
|
|
1172
|
+
nested.result ||
|
|
1173
|
+
nested.error) {
|
|
1174
|
+
return nested;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return payload;
|
|
1178
|
+
}
|
|
1136
1179
|
function formatBlockDetails(raw) {
|
|
1137
1180
|
if (!raw || typeof raw !== "object")
|
|
1138
1181
|
return "";
|
|
@@ -2,6 +2,22 @@ import type { GatewayLogger } from "./log.js";
|
|
|
2
2
|
import { type SessionStore } from "./session-store.js";
|
|
3
3
|
import { type TranscriptWriter } from "./transcript.js";
|
|
4
4
|
import type { ChannelAdapter, GatewayConfig, GatewayInboundEnvelope, GatewayInboundMessage, GatewayRoute, InboundObserver, MemoryContextBuilder, OutboundObserver, RuntimeAdapter, RuntimeRecoveryContextBuilder, RuntimeRunResult, RuntimeCircuitBreakerSnapshot, SystemContextBuilder, TurnStatusSnapshot, UserTurnBuilder } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Pick the canonical reply_to value to attach to outbound replies for a given
|
|
7
|
+
* inbound `GatewayInboundMessage`. Priority:
|
|
8
|
+
*
|
|
9
|
+
* 1. `msg.replyTo` — the inbound was itself a reply; preserve the chain so
|
|
10
|
+
* receipts and threaded replies point at the original target.
|
|
11
|
+
* 2. `raw.envelope.msg_id` — the wire-protocol identifier (UUID per a2a/0.1).
|
|
12
|
+
* This is the canonical form the hub stores in `reply_to_msg_id`.
|
|
13
|
+
* 3. `msg.id` — fallback to the hub_msg_id (`h_*`) the BotCord channel
|
|
14
|
+
* stamps on every inbound. The hub accepts this form via
|
|
15
|
+
* `_load_reply_target`'s prefix-based discriminator, but emitting it is
|
|
16
|
+
* lossy because the hub then has to resolve it back to msg_id.
|
|
17
|
+
*
|
|
18
|
+
* Exported for unit testing; production code paths use Dispatcher.providerReplyTo.
|
|
19
|
+
*/
|
|
20
|
+
export declare function pickReplyToTarget(msg: GatewayInboundMessage): string;
|
|
5
21
|
/** Factory signature for building a runtime adapter at turn dispatch time. */
|
|
6
22
|
export type RuntimeFactory = (runtimeId: string, extraArgs?: string[]) => RuntimeAdapter;
|
|
7
23
|
/** Constructor options for `Dispatcher`. */
|
|
@@ -143,6 +143,30 @@ function buildRuntimeRecoveryPrompt(args) {
|
|
|
143
143
|
args.userTurn,
|
|
144
144
|
].join("\n");
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Pick the canonical reply_to value to attach to outbound replies for a given
|
|
148
|
+
* inbound `GatewayInboundMessage`. Priority:
|
|
149
|
+
*
|
|
150
|
+
* 1. `msg.replyTo` — the inbound was itself a reply; preserve the chain so
|
|
151
|
+
* receipts and threaded replies point at the original target.
|
|
152
|
+
* 2. `raw.envelope.msg_id` — the wire-protocol identifier (UUID per a2a/0.1).
|
|
153
|
+
* This is the canonical form the hub stores in `reply_to_msg_id`.
|
|
154
|
+
* 3. `msg.id` — fallback to the hub_msg_id (`h_*`) the BotCord channel
|
|
155
|
+
* stamps on every inbound. The hub accepts this form via
|
|
156
|
+
* `_load_reply_target`'s prefix-based discriminator, but emitting it is
|
|
157
|
+
* lossy because the hub then has to resolve it back to msg_id.
|
|
158
|
+
*
|
|
159
|
+
* Exported for unit testing; production code paths use Dispatcher.providerReplyTo.
|
|
160
|
+
*/
|
|
161
|
+
export function pickReplyToTarget(msg) {
|
|
162
|
+
if (msg.replyTo)
|
|
163
|
+
return msg.replyTo;
|
|
164
|
+
const raw = msg.raw;
|
|
165
|
+
const envMsgId = raw && typeof raw.envelope?.msg_id === "string" && raw.envelope.msg_id
|
|
166
|
+
? raw.envelope.msg_id
|
|
167
|
+
: null;
|
|
168
|
+
return envMsgId ?? msg.id;
|
|
169
|
+
}
|
|
146
170
|
/**
|
|
147
171
|
* Reason carried on `AbortController.abort()` when a cancel-previous wave
|
|
148
172
|
* is taking over the slot. Distinguishing this from a timeout abort lets
|
|
@@ -1775,7 +1799,7 @@ export class Dispatcher {
|
|
|
1775
1799
|
return { ok: true };
|
|
1776
1800
|
}
|
|
1777
1801
|
providerReplyTo(msg) {
|
|
1778
|
-
return msg
|
|
1802
|
+
return pickReplyToTarget(msg);
|
|
1779
1803
|
}
|
|
1780
1804
|
emitInbound(turnId, msg) {
|
|
1781
1805
|
if (!this.transcript.enabled)
|
|
@@ -426,8 +426,12 @@ function isDeepseekTerminalEvent(eventName, payload) {
|
|
|
426
426
|
embedded === "done");
|
|
427
427
|
}
|
|
428
428
|
function isToolStarted(eventName, payload) {
|
|
429
|
+
const itemKind = payload?.payload?.item?.kind ?? payload?.item?.kind;
|
|
429
430
|
return ((eventName === "item.started" &&
|
|
430
|
-
(!!payload?.tool ||
|
|
431
|
+
(!!payload?.tool ||
|
|
432
|
+
itemKind === "tool_call" ||
|
|
433
|
+
itemKind === "command_execution" ||
|
|
434
|
+
itemKind === "file_change")) ||
|
|
431
435
|
(payload?.event === "item.started" && !!payload?.payload?.tool));
|
|
432
436
|
}
|
|
433
437
|
function isToolCompleted(eventName, payload) {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir, hostname } from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { augmentProcessPath } from "./path-env.js";
|
|
7
7
|
import { loadConfig, saveConfig, initDefaultConfig, resolveConfiguredAgentIds, SNAPSHOT_PATH, CONFIG_FILE_PATH, CONFIG_MISSING, } from "./config.js";
|
|
8
|
-
import { ensureNoOtherDaemonFromPidFile, findOtherDaemonProcesses, pidAlive, readPid, removePidFile, stopDaemonFromPidFileForRestart, stopOtherDaemonProcessesForRestart, writeCurrentPid, } from "./daemon-singleton.js";
|
|
8
|
+
import { acquireDaemonSingletonLock, ensureNoOtherDaemonFromPidFile, findOtherDaemonProcesses, pidAlive, readPid, removePidFile, stopDaemonFromPidFileForRestart, stopOtherDaemonProcessesForRestart, writeCurrentPid, } from "./daemon-singleton.js";
|
|
9
9
|
import { resolveBootAgents } from "./agent-discovery.js";
|
|
10
10
|
import { defaultTranscriptRoot, resolveTranscriptEnabled, transcriptAgentRoot, transcriptFilePath, } from "./gateway/index.js";
|
|
11
11
|
import { startDaemon } from "./daemon.js";
|
|
@@ -506,12 +506,22 @@ async function cmdStart(args) {
|
|
|
506
506
|
return;
|
|
507
507
|
}
|
|
508
508
|
// Foreground: we ARE the daemon.
|
|
509
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
509
510
|
writeCurrentPid();
|
|
510
|
-
|
|
511
|
+
let handle;
|
|
512
|
+
try {
|
|
513
|
+
handle = await startDaemon({ config: cfg, configPath: CONFIG_FILE_PATH });
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
removePidFile();
|
|
517
|
+
singletonLock.release();
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
511
520
|
const shutdown = async (sig) => {
|
|
512
521
|
log.info("signal received", { sig });
|
|
513
522
|
await handle.stop(sig);
|
|
514
523
|
removePidFile();
|
|
524
|
+
singletonLock.release();
|
|
515
525
|
process.exit(0);
|
|
516
526
|
};
|
|
517
527
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -539,6 +549,7 @@ async function cmdStartCloud(_args) {
|
|
|
539
549
|
daemonInstanceId: cloudConfig.daemonInstanceId,
|
|
540
550
|
hubUrl: cloudConfig.hubUrl,
|
|
541
551
|
});
|
|
552
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
542
553
|
await stopDaemonFromPidFileForRestart({ logger: log });
|
|
543
554
|
await stopOtherDaemonProcessesForRestart({ logger: log });
|
|
544
555
|
writeCurrentPid();
|
|
@@ -552,15 +563,24 @@ async function cmdStartCloud(_args) {
|
|
|
552
563
|
};
|
|
553
564
|
saveConfig(cfg);
|
|
554
565
|
log.info("cloud mode config initialized", { configPath: CONFIG_FILE_PATH });
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
566
|
+
let handle;
|
|
567
|
+
try {
|
|
568
|
+
handle = await startCloudDaemon({
|
|
569
|
+
cloudConfig,
|
|
570
|
+
config: cfg,
|
|
571
|
+
configPath: CONFIG_FILE_PATH,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
removePidFile();
|
|
576
|
+
singletonLock.release();
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
560
579
|
const shutdown = async (sig) => {
|
|
561
580
|
log.info("signal received", { sig });
|
|
562
581
|
await handle.stop(sig);
|
|
563
582
|
removePidFile();
|
|
583
|
+
singletonLock.release();
|
|
564
584
|
process.exit(0);
|
|
565
585
|
};
|
|
566
586
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
package/dist/provision.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
|
-
import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, writeCredentialsFile, } from "@botcord/protocol-core";
|
|
10
|
+
import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, normalizeTokenExpiresAt, writeCredentialsFile, } from "@botcord/protocol-core";
|
|
11
11
|
import { loadConfig, resolveConfiguredAgentIds, saveConfig, } from "./config.js";
|
|
12
12
|
import { BOTCORD_CHANNEL_TYPE, buildManagedRoutes, prepareGatewayProfile, } from "./daemon-config-map.js";
|
|
13
13
|
import { discoverLocalOpenclawGateways, mergeOpenclawGateways, openclawDiscoveryConfigEnabled, } from "./openclaw-discovery.js";
|
|
@@ -19,8 +19,14 @@ import { log as daemonLog } from "./log.js";
|
|
|
19
19
|
import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
20
20
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
21
21
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
22
|
+
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
22
23
|
import { buildRuntimeSelectionExtraArgs, mergeRuntimeExtraArgs, } from "./runtime-route-options.js";
|
|
23
24
|
import { handleCloudGatewayRuntimeInbound, } from "./cloud-gateway-runtime.js";
|
|
25
|
+
function runtimeForLoadedAgent(gateway, agentId) {
|
|
26
|
+
return gateway.listManagedRoutes()
|
|
27
|
+
.find((route) => route.match?.accountId === agentId)
|
|
28
|
+
?.runtime;
|
|
29
|
+
}
|
|
24
30
|
/**
|
|
25
31
|
* Build a dispatcher function that routes a `ControlFrame` to the right
|
|
26
32
|
* handler. Returned function signature matches
|
|
@@ -315,6 +321,33 @@ export function createProvisioner(opts) {
|
|
|
315
321
|
});
|
|
316
322
|
return { ok: true, result };
|
|
317
323
|
}
|
|
324
|
+
case "list_agent_skills": {
|
|
325
|
+
const params = (frame.params ?? {});
|
|
326
|
+
if (!params.agentId) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
error: { code: "bad_params", message: "list_agent_skills requires params.agentId" },
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const channels = gateway.snapshot().channels;
|
|
333
|
+
if (!channels[params.agentId]) {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
error: {
|
|
337
|
+
code: "agent_not_loaded",
|
|
338
|
+
message: `agent ${params.agentId} is not loaded in daemon gateway`,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const runtime = runtimeForLoadedAgent(gateway, params.agentId);
|
|
343
|
+
const result = collectAgentSkillSnapshot(params.agentId, { runtime });
|
|
344
|
+
daemonLog.debug("list_agent_skills", {
|
|
345
|
+
agentId: params.agentId,
|
|
346
|
+
runtime,
|
|
347
|
+
count: result.skills.length,
|
|
348
|
+
});
|
|
349
|
+
return { ok: true, result };
|
|
350
|
+
}
|
|
318
351
|
case "wake_agent": {
|
|
319
352
|
return handleWakeAgent(gateway, frame.params);
|
|
320
353
|
}
|
|
@@ -921,8 +954,9 @@ async function materializeCredentials(params, cfg, ctx, explicitCwd) {
|
|
|
921
954
|
record.displayName = c.displayName;
|
|
922
955
|
if (c.token)
|
|
923
956
|
record.token = c.token;
|
|
924
|
-
|
|
925
|
-
|
|
957
|
+
const tokenExpiresAt = normalizeTokenExpiresAt(c.tokenExpiresAt);
|
|
958
|
+
if (tokenExpiresAt !== undefined)
|
|
959
|
+
record.tokenExpiresAt = tokenExpiresAt;
|
|
926
960
|
if (runtime)
|
|
927
961
|
record.runtime = runtime;
|
|
928
962
|
const runtimeSelection = pickRuntimeSelection(params);
|