@botcord/daemon 0.2.27 → 0.2.28
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/dist/agent-discovery.d.ts +2 -0
- package/dist/agent-discovery.js +2 -0
- package/dist/agent-workspace.d.ts +3 -1
- package/dist/agent-workspace.js +10 -2
- package/dist/daemon-config-map.d.ts +2 -0
- package/dist/daemon-config-map.js +3 -0
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +2 -1
- package/dist/gateway/dispatcher.js +1 -0
- package/dist/gateway/runtimes/hermes-agent.d.ts +35 -0
- package/dist/gateway/runtimes/hermes-agent.js +130 -3
- package/dist/gateway/runtimes/openclaw-acp.d.ts +6 -3
- package/dist/gateway/runtimes/openclaw-acp.js +75 -9
- package/dist/gateway/types.d.ts +17 -0
- package/dist/openclaw-discovery.d.ts +3 -1
- package/dist/openclaw-discovery.js +176 -2
- package/dist/provision.d.ts +12 -8
- package/dist/provision.js +194 -3
- package/package.json +1 -1
- package/src/__tests__/openclaw-acp.test.ts +172 -0
- package/src/__tests__/openclaw-discovery.test.ts +64 -0
- package/src/__tests__/provision.test.ts +159 -0
- package/src/agent-discovery.ts +3 -0
- package/src/agent-workspace.ts +13 -2
- package/src/daemon-config-map.ts +5 -0
- package/src/daemon.ts +3 -2
- package/src/gateway/__tests__/hermes-agent-adapter.test.ts +87 -0
- package/src/gateway/dispatcher.ts +1 -0
- package/src/gateway/runtimes/hermes-agent.ts +146 -3
- package/src/gateway/runtimes/openclaw-acp.ts +82 -9
- package/src/gateway/types.ts +17 -0
- package/src/openclaw-discovery.ts +180 -3
- package/src/provision.ts +217 -6
|
@@ -28,6 +28,8 @@ export interface DiscoveredAgentCredential {
|
|
|
28
28
|
openclawGateway?: string;
|
|
29
29
|
/** OpenClaw agent profile override from credentials. */
|
|
30
30
|
openclawAgent?: string;
|
|
31
|
+
/** Hermes profile name from credentials (only meaningful for hermes-agent). */
|
|
32
|
+
hermesProfile?: string;
|
|
31
33
|
/** Key id from the credentials file — surfaced so boot-time workspace
|
|
32
34
|
* seeding (see daemon-agent-workspace-plan.md §9) can render identity.md
|
|
33
35
|
* without re-reading the file. */
|
package/dist/agent-discovery.js
CHANGED
|
@@ -102,6 +102,8 @@ export function discoverAgentCredentials(opts = {}) {
|
|
|
102
102
|
entry.openclawGateway = creds.openclawGateway;
|
|
103
103
|
if (creds.openclawAgent)
|
|
104
104
|
entry.openclawAgent = creds.openclawAgent;
|
|
105
|
+
if (creds.hermesProfile)
|
|
106
|
+
entry.hermesProfile = creds.hermesProfile;
|
|
105
107
|
if (creds.keyId)
|
|
106
108
|
entry.keyId = creds.keyId;
|
|
107
109
|
if (creds.savedAt)
|
|
@@ -48,7 +48,9 @@ export declare function ensureAgentCodexHome(agentId: string): string;
|
|
|
48
48
|
* loader (which only sees this isolated HERMES_HOME, not `~/.hermes`)
|
|
49
49
|
* can discover them.
|
|
50
50
|
*/
|
|
51
|
-
export declare function ensureAgentHermesWorkspace(agentId: string
|
|
51
|
+
export declare function ensureAgentHermesWorkspace(agentId: string, opts?: {
|
|
52
|
+
attached?: boolean;
|
|
53
|
+
}): {
|
|
52
54
|
hermesHome: string;
|
|
53
55
|
hermesWorkspace: string;
|
|
54
56
|
};
|
package/dist/agent-workspace.js
CHANGED
|
@@ -327,11 +327,19 @@ export function ensureAgentCodexHome(agentId) {
|
|
|
327
327
|
* loader (which only sees this isolated HERMES_HOME, not `~/.hermes`)
|
|
328
328
|
* can discover them.
|
|
329
329
|
*/
|
|
330
|
-
export function ensureAgentHermesWorkspace(agentId) {
|
|
330
|
+
export function ensureAgentHermesWorkspace(agentId, opts = {}) {
|
|
331
331
|
const hermesHome = agentHermesHomeDir(agentId);
|
|
332
332
|
const hermesWorkspace = agentHermesWorkspaceDir(agentId);
|
|
333
|
-
mkdirTolerant(hermesHome);
|
|
334
333
|
mkdirTolerant(hermesWorkspace);
|
|
334
|
+
// Attach mode: HERMES_HOME points at the user's `~/.hermes/profiles/<n>/`
|
|
335
|
+
// so we MUST NOT touch the per-agent isolated home. The cwd
|
|
336
|
+
// (`hermesWorkspace`) is still ours and `prepareTurn` writes AGENTS.md
|
|
337
|
+
// there — that's the only thing the daemon is allowed to author when
|
|
338
|
+
// attached to a user-owned profile.
|
|
339
|
+
if (opts.attached) {
|
|
340
|
+
return { hermesHome, hermesWorkspace };
|
|
341
|
+
}
|
|
342
|
+
mkdirTolerant(hermesHome);
|
|
335
343
|
writeIfMissing(path.join(hermesHome, ".env"), "# hermes-agent environment overrides for this BotCord agent.\n" +
|
|
336
344
|
"# Add e.g. HERMES_INFERENCE_PROVIDER=openrouter, OPENROUTER_API_KEY=...\n");
|
|
337
345
|
seedHermesConfig(hermesHome);
|
|
@@ -8,6 +8,8 @@ export interface AgentRuntimeMeta {
|
|
|
8
8
|
openclawGateway?: string;
|
|
9
9
|
/** Optional override of the OpenClaw agent profile within the gateway. */
|
|
10
10
|
openclawAgent?: string;
|
|
11
|
+
/** Hermes profile name to attach to (`runtime === "hermes-agent"` only). */
|
|
12
|
+
hermesProfile?: string;
|
|
11
13
|
}
|
|
12
14
|
/** Profile + tokenFile-resolved bearer token. Exported so other module-boundary
|
|
13
15
|
* paths (runtime probing, post-provision hot-add) reuse the same resolver
|
|
@@ -245,6 +245,9 @@ export function buildManagedRoutes(agentIds, agentRuntimes, defaultRoute, opencl
|
|
|
245
245
|
}
|
|
246
246
|
route.gateway = resolved;
|
|
247
247
|
}
|
|
248
|
+
if (runtime === "hermes-agent" && meta.hermesProfile) {
|
|
249
|
+
route.hermesProfile = meta.hermesProfile;
|
|
250
|
+
}
|
|
248
251
|
out.set(agentId, route);
|
|
249
252
|
}
|
|
250
253
|
return out;
|
package/dist/daemon.d.ts
CHANGED
package/dist/daemon.js
CHANGED
|
@@ -425,12 +425,13 @@ export function backfillBootAgents(agents, opts) {
|
|
|
425
425
|
for (const a of agents) {
|
|
426
426
|
if (a.credentialsFile)
|
|
427
427
|
credentialPathByAgentId.set(a.agentId, a.credentialsFile);
|
|
428
|
-
if (a.runtime || a.cwd || a.openclawGateway || a.openclawAgent) {
|
|
428
|
+
if (a.runtime || a.cwd || a.openclawGateway || a.openclawAgent || a.hermesProfile) {
|
|
429
429
|
agentRuntimes[a.agentId] = {
|
|
430
430
|
...(a.runtime ? { runtime: a.runtime } : {}),
|
|
431
431
|
...(a.cwd ? { cwd: a.cwd } : {}),
|
|
432
432
|
...(a.openclawGateway ? { openclawGateway: a.openclawGateway } : {}),
|
|
433
433
|
...(a.openclawAgent ? { openclawAgent: a.openclawAgent } : {}),
|
|
434
|
+
...(a.hermesProfile ? { hermesProfile: a.hermesProfile } : {}),
|
|
434
435
|
};
|
|
435
436
|
}
|
|
436
437
|
// Seed files are written only when missing (see `ensureAgentWorkspace`),
|
|
@@ -9,6 +9,41 @@ import type { RuntimeProbeResult, RuntimeRunOptions } from "../types.js";
|
|
|
9
9
|
export declare function resolveHermesAcpCommand(deps?: ProbeDeps): string | null;
|
|
10
10
|
/** Probe whether `hermes-acp` is installed and report its version. */
|
|
11
11
|
export declare function probeHermesAgent(deps?: ProbeDeps): RuntimeProbeResult;
|
|
12
|
+
/**
|
|
13
|
+
* Discovered hermes profile entry (daemon-side shape; wire shape lives in
|
|
14
|
+
* protocol-core's `HermesProfileProbe`). Occupancy is filled in later by
|
|
15
|
+
* `provision.ts` from local credentials, not here.
|
|
16
|
+
*/
|
|
17
|
+
export interface HermesProfileInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
home: string;
|
|
20
|
+
isDefault?: boolean;
|
|
21
|
+
isActive?: boolean;
|
|
22
|
+
modelName?: string;
|
|
23
|
+
sessionsCount?: number;
|
|
24
|
+
hasSoul?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the hermes root (`~/.hermes`) — this is the location of the
|
|
28
|
+
* synthetic `default` profile per upstream's "default profile = HERMES_HOME
|
|
29
|
+
* itself" convention (`hermes_cli/profiles.py:8`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function hermesRootDir(): string;
|
|
32
|
+
export declare function isValidHermesProfileName(name: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a hermes profile's HERMES_HOME directory. `default` maps to
|
|
35
|
+
* `~/.hermes`; all other names map to `~/.hermes/profiles/<name>`. Mirrors
|
|
36
|
+
* `hermes_cli/profiles.py:get_profile_dir`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function hermesProfileHomeDir(name: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Enumerate available hermes profiles on this device. Pure local filesystem
|
|
41
|
+
* scan — does not invoke any hermes binary. Returns the synthetic `default`
|
|
42
|
+
* entry first when `~/.hermes` exists (which it should, given that the probe
|
|
43
|
+
* already located `hermes-acp`); each `~/.hermes/profiles/<name>/` directory
|
|
44
|
+
* follows.
|
|
45
|
+
*/
|
|
46
|
+
export declare function listHermesProfiles(): HermesProfileInfo[];
|
|
12
47
|
/**
|
|
13
48
|
* Hermes Agent adapter. Drives `hermes-acp` (the ACP stdio adapter shipped
|
|
14
49
|
* with `pip install "hermes-agent[acp]"`).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { agentHermesHomeDir, agentHermesWorkspaceDir, ensureAgentHermesWorkspace, } from "../../agent-workspace.js";
|
|
4
5
|
import { buildCliEnv } from "../cli-resolver.js";
|
|
@@ -43,6 +44,122 @@ export function probeHermesAgent(deps = {}) {
|
|
|
43
44
|
version: readCommandVersion(command, [], deps) ?? undefined,
|
|
44
45
|
};
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the hermes root (`~/.hermes`) — this is the location of the
|
|
49
|
+
* synthetic `default` profile per upstream's "default profile = HERMES_HOME
|
|
50
|
+
* itself" convention (`hermes_cli/profiles.py:8`).
|
|
51
|
+
*/
|
|
52
|
+
export function hermesRootDir() {
|
|
53
|
+
return path.join(homedir(), ".hermes");
|
|
54
|
+
}
|
|
55
|
+
/** Profile-name shape mirrors `hermes_cli/profiles.py:_PROFILE_ID_RE`. */
|
|
56
|
+
const HERMES_PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
57
|
+
export function isValidHermesProfileName(name) {
|
|
58
|
+
return name === "default" || HERMES_PROFILE_NAME_RE.test(name);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a hermes profile's HERMES_HOME directory. `default` maps to
|
|
62
|
+
* `~/.hermes`; all other names map to `~/.hermes/profiles/<name>`. Mirrors
|
|
63
|
+
* `hermes_cli/profiles.py:get_profile_dir`.
|
|
64
|
+
*/
|
|
65
|
+
export function hermesProfileHomeDir(name) {
|
|
66
|
+
if (!isValidHermesProfileName(name)) {
|
|
67
|
+
throw new Error(`Invalid hermes profile name: ${name}`);
|
|
68
|
+
}
|
|
69
|
+
if (name === "default")
|
|
70
|
+
return hermesRootDir();
|
|
71
|
+
return path.join(hermesRootDir(), "profiles", name);
|
|
72
|
+
}
|
|
73
|
+
function readActiveProfileName() {
|
|
74
|
+
try {
|
|
75
|
+
const raw = readFileSync(path.join(hermesRootDir(), "active_profile"), "utf8").trim();
|
|
76
|
+
return raw || "default";
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return "default";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function readProfileModelName(profileHome) {
|
|
83
|
+
try {
|
|
84
|
+
const raw = readFileSync(path.join(profileHome, "config.yaml"), "utf8");
|
|
85
|
+
// Cheap surface-level YAML peek — config.yaml's first block is
|
|
86
|
+
// `model:\n default: <name>`. Avoid pulling in a YAML dependency for
|
|
87
|
+
// a single optional field.
|
|
88
|
+
const match = raw.match(/^model:\s*\n(?:[ \t]+[^\n]*\n)*?[ \t]+default:\s*([^\n#]+)/m);
|
|
89
|
+
if (!match)
|
|
90
|
+
return undefined;
|
|
91
|
+
return match[1].trim().replace(/^['"]|['"]$/g, "") || undefined;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function countSessions(profileHome) {
|
|
98
|
+
try {
|
|
99
|
+
const dir = path.join(profileHome, "sessions");
|
|
100
|
+
if (!existsSync(dir))
|
|
101
|
+
return 0;
|
|
102
|
+
return readdirSync(dir).filter((f) => f.endsWith(".jsonl")).length;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function hasSoul(profileHome) {
|
|
109
|
+
return existsSync(path.join(profileHome, "SOUL.md"));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Enumerate available hermes profiles on this device. Pure local filesystem
|
|
113
|
+
* scan — does not invoke any hermes binary. Returns the synthetic `default`
|
|
114
|
+
* entry first when `~/.hermes` exists (which it should, given that the probe
|
|
115
|
+
* already located `hermes-acp`); each `~/.hermes/profiles/<name>/` directory
|
|
116
|
+
* follows.
|
|
117
|
+
*/
|
|
118
|
+
export function listHermesProfiles() {
|
|
119
|
+
const out = [];
|
|
120
|
+
const root = hermesRootDir();
|
|
121
|
+
const active = readActiveProfileName();
|
|
122
|
+
if (existsSync(root)) {
|
|
123
|
+
out.push({
|
|
124
|
+
name: "default",
|
|
125
|
+
home: root,
|
|
126
|
+
isDefault: true,
|
|
127
|
+
isActive: active === "default",
|
|
128
|
+
modelName: readProfileModelName(root),
|
|
129
|
+
sessionsCount: countSessions(root),
|
|
130
|
+
hasSoul: hasSoul(root),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const profilesDir = path.join(root, "profiles");
|
|
134
|
+
let entries = [];
|
|
135
|
+
try {
|
|
136
|
+
entries = readdirSync(profilesDir);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
for (const name of entries) {
|
|
142
|
+
if (!HERMES_PROFILE_NAME_RE.test(name))
|
|
143
|
+
continue;
|
|
144
|
+
const home = path.join(profilesDir, name);
|
|
145
|
+
try {
|
|
146
|
+
if (!statSync(home).isDirectory())
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
out.push({
|
|
153
|
+
name,
|
|
154
|
+
home,
|
|
155
|
+
isActive: active === name,
|
|
156
|
+
modelName: readProfileModelName(home),
|
|
157
|
+
sessionsCount: countSessions(home),
|
|
158
|
+
hasSoul: hasSoul(home),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
46
163
|
/**
|
|
47
164
|
* Hermes Agent adapter. Drives `hermes-acp` (the ACP stdio adapter shipped
|
|
48
165
|
* with `pip install "hermes-agent[acp]"`).
|
|
@@ -115,7 +232,15 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
115
232
|
// Route dangerous tool calls through ACP request_permission.
|
|
116
233
|
HERMES_INTERACTIVE: "1",
|
|
117
234
|
};
|
|
118
|
-
|
|
235
|
+
// Attach mode: BotCord agent shares a hermes profile (state.db /
|
|
236
|
+
// sessions / skills / .env) with the user's command-line `hermes`. In
|
|
237
|
+
// this mode we DO NOT seed a private home — the profile is wholly owned
|
|
238
|
+
// by the user, and AGENTS.md is written under the per-agent
|
|
239
|
+
// hermes-workspace cwd (NOT into the profile root) by `prepareTurn`.
|
|
240
|
+
if (opts.hermesProfile) {
|
|
241
|
+
env.HERMES_HOME = hermesProfileHomeDir(opts.hermesProfile);
|
|
242
|
+
}
|
|
243
|
+
else if (opts.accountId) {
|
|
119
244
|
env.HERMES_HOME = agentHermesHomeDir(opts.accountId);
|
|
120
245
|
}
|
|
121
246
|
return env;
|
|
@@ -134,7 +259,9 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
134
259
|
prepareTurn(opts) {
|
|
135
260
|
if (!opts.accountId)
|
|
136
261
|
return;
|
|
137
|
-
const { hermesWorkspace } = ensureAgentHermesWorkspace(opts.accountId
|
|
262
|
+
const { hermesWorkspace } = ensureAgentHermesWorkspace(opts.accountId, {
|
|
263
|
+
attached: !!opts.hermesProfile,
|
|
264
|
+
});
|
|
138
265
|
const target = path.join(hermesWorkspace, "AGENTS.md");
|
|
139
266
|
const tmp = path.join(hermesWorkspace, `.AGENTS.md.${process.pid}.tmp`);
|
|
140
267
|
mkdirSync(hermesWorkspace, { recursive: true, mode: 0o700 });
|
|
@@ -12,9 +12,11 @@ interface SpawnDeps {
|
|
|
12
12
|
*
|
|
13
13
|
* Spawns `openclaw acp --url <gateway> [--token <token>]` per
|
|
14
14
|
* `(accountId, gatewayName)` pair and reuses the process across turns. The
|
|
15
|
-
* child speaks JSON-RPC over stdio; we send `initialize` once, then
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* child speaks JSON-RPC over stdio; we send `initialize` once, then derive a
|
|
16
|
+
* stable OpenClaw `sessionKey` for the BotCord conversation. The persisted
|
|
17
|
+
* `runtimeSessionId` is only an ACP transport handle cached from a previous
|
|
18
|
+
* turn, so every resume first goes through `session/load` with
|
|
19
|
+
* `_meta.sessionKey` before `prompt`. Streaming `session/update`
|
|
18
20
|
* notifications are relayed to `onBlock`.
|
|
19
21
|
*
|
|
20
22
|
* Process-pool lifetime + abort/cancel semantics live at module scope; see
|
|
@@ -29,6 +31,7 @@ export declare class OpenclawAcpAdapter implements RuntimeAdapter {
|
|
|
29
31
|
private acquireHandle;
|
|
30
32
|
private spawnAcpProcess;
|
|
31
33
|
private newSession;
|
|
34
|
+
private loadSession;
|
|
32
35
|
private prompt;
|
|
33
36
|
}
|
|
34
37
|
/**
|
|
@@ -75,9 +75,11 @@ export function probeOpenclaw(deps = {}) {
|
|
|
75
75
|
*
|
|
76
76
|
* Spawns `openclaw acp --url <gateway> [--token <token>]` per
|
|
77
77
|
* `(accountId, gatewayName)` pair and reuses the process across turns. The
|
|
78
|
-
* child speaks JSON-RPC over stdio; we send `initialize` once, then
|
|
79
|
-
*
|
|
80
|
-
*
|
|
78
|
+
* child speaks JSON-RPC over stdio; we send `initialize` once, then derive a
|
|
79
|
+
* stable OpenClaw `sessionKey` for the BotCord conversation. The persisted
|
|
80
|
+
* `runtimeSessionId` is only an ACP transport handle cached from a previous
|
|
81
|
+
* turn, so every resume first goes through `session/load` with
|
|
82
|
+
* `_meta.sessionKey` before `prompt`. Streaming `session/update`
|
|
81
83
|
* notifications are relayed to `onBlock`.
|
|
82
84
|
*
|
|
83
85
|
* Process-pool lifetime + abort/cancel semantics live at module scope; see
|
|
@@ -117,6 +119,8 @@ export class OpenclawAcpAdapter {
|
|
|
117
119
|
handle.inFlight += 1;
|
|
118
120
|
if (handle.idleTimer)
|
|
119
121
|
clearTimeout(handle.idleTimer);
|
|
122
|
+
// ACP session ids are process-local transport handles. They are useful as
|
|
123
|
+
// a cache, but the stable conversation identity is `sessionKey`.
|
|
120
124
|
let acpSessionId = opts.sessionId ?? "";
|
|
121
125
|
let seq = 0;
|
|
122
126
|
let assistantText = "";
|
|
@@ -155,8 +159,28 @@ export class OpenclawAcpAdapter {
|
|
|
155
159
|
};
|
|
156
160
|
let abortListener;
|
|
157
161
|
try {
|
|
158
|
-
// Ensure we have
|
|
159
|
-
//
|
|
162
|
+
// Ensure we have a live ACP transport session. If the dispatcher passes a
|
|
163
|
+
// cached session id, ask OpenClaw to load/rebind it with the stable
|
|
164
|
+
// sessionKey. If that handle is gone, discard it and create a fresh one.
|
|
165
|
+
if (acpSessionId) {
|
|
166
|
+
try {
|
|
167
|
+
acpSessionId = await this.loadSession(handle, {
|
|
168
|
+
sessionId: acpSessionId,
|
|
169
|
+
cwd: opts.cwd,
|
|
170
|
+
sessionKey,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
if (!isSessionNotFoundError(err))
|
|
175
|
+
throw err;
|
|
176
|
+
log.warn("openclaw-acp.session-load-not-found", {
|
|
177
|
+
accountId: opts.accountId,
|
|
178
|
+
oldSessionId: acpSessionId,
|
|
179
|
+
sessionKey,
|
|
180
|
+
});
|
|
181
|
+
acpSessionId = "";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
160
184
|
if (!acpSessionId) {
|
|
161
185
|
try {
|
|
162
186
|
acpSessionId = await this.newSession(handle, {
|
|
@@ -185,11 +209,16 @@ export class OpenclawAcpAdapter {
|
|
|
185
209
|
});
|
|
186
210
|
}
|
|
187
211
|
catch (err) {
|
|
188
|
-
const msg = err.message ?? "prompt failed";
|
|
189
212
|
// If the child says the session is gone (process restart, GC),
|
|
190
213
|
// recreate it so the next turn doesn't hard-fail.
|
|
191
|
-
if (
|
|
214
|
+
if (isSessionNotFoundError(err)) {
|
|
192
215
|
try {
|
|
216
|
+
const oldSessionId = acpSessionId;
|
|
217
|
+
log.warn("openclaw-acp.prompt-session-not-found-retry", {
|
|
218
|
+
accountId: opts.accountId,
|
|
219
|
+
oldSessionId,
|
|
220
|
+
sessionKey,
|
|
221
|
+
});
|
|
193
222
|
const fresh = await this.newSession(handle, {
|
|
194
223
|
cwd: opts.cwd,
|
|
195
224
|
sessionKey,
|
|
@@ -197,6 +226,12 @@ export class OpenclawAcpAdapter {
|
|
|
197
226
|
handle.subscribers.delete(acpSessionId);
|
|
198
227
|
acpSessionId = fresh;
|
|
199
228
|
handle.subscribers.set(acpSessionId, onNotification);
|
|
229
|
+
log.info("openclaw-acp.session-recreated", {
|
|
230
|
+
accountId: opts.accountId,
|
|
231
|
+
oldSessionId,
|
|
232
|
+
newSessionId: acpSessionId,
|
|
233
|
+
sessionKey,
|
|
234
|
+
});
|
|
200
235
|
promptResult = await this.prompt(handle, {
|
|
201
236
|
sessionId: acpSessionId,
|
|
202
237
|
text: opts.text,
|
|
@@ -223,7 +258,8 @@ export class OpenclawAcpAdapter {
|
|
|
223
258
|
};
|
|
224
259
|
}
|
|
225
260
|
catch (err) {
|
|
226
|
-
|
|
261
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
+
return failResult(isSessionNotFoundError(err) ? "" : acpSessionId, `openclaw-acp: ${message}`);
|
|
227
263
|
}
|
|
228
264
|
finally {
|
|
229
265
|
if (abortListener && opts.signal) {
|
|
@@ -332,6 +368,18 @@ export class OpenclawAcpAdapter {
|
|
|
332
368
|
}
|
|
333
369
|
return result.sessionId;
|
|
334
370
|
}
|
|
371
|
+
async loadSession(handle, args) {
|
|
372
|
+
const result = (await sendRequest(handle, "session/load", {
|
|
373
|
+
sessionId: args.sessionId,
|
|
374
|
+
cwd: args.cwd,
|
|
375
|
+
mcpServers: [],
|
|
376
|
+
_meta: { sessionKey: args.sessionKey },
|
|
377
|
+
}));
|
|
378
|
+
if (result?.sessionId && typeof result.sessionId === "string") {
|
|
379
|
+
return result.sessionId;
|
|
380
|
+
}
|
|
381
|
+
return args.sessionId;
|
|
382
|
+
}
|
|
335
383
|
async prompt(handle, args) {
|
|
336
384
|
return sendRequest(handle, "session/prompt", {
|
|
337
385
|
sessionId: args.sessionId,
|
|
@@ -372,7 +420,7 @@ function routeMessage(handle, msg) {
|
|
|
372
420
|
return;
|
|
373
421
|
handle.pending.delete(id);
|
|
374
422
|
if (msg.error) {
|
|
375
|
-
const message =
|
|
423
|
+
const message = formatRpcError(msg.error);
|
|
376
424
|
pending.reject(new Error(message));
|
|
377
425
|
}
|
|
378
426
|
else {
|
|
@@ -435,6 +483,24 @@ function failResult(sessionId, error) {
|
|
|
435
483
|
error,
|
|
436
484
|
};
|
|
437
485
|
}
|
|
486
|
+
function formatRpcError(error) {
|
|
487
|
+
if (!error || typeof error !== "object")
|
|
488
|
+
return "rpc error";
|
|
489
|
+
const e = error;
|
|
490
|
+
const message = typeof e.message === "string" ? e.message : "rpc error";
|
|
491
|
+
const data = e.data;
|
|
492
|
+
if (data && typeof data === "object") {
|
|
493
|
+
const details = data.details;
|
|
494
|
+
if (typeof details === "string" && details.length > 0) {
|
|
495
|
+
return `${message}: ${details}`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return message;
|
|
499
|
+
}
|
|
500
|
+
function isSessionNotFoundError(err) {
|
|
501
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
502
|
+
return /session(?:\s+[\w-]+)?\s+not\s+found|unknown\s+session/i.test(msg);
|
|
503
|
+
}
|
|
438
504
|
function classifyAcpUpdate(note) {
|
|
439
505
|
const update = note.params?.update;
|
|
440
506
|
const kind = update?.sessionUpdate;
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ export interface GatewayRoute {
|
|
|
36
36
|
trustLevel?: TrustLevel;
|
|
37
37
|
/** Required when `runtime === "openclaw-acp"`. Resolved at config-load time. */
|
|
38
38
|
gateway?: ResolvedOpenclawGateway;
|
|
39
|
+
/**
|
|
40
|
+
* Hermes profile name to attach to. Set when `runtime === "hermes-agent"`
|
|
41
|
+
* and the agent is bound to a specific `~/.hermes/profiles/<name>/`. The
|
|
42
|
+
* dispatcher forwards this to the adapter as
|
|
43
|
+
* {@link RuntimeRunOptions.hermesProfile}, which is what the adapter uses
|
|
44
|
+
* to switch `HERMES_HOME` at spawn time.
|
|
45
|
+
*/
|
|
46
|
+
hermesProfile?: string;
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Per-channel configuration entry. Channel-specific extras (e.g. BotCord
|
|
@@ -306,6 +314,15 @@ export interface RuntimeRunOptions {
|
|
|
306
314
|
* lifting service URLs out of `extraArgs` into typed first-class fields.
|
|
307
315
|
*/
|
|
308
316
|
gateway?: ResolvedOpenclawGateway;
|
|
317
|
+
/**
|
|
318
|
+
* Hermes profile to attach to. Only meaningful when `runtime ===
|
|
319
|
+
* "hermes-agent"`. When set, the adapter switches
|
|
320
|
+
* `HERMES_HOME=~/.hermes/profiles/<name>/` (or `~/.hermes` for `default`)
|
|
321
|
+
* so the BotCord agent shares state.db / sessions / skills with the
|
|
322
|
+
* user's command-line `hermes`. Mirrors how `gateway` is lifted out of
|
|
323
|
+
* `extraArgs` for the openclaw-acp runtime.
|
|
324
|
+
*/
|
|
325
|
+
hermesProfile?: string;
|
|
309
326
|
}
|
|
310
327
|
/** Result returned by a runtime adapter after a turn completes. */
|
|
311
328
|
export interface RuntimeRunResult {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DaemonConfig, OpenclawGatewayProfile } from "./config.js";
|
|
2
2
|
import { type WsEndpointProbeFn } from "./provision.js";
|
|
3
|
-
export type DiscoveredOpenclawGatewaySource = "config-file" | "env" | "default-port";
|
|
3
|
+
export type DiscoveredOpenclawGatewaySource = "config-file" | "env" | "systemd-unit" | "default-port";
|
|
4
4
|
export interface DiscoveredOpenclawGateway {
|
|
5
5
|
name: string;
|
|
6
6
|
url: string;
|
|
@@ -14,6 +14,7 @@ export interface OpenclawGatewayDiscoveryOptions {
|
|
|
14
14
|
probe?: WsEndpointProbeFn;
|
|
15
15
|
timeoutMs?: number;
|
|
16
16
|
env?: NodeJS.ProcessEnv;
|
|
17
|
+
systemdUnitPaths?: string[];
|
|
17
18
|
}
|
|
18
19
|
export interface MergeOpenclawGatewayResult {
|
|
19
20
|
cfg: DaemonConfig;
|
|
@@ -25,5 +26,6 @@ export declare function mergeOpenclawGateways(cfg: DaemonConfig, found: Discover
|
|
|
25
26
|
export declare function defaultOpenclawDiscoverySearchPaths(): string[];
|
|
26
27
|
export declare function defaultOpenclawDiscoveryPorts(): number[];
|
|
27
28
|
export declare function defaultOpenclawDiscoveryTokenFilePaths(): string[];
|
|
29
|
+
export declare function defaultOpenclawDiscoverySystemdUnitPaths(): string[];
|
|
28
30
|
export declare function openclawDiscoveryConfigEnabled(cfg: DaemonConfig): boolean;
|
|
29
31
|
export declare function openclawAutoProvisionEnabled(cfg: DaemonConfig): boolean;
|