@botcord/daemon 0.2.84 → 0.2.86
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 +18 -2
- package/dist/daemon-singleton.d.ts +12 -0
- package/dist/daemon-singleton.js +83 -7
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +24 -0
- package/dist/gateway/channels/botcord.js +72 -29
- package/dist/gateway/runtimes/deepseek-tui.js +5 -1
- package/dist/index.js +27 -7
- package/dist/provision.js +30 -3
- package/dist/runtime-models.js +48 -6
- package/dist/skill-index.d.ts +12 -0
- package/dist/skill-index.js +15 -4
- 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__/provision.test.ts +42 -0
- package/src/__tests__/runtime-discovery.test.ts +17 -1
- package/src/__tests__/runtime-models.test.ts +50 -0
- package/src/__tests__/skill-index.test.ts +91 -0
- package/src/cloud-daemon.ts +18 -2
- package/src/daemon-singleton.ts +98 -6
- package/src/daemon.ts +28 -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/runtimes/deepseek-tui.ts +7 -1
- package/src/index.ts +25 -6
- package/src/provision.ts +34 -1
- package/src/runtime-models.ts +46 -6
- package/src/skill-index.ts +32 -4
package/src/daemon-singleton.ts
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
|
|
|
@@ -17,6 +17,18 @@ const noopLogger: SingletonLogger = {
|
|
|
17
17
|
},
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const DEFAULT_LOCK_WAIT_MS = 15_000;
|
|
21
|
+
const DEFAULT_LOCK_RETRY_MS = 50;
|
|
22
|
+
|
|
23
|
+
export interface DaemonSingletonLock {
|
|
24
|
+
lockPath: string;
|
|
25
|
+
release(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defaultLockPath(pidPath = PID_PATH): string {
|
|
29
|
+
return `${pidPath}.lock`;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
export function readPid(pidPath = PID_PATH): number | null {
|
|
21
33
|
if (!existsSync(pidPath)) return null;
|
|
22
34
|
const raw = readFileSync(pidPath, "utf8").trim();
|
|
@@ -24,6 +36,10 @@ export function readPid(pidPath = PID_PATH): number | null {
|
|
|
24
36
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
function readLockOwner(lockPath: string): number | null {
|
|
40
|
+
return readPid(path.join(lockPath, "owner.pid"));
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export function pidAlive(pid: number): boolean {
|
|
28
44
|
try {
|
|
29
45
|
process.kill(pid, 0);
|
|
@@ -127,6 +143,78 @@ export async function stopDaemonFromPidFileForRestart(
|
|
|
127
143
|
}
|
|
128
144
|
}
|
|
129
145
|
|
|
146
|
+
export async function acquireDaemonSingletonLock(
|
|
147
|
+
opts: {
|
|
148
|
+
lockPath?: string;
|
|
149
|
+
pidPath?: string;
|
|
150
|
+
currentPid?: number;
|
|
151
|
+
logger?: SingletonLogger;
|
|
152
|
+
timeoutMs?: number;
|
|
153
|
+
} = {},
|
|
154
|
+
): Promise<DaemonSingletonLock> {
|
|
155
|
+
const pidPath = opts.pidPath ?? PID_PATH;
|
|
156
|
+
const lockPath = opts.lockPath ?? defaultLockPath(pidPath);
|
|
157
|
+
const currentPid = opts.currentPid ?? process.pid;
|
|
158
|
+
const logger = opts.logger ?? noopLogger;
|
|
159
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_LOCK_WAIT_MS;
|
|
160
|
+
const deadline = Date.now() + timeoutMs;
|
|
161
|
+
|
|
162
|
+
ensureParentDir(lockPath);
|
|
163
|
+
while (true) {
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
166
|
+
writeFileSync(path.join(lockPath, "owner.pid"), String(currentPid), { mode: 0o600 });
|
|
167
|
+
return {
|
|
168
|
+
lockPath,
|
|
169
|
+
release() {
|
|
170
|
+
const owner = readLockOwner(lockPath);
|
|
171
|
+
if (owner !== null && owner !== currentPid) return;
|
|
172
|
+
try {
|
|
173
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
174
|
+
} catch {
|
|
175
|
+
// ignore
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
181
|
+
if (code !== "EEXIST") throw err;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const owner = readLockOwner(lockPath);
|
|
185
|
+
if (owner === currentPid) {
|
|
186
|
+
return {
|
|
187
|
+
lockPath,
|
|
188
|
+
release() {
|
|
189
|
+
try {
|
|
190
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
191
|
+
} catch {
|
|
192
|
+
// ignore
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (owner !== null && pidAlive(owner)) {
|
|
198
|
+
logger.info("daemon singleton lock owner found; restarting", { pid: owner });
|
|
199
|
+
await stopExistingDaemonForRestart(owner, { pidPath, currentPid, logger });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const refreshedOwner = readLockOwner(lockPath);
|
|
203
|
+
if (refreshedOwner === null || !pidAlive(refreshedOwner)) {
|
|
204
|
+
try {
|
|
205
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
206
|
+
} catch {
|
|
207
|
+
// another starter may have removed/recreated it
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (Date.now() >= deadline) {
|
|
212
|
+
throw new Error(`timed out acquiring daemon singleton lock at ${lockPath}`);
|
|
213
|
+
}
|
|
214
|
+
await delay(DEFAULT_LOCK_RETRY_MS);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
130
218
|
export async function stopOtherDaemonProcessesForRestart(
|
|
131
219
|
opts: {
|
|
132
220
|
currentPid?: number;
|
|
@@ -187,11 +275,7 @@ export function writeCurrentPid(
|
|
|
187
275
|
// Cloud-mode startup writes the PID file before `saveConfig` runs, so
|
|
188
276
|
// the daemon dir may not exist yet. mkdir its parent (0700) so the
|
|
189
277
|
// first write doesn't crash with ENOENT.
|
|
190
|
-
|
|
191
|
-
mkdirSync(path.dirname(pidPath), { recursive: true, mode: 0o700 });
|
|
192
|
-
} catch {
|
|
193
|
-
// best-effort — writeFileSync below will surface the real error
|
|
194
|
-
}
|
|
278
|
+
ensureParentDir(pidPath);
|
|
195
279
|
writeFileSync(pidPath, String(opts.currentPid ?? process.pid), { mode: 0o600 });
|
|
196
280
|
}
|
|
197
281
|
|
|
@@ -231,3 +315,11 @@ export function isBotCordDaemonStartCommand(command: string): boolean {
|
|
|
231
315
|
function delay(ms: number): Promise<void> {
|
|
232
316
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
233
317
|
}
|
|
318
|
+
|
|
319
|
+
function ensureParentDir(filePath: string): void {
|
|
320
|
+
try {
|
|
321
|
+
mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
322
|
+
} catch {
|
|
323
|
+
// best-effort — the next filesystem operation will surface real errors
|
|
324
|
+
}
|
|
325
|
+
}
|
package/src/daemon.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { PolicyResolver, type DaemonAttentionPolicy } from "./gateway/policy-res
|
|
|
52
52
|
import { scanMention } from "./mention-scan.js";
|
|
53
53
|
import { createDiagnosticBundle, uploadDiagnosticBundle } from "./diagnostics.js";
|
|
54
54
|
import { createAttentionPolicyFetcher } from "./attention-policy-fetcher.js";
|
|
55
|
+
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Default hard cap for a single runtime turn. Long-running coding/research
|
|
@@ -245,6 +246,26 @@ export function pushRuntimeSnapshot(
|
|
|
245
246
|
return ok;
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
export function pushAgentSkillSnapshot(
|
|
250
|
+
sink: RuntimeSnapshotSink,
|
|
251
|
+
agentId: string,
|
|
252
|
+
): boolean {
|
|
253
|
+
const snap = collectAgentSkillSnapshot(agentId);
|
|
254
|
+
const ok = sink.send({
|
|
255
|
+
id: `skill_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
256
|
+
type: "agent_skill_snapshot",
|
|
257
|
+
params: snap as unknown as Record<string, unknown>,
|
|
258
|
+
ts: Date.now(),
|
|
259
|
+
});
|
|
260
|
+
if (!ok) {
|
|
261
|
+
daemonLog.warn("agent-skill-snapshot: control-channel send returned false", {
|
|
262
|
+
agentId,
|
|
263
|
+
skills: snap.skills.length,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return ok;
|
|
267
|
+
}
|
|
268
|
+
|
|
248
269
|
/** Options accepted by {@link startDaemon} — the P0.5 compatibility shim. */
|
|
249
270
|
export interface DaemonRuntimeOptions {
|
|
250
271
|
config: DaemonConfig;
|
|
@@ -648,6 +669,13 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
648
669
|
logger.info("control-channel: initial runtime_snapshot push", {
|
|
649
670
|
ok: pushed,
|
|
650
671
|
});
|
|
672
|
+
for (const agentId of agentIds) {
|
|
673
|
+
const skillsPushed = pushAgentSkillSnapshot(controlChannel, agentId);
|
|
674
|
+
logger.info("control-channel: initial agent_skill_snapshot push", {
|
|
675
|
+
agentId,
|
|
676
|
+
ok: skillsPushed,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
651
679
|
} catch (err) {
|
|
652
680
|
logger.warn("control-channel failed to start; continuing without it", {
|
|
653
681
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -793,6 +793,85 @@ describe("createBotCordChannel — streamBlock()", () => {
|
|
|
793
793
|
});
|
|
794
794
|
});
|
|
795
795
|
|
|
796
|
+
it("normalizes wrapped DeepSeek item.started tool input from the runtime event stream", () => {
|
|
797
|
+
expect(
|
|
798
|
+
__normalizeBlockForHubForTests(
|
|
799
|
+
{
|
|
800
|
+
kind: "tool_use",
|
|
801
|
+
seq: 5,
|
|
802
|
+
raw: {
|
|
803
|
+
event: "item.started",
|
|
804
|
+
payload: {
|
|
805
|
+
seq: 922,
|
|
806
|
+
thread_id: "thr_test",
|
|
807
|
+
turn_id: "turn_test",
|
|
808
|
+
item_id: "item_exec",
|
|
809
|
+
event: "item.started",
|
|
810
|
+
payload: {
|
|
811
|
+
item: {
|
|
812
|
+
id: "item_exec",
|
|
813
|
+
kind: "tool_call",
|
|
814
|
+
status: "in_progress",
|
|
815
|
+
summary: "exec_shell started",
|
|
816
|
+
detail: "{\"cmd\":\"botcord-daemon status\"}",
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
5,
|
|
823
|
+
),
|
|
824
|
+
).toMatchObject({
|
|
825
|
+
kind: "tool_call",
|
|
826
|
+
seq: 5,
|
|
827
|
+
payload: {
|
|
828
|
+
id: "item_exec",
|
|
829
|
+
name: "exec_shell",
|
|
830
|
+
params: { cmd: "botcord-daemon status" },
|
|
831
|
+
status: "in_progress",
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
it("normalizes wrapped DeepSeek item.completed output without showing the event envelope", () => {
|
|
837
|
+
expect(
|
|
838
|
+
__normalizeBlockForHubForTests(
|
|
839
|
+
{
|
|
840
|
+
kind: "tool_result",
|
|
841
|
+
seq: 6,
|
|
842
|
+
raw: {
|
|
843
|
+
event: "item.completed",
|
|
844
|
+
payload: {
|
|
845
|
+
seq: 955,
|
|
846
|
+
thread_id: "thr_test",
|
|
847
|
+
turn_id: "turn_test",
|
|
848
|
+
item_id: "item_exec",
|
|
849
|
+
event: "item.completed",
|
|
850
|
+
payload: {
|
|
851
|
+
item: {
|
|
852
|
+
id: "item_exec",
|
|
853
|
+
kind: "command_execution",
|
|
854
|
+
status: "completed",
|
|
855
|
+
summary: "exec_shell: daemon: pid 49616",
|
|
856
|
+
detail: "daemon: pid 49616 (alive)",
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
6,
|
|
863
|
+
),
|
|
864
|
+
).toMatchObject({
|
|
865
|
+
kind: "tool_result",
|
|
866
|
+
seq: 6,
|
|
867
|
+
payload: {
|
|
868
|
+
name: "exec_shell",
|
|
869
|
+
result: "daemon: pid 49616 (alive)",
|
|
870
|
+
tool_use_id: "item_exec",
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
796
875
|
it("POSTs to /hub/stream-block with the right trace_id + block", async () => {
|
|
797
876
|
const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
|
|
798
877
|
const realFetch = globalThis.fetch;
|
|
@@ -353,6 +353,55 @@ describe("DeepseekTuiAdapter", () => {
|
|
|
353
353
|
}
|
|
354
354
|
});
|
|
355
355
|
|
|
356
|
+
it("treats DeepSeek command_execution item.started events as tool blocks", async () => {
|
|
357
|
+
const server = await startMockDeepseekServer({
|
|
358
|
+
events: [
|
|
359
|
+
{
|
|
360
|
+
event: "turn.started",
|
|
361
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.started" },
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
event: "item.started",
|
|
365
|
+
data: {
|
|
366
|
+
thread_id: "thr_test",
|
|
367
|
+
turn_id: "turn_test",
|
|
368
|
+
event: "item.started",
|
|
369
|
+
payload: {
|
|
370
|
+
item: {
|
|
371
|
+
id: "item_exec",
|
|
372
|
+
kind: "command_execution",
|
|
373
|
+
status: "in_progress",
|
|
374
|
+
summary: "exec_shell started",
|
|
375
|
+
detail: "{\"cmd\":\"date\"}",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
event: "item.delta",
|
|
382
|
+
data: {
|
|
383
|
+
thread_id: "thr_test",
|
|
384
|
+
turn_id: "turn_test",
|
|
385
|
+
event: "item.delta",
|
|
386
|
+
payload: { kind: "agent_message", delta: "done" },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
event: "turn.completed",
|
|
391
|
+
data: { thread_id: "thr_test", turn_id: "turn_test", event: "turn.completed" },
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
const { result, blocks, status } = runAdapter(server.baseUrl, server.token);
|
|
397
|
+
await expect(result).resolves.toMatchObject({ text: "done" });
|
|
398
|
+
expect(blocks).toEqual(expect.arrayContaining(["tool_use", "assistant_text"]));
|
|
399
|
+
expect(status).toContainEqual({ phase: "updated", label: "exec_shell" });
|
|
400
|
+
} finally {
|
|
401
|
+
await server.close();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
356
405
|
it("emits current DeepSeek agent_reasoning completions as thinking blocks", async () => {
|
|
357
406
|
const server = await startMockDeepseekServer({
|
|
358
407
|
events: [
|
|
@@ -1196,27 +1196,46 @@ function extractToolResult(raw: any): { name?: string; result: string; id?: stri
|
|
|
1196
1196
|
function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id?: string; status?: string } | null {
|
|
1197
1197
|
const payload = raw?.payload;
|
|
1198
1198
|
if (!payload || typeof payload !== "object") return null;
|
|
1199
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1200
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1199
1201
|
|
|
1200
|
-
if (
|
|
1201
|
-
const tool =
|
|
1202
|
+
if (event === "tool.started") {
|
|
1203
|
+
const tool = innerPayload?.tool && typeof innerPayload.tool === "object" ? innerPayload.tool : undefined;
|
|
1202
1204
|
return {
|
|
1203
|
-
name: stringField(
|
|
1204
|
-
params: parseMaybeJson(
|
|
1205
|
-
|
|
1206
|
-
|
|
1205
|
+
name: stringField(innerPayload, "name") ?? stringField(tool, "name") ?? "tool",
|
|
1206
|
+
params: parseMaybeJson(
|
|
1207
|
+
innerPayload?.input ??
|
|
1208
|
+
innerPayload?.arguments ??
|
|
1209
|
+
innerPayload?.params ??
|
|
1210
|
+
tool?.input ??
|
|
1211
|
+
tool?.rawInput ??
|
|
1212
|
+
tool?.arguments ??
|
|
1213
|
+
tool?.params,
|
|
1214
|
+
),
|
|
1215
|
+
id: stringField(innerPayload, "id") ?? stringField(tool, "id"),
|
|
1216
|
+
status: stringField(innerPayload, "status") ?? stringField(tool, "status"),
|
|
1207
1217
|
};
|
|
1208
1218
|
}
|
|
1209
1219
|
|
|
1210
|
-
if (
|
|
1211
|
-
const inner =
|
|
1212
|
-
raw?.event === "item.started"
|
|
1213
|
-
? payload
|
|
1214
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1215
|
-
? payload.payload
|
|
1216
|
-
: {};
|
|
1220
|
+
if (event === "item.started") {
|
|
1221
|
+
const inner = innerPayload ?? {};
|
|
1217
1222
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1218
1223
|
const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
|
|
1219
|
-
const
|
|
1224
|
+
const metadata = item?.metadata && typeof item.metadata === "object" ? item.metadata : undefined;
|
|
1225
|
+
const metadataCommand =
|
|
1226
|
+
metadata && (metadata.command ?? metadata.cmd)
|
|
1227
|
+
? { [metadata.command ? "command" : "cmd"]: metadata.command ?? metadata.cmd }
|
|
1228
|
+
: undefined;
|
|
1229
|
+
const itemParams = parseMaybeJson(
|
|
1230
|
+
item?.input ??
|
|
1231
|
+
item?.arguments ??
|
|
1232
|
+
item?.params ??
|
|
1233
|
+
metadata?.input ??
|
|
1234
|
+
metadata?.arguments ??
|
|
1235
|
+
metadata?.params ??
|
|
1236
|
+
metadataCommand ??
|
|
1237
|
+
item?.detail,
|
|
1238
|
+
);
|
|
1220
1239
|
const detailParams =
|
|
1221
1240
|
itemParams !== undefined
|
|
1222
1241
|
? itemParams
|
|
@@ -1240,9 +1259,18 @@ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id
|
|
|
1240
1259
|
inner.arguments ??
|
|
1241
1260
|
inner.params ??
|
|
1242
1261
|
item?.input ??
|
|
1243
|
-
item?.arguments
|
|
1262
|
+
item?.arguments ??
|
|
1263
|
+
item?.params ??
|
|
1264
|
+
metadata?.input ??
|
|
1265
|
+
metadata?.arguments ??
|
|
1266
|
+
metadata?.params ??
|
|
1267
|
+
metadataCommand,
|
|
1244
1268
|
) ?? detailParams ?? tool ?? item,
|
|
1245
|
-
id:
|
|
1269
|
+
id:
|
|
1270
|
+
stringField(tool, "id") ??
|
|
1271
|
+
stringField(inner, "id") ??
|
|
1272
|
+
stringField(item, "id") ??
|
|
1273
|
+
stringField(payload, "item_id"),
|
|
1246
1274
|
status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
|
|
1247
1275
|
};
|
|
1248
1276
|
}
|
|
@@ -1253,28 +1281,26 @@ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id
|
|
|
1253
1281
|
function extractDeepseekToolResult(raw: any): { name?: string; result: string; id?: string } | null {
|
|
1254
1282
|
const payload = raw?.payload;
|
|
1255
1283
|
if (!payload || typeof payload !== "object") return null;
|
|
1284
|
+
const innerPayload = unwrapDeepseekPayload(raw);
|
|
1285
|
+
const event = stringField(raw, "event") ?? stringField(payload, "event");
|
|
1256
1286
|
|
|
1257
|
-
if (
|
|
1258
|
-
const result =
|
|
1287
|
+
if (event === "tool.completed") {
|
|
1288
|
+
const result =
|
|
1289
|
+
innerPayload?.output ??
|
|
1290
|
+
innerPayload?.result ??
|
|
1291
|
+
innerPayload?.content ??
|
|
1292
|
+
innerPayload?.error ??
|
|
1293
|
+
innerPayload ??
|
|
1294
|
+
payload;
|
|
1259
1295
|
return {
|
|
1260
|
-
name: stringField(
|
|
1296
|
+
name: stringField(innerPayload, "name"),
|
|
1261
1297
|
result: stringifyToolResult(result),
|
|
1262
|
-
id: stringField(
|
|
1298
|
+
id: stringField(innerPayload, "id"),
|
|
1263
1299
|
};
|
|
1264
1300
|
}
|
|
1265
1301
|
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
raw?.event === "item.failed" ||
|
|
1269
|
-
payload.event === "item.completed" ||
|
|
1270
|
-
payload.event === "item.failed"
|
|
1271
|
-
) {
|
|
1272
|
-
const inner =
|
|
1273
|
-
raw?.event === "item.completed" || raw?.event === "item.failed"
|
|
1274
|
-
? payload
|
|
1275
|
-
: payload.payload && typeof payload.payload === "object"
|
|
1276
|
-
? payload.payload
|
|
1277
|
-
: {};
|
|
1302
|
+
if (event === "item.completed" || event === "item.failed") {
|
|
1303
|
+
const inner = innerPayload ?? {};
|
|
1278
1304
|
const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
|
|
1279
1305
|
const result =
|
|
1280
1306
|
item?.output ??
|
|
@@ -1295,13 +1321,35 @@ function extractDeepseekToolResult(raw: any): { name?: string; result: string; i
|
|
|
1295
1321
|
stringField(inner, "name") ??
|
|
1296
1322
|
stringField(item, "type"),
|
|
1297
1323
|
result: stringifyToolResult(result),
|
|
1298
|
-
id: stringField(item, "id") ?? stringField(inner, "id"),
|
|
1324
|
+
id: stringField(item, "id") ?? stringField(inner, "id") ?? stringField(payload, "item_id"),
|
|
1299
1325
|
};
|
|
1300
1326
|
}
|
|
1301
1327
|
|
|
1302
1328
|
return null;
|
|
1303
1329
|
}
|
|
1304
1330
|
|
|
1331
|
+
function unwrapDeepseekPayload(raw: any): any {
|
|
1332
|
+
const payload = raw?.payload;
|
|
1333
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
1334
|
+
const nested = payload.payload;
|
|
1335
|
+
if (nested && typeof nested === "object") {
|
|
1336
|
+
const outerEvent = stringField(payload, "event");
|
|
1337
|
+
if (
|
|
1338
|
+
outerEvent ||
|
|
1339
|
+
nested.item ||
|
|
1340
|
+
nested.tool ||
|
|
1341
|
+
nested.turn ||
|
|
1342
|
+
nested.kind ||
|
|
1343
|
+
nested.output ||
|
|
1344
|
+
nested.result ||
|
|
1345
|
+
nested.error
|
|
1346
|
+
) {
|
|
1347
|
+
return nested;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return payload;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1305
1353
|
function formatBlockDetails(raw: unknown): string {
|
|
1306
1354
|
if (!raw || typeof raw !== "object") return "";
|
|
1307
1355
|
const r = raw as any;
|
|
@@ -497,9 +497,15 @@ function isDeepseekTerminalEvent(eventName: string, payload: any): boolean {
|
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
function isToolStarted(eventName: string, payload: any): boolean {
|
|
500
|
+
const itemKind = payload?.payload?.item?.kind ?? payload?.item?.kind;
|
|
500
501
|
return (
|
|
501
502
|
(eventName === "item.started" &&
|
|
502
|
-
(
|
|
503
|
+
(
|
|
504
|
+
!!payload?.tool ||
|
|
505
|
+
itemKind === "tool_call" ||
|
|
506
|
+
itemKind === "command_execution" ||
|
|
507
|
+
itemKind === "file_change"
|
|
508
|
+
)) ||
|
|
503
509
|
(payload?.event === "item.started" && !!payload?.payload?.tool)
|
|
504
510
|
);
|
|
505
511
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type RouteRuleMatch,
|
|
18
18
|
} from "./config.js";
|
|
19
19
|
import {
|
|
20
|
+
acquireDaemonSingletonLock,
|
|
20
21
|
ensureNoOtherDaemonFromPidFile,
|
|
21
22
|
findOtherDaemonProcesses,
|
|
22
23
|
pidAlive,
|
|
@@ -625,13 +626,22 @@ async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
|
625
626
|
}
|
|
626
627
|
|
|
627
628
|
// Foreground: we ARE the daemon.
|
|
629
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
628
630
|
writeCurrentPid();
|
|
629
|
-
|
|
631
|
+
let handle: Awaited<ReturnType<typeof startDaemon>>;
|
|
632
|
+
try {
|
|
633
|
+
handle = await startDaemon({ config: cfg, configPath: CONFIG_FILE_PATH });
|
|
634
|
+
} catch (err) {
|
|
635
|
+
removePidFile();
|
|
636
|
+
singletonLock.release();
|
|
637
|
+
throw err;
|
|
638
|
+
}
|
|
630
639
|
|
|
631
640
|
const shutdown = async (sig: string) => {
|
|
632
641
|
log.info("signal received", { sig });
|
|
633
642
|
await handle.stop(sig);
|
|
634
643
|
removePidFile();
|
|
644
|
+
singletonLock.release();
|
|
635
645
|
process.exit(0);
|
|
636
646
|
};
|
|
637
647
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -661,6 +671,7 @@ async function cmdStartCloud(_args: ParsedArgs): Promise<void> {
|
|
|
661
671
|
daemonInstanceId: cloudConfig.daemonInstanceId,
|
|
662
672
|
hubUrl: cloudConfig.hubUrl,
|
|
663
673
|
});
|
|
674
|
+
const singletonLock = await acquireDaemonSingletonLock({ logger: log });
|
|
664
675
|
await stopDaemonFromPidFileForRestart({ logger: log });
|
|
665
676
|
await stopOtherDaemonProcessesForRestart({ logger: log });
|
|
666
677
|
writeCurrentPid();
|
|
@@ -676,16 +687,24 @@ async function cmdStartCloud(_args: ParsedArgs): Promise<void> {
|
|
|
676
687
|
saveConfig(cfg);
|
|
677
688
|
log.info("cloud mode config initialized", { configPath: CONFIG_FILE_PATH });
|
|
678
689
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
690
|
+
let handle: Awaited<ReturnType<typeof startCloudDaemon>>;
|
|
691
|
+
try {
|
|
692
|
+
handle = await startCloudDaemon({
|
|
693
|
+
cloudConfig,
|
|
694
|
+
config: cfg,
|
|
695
|
+
configPath: CONFIG_FILE_PATH,
|
|
696
|
+
});
|
|
697
|
+
} catch (err) {
|
|
698
|
+
removePidFile();
|
|
699
|
+
singletonLock.release();
|
|
700
|
+
throw err;
|
|
701
|
+
}
|
|
684
702
|
|
|
685
703
|
const shutdown = async (sig: string): Promise<void> => {
|
|
686
704
|
log.info("signal received", { sig });
|
|
687
705
|
await handle.stop(sig);
|
|
688
706
|
removePidFile();
|
|
707
|
+
singletonLock.release();
|
|
689
708
|
process.exit(0);
|
|
690
709
|
};
|
|
691
710
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
package/src/provision.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
defaultCredentialsFile,
|
|
14
14
|
derivePublicKey,
|
|
15
15
|
loadStoredCredentials,
|
|
16
|
+
normalizeTokenExpiresAt,
|
|
16
17
|
writeCredentialsFile,
|
|
17
18
|
type AgentIdentitySnapshot,
|
|
18
19
|
type ControlAck,
|
|
@@ -74,6 +75,7 @@ import { log as daemonLog } from "./log.js";
|
|
|
74
75
|
import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
75
76
|
import { resolveMemoryDir } from "./working-memory.js";
|
|
76
77
|
import { discoverRuntimeModelCatalog } from "./runtime-models.js";
|
|
78
|
+
import { collectAgentSkillSnapshot } from "./skill-index.js";
|
|
77
79
|
import {
|
|
78
80
|
buildRuntimeSelectionExtraArgs,
|
|
79
81
|
mergeRuntimeExtraArgs,
|
|
@@ -83,6 +85,10 @@ import {
|
|
|
83
85
|
type CloudGatewayTypingEmitter,
|
|
84
86
|
} from "./cloud-gateway-runtime.js";
|
|
85
87
|
|
|
88
|
+
interface ListAgentSkillsParams {
|
|
89
|
+
agentId: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
/**
|
|
87
93
|
* Information passed to {@link OnAgentInstalledHook} after a successful
|
|
88
94
|
* provision. Mirrors the credential fields the daemon's per-agent caches
|
|
@@ -486,6 +492,32 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
486
492
|
return { ok: true, result };
|
|
487
493
|
}
|
|
488
494
|
|
|
495
|
+
case "list_agent_skills": {
|
|
496
|
+
const params = (frame.params ?? {}) as unknown as ListAgentSkillsParams;
|
|
497
|
+
if (!params.agentId) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
error: { code: "bad_params", message: "list_agent_skills requires params.agentId" },
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const channels = gateway.snapshot().channels;
|
|
504
|
+
if (!channels[params.agentId]) {
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
error: {
|
|
508
|
+
code: "agent_not_loaded",
|
|
509
|
+
message: `agent ${params.agentId} is not loaded in daemon gateway`,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const result = collectAgentSkillSnapshot(params.agentId);
|
|
514
|
+
daemonLog.debug("list_agent_skills", {
|
|
515
|
+
agentId: params.agentId,
|
|
516
|
+
count: result.skills.length,
|
|
517
|
+
});
|
|
518
|
+
return { ok: true, result };
|
|
519
|
+
}
|
|
520
|
+
|
|
489
521
|
case "wake_agent": {
|
|
490
522
|
return handleWakeAgent(gateway, frame.params);
|
|
491
523
|
}
|
|
@@ -1227,7 +1259,8 @@ async function materializeCredentials(
|
|
|
1227
1259
|
};
|
|
1228
1260
|
if (c.displayName) record.displayName = c.displayName;
|
|
1229
1261
|
if (c.token) record.token = c.token;
|
|
1230
|
-
|
|
1262
|
+
const tokenExpiresAt = normalizeTokenExpiresAt(c.tokenExpiresAt);
|
|
1263
|
+
if (tokenExpiresAt !== undefined) record.tokenExpiresAt = tokenExpiresAt;
|
|
1231
1264
|
if (runtime) record.runtime = runtime;
|
|
1232
1265
|
const runtimeSelection = pickRuntimeSelection(params);
|
|
1233
1266
|
if (runtimeSelection.runtimeModel) record.runtimeModel = runtimeSelection.runtimeModel;
|