@openclawbrain/openclaw 0.3.0 → 0.3.2
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/README.md +3 -1
- package/dist/extension/index.js +9 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/runtime-guard.js +6 -1
- package/dist/extension/runtime-guard.js.map +1 -1
- package/dist/src/cli.d.ts +18 -6
- package/dist/src/cli.js +1991 -293
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +42 -1
- package/dist/src/daemon.js +360 -50
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/index.d.ts +65 -1
- package/dist/src/index.js +627 -56
- package/dist/src/index.js.map +1 -1
- package/dist/src/learning-spine.d.ts +3 -1
- package/dist/src/learning-spine.js +1 -0
- package/dist/src/learning-spine.js.map +1 -1
- package/dist/src/local-session-passive-learning.js +6 -1
- package/dist/src/local-session-passive-learning.js.map +1 -1
- package/dist/src/openclaw-home-layout.d.ts +17 -0
- package/dist/src/openclaw-home-layout.js +182 -0
- package/dist/src/openclaw-home-layout.js.map +1 -0
- package/dist/src/provider-config.d.ts +36 -0
- package/dist/src/provider-config.js +181 -25
- package/dist/src/provider-config.js.map +1 -1
- package/dist/src/resolve-activation-root.d.ts +3 -3
- package/dist/src/resolve-activation-root.js +21 -26
- package/dist/src/resolve-activation-root.js.map +1 -1
- package/dist/src/semantic-metadata.d.ts +4 -0
- package/dist/src/semantic-metadata.js +41 -0
- package/dist/src/semantic-metadata.js.map +1 -0
- package/dist/src/session-store.js +16 -5
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/session-tail.d.ts +2 -0
- package/dist/src/session-tail.js +68 -16
- package/dist/src/session-tail.js.map +1 -1
- package/dist/src/shadow-extension-proof.js +4 -0
- package/dist/src/shadow-extension-proof.js.map +1 -1
- package/extension/index.ts +17 -0
- package/extension/runtime-guard.ts +7 -1
- package/package.json +7 -7
package/dist/src/cli.js
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync, execSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync,
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, symlinkSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
8
|
-
import {
|
|
8
|
+
import { DEFAULT_OLLAMA_EMBEDDING_MODEL, createOllamaEmbedder } from "@openclawbrain/compiler";
|
|
9
|
+
import { ensureManagedLearnerServiceForActivationRoot, inspectManagedLearnerService, removeManagedLearnerServiceForActivationRoot, parseDaemonArgs, runDaemonCommand } from "./daemon.js";
|
|
9
10
|
import { exportBrain, importBrain } from "./import-export.js";
|
|
10
11
|
import { buildNormalizedEventExport } from "@openclawbrain/contracts";
|
|
11
|
-
import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
|
|
12
|
+
import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, reindexCandidatePackBuildResultWithEmbedder, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
|
|
12
13
|
import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
|
|
13
14
|
import { resolveActivationRoot } from "./resolve-activation-root.js";
|
|
14
|
-
import {
|
|
15
|
+
import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
|
|
16
|
+
import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
|
|
15
17
|
import { appendLearningUpdateLogs } from "./learning-spine.js";
|
|
16
18
|
import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
|
|
17
19
|
import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
18
|
-
import { readOpenClawBrainProviderConfig } from "./provider-config.js";
|
|
20
|
+
import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
|
|
21
|
+
const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
|
|
22
|
+
const OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV = "OPENCLAWBRAIN_EMBEDDER_PROVIDER";
|
|
23
|
+
const OPENCLAWBRAIN_EMBEDDER_MODEL_ENV = "OPENCLAWBRAIN_EMBEDDER_MODEL";
|
|
24
|
+
const OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV = "OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION";
|
|
25
|
+
const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
|
|
26
|
+
"qwen3.5:9b",
|
|
27
|
+
"qwen3.5:8b",
|
|
28
|
+
"qwen3:8b",
|
|
29
|
+
"qwen2.5:7b"
|
|
30
|
+
];
|
|
19
31
|
function quoteShellArg(value) {
|
|
20
32
|
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
21
33
|
}
|
|
@@ -26,23 +38,68 @@ function normalizeOptionalCliString(value) {
|
|
|
26
38
|
const trimmed = value.trim();
|
|
27
39
|
return trimmed.length > 0 ? trimmed : null;
|
|
28
40
|
}
|
|
41
|
+
function readTruthyEnvFlag(name, env = process.env) {
|
|
42
|
+
const value = normalizeOptionalCliString(env[name]);
|
|
43
|
+
if (value === null) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
47
|
+
}
|
|
29
48
|
function getCliHomeDir() {
|
|
30
49
|
return process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
31
50
|
}
|
|
32
51
|
function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
|
|
53
|
+
}
|
|
54
|
+
function findInstalledHookReferencesForActivationRoot(input) {
|
|
55
|
+
const resolvedActivationRoot = path.resolve(input.activationRoot);
|
|
56
|
+
const resolvedExcludedHome = input.excludingOpenClawHome === undefined || input.excludingOpenClawHome === null
|
|
57
|
+
? null
|
|
58
|
+
: path.resolve(input.excludingOpenClawHome);
|
|
59
|
+
return discoverOpenClawHomes(input.homeDir ?? getCliHomeDir())
|
|
60
|
+
.filter((inspection) => resolvedExcludedHome === null || path.resolve(inspection.openclawHome) !== resolvedExcludedHome)
|
|
61
|
+
.flatMap((inspection) => {
|
|
62
|
+
const installedActivationRoot = resolveActivationRoot({
|
|
63
|
+
openclawHome: inspection.openclawHome,
|
|
64
|
+
quiet: true
|
|
65
|
+
});
|
|
66
|
+
if (installedActivationRoot.trim().length === 0) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return path.resolve(installedActivationRoot) === resolvedActivationRoot
|
|
70
|
+
? [{ openclawHome: inspection.openclawHome, inspection }]
|
|
71
|
+
: [];
|
|
72
|
+
})
|
|
73
|
+
.sort((left, right) => left.openclawHome.localeCompare(right.openclawHome));
|
|
74
|
+
}
|
|
75
|
+
function findOtherInstalledHookReferencesForActivationRoot(input) {
|
|
76
|
+
return findInstalledHookReferencesForActivationRoot(input);
|
|
77
|
+
}
|
|
78
|
+
function resolveWatchProfileRootsForActivationRoot(activationRoot, homeDir = getCliHomeDir()) {
|
|
79
|
+
const attachedProfileRoots = findInstalledHookReferencesForActivationRoot({
|
|
80
|
+
activationRoot,
|
|
81
|
+
homeDir
|
|
82
|
+
}).map((reference) => path.resolve(reference.openclawHome));
|
|
83
|
+
return attachedProfileRoots.length > 0 ? attachedProfileRoots : undefined;
|
|
84
|
+
}
|
|
85
|
+
function assertActivationRootPurgeIsNotShared(input) {
|
|
86
|
+
const sharedReferences = findOtherInstalledHookReferencesForActivationRoot({
|
|
87
|
+
activationRoot: input.activationRoot,
|
|
88
|
+
excludingOpenClawHome: input.openclawHome
|
|
89
|
+
});
|
|
90
|
+
if (sharedReferences.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const attachedProfiles = sharedReferences
|
|
94
|
+
.map(({ openclawHome, inspection }) => ` - ${path.resolve(openclawHome)} (${describeOpenClawHomeInspection(inspection)})`)
|
|
95
|
+
.join("\n");
|
|
96
|
+
throw new Error([
|
|
97
|
+
`Refusing to purge activation root ${path.resolve(input.activationRoot)} because another installed OpenClaw profile still points at it.`,
|
|
98
|
+
"Other attached profiles:",
|
|
99
|
+
attachedProfiles,
|
|
100
|
+
"Use uninstall --keep-data or detach on this profile first, then remove the remaining profile hooks before purging shared brain data.",
|
|
101
|
+
"For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
|
|
102
|
+
].join("\n"));
|
|
46
103
|
}
|
|
47
104
|
function formatInstallOpenClawHomeSource(source) {
|
|
48
105
|
switch (source) {
|
|
@@ -50,8 +107,8 @@ function formatInstallOpenClawHomeSource(source) {
|
|
|
50
107
|
return "--openclaw-home";
|
|
51
108
|
case "env":
|
|
52
109
|
return "OPENCLAW_HOME";
|
|
53
|
-
case "
|
|
54
|
-
return "single discovered
|
|
110
|
+
case "discovered_single_home":
|
|
111
|
+
return "single discovered install target";
|
|
55
112
|
default:
|
|
56
113
|
return source;
|
|
57
114
|
}
|
|
@@ -75,24 +132,24 @@ function resolveInstallOpenClawHome(explicitOpenclawHome) {
|
|
|
75
132
|
if (discoveredHomes.length === 1) {
|
|
76
133
|
return {
|
|
77
134
|
openclawHome: path.resolve(discoveredHomes[0]),
|
|
78
|
-
openclawHomeSource: "
|
|
135
|
+
openclawHomeSource: "discovered_single_home"
|
|
79
136
|
};
|
|
80
137
|
}
|
|
81
138
|
if (discoveredHomes.length > 1) {
|
|
82
139
|
const installPrefix = detectConsumerSafeOperatorCliPrefix();
|
|
83
|
-
const targetChoices =
|
|
84
|
-
.map((
|
|
85
|
-
const resolvedCandidate = path.resolve(
|
|
86
|
-
return ` - ${resolvedCandidate}\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
|
|
140
|
+
const targetChoices = discoverOpenClawHomes()
|
|
141
|
+
.map((inspection) => {
|
|
142
|
+
const resolvedCandidate = path.resolve(inspection.openclawHome);
|
|
143
|
+
return ` - ${resolvedCandidate} (${describeOpenClawHomeInspection(inspection)})\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
|
|
87
144
|
})
|
|
88
145
|
.join("\n");
|
|
89
146
|
throw new Error([
|
|
90
|
-
"Refusing ambiguous
|
|
147
|
+
"Refusing ambiguous OpenClaw install targets.",
|
|
91
148
|
targetChoices,
|
|
92
|
-
"Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one
|
|
149
|
+
"Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one OpenClaw home."
|
|
93
150
|
].join("\n"));
|
|
94
151
|
}
|
|
95
|
-
throw new Error("No OpenClaw
|
|
152
|
+
throw new Error("No OpenClaw home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
|
|
96
153
|
}
|
|
97
154
|
function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
|
|
98
155
|
const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
|
|
@@ -115,18 +172,24 @@ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
|
115
172
|
source: "explicit"
|
|
116
173
|
};
|
|
117
174
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
175
|
+
const inspection = inspectOpenClawHome(openclawHome);
|
|
176
|
+
if (inspection.profileId !== null) {
|
|
177
|
+
return {
|
|
178
|
+
workspaceId: inspection.profileId,
|
|
179
|
+
source: inspection.profileSource === "directory_name"
|
|
180
|
+
? "openclaw_home_dir"
|
|
181
|
+
: inspection.profileSource === "openclaw_json_profile"
|
|
182
|
+
? "openclaw_json_profile"
|
|
183
|
+
: inspection.profileSource === "openclaw_json_single_profile_key"
|
|
184
|
+
? "openclaw_json_single_profile_key"
|
|
185
|
+
: "fallback"
|
|
186
|
+
};
|
|
127
187
|
}
|
|
128
|
-
|
|
129
|
-
|
|
188
|
+
if (inspection.layout === "shared_home_profiles_in_config" || inspection.layout === "single_openclaw_home") {
|
|
189
|
+
return {
|
|
190
|
+
workspaceId: "current_profile",
|
|
191
|
+
source: "current_profile_boundary"
|
|
192
|
+
};
|
|
130
193
|
}
|
|
131
194
|
const dirName = path.basename(openclawHome);
|
|
132
195
|
if (dirName === ".openclaw") {
|
|
@@ -147,6 +210,24 @@ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
|
147
210
|
source: "fallback"
|
|
148
211
|
};
|
|
149
212
|
}
|
|
213
|
+
function resolveInstallEmbedderProvisionSkip(explicitSkip) {
|
|
214
|
+
if (explicitSkip) {
|
|
215
|
+
return {
|
|
216
|
+
skipEmbedderProvision: true,
|
|
217
|
+
skipEmbedderProvisionSource: "flag"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (readTruthyEnvFlag(OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV)) {
|
|
221
|
+
return {
|
|
222
|
+
skipEmbedderProvision: true,
|
|
223
|
+
skipEmbedderProvisionSource: "env"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
skipEmbedderProvision: false,
|
|
228
|
+
skipEmbedderProvisionSource: null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
150
231
|
function formatInstallActivationRootSource(source) {
|
|
151
232
|
if (source === "explicit") {
|
|
152
233
|
return "explicit --activation-root";
|
|
@@ -159,6 +240,10 @@ function formatInstallWorkspaceIdSource(source) {
|
|
|
159
240
|
return "explicit --workspace-id";
|
|
160
241
|
case "openclaw_json_profile":
|
|
161
242
|
return "from openclaw.json profile";
|
|
243
|
+
case "openclaw_json_single_profile_key":
|
|
244
|
+
return "from the only openclaw.json profiles entry";
|
|
245
|
+
case "current_profile_boundary":
|
|
246
|
+
return "current_profile boundary for a shared OpenClaw home";
|
|
162
247
|
case "openclaw_home_dir":
|
|
163
248
|
return "from OpenClaw home dir";
|
|
164
249
|
default:
|
|
@@ -289,29 +374,29 @@ function operatorCliHelp() {
|
|
|
289
374
|
return [
|
|
290
375
|
"Usage:",
|
|
291
376
|
" openclawbrain install [--openclaw-home <path>] [options]",
|
|
377
|
+
" openclawbrain attach --openclaw-home <path> [options]",
|
|
378
|
+
" openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
|
|
379
|
+
" openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
|
|
380
|
+
" openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
|
|
292
381
|
" openclawbrain detach --openclaw-home <path> [options]",
|
|
293
382
|
" openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
|
|
294
|
-
" openclawbrain attach --activation-root <path> [options]",
|
|
295
|
-
" openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
|
|
296
383
|
" openclawbrain context \"message\" [--activation-root <path>|--openclaw-home <path>]",
|
|
297
384
|
" openclawbrain history [--activation-root <path>|--openclaw-home <path>] [--limit N] [--json]",
|
|
298
385
|
" openclawbrain scan --session <trace.json> --root <path> [options]",
|
|
299
386
|
" openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
|
|
300
387
|
" openclawbrain learn [--activation-root <path>|--openclaw-home <path>] [--json]",
|
|
301
|
-
" openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
|
|
302
|
-
" openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
|
|
303
388
|
" openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
|
|
304
389
|
" openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
|
|
305
390
|
"",
|
|
306
391
|
"Options:",
|
|
307
|
-
" --openclaw-home <path> OpenClaw
|
|
308
|
-
" --shared Set brain-attachment-policy to shared instead of dedicated (install only).",
|
|
309
|
-
|
|
392
|
+
" --openclaw-home <path> OpenClaw home dir for install/attach/detach/uninstall (e.g. ~/.openclaw-Tern or ~/.openclaw). Also pins status/rollback/context/history/learn to that installed target when applicable.",
|
|
393
|
+
" --shared Set brain-attachment-policy to shared instead of dedicated (install/attach only).",
|
|
394
|
+
` --skip-embedder-provision Skip the default Ollama ${DEFAULT_OLLAMA_EMBEDDING_MODEL} pull before install/attach bootstrap. Use only when intentionally deferring embedder setup. Also supports ${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1.`,
|
|
395
|
+
" --activation-root <path> Explicit activation root for attach/watch/daemon and other stateful commands; install/attach default to sibling .openclawbrain/activation next to the selected OpenClaw home.",
|
|
310
396
|
" --keep-data Preserve activation data on uninstall; detach always behaves this way.",
|
|
311
397
|
" --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
|
|
312
398
|
" --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
|
|
313
|
-
" --
|
|
314
|
-
" --workspace-id <id> Workspace identifier for attach/install provenance; install defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
|
|
399
|
+
" --workspace-id <id> Workspace identifier for install/attach provenance; defaults to the detected profile target from openclaw.json when possible, otherwise the profile name or current_profile boundary.",
|
|
315
400
|
" --event-export <path> Event-export bundle root or normalized export JSON payload.",
|
|
316
401
|
" --teacher-snapshot <path> Canonical watch teacher snapshot JSON or raw async teacher snapshot JSON; keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
|
|
317
402
|
" --updated-at <iso> Observation time to use for freshness checks.",
|
|
@@ -321,7 +406,7 @@ function operatorCliHelp() {
|
|
|
321
406
|
" --session <path> Sanitized recorded-session trace JSON to replay.",
|
|
322
407
|
" --live <path> Runtime event-export bundle root or normalized export JSON to scan once.",
|
|
323
408
|
" --root <path> Output root for scan --session replay artifacts.",
|
|
324
|
-
" --workspace <path> Workspace metadata JSON for scan --live candidate
|
|
409
|
+
" --workspace <path> Workspace metadata JSON for scan --live candidate materialization.",
|
|
325
410
|
" --pack-label <label> Candidate-pack label for scan --live. Defaults to scanner-live-cli.",
|
|
326
411
|
" --observed-at <iso> Observation time for scan --live freshness checks.",
|
|
327
412
|
" --snapshot-out <path> Write the one-shot scan --live snapshot JSON.",
|
|
@@ -331,25 +416,31 @@ function operatorCliHelp() {
|
|
|
331
416
|
" --json Emit machine-readable JSON instead of text.",
|
|
332
417
|
" --help Show this help.",
|
|
333
418
|
"",
|
|
334
|
-
"
|
|
335
|
-
"
|
|
336
|
-
"
|
|
337
|
-
"
|
|
338
|
-
"
|
|
339
|
-
"
|
|
340
|
-
"
|
|
341
|
-
"
|
|
342
|
-
"
|
|
343
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"
|
|
419
|
+
"Lifecycle flow:",
|
|
420
|
+
" 1. install openclawbrain install — safe first-time default; pass --openclaw-home when more than one OpenClaw home/layout is present",
|
|
421
|
+
" 2. attach openclawbrain attach --openclaw-home <path> [--activation-root <path>] — explicit reattach/manual hook path for known brain data; use install first",
|
|
422
|
+
" 3. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
|
|
423
|
+
" 4. status --detailed openclawbrain status --activation-root <path> --detailed — explain serve path, freshness, backlog, and failure mode",
|
|
424
|
+
" 5. watch openclawbrain watch --activation-root <path> — run the foreground learning/watch loop",
|
|
425
|
+
" 6. daemon start openclawbrain daemon start --activation-root <path> — keep watch running in the background on macOS",
|
|
426
|
+
" 7. daemon status openclawbrain daemon status --activation-root <path> — inspect the background watch state",
|
|
427
|
+
" 8. detach openclawbrain detach --openclaw-home <path> — remove the profile hookup only and keep brain data",
|
|
428
|
+
" 9. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data — remove the hookup and choose the data outcome explicitly",
|
|
429
|
+
"",
|
|
430
|
+
"Advanced/operator surfaces:",
|
|
431
|
+
" context preview the brain context that would be injected for a message",
|
|
432
|
+
" rollback preview or apply active <- previous, active -> candidate pointer movement",
|
|
433
|
+
" scan inspect one recorded session or live event export without claiming a daemon is running",
|
|
434
|
+
" learn one-shot local-session learning pass against the resolved activation root",
|
|
346
435
|
" status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
|
|
347
436
|
" watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
|
|
348
|
-
" watch
|
|
437
|
+
" watch teacher defaults come from install-written provider-defaults.json under the activation root; OPENCLAWBRAIN_TEACHER_* and OPENCLAWBRAIN_EMBEDDER_* are host-shell overrides only, not live gateway wiring",
|
|
349
438
|
"",
|
|
350
439
|
"Exit codes:",
|
|
440
|
+
" install: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
|
|
351
441
|
" status: 0 on successful inspection, 1 on input/read failure.",
|
|
352
442
|
" rollback: 0 when ready/applied, 1 when blocked or on input/read failure.",
|
|
443
|
+
" attach: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
|
|
353
444
|
" detach: 0 on successful unhook, 1 on input/read failure.",
|
|
354
445
|
" uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
|
|
355
446
|
" scan: 0 on successful replay/scan, 1 on input/read failure."
|
|
@@ -380,6 +471,24 @@ function formatStructuralOps(report) {
|
|
|
380
471
|
? "none"
|
|
381
472
|
: `split:${structuralOps.split},merge:${structuralOps.merge},prune:${structuralOps.prune},connect:${structuralOps.connect}`;
|
|
382
473
|
}
|
|
474
|
+
function formatGraphConnectDiagnostics(diagnostics) {
|
|
475
|
+
if (diagnostics === null) {
|
|
476
|
+
return "none";
|
|
477
|
+
}
|
|
478
|
+
return `budget:${diagnostics.requestedBudget},threshold:${diagnostics.scoreThreshold},pairs:${diagnostics.appliedPairCount}/${diagnostics.candidatePairCount},edges:${diagnostics.createdEdgeCount}`;
|
|
479
|
+
}
|
|
480
|
+
function formatCompactGraphConnectDiagnostics(diagnostics) {
|
|
481
|
+
if (diagnostics === null) {
|
|
482
|
+
return "none";
|
|
483
|
+
}
|
|
484
|
+
return `pairs:${diagnostics.appliedPairCount},edges:${diagnostics.createdEdgeCount}`;
|
|
485
|
+
}
|
|
486
|
+
function formatGraphSummary(report) {
|
|
487
|
+
return (report.graph.latestMaterialization.operatorSummary ??
|
|
488
|
+
report.graph.operatorSummary ??
|
|
489
|
+
report.graph.latestMaterialization.detail ??
|
|
490
|
+
report.graph.detail);
|
|
491
|
+
}
|
|
383
492
|
function formatScannerSurfaces(report) {
|
|
384
493
|
return report.supervision.scanSurfaces.length === 0 ? "none" : report.supervision.scanSurfaces.join("|");
|
|
385
494
|
}
|
|
@@ -391,7 +500,8 @@ function formatLearningBuckets(report) {
|
|
|
391
500
|
return `pi:${buckets.principal_immediate},pb:${buckets.principal_backfill},live:${buckets.live},backfill:${buckets.backfill}`;
|
|
392
501
|
}
|
|
393
502
|
function formatLearningWarnings(report) {
|
|
394
|
-
|
|
503
|
+
const warnings = report.learning.warningStates.filter((warning) => warning !== "teacher_snapshot_unavailable");
|
|
504
|
+
return warnings.length === 0 ? "none" : warnings.join("|");
|
|
395
505
|
}
|
|
396
506
|
function formatLabelFlowSummary(labelFlow) {
|
|
397
507
|
return `source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`;
|
|
@@ -399,11 +509,527 @@ function formatLabelFlowSummary(labelFlow) {
|
|
|
399
509
|
function formatLearningPathSummary(learningPath) {
|
|
400
510
|
return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
|
|
401
511
|
}
|
|
402
|
-
function
|
|
512
|
+
function formatTeacherLoopSummary(report) {
|
|
513
|
+
const parts = [
|
|
514
|
+
`snapshot=${report.teacherLoop.sourcePath ?? "none"}`,
|
|
515
|
+
`kind=${report.teacherLoop.sourceKind}`,
|
|
516
|
+
`lastRun=${report.teacherLoop.lastRunAt ?? "none"}`,
|
|
517
|
+
`artifacts=${report.teacherLoop.artifactCount ?? "none"}`,
|
|
518
|
+
`freshness=${report.teacherLoop.latestFreshness}`,
|
|
519
|
+
`queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"}`,
|
|
520
|
+
`running=${yesNo(report.teacherLoop.running)}`
|
|
521
|
+
];
|
|
522
|
+
if (report.teacherLoop.lastNoOpReason !== "none") {
|
|
523
|
+
parts.push(`noOp=${report.teacherLoop.lastNoOpReason}`);
|
|
524
|
+
}
|
|
525
|
+
if (report.teacherLoop.failureMode !== "none") {
|
|
526
|
+
const failureDetail = report.teacherLoop.failureDetail === null
|
|
527
|
+
? report.teacherLoop.failureMode
|
|
528
|
+
: `${report.teacherLoop.failureMode}(${report.teacherLoop.failureDetail})`;
|
|
529
|
+
parts.push(`failure=${failureDetail}`);
|
|
530
|
+
}
|
|
531
|
+
return parts.join(" ");
|
|
532
|
+
}
|
|
533
|
+
function formatCompactValue(value, maxLength = 64) {
|
|
534
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}...`;
|
|
535
|
+
}
|
|
536
|
+
function formatCompactList(values, maxItems = 2, maxLength = 64) {
|
|
537
|
+
if (values.length === 0) {
|
|
538
|
+
return "none";
|
|
539
|
+
}
|
|
540
|
+
const visible = values.slice(0, maxItems).map((value) => formatCompactValue(value, maxLength));
|
|
541
|
+
return values.length > maxItems ? `${visible.join("|")}+${values.length - maxItems}more` : visible.join("|");
|
|
542
|
+
}
|
|
543
|
+
const SERVICE_RISK_FINDING_CODES = new Set([
|
|
544
|
+
"activation_broken_install",
|
|
545
|
+
"activation_stale_incomplete",
|
|
546
|
+
"active_missing",
|
|
547
|
+
"active_unhealthy",
|
|
548
|
+
"learned_route_missing",
|
|
549
|
+
"serve_path_fail_open",
|
|
550
|
+
"serve_path_hard_fail",
|
|
551
|
+
"serve_path_route_evidence_missing"
|
|
552
|
+
]);
|
|
553
|
+
const DEGRADED_BRAIN_FINDING_CODES = new Set([
|
|
554
|
+
"bootstrap_waiting_for_first_export",
|
|
555
|
+
"serve_path_unprobed",
|
|
556
|
+
"brain_context_kernel_only",
|
|
557
|
+
"candidate_unhealthy",
|
|
558
|
+
"promotion_blocked",
|
|
559
|
+
"supervision_not_flowing",
|
|
560
|
+
"scan_surfaces_missing"
|
|
561
|
+
]);
|
|
562
|
+
const COSMETIC_FINDING_CODES = new Set([
|
|
563
|
+
"last_promotion_unknown",
|
|
564
|
+
"rollback_blocked",
|
|
565
|
+
"supervision_unavailable",
|
|
566
|
+
"turn_attribution_partial",
|
|
567
|
+
"teacher_snapshot_unavailable"
|
|
568
|
+
]);
|
|
569
|
+
const LEARNING_WARNING_MESSAGES = {
|
|
570
|
+
awaiting_first_export: "awaiting first export",
|
|
571
|
+
principal_live_backlog: "principal live backlog is ahead of serving",
|
|
572
|
+
principal_backfill_pending: "principal backfill is still queued",
|
|
573
|
+
active_pack_behind_latest_principal: "active pack is behind the latest principal correction",
|
|
574
|
+
passive_backfill_pending: "passive backfill remains queued",
|
|
575
|
+
teacher_queue_full: "teacher queue is full",
|
|
576
|
+
teacher_labels_stale: "teacher labels are stale",
|
|
577
|
+
teacher_no_artifacts: "teacher produced no artifacts",
|
|
578
|
+
teacher_snapshot_unavailable: "teacher snapshot is unavailable"
|
|
579
|
+
};
|
|
580
|
+
const TEACHER_NO_OP_MESSAGES = {
|
|
581
|
+
none: "the latest processed export produced teacher artifacts",
|
|
582
|
+
duplicate_export: "the latest cycle was a no-op because the export was already seen",
|
|
583
|
+
queue_full: "the latest cycle was a no-op because the teacher queue was full",
|
|
584
|
+
no_teacher_artifacts: "the latest cycle was a no-op because no teacher artifacts were produced",
|
|
585
|
+
empty_scan: "the latest cycle was a no-op because the scanner did not produce any events",
|
|
586
|
+
unavailable: "the latest cycle is not visible from the current operator snapshot"
|
|
587
|
+
};
|
|
588
|
+
function summarizeStatusInstallHook(openclawHome) {
|
|
589
|
+
if (openclawHome === null) {
|
|
590
|
+
return {
|
|
591
|
+
state: "unknown",
|
|
592
|
+
detail: "profile hook state is unknown from activation-root-only status; pin --openclaw-home to prove install state"
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const extensionDir = path.join(path.resolve(openclawHome), "extensions", "openclawbrain");
|
|
596
|
+
const indexPath = path.join(extensionDir, "index.ts");
|
|
597
|
+
const runtimeGuardPath = path.join(extensionDir, "runtime-guard.js");
|
|
598
|
+
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
599
|
+
if (existsSync(indexPath) && existsSync(runtimeGuardPath) && existsSync(manifestPath)) {
|
|
600
|
+
return {
|
|
601
|
+
state: "installed",
|
|
602
|
+
detail: `profile hook is installed at ${shortenPath(extensionDir)}`
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
state: "not_installed",
|
|
607
|
+
detail: `profile hook is not present at ${shortenPath(extensionDir)}`
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function summarizeStatusHookLoad(installHook, status) {
|
|
611
|
+
return {
|
|
612
|
+
installState: installHook.state === "unknown" ? "unverified" : installHook.state,
|
|
613
|
+
loadProof: status.attachment.state === "attached" && status.brainStatus.serveState === "serving_active_pack"
|
|
614
|
+
? "status_probe_ready"
|
|
615
|
+
: "not_ready",
|
|
616
|
+
detail: installHook.detail
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function runOllamaProbe(args, baseUrl) {
|
|
620
|
+
try {
|
|
621
|
+
execFileSync("ollama", [...args], {
|
|
622
|
+
stdio: "pipe",
|
|
623
|
+
timeout: 2_000,
|
|
624
|
+
env: {
|
|
625
|
+
...process.env,
|
|
626
|
+
OLLAMA_HOST: baseUrl
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
return {
|
|
630
|
+
detected: true,
|
|
631
|
+
detail: `ollama responded to ${args.join(" ")} at ${baseUrl}`
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
636
|
+
return {
|
|
637
|
+
detected: false,
|
|
638
|
+
detail: "ollama CLI was not found on PATH"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
detected: true,
|
|
643
|
+
detail: describeExecFailure(error)
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function summarizeStatusEmbeddings(report, providerConfig) {
|
|
648
|
+
let embeddedEntryCount = null;
|
|
649
|
+
let totalEntryCount = null;
|
|
650
|
+
let models = [];
|
|
651
|
+
let liveState = "unknown";
|
|
652
|
+
let liveDetail = "no activation-ready active pack is available for embedding inspection";
|
|
653
|
+
if (report.active !== null && report.active.activationReady) {
|
|
654
|
+
try {
|
|
655
|
+
const activePack = loadPackFromActivation(report.activationRoot, "active", {
|
|
656
|
+
requireActivationReady: true
|
|
657
|
+
});
|
|
658
|
+
if (activePack !== null) {
|
|
659
|
+
totalEntryCount = activePack.vectors.entries.length;
|
|
660
|
+
embeddedEntryCount = activePack.vectors.entries.filter((entry) => entry.embedding !== undefined).length;
|
|
661
|
+
models = [...new Set(activePack.vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
|
|
662
|
+
liveState = embeddedEntryCount > 0 ? "yes" : "no";
|
|
663
|
+
liveDetail = `active pack stores ${embeddedEntryCount}/${totalEntryCount} numeric embeddings`;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
liveDetail = `embedding inspection failed: ${toErrorMessage(error)}`;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (providerConfig.embedder.provider === "off") {
|
|
671
|
+
return {
|
|
672
|
+
provider: providerConfig.embedder.provider,
|
|
673
|
+
model: providerConfig.embedder.model,
|
|
674
|
+
provisionedState: "off",
|
|
675
|
+
liveState,
|
|
676
|
+
embeddedEntryCount,
|
|
677
|
+
totalEntryCount,
|
|
678
|
+
models,
|
|
679
|
+
detail: `${liveDetail}; embedder provider is off`
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (providerConfig.embedder.provider === "keywords") {
|
|
683
|
+
return {
|
|
684
|
+
provider: providerConfig.embedder.provider,
|
|
685
|
+
model: providerConfig.embedder.model,
|
|
686
|
+
provisionedState: "builtin",
|
|
687
|
+
liveState,
|
|
688
|
+
embeddedEntryCount,
|
|
689
|
+
totalEntryCount,
|
|
690
|
+
models,
|
|
691
|
+
detail: `${liveDetail}; keyword embedder needs no Ollama model provision`
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const modelProbe = runOllamaProbe(["show", providerConfig.embedder.model], providerConfig.embedderBaseUrl);
|
|
695
|
+
return {
|
|
696
|
+
provider: providerConfig.embedder.provider,
|
|
697
|
+
model: providerConfig.embedder.model,
|
|
698
|
+
provisionedState: modelProbe.detected && /responded to/.test(modelProbe.detail) ? "confirmed" : "not_confirmed",
|
|
699
|
+
liveState,
|
|
700
|
+
embeddedEntryCount,
|
|
701
|
+
totalEntryCount,
|
|
702
|
+
models,
|
|
703
|
+
detail: `${liveDetail}; ollama model check: ${modelProbe.detail}`
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function summarizeStatusLocalLlm(providerConfig) {
|
|
707
|
+
const detection = runOllamaProbe(["--version"], providerConfig.teacherBaseUrl);
|
|
708
|
+
const enabled = providerConfig.teacher.provider === "ollama";
|
|
709
|
+
if (enabled) {
|
|
710
|
+
return {
|
|
711
|
+
detected: detection.detected,
|
|
712
|
+
enabled,
|
|
713
|
+
provider: providerConfig.teacher.provider,
|
|
714
|
+
model: providerConfig.teacher.model,
|
|
715
|
+
detail: detection.detected
|
|
716
|
+
? `teacher provider is ollama and the local LLM surface answered at ${providerConfig.teacherBaseUrl}`
|
|
717
|
+
: `teacher provider is ollama but the local LLM surface was not detected (${detection.detail})`
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
detected: detection.detected,
|
|
722
|
+
enabled,
|
|
723
|
+
provider: providerConfig.teacher.provider,
|
|
724
|
+
model: providerConfig.teacher.model,
|
|
725
|
+
detail: detection.detected
|
|
726
|
+
? `local Ollama is detectable, but teacher labeling is ${providerConfig.teacher.provider}`
|
|
727
|
+
: `teacher labeling is ${providerConfig.teacher.provider}; no local Ollama CLI was detected`
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function summarizeStatusTeacher(report, providerConfig, localLlm) {
|
|
731
|
+
const enabled = providerConfig.teacher.provider === "ollama";
|
|
732
|
+
const latestCycle = report.teacherLoop.lastNoOpReason === "unavailable"
|
|
733
|
+
? "unknown"
|
|
734
|
+
: report.teacherLoop.lastNoOpReason === "none"
|
|
735
|
+
? "teacher_artifact"
|
|
736
|
+
: "no_op";
|
|
737
|
+
if (!enabled) {
|
|
738
|
+
return {
|
|
739
|
+
model: providerConfig.teacher.model,
|
|
740
|
+
enabled,
|
|
741
|
+
healthy: false,
|
|
742
|
+
stale: false,
|
|
743
|
+
idle: false,
|
|
744
|
+
latestCycle,
|
|
745
|
+
detail: `${providerConfig.teacher.model} is not enabled because teacher labeling is ${providerConfig.teacher.provider}`
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (!localLlm.detected) {
|
|
749
|
+
return {
|
|
750
|
+
model: providerConfig.teacher.model,
|
|
751
|
+
enabled,
|
|
752
|
+
healthy: false,
|
|
753
|
+
stale: null,
|
|
754
|
+
idle: false,
|
|
755
|
+
latestCycle,
|
|
756
|
+
detail: `${providerConfig.teacher.model} is configured on Ollama, but the local LLM surface is not answering at ${providerConfig.teacherBaseUrl}`
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (!report.teacherLoop.available) {
|
|
760
|
+
return {
|
|
761
|
+
model: providerConfig.teacher.model,
|
|
762
|
+
enabled,
|
|
763
|
+
healthy: null,
|
|
764
|
+
stale: null,
|
|
765
|
+
idle: null,
|
|
766
|
+
latestCycle,
|
|
767
|
+
detail: `${providerConfig.teacher.model} is enabled on Ollama, but no watch teacher snapshot is visible yet`
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const stale = report.teacherLoop.latestFreshness === "stale" || report.teacherLoop.watchState === "stale_snapshot";
|
|
771
|
+
const idle = report.teacherLoop.running === false &&
|
|
772
|
+
(report.teacherLoop.queueDepth ?? 0) === 0 &&
|
|
773
|
+
report.teacherLoop.failureMode === "none";
|
|
774
|
+
const healthy = report.teacherLoop.failureMode === "none" &&
|
|
775
|
+
stale === false &&
|
|
776
|
+
report.teacherLoop.watchState !== "not_visible";
|
|
777
|
+
const cycleDetail = TEACHER_NO_OP_MESSAGES[report.teacherLoop.lastNoOpReason] ?? "the latest teacher cycle detail is unavailable";
|
|
778
|
+
if (report.teacherLoop.failureMode !== "none" && report.teacherLoop.failureMode !== "unavailable") {
|
|
779
|
+
return {
|
|
780
|
+
model: providerConfig.teacher.model,
|
|
781
|
+
enabled,
|
|
782
|
+
healthy: false,
|
|
783
|
+
stale,
|
|
784
|
+
idle,
|
|
785
|
+
latestCycle,
|
|
786
|
+
detail: report.teacherLoop.failureDetail === null
|
|
787
|
+
? `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}`
|
|
788
|
+
: `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}: ${report.teacherLoop.failureDetail}`
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
model: providerConfig.teacher.model,
|
|
793
|
+
enabled,
|
|
794
|
+
healthy,
|
|
795
|
+
stale,
|
|
796
|
+
idle,
|
|
797
|
+
latestCycle,
|
|
798
|
+
detail: `${providerConfig.teacher.model} is enabled on Ollama; ${cycleDetail}`
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function summarizeStatusEmbedder(embeddings) {
|
|
802
|
+
const provisioned = embeddings.provisionedState === "confirmed" || embeddings.provisionedState === "builtin"
|
|
803
|
+
? true
|
|
804
|
+
: embeddings.provisionedState === "not_confirmed" || embeddings.provisionedState === "off"
|
|
805
|
+
? false
|
|
806
|
+
: null;
|
|
807
|
+
const live = embeddings.liveState === "yes" ? true : embeddings.liveState === "no" ? false : null;
|
|
808
|
+
if (embeddings.provider === "off") {
|
|
809
|
+
return {
|
|
810
|
+
model: embeddings.model,
|
|
811
|
+
provisioned,
|
|
812
|
+
live,
|
|
813
|
+
detail: `${embeddings.model} is not provisioned because the embedder provider is off`
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
if (embeddings.provider === "keywords") {
|
|
817
|
+
return {
|
|
818
|
+
model: embeddings.model,
|
|
819
|
+
provisioned,
|
|
820
|
+
live,
|
|
821
|
+
detail: "keyword embeddings are builtin, so there is no Ollama model to provision"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
if (provisioned === true && live === true) {
|
|
825
|
+
return {
|
|
826
|
+
model: embeddings.model,
|
|
827
|
+
provisioned,
|
|
828
|
+
live,
|
|
829
|
+
detail: `${embeddings.model} is confirmed on Ollama and the active pack stores live numeric embeddings`
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
if (provisioned === true && live === false) {
|
|
833
|
+
return {
|
|
834
|
+
model: embeddings.model,
|
|
835
|
+
provisioned,
|
|
836
|
+
live,
|
|
837
|
+
detail: `${embeddings.model} is confirmed on Ollama, but the active pack still has no live numeric embeddings`
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (provisioned === false && live === true) {
|
|
841
|
+
return {
|
|
842
|
+
model: embeddings.model,
|
|
843
|
+
provisioned,
|
|
844
|
+
live,
|
|
845
|
+
detail: `${embeddings.model} is not confirmed on Ollama, but the active pack already carries numeric embeddings from an earlier materialization`
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
model: embeddings.model,
|
|
850
|
+
provisioned,
|
|
851
|
+
live,
|
|
852
|
+
detail: embeddings.detail
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function summarizeStatusRouteFn(status, report) {
|
|
856
|
+
const freshness = report.servePath.refreshStatus ?? status.brain.routeFreshness;
|
|
857
|
+
if (!report.routeFn.available) {
|
|
858
|
+
return {
|
|
859
|
+
available: false,
|
|
860
|
+
freshness,
|
|
861
|
+
trainedAt: report.routeFn.trainedAt,
|
|
862
|
+
updatedAt: report.routeFn.updatedAt,
|
|
863
|
+
usedAt: report.routeFn.usedAt,
|
|
864
|
+
detail: report.routeFn.detail
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
let detail = report.routeFn.detail;
|
|
868
|
+
if (report.servePath.usedLearnedRouteFn === true) {
|
|
869
|
+
detail = `current serve proof used the learned route_fn; ${report.routeFn.detail}`;
|
|
870
|
+
}
|
|
871
|
+
else if (report.routeFn.usedAt !== null) {
|
|
872
|
+
detail = `current serve proof did not use the learned route_fn, but the active route_fn last served a learned turn at ${report.routeFn.usedAt}`;
|
|
873
|
+
}
|
|
874
|
+
else if (report.routeFn.updatedAt !== null) {
|
|
875
|
+
detail = `active route_fn was last updated at ${report.routeFn.updatedAt}, but no learned serve use is visible yet for the current pack`;
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
available: true,
|
|
879
|
+
freshness,
|
|
880
|
+
trainedAt: report.routeFn.trainedAt,
|
|
881
|
+
updatedAt: report.routeFn.updatedAt,
|
|
882
|
+
usedAt: report.routeFn.usedAt,
|
|
883
|
+
detail
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function pushUniqueAlert(target, value) {
|
|
887
|
+
const normalized = value.trim();
|
|
888
|
+
if (normalized.length === 0) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (target.includes(normalized) === false) {
|
|
892
|
+
target.push(normalized);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function summarizeStatusAlerts(report, providerConfig, embeddings, localLlm) {
|
|
896
|
+
const buckets = {
|
|
897
|
+
serviceRisk: [],
|
|
898
|
+
degradedBrain: [],
|
|
899
|
+
cosmeticNoise: []
|
|
900
|
+
};
|
|
901
|
+
for (const finding of report.findings) {
|
|
902
|
+
if (finding.severity === "pass") {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (SERVICE_RISK_FINDING_CODES.has(finding.code)) {
|
|
906
|
+
pushUniqueAlert(buckets.serviceRisk, finding.summary);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (DEGRADED_BRAIN_FINDING_CODES.has(finding.code)) {
|
|
910
|
+
pushUniqueAlert(buckets.degradedBrain, finding.summary);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (COSMETIC_FINDING_CODES.has(finding.code)) {
|
|
914
|
+
pushUniqueAlert(buckets.cosmeticNoise, finding.summary);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
pushUniqueAlert(finding.severity === "fail" ? buckets.serviceRisk : buckets.degradedBrain, finding.summary);
|
|
918
|
+
}
|
|
919
|
+
for (const warningState of report.learning.warningStates) {
|
|
920
|
+
const message = LEARNING_WARNING_MESSAGES[warningState];
|
|
921
|
+
if (message === undefined) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (warningState === "teacher_snapshot_unavailable") {
|
|
925
|
+
pushUniqueAlert(buckets.cosmeticNoise, message);
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
pushUniqueAlert(buckets.degradedBrain, message);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (providerConfig.warnings.length > 0) {
|
|
932
|
+
pushUniqueAlert(buckets.cosmeticNoise, "provider env warnings forced fallback defaults");
|
|
933
|
+
}
|
|
934
|
+
if (localLlm.enabled && !localLlm.detected) {
|
|
935
|
+
pushUniqueAlert(buckets.degradedBrain, "local LLM is enabled but not detected");
|
|
936
|
+
}
|
|
937
|
+
if (embeddings.provider === "ollama" && embeddings.provisionedState !== "confirmed") {
|
|
938
|
+
pushUniqueAlert(buckets.degradedBrain, `embedder model ${embeddings.model} is not confirmed on Ollama`);
|
|
939
|
+
}
|
|
940
|
+
if (embeddings.provider === "ollama" && embeddings.liveState === "no") {
|
|
941
|
+
pushUniqueAlert(buckets.degradedBrain, "embedder is provisioned but the active pack has no live numeric embeddings");
|
|
942
|
+
}
|
|
943
|
+
return buckets;
|
|
944
|
+
}
|
|
945
|
+
function summarizeStatusWatchState(status) {
|
|
946
|
+
return status.passiveLearning.watchState;
|
|
947
|
+
}
|
|
948
|
+
function summarizeStatusServeReality(status) {
|
|
949
|
+
if (status.brainStatus.serveState === "serving_active_pack") {
|
|
950
|
+
return "proven_active_pack";
|
|
951
|
+
}
|
|
952
|
+
return status.brainStatus.serveState;
|
|
953
|
+
}
|
|
954
|
+
function summarizeStatusPromotionState(status) {
|
|
955
|
+
if (status.brain.state === "pg_promoted_pack_authoritative") {
|
|
956
|
+
return "promoted";
|
|
957
|
+
}
|
|
958
|
+
if (status.brain.state === "seed_state_authoritative") {
|
|
959
|
+
return status.passiveLearning.firstExportOccurred ? "seed_authoritative" : "awaiting_first_export";
|
|
960
|
+
}
|
|
961
|
+
return status.brain.state;
|
|
962
|
+
}
|
|
963
|
+
function formatStatusAlertLine(values) {
|
|
964
|
+
const normalized = values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
965
|
+
return normalized.length === 0 ? "none" : formatCompactList(normalized, 2, 64);
|
|
966
|
+
}
|
|
967
|
+
function formatStatusNullableNumber(value, unknown = "unknown") {
|
|
968
|
+
return value === null ? unknown : String(value);
|
|
969
|
+
}
|
|
970
|
+
function formatStatusNullableYesNo(value) {
|
|
971
|
+
return value === null ? "unknown" : yesNo(value);
|
|
972
|
+
}
|
|
973
|
+
function formatStatusNullableMilliseconds(value) {
|
|
974
|
+
return value === null ? "none" : `${value.toFixed(2)}ms`;
|
|
975
|
+
}
|
|
976
|
+
function formatStatusHotPathTiming(timing) {
|
|
977
|
+
return [
|
|
978
|
+
`hotPath=${formatStatusNullableMilliseconds(timing.totalMs)}`,
|
|
979
|
+
`route=${formatStatusNullableMilliseconds(timing.routeSelectionMs)}`,
|
|
980
|
+
`prompt=${formatStatusNullableMilliseconds(timing.promptAssemblyMs)}`,
|
|
981
|
+
`other=${formatStatusNullableMilliseconds(timing.otherMs)}`,
|
|
982
|
+
`background=${timing.backgroundWorkIncluded ? "included" : "excluded"}`
|
|
983
|
+
].join(" ");
|
|
984
|
+
}
|
|
985
|
+
function formatStatusObservedDeltaTransition(delta) {
|
|
986
|
+
if (delta.latestPackTransition === null) {
|
|
987
|
+
return "none";
|
|
988
|
+
}
|
|
989
|
+
return `${delta.latestPackTransition.kind}:${delta.latestPackTransition.fromPackId ?? "none"}->${delta.latestPackTransition.toPackId}`;
|
|
990
|
+
}
|
|
991
|
+
function buildCompactStatusHeader(status, report, options) {
|
|
992
|
+
const installHook = summarizeStatusInstallHook(options.openclawHome);
|
|
993
|
+
const hookLoad = summarizeStatusHookLoad(installHook, status);
|
|
994
|
+
const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
|
|
995
|
+
const localLlm = summarizeStatusLocalLlm(options.providerConfig);
|
|
996
|
+
const teacher = summarizeStatusTeacher(report, options.providerConfig, localLlm);
|
|
997
|
+
const embedder = summarizeStatusEmbedder(embeddings);
|
|
998
|
+
const routeFn = summarizeStatusRouteFn(status, report);
|
|
999
|
+
const alerts = summarizeStatusAlerts(report, options.providerConfig, embeddings, localLlm);
|
|
1000
|
+
const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
|
|
1001
|
+
return [
|
|
1002
|
+
`lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
|
|
1003
|
+
`hook install=${hookLoad.installState} loadProof=${hookLoad.loadProof} detail=${hookLoad.detail}`,
|
|
1004
|
+
`passive firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)}`,
|
|
1005
|
+
`serving pack=${status.passiveLearning.currentServingPackId ?? "none"} lastExport=${status.passiveLearning.lastExportAt ?? "none"} lastPromotion=${status.passiveLearning.lastPromotionAt ?? "none"}`,
|
|
1006
|
+
`timing ${formatStatusHotPathTiming(status.brainStatus.timing)}`,
|
|
1007
|
+
`delta observed=${status.passiveLearning.lastObservedDelta.observedAt ?? "none"} exported=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.exported)} labeled=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.labeled)} promoted=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.promoted)} served=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.served)} transition=${formatStatusObservedDeltaTransition(status.passiveLearning.lastObservedDelta)}`,
|
|
1008
|
+
`changed ${status.passiveLearning.lastObservedDelta.explanation}`,
|
|
1009
|
+
`explain ${status.brain.summary}`,
|
|
1010
|
+
`graph blocks=${report.graph.blockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} latest=${report.graph.latestMaterialization.packId ?? "none"} latestChanged=${yesNo(report.graph.latestMaterialization.changed)} connect=${formatCompactGraphConnectDiagnostics(report.graph.latestMaterialization.connectDiagnostics ?? report.graph.connectDiagnostics)}`,
|
|
1011
|
+
`teacher model=${teacher.model} enabled=${yesNo(teacher.enabled)} healthy=${yesNo(teacher.healthy)} stale=${yesNo(teacher.stale)} idle=${yesNo(teacher.idle)} cycle=${teacher.latestCycle} why=${teacher.detail}`,
|
|
1012
|
+
`embedder model=${embedder.model} provisioned=${yesNo(embedder.provisioned)} live=${yesNo(embedder.live)} why=${embedder.detail}`,
|
|
1013
|
+
`routeFn available=${yesNo(routeFn.available)} freshness=${routeFn.freshness} trained=${routeFn.trainedAt ?? "none"} updated=${routeFn.updatedAt ?? "none"} used=${routeFn.usedAt ?? "none"} why=${routeFn.detail}`,
|
|
1014
|
+
`embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
|
|
1015
|
+
`localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
|
|
1016
|
+
`alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
|
|
1017
|
+
];
|
|
1018
|
+
}
|
|
1019
|
+
function formatCurrentProfileStatusSummary(status, report, targetInspection, options) {
|
|
1020
|
+
const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
|
|
1021
|
+
const localLlm = summarizeStatusLocalLlm(options.providerConfig);
|
|
1022
|
+
const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
|
|
403
1023
|
const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
|
|
1024
|
+
const targetLine = targetInspection === null
|
|
1025
|
+
? `target activation=${status.host.activationRoot} source=activation_root_only`
|
|
1026
|
+
: `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${shortenPath(path.join(targetInspection.openclawHome, "extensions", "openclawbrain", "index.ts"))}`;
|
|
404
1027
|
return [
|
|
405
1028
|
`STATUS ${status.brainStatus.status}`,
|
|
1029
|
+
...buildCompactStatusHeader(status, report, options),
|
|
406
1030
|
`answer ${status.brain.summary}`,
|
|
1031
|
+
targetLine,
|
|
1032
|
+
...(targetInspection === null ? [] : [`preflight ${formatOpenClawTargetExplanation(targetInspection)}`]),
|
|
407
1033
|
`host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
|
|
408
1034
|
`profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
|
|
409
1035
|
`manyProfile surface=${report.manyProfile.operatorSurface} policy=${report.manyProfile.declaredAttachmentPolicy} intent=${report.manyProfile.sameGatewayIntent} checkedProof=${report.manyProfile.checkedInProofTopology} sameGatewayProof=${yesNo(report.manyProfile.sameGatewayProof)} sharedWriteProof=${yesNo(report.manyProfile.sharedWriteSafetyProof)}`,
|
|
@@ -414,13 +1040,17 @@ function formatCurrentProfileStatusSummary(status, report) {
|
|
|
414
1040
|
`budget requested=${report.servePath.requestedBudgetStrategy ?? "none"} resolved=${report.servePath.resolvedBudgetStrategy ?? "none"} maxBlocks=${report.servePath.resolvedMaxContextBlocks ?? "none"} source=${report.servePath.structuralBudgetSource ?? "none"} origin=${status.brainStatus.structuralDecision.origin} basis=${status.brainStatus.structuralDecision.basis}`,
|
|
415
1041
|
`decision ${status.brainStatus.structuralDecision.detail}`,
|
|
416
1042
|
`principal latest=${formatPrincipalLatest(report)} pending=${report.principal.pendingCount ?? report.learning.pendingPrincipalCount ?? "none"} checkpoint=${formatPrincipalCheckpointFrontier(report)} downstream=${yesNo(report.principal.servingDownstreamOfLatestCorrection)} lag=${report.learning.principalLagToPromotion.sequenceLag ?? "none"}`,
|
|
1043
|
+
`passive learner=${yesNo(status.passiveLearning.learnerRunning)} firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} watch=${status.passiveLearning.watchState} export=${status.passiveLearning.exportState} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)} detail=${status.passiveLearning.detail}`,
|
|
1044
|
+
`delta observed=${status.passiveLearning.lastObservedDelta.observedAt ?? "none"} exported=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.exported)} labeled=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.labeled)} promoted=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.promoted)} served=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.served)} transition=${formatStatusObservedDeltaTransition(status.passiveLearning.lastObservedDelta)} detail=${status.passiveLearning.lastObservedDelta.explanation}`,
|
|
417
1045
|
`scanner flowing=${yesNo(report.supervision.flowing)} scan=${report.supervision.scanPolicy ?? "none"} surfaces=${formatScannerSurfaces(report)} labels=${report.supervision.humanLabelCount ?? "none"}/${report.supervision.selfLabelCount ?? "none"} attributable=${report.supervision.attributedEventCount ?? "none"}/${report.supervision.totalEventCount ?? "none"} digests=${report.supervision.selectionDigestCount ?? "none"}`,
|
|
418
1046
|
`labels ${formatLabelFlowSummary(report.labelFlow)}`,
|
|
419
|
-
`graph source=${report.graph.runtimePlasticitySource ?? "none"}
|
|
1047
|
+
`graph source=${report.graph.runtimePlasticitySource ?? "none"} blocks=${report.graph.blockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} ops=${formatStructuralOps(report)} latest=${report.graph.latestMaterialization.packId ?? "none"} latestChanged=${yesNo(report.graph.latestMaterialization.changed)} connect=${formatGraphConnectDiagnostics(report.graph.latestMaterialization.connectDiagnostics ?? report.graph.connectDiagnostics)} summary=${formatGraphSummary(report)}`,
|
|
420
1048
|
`path ${formatLearningPathSummary(report.learningPath)}`,
|
|
421
1049
|
`learning state=${report.learning.backlogState} bootstrapped=${yesNo(report.learning.bootstrapped)} mode=${report.learning.mode} next=${report.learning.nextPriorityLane} priority=${report.learning.nextPriorityBucket} pending=${report.learning.pendingLive ?? "none"}/${report.learning.pendingBackfill ?? "none"} buckets=${formatLearningBuckets(report)} warn=${formatLearningWarnings(report)} lastPack=${report.learning.lastMaterializedPackId ?? "none"} detail=${report.learning.detail}`,
|
|
422
|
-
`
|
|
423
|
-
`
|
|
1050
|
+
`teacherProof ${formatTeacherLoopSummary(report)}`,
|
|
1051
|
+
`watch cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} heartbeat=${report.teacherLoop.lastHeartbeatAt ?? "none"} interval=${report.teacherLoop.pollIntervalSeconds ?? "none"} replayed=${report.teacherLoop.replayedBundleCount ?? "none"}/${report.teacherLoop.replayedEventCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} tail=${report.teacherLoop.sessionTailSessionsTracked ?? "none"}/${report.teacherLoop.sessionTailBridgedEventCount ?? "none"} tailState=${report.teacherLoop.localSessionTailNoopReason ?? "none"} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
|
|
1052
|
+
`embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
|
|
1053
|
+
`localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
|
|
424
1054
|
`rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
425
1055
|
`proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
|
|
426
1056
|
`logs root=${status.brain.logRoot ?? "none"}`,
|
|
@@ -437,14 +1067,399 @@ function shortenPath(fullPath) {
|
|
|
437
1067
|
}
|
|
438
1068
|
return fullPath;
|
|
439
1069
|
}
|
|
1070
|
+
function formatOpenClawTargetLine(inspection) {
|
|
1071
|
+
const profilePart = inspection.profileId === null
|
|
1072
|
+
? "profile=current_profile"
|
|
1073
|
+
: `profile=${inspection.profileId} via ${formatOpenClawHomeProfileSource(inspection.profileSource)}`;
|
|
1074
|
+
return `home=${shortenPath(inspection.openclawHome)} layout=${formatOpenClawHomeLayout(inspection.layout)} ${profilePart}`;
|
|
1075
|
+
}
|
|
1076
|
+
function formatOpenClawTargetExplanation(inspection) {
|
|
1077
|
+
return describeOpenClawHomeInspection(inspection);
|
|
1078
|
+
}
|
|
440
1079
|
function buildInstallStatusCommand(activationRoot) {
|
|
441
1080
|
return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
|
|
442
1081
|
}
|
|
1082
|
+
function buildLearnerServiceStatusCommand(activationRoot) {
|
|
1083
|
+
return `openclawbrain daemon status --activation-root ${quoteShellArg(activationRoot)}`;
|
|
1084
|
+
}
|
|
1085
|
+
function buildGatewayRestartCommand(profileId) {
|
|
1086
|
+
return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway restart`;
|
|
1087
|
+
}
|
|
1088
|
+
function buildGatewayStatusCommand(profileId) {
|
|
1089
|
+
return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway status`;
|
|
1090
|
+
}
|
|
443
1091
|
function buildInstallCommand(openclawHome) {
|
|
444
1092
|
return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
|
|
445
1093
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
1094
|
+
function buildAttachCommand(openclawHome, activationRoot = null) {
|
|
1095
|
+
const parts = ["openclawbrain", "attach", "--openclaw-home", quoteShellArg(openclawHome)];
|
|
1096
|
+
if (activationRoot !== null) {
|
|
1097
|
+
parts.push("--activation-root", quoteShellArg(activationRoot));
|
|
1098
|
+
}
|
|
1099
|
+
return parts.join(" ");
|
|
1100
|
+
}
|
|
1101
|
+
function buildInstallEmbedderProvisionCommand(baseUrl, model) {
|
|
1102
|
+
return `OLLAMA_HOST=${quoteShellArg(baseUrl)} ollama pull ${quoteShellArg(model)}`;
|
|
1103
|
+
}
|
|
1104
|
+
function describeExecOutput(value) {
|
|
1105
|
+
if (typeof value === "string") {
|
|
1106
|
+
const normalized = value.trim();
|
|
1107
|
+
return normalized.length > 0 ? normalized : null;
|
|
1108
|
+
}
|
|
1109
|
+
if (value instanceof Buffer) {
|
|
1110
|
+
const normalized = value.toString("utf8").trim();
|
|
1111
|
+
return normalized.length > 0 ? normalized : null;
|
|
1112
|
+
}
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
function describeExecFailure(error) {
|
|
1116
|
+
if (error instanceof Error) {
|
|
1117
|
+
const childError = error;
|
|
1118
|
+
if (childError.code === "ENOENT") {
|
|
1119
|
+
return "ollama was not found on PATH";
|
|
1120
|
+
}
|
|
1121
|
+
const stderr = describeExecOutput(childError.stderr);
|
|
1122
|
+
if (stderr !== null) {
|
|
1123
|
+
return stderr;
|
|
1124
|
+
}
|
|
1125
|
+
const stdout = describeExecOutput(childError.stdout);
|
|
1126
|
+
if (stdout !== null) {
|
|
1127
|
+
return stdout;
|
|
1128
|
+
}
|
|
1129
|
+
const message = childError.message.trim();
|
|
1130
|
+
if (message.length > 0) {
|
|
1131
|
+
return message;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return String(error);
|
|
1135
|
+
}
|
|
1136
|
+
function toErrorMessage(error) {
|
|
1137
|
+
return error instanceof Error ? error.message : String(error);
|
|
1138
|
+
}
|
|
1139
|
+
function ensureInstallEmbedderReady(parsed) {
|
|
1140
|
+
const providerConfig = readOpenClawBrainProviderConfig(process.env);
|
|
1141
|
+
const model = DEFAULT_OLLAMA_EMBEDDING_MODEL;
|
|
1142
|
+
const baseUrl = providerConfig.embedderBaseUrl;
|
|
1143
|
+
if (parsed.skipEmbedderProvision) {
|
|
1144
|
+
const skipReason = parsed.skipEmbedderProvisionSource === "flag"
|
|
1145
|
+
? "--skip-embedder-provision"
|
|
1146
|
+
: `${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1`;
|
|
1147
|
+
return {
|
|
1148
|
+
state: "skipped",
|
|
1149
|
+
model,
|
|
1150
|
+
baseUrl,
|
|
1151
|
+
detail: `Skipped default embedder provisioning (${skipReason}); ${parsed.command} continued only because the operator explicitly opted out. ` +
|
|
1152
|
+
`Provision it later with ${buildInstallEmbedderProvisionCommand(baseUrl, model)}.`
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
execFileSync("ollama", ["pull", model], {
|
|
1157
|
+
stdio: "pipe",
|
|
1158
|
+
env: {
|
|
1159
|
+
...process.env,
|
|
1160
|
+
OLLAMA_HOST: baseUrl
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
const detail = describeExecFailure(error);
|
|
1166
|
+
throw new Error(`Default embedder provisioning failed before brain init. Tried ${buildInstallEmbedderProvisionCommand(baseUrl, model)}. ` +
|
|
1167
|
+
`${parsed.command === "install" ? "Install" : "Attach"} stops here so the bootstrap path does not quietly continue without ${model}. ` +
|
|
1168
|
+
`Fix Ollama and rerun ${parsed.command}, or explicitly skip with --skip-embedder-provision or ${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1. ` +
|
|
1169
|
+
`Detail: ${detail}`);
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
state: "ensured",
|
|
1173
|
+
model,
|
|
1174
|
+
baseUrl,
|
|
1175
|
+
detail: `Ensured default embedder before brain bootstrap: ${buildInstallEmbedderProvisionCommand(baseUrl, model)}`
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function parseOllamaListModelNames(output) {
|
|
1179
|
+
return output
|
|
1180
|
+
.split(/\r?\n/u)
|
|
1181
|
+
.map((line) => line.trim())
|
|
1182
|
+
.filter((line) => line.length > 0 && !/^name\s+/iu.test(line))
|
|
1183
|
+
.map((line) => line.split(/\s+/u)[0] ?? "")
|
|
1184
|
+
.filter((name) => name.length > 0);
|
|
1185
|
+
}
|
|
1186
|
+
function selectCompatibleLocalTeacherModel(models) {
|
|
1187
|
+
const normalized = models.map((model) => model.trim()).filter((model) => model.length > 0);
|
|
1188
|
+
for (const prefix of INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES) {
|
|
1189
|
+
const exact = normalized.find((model) => model === prefix);
|
|
1190
|
+
if (exact !== undefined) {
|
|
1191
|
+
return exact;
|
|
1192
|
+
}
|
|
1193
|
+
const variant = normalized.find((model) => model.startsWith(`${prefix}-`) ||
|
|
1194
|
+
model.startsWith(`${prefix}_`) ||
|
|
1195
|
+
model.startsWith(`${prefix}.`));
|
|
1196
|
+
if (variant !== undefined) {
|
|
1197
|
+
return variant;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
function detectInstallTeacherDefaults(baseUrl) {
|
|
1203
|
+
try {
|
|
1204
|
+
const output = execFileSync("ollama", ["list"], {
|
|
1205
|
+
stdio: "pipe",
|
|
1206
|
+
env: {
|
|
1207
|
+
...process.env,
|
|
1208
|
+
OLLAMA_HOST: baseUrl
|
|
1209
|
+
}
|
|
1210
|
+
}).toString("utf8");
|
|
1211
|
+
const availableModels = parseOllamaListModelNames(output);
|
|
1212
|
+
const model = selectCompatibleLocalTeacherModel(availableModels);
|
|
1213
|
+
if (model === null) {
|
|
1214
|
+
return {
|
|
1215
|
+
provider: "heuristic",
|
|
1216
|
+
model: null,
|
|
1217
|
+
baseUrl,
|
|
1218
|
+
availableModels,
|
|
1219
|
+
detectionDetail: availableModels.length === 0
|
|
1220
|
+
? `No compatible local Ollama teacher model detected on ${baseUrl}; watch keeps heuristic teacher defaults.`
|
|
1221
|
+
: `No compatible local Ollama teacher model detected on ${baseUrl}; saw ${availableModels.join(", ")} and kept heuristic teacher defaults.`
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
provider: "ollama",
|
|
1226
|
+
model,
|
|
1227
|
+
baseUrl,
|
|
1228
|
+
availableModels,
|
|
1229
|
+
detectionDetail: `Detected compatible local Ollama teacher model ${model} on ${baseUrl}; watch will enable it by default from the installed activation root.`
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
const detail = describeExecFailure(error);
|
|
1234
|
+
return {
|
|
1235
|
+
provider: "heuristic",
|
|
1236
|
+
model: null,
|
|
1237
|
+
baseUrl,
|
|
1238
|
+
availableModels: [],
|
|
1239
|
+
detectionDetail: `Local Ollama teacher autodetect failed on ${baseUrl}; kept heuristic teacher defaults. Detail: ${detail}`
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function writeInstallProviderDefaults(parsed) {
|
|
1244
|
+
const providerConfig = readOpenClawBrainProviderConfig(process.env);
|
|
1245
|
+
const teacherDetection = detectInstallTeacherDefaults(providerConfig.teacherBaseUrl);
|
|
1246
|
+
const defaultsPath = resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot);
|
|
1247
|
+
const defaults = {
|
|
1248
|
+
contract: "openclawbrain_provider_defaults.v1",
|
|
1249
|
+
writtenAt: new Date().toISOString(),
|
|
1250
|
+
source: "install",
|
|
1251
|
+
teacherBaseUrl: providerConfig.teacherBaseUrl,
|
|
1252
|
+
embedderBaseUrl: providerConfig.embedderBaseUrl,
|
|
1253
|
+
teacher: {
|
|
1254
|
+
provider: teacherDetection.provider,
|
|
1255
|
+
model: teacherDetection.model,
|
|
1256
|
+
detectedLocally: teacherDetection.provider === "ollama",
|
|
1257
|
+
detectedFromModel: teacherDetection.model
|
|
1258
|
+
},
|
|
1259
|
+
embedder: {
|
|
1260
|
+
provider: "ollama",
|
|
1261
|
+
model: DEFAULT_OLLAMA_EMBEDDING_MODEL
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + "\n", "utf8");
|
|
1265
|
+
return {
|
|
1266
|
+
path: defaultsPath,
|
|
1267
|
+
defaults,
|
|
1268
|
+
detail: `Wrote local provider defaults: ${teacherDetection.detectionDetail}`,
|
|
1269
|
+
lifecycleSummary: teacherDetection.provider === "ollama" && teacherDetection.model !== null
|
|
1270
|
+
? `Teacher: auto-enabled local Ollama model ${teacherDetection.model} from install-written defaults`
|
|
1271
|
+
: "Teacher: no compatible local Ollama model detected; watch stays heuristic unless explicitly overridden"
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall) {
|
|
1275
|
+
if (isInstall || activationPlan.action === "bootstrap") {
|
|
1276
|
+
return true;
|
|
1277
|
+
}
|
|
1278
|
+
return !existsSync(resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot));
|
|
1279
|
+
}
|
|
1280
|
+
function buildInstallBrainFeedbackSummary(input) {
|
|
1281
|
+
const providerDefaultsPath = resolveOpenClawBrainProviderDefaultsPath(input.parsed.activationRoot);
|
|
1282
|
+
const embedderState = input.embedderProvision === null ? "unchanged" : input.embedderProvision.state;
|
|
1283
|
+
const teacherDefaults = input.providerDefaults?.defaults.teacher;
|
|
1284
|
+
const teacherProvider = teacherDefaults?.provider ?? "unknown";
|
|
1285
|
+
const teacherModel = teacherDefaults?.model ?? null;
|
|
1286
|
+
const detectedLocalLlm = teacherDefaults?.detectedLocally ?? null;
|
|
1287
|
+
const profileName = input.targetInspection.profileId;
|
|
1288
|
+
const profileSource = input.targetInspection.profileSource;
|
|
1289
|
+
const casingGuidance = profileName === null
|
|
1290
|
+
? "Exact OpenClaw --profile casing is unresolved here because this target stays on the host-selected current_profile boundary."
|
|
1291
|
+
: `Use the exact OpenClaw profile casing shown here for host-side restart/status commands: ${quoteShellArg(profileName)}.`;
|
|
1292
|
+
const attachment = input.parsed.shared
|
|
1293
|
+
? {
|
|
1294
|
+
policy: "shared",
|
|
1295
|
+
activationRootMode: "shared_root_declared",
|
|
1296
|
+
sameGatewayProof: "not_checked_in",
|
|
1297
|
+
detail: "Shared activation root declared. Other profiles may point at this same root, but same-gateway many-profile load/serve proof is not checked in."
|
|
1298
|
+
}
|
|
1299
|
+
: {
|
|
1300
|
+
policy: "dedicated",
|
|
1301
|
+
activationRootMode: "dedicated_per_profile",
|
|
1302
|
+
sameGatewayProof: "not_applicable",
|
|
1303
|
+
detail: "Dedicated activation root for this profile/home boundary."
|
|
1304
|
+
};
|
|
1305
|
+
const restart = profileName === null
|
|
1306
|
+
? {
|
|
1307
|
+
exactProfile: false,
|
|
1308
|
+
profile: null,
|
|
1309
|
+
profileSource,
|
|
1310
|
+
guidance: `Operator-owned restart step: this install did not infer an exact --profile token from ${shortenPath(input.targetInspection.openclawHome)}. ` +
|
|
1311
|
+
"If immediate load matters, restart the host-selected current_profile from OpenClaw itself; otherwise the next natural launch will pick up the hook.",
|
|
1312
|
+
restartCommand: null,
|
|
1313
|
+
gatewayStatusCommand: null
|
|
1314
|
+
}
|
|
1315
|
+
: {
|
|
1316
|
+
exactProfile: true,
|
|
1317
|
+
profile: profileName,
|
|
1318
|
+
profileSource,
|
|
1319
|
+
guidance: `Operator-owned restart step: if immediate load matters and profile ${quoteShellArg(profileName)} is already running, run ${buildGatewayRestartCommand(profileName)}. ` +
|
|
1320
|
+
`If it is stopped, the next launch of profile ${quoteShellArg(profileName)} will pick up the hook. ${casingGuidance}`,
|
|
1321
|
+
restartCommand: buildGatewayRestartCommand(profileName),
|
|
1322
|
+
gatewayStatusCommand: buildGatewayStatusCommand(profileName)
|
|
1323
|
+
};
|
|
1324
|
+
const provedNow = input.activationPlan.action === "bootstrap"
|
|
1325
|
+
? `hook written, activation root ready, seed/current-profile attach bootstrapped, learner service ${input.learnerService.state}, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
|
|
1326
|
+
: `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved, learner service ${input.learnerService.state}${input.providerDefaults === null ? "" : ", provider defaults written"}`;
|
|
1327
|
+
const notYetProved = input.learnerService.state === "deferred"
|
|
1328
|
+
? `OpenClaw has not reloaded this hook yet, and passive learner auto-start was deferred; restart plus status still must prove serve-path load, while learner-service start remains a separate operator check`
|
|
1329
|
+
: input.activationPlan.action === "bootstrap"
|
|
1330
|
+
? `Passive learning is wired for this activation root, but OpenClaw has not reloaded the hook yet; restart plus status still must prove live startup/load and the first exported turn`
|
|
1331
|
+
: `Passive learning is wired for this activation root, but this ${input.parsed.command} run does not itself prove live startup/load after restart`;
|
|
1332
|
+
return {
|
|
1333
|
+
hookPath: input.extensionDir,
|
|
1334
|
+
providerDefaultsPath,
|
|
1335
|
+
profile: {
|
|
1336
|
+
exactProfileName: profileName,
|
|
1337
|
+
profileSource,
|
|
1338
|
+
casingGuidance
|
|
1339
|
+
},
|
|
1340
|
+
attachment,
|
|
1341
|
+
restart,
|
|
1342
|
+
embedder: {
|
|
1343
|
+
provider: "ollama",
|
|
1344
|
+
model: DEFAULT_OLLAMA_EMBEDDING_MODEL,
|
|
1345
|
+
state: embedderState
|
|
1346
|
+
},
|
|
1347
|
+
teacher: {
|
|
1348
|
+
provider: teacherProvider,
|
|
1349
|
+
model: teacherModel,
|
|
1350
|
+
detectedLocalLlm
|
|
1351
|
+
},
|
|
1352
|
+
learnerService: {
|
|
1353
|
+
state: input.learnerService.state,
|
|
1354
|
+
detail: input.learnerService.detail,
|
|
1355
|
+
plistPath: input.learnerService.plistPath,
|
|
1356
|
+
logPath: input.learnerService.logPath,
|
|
1357
|
+
configuredActivationRoot: input.learnerService.configuredActivationRoot,
|
|
1358
|
+
matchesRequestedActivationRoot: input.learnerService.matchesRequestedActivationRoot
|
|
1359
|
+
},
|
|
1360
|
+
startup: {
|
|
1361
|
+
token: "BRAIN_NOT_YET_LOADED",
|
|
1362
|
+
proof: "restart_required"
|
|
1363
|
+
},
|
|
1364
|
+
provedNow,
|
|
1365
|
+
notYetProved,
|
|
1366
|
+
lines: [
|
|
1367
|
+
`target ${formatOpenClawTargetLine(input.targetInspection)} source=${formatInstallOpenClawHomeSource(input.parsed.openclawHomeSource)}`,
|
|
1368
|
+
profileName === null
|
|
1369
|
+
? "profile exactName=unresolved selector=current_profile casing=not_available"
|
|
1370
|
+
: `profile exactName=${quoteShellArg(profileName)} source=${profileSource} casing=preserved`,
|
|
1371
|
+
`hook written=${shortenPath(input.extensionDir)}`,
|
|
1372
|
+
`activation root=${shortenPath(input.parsed.activationRoot)} source=${formatInstallActivationRootSource(input.parsed.activationRootSource)}`,
|
|
1373
|
+
`attachment policy=${attachment.policy} rootMode=${attachment.activationRootMode} sameGatewayProof=${attachment.sameGatewayProof} detail=${attachment.detail}`,
|
|
1374
|
+
`defaults provider-defaults=${shortenPath(providerDefaultsPath)} state=${input.providerDefaults === null ? "unchanged" : "written"}`,
|
|
1375
|
+
`embedder provider=ollama model=${DEFAULT_OLLAMA_EMBEDDING_MODEL} state=${embedderState}`,
|
|
1376
|
+
`teacher provider=${teacherProvider} model=${teacherModel ?? "none"} localLLM=${detectedLocalLlm === null ? "unknown" : yesNo(detectedLocalLlm)}`,
|
|
1377
|
+
`learner state=${input.learnerService.state} detail=${input.learnerService.detail}`,
|
|
1378
|
+
`restart operator=manual exactProfile=${yesNo(restart.exactProfile)} command=${restart.restartCommand ?? "unavailable"}`,
|
|
1379
|
+
"startup BRAIN_NOT_YET_LOADED proof=restart_required",
|
|
1380
|
+
`provedNow ${provedNow}`,
|
|
1381
|
+
`notYet ${notYetProved}`
|
|
1382
|
+
]
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function buildInstallReloadGuidance(input) {
|
|
1386
|
+
if (input.targetInspection.profileId === null) {
|
|
1387
|
+
return `Restart later from OpenClaw for the host-selected current_profile behind ${shortenPath(input.targetInspection.openclawHome)} if immediate load matters; this install did not infer an exact --profile token.`;
|
|
1388
|
+
}
|
|
1389
|
+
return `Restart now if immediate load matters: ${buildGatewayRestartCommand(input.targetInspection.profileId)}`;
|
|
1390
|
+
}
|
|
1391
|
+
const LEGACY_PROFILE_NOTE_FILENAMES = ["BRAIN.md", "brain.md"];
|
|
1392
|
+
const LEGACY_BRAIN_AGENTS_LINE = "5. Read `BRAIN.md` — your learning brain context";
|
|
1393
|
+
function isLegacyBrainAdvisoryContent(content) {
|
|
1394
|
+
return content.includes("## OpenClawBrain")
|
|
1395
|
+
&& content.includes("You have a learning brain attached at ")
|
|
1396
|
+
&& content.includes("openclawbrain status --activation-root")
|
|
1397
|
+
&& content.includes("openclawbrain rollback --activation-root");
|
|
1398
|
+
}
|
|
1399
|
+
function writeUpdatedTextFile(filePath, nextText, previousText) {
|
|
1400
|
+
const normalizedNextText = previousText.endsWith("\n") ? `${nextText}\n` : nextText;
|
|
1401
|
+
writeFileSync(filePath, normalizedNextText, "utf8");
|
|
1402
|
+
}
|
|
1403
|
+
function collectProfileResidueDirs(openclawHome) {
|
|
1404
|
+
const directories = [path.resolve(openclawHome)];
|
|
1405
|
+
try {
|
|
1406
|
+
const entries = readdirSync(openclawHome, { withFileTypes: true });
|
|
1407
|
+
for (const entry of entries) {
|
|
1408
|
+
if (entry.isDirectory() && entry.name.startsWith("workspace-")) {
|
|
1409
|
+
directories.push(path.join(openclawHome, entry.name));
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
catch {
|
|
1414
|
+
// Residue cleanup stays best-effort.
|
|
1415
|
+
}
|
|
1416
|
+
return directories;
|
|
1417
|
+
}
|
|
1418
|
+
function removeLegacyProfileResidue(openclawHome) {
|
|
1419
|
+
const removedNotes = [];
|
|
1420
|
+
const updatedAgents = [];
|
|
1421
|
+
for (const directory of collectProfileResidueDirs(openclawHome)) {
|
|
1422
|
+
for (const fileName of LEGACY_PROFILE_NOTE_FILENAMES) {
|
|
1423
|
+
const notePath = path.join(directory, fileName);
|
|
1424
|
+
if (!existsSync(notePath)) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
const content = readFileSync(notePath, "utf8");
|
|
1429
|
+
if (!isLegacyBrainAdvisoryContent(content)) {
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
catch {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
rmSync(notePath, { force: true });
|
|
1437
|
+
removedNotes.push(notePath);
|
|
1438
|
+
}
|
|
1439
|
+
const agentsPath = path.join(directory, "AGENTS.md");
|
|
1440
|
+
if (!existsSync(agentsPath)) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
let agentsContent;
|
|
1444
|
+
try {
|
|
1445
|
+
agentsContent = readFileSync(agentsPath, "utf8");
|
|
1446
|
+
}
|
|
1447
|
+
catch {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const nextContent = agentsContent
|
|
1451
|
+
.split("\n")
|
|
1452
|
+
.filter((line) => line.trim() !== LEGACY_BRAIN_AGENTS_LINE)
|
|
1453
|
+
.join("\n");
|
|
1454
|
+
if (nextContent !== agentsContent) {
|
|
1455
|
+
writeUpdatedTextFile(agentsPath, nextContent, agentsContent);
|
|
1456
|
+
updatedAgents.push(agentsPath);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
removedNotes,
|
|
1461
|
+
updatedAgents
|
|
1462
|
+
};
|
|
448
1463
|
}
|
|
449
1464
|
function buildCleanupRestartGuidance(restart) {
|
|
450
1465
|
if (restart === "never") {
|
|
@@ -455,7 +1470,7 @@ function buildCleanupRestartGuidance(restart) {
|
|
|
455
1470
|
}
|
|
456
1471
|
return "If this OpenClaw profile is currently running, restart it before expecting the new hook state to take effect. If it is stopped, the next launch will pick it up.";
|
|
457
1472
|
}
|
|
458
|
-
function buildStatusNextStep(status, report) {
|
|
1473
|
+
function buildStatusNextStep(status, report, options) {
|
|
459
1474
|
const activationRootArg = quoteShellArg(status.host.activationRoot);
|
|
460
1475
|
if (status.brainStatus.activationState === "broken_install") {
|
|
461
1476
|
return "Repair or replace the activation root before trusting serve-path status again.";
|
|
@@ -466,9 +1481,18 @@ function buildStatusNextStep(status, report) {
|
|
|
466
1481
|
if (status.brainStatus.status === "fail") {
|
|
467
1482
|
return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
|
|
468
1483
|
}
|
|
1484
|
+
if (options.openclawHome !== null && options.installHook.state === "not_installed") {
|
|
1485
|
+
return `Run \`${buildInstallCommand(options.openclawHome)}\` before expecting this OpenClaw home to load the brain hook.`;
|
|
1486
|
+
}
|
|
469
1487
|
if (status.brainStatus.awaitingFirstExport) {
|
|
470
1488
|
return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
|
|
471
1489
|
}
|
|
1490
|
+
if (options.openclawHome === null) {
|
|
1491
|
+
return `Pin \`--openclaw-home <path>\` when you need exact hook-install truth; activation-root-only status only proves this root's serve-path state.`;
|
|
1492
|
+
}
|
|
1493
|
+
if (options.installHook.state === "installed" && status.brainStatus.serveState === "serving_active_pack") {
|
|
1494
|
+
return "Check the OpenClaw startup log for the `[openclawbrain] BRAIN LOADED` breadcrumb when you need live hook-load proof.";
|
|
1495
|
+
}
|
|
472
1496
|
if (report.learning.warningStates.includes("principal_live_backlog") ||
|
|
473
1497
|
report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
|
|
474
1498
|
return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
|
|
@@ -478,43 +1502,19 @@ function buildStatusNextStep(status, report) {
|
|
|
478
1502
|
}
|
|
479
1503
|
return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
|
|
480
1504
|
}
|
|
481
|
-
function formatHumanFriendlyStatus(status, report) {
|
|
482
|
-
// Brain status line
|
|
483
|
-
const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
|
|
484
|
-
const brainIcon = brainActive ? "Active ✓" : status.brainStatus.status === "fail" ? "Inactive ✗" : `${status.brainStatus.status}`;
|
|
485
|
-
// Pack line
|
|
486
|
-
const packId = status.brain.activePackId ?? "none";
|
|
487
|
-
const packShort = packId.length > 9 ? packId.slice(0, 9) : packId;
|
|
488
|
-
const state = status.brain.state ?? "unknown";
|
|
489
|
-
// Activation root
|
|
490
|
-
const activationPath = shortenPath(status.host.activationRoot);
|
|
491
|
-
// Policy
|
|
492
|
-
const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
|
|
493
|
-
const principalFrontier = formatPrincipalCheckpointFrontier(report);
|
|
494
|
-
const pendingLive = String(report.learning.pendingLive ?? "none");
|
|
495
|
-
const pendingBackfill = String(report.learning.pendingBackfill ?? "none");
|
|
496
|
-
const nextLane = report.learning.nextPriorityLane ?? "none";
|
|
497
|
-
const nextBucket = report.learning.nextPriorityBucket ?? "none";
|
|
1505
|
+
function formatHumanFriendlyStatus(status, report, targetInspection, options) {
|
|
498
1506
|
const lines = [
|
|
499
|
-
`
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
`
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1507
|
+
`STATUS ${status.brainStatus.status}`,
|
|
1508
|
+
...buildCompactStatusHeader(status, report, options),
|
|
1509
|
+
...(targetInspection === null ? [] : [
|
|
1510
|
+
`target ${formatOpenClawTargetLine(targetInspection)}`,
|
|
1511
|
+
`preflight ${formatOpenClawTargetExplanation(targetInspection)}`
|
|
1512
|
+
]),
|
|
1513
|
+
`next ${buildStatusNextStep(status, report, {
|
|
1514
|
+
openclawHome: options.openclawHome,
|
|
1515
|
+
installHook: summarizeStatusInstallHook(options.openclawHome)
|
|
1516
|
+
})}`
|
|
509
1517
|
];
|
|
510
|
-
// Add learning/serve warnings if relevant
|
|
511
|
-
if (report.learning.warningStates.length > 0) {
|
|
512
|
-
lines.push(`Warnings: ${report.learning.warningStates.join(", ")}`);
|
|
513
|
-
}
|
|
514
|
-
if (status.brainStatus.awaitingFirstExport) {
|
|
515
|
-
lines.push(`Note: Awaiting first event export`);
|
|
516
|
-
}
|
|
517
|
-
lines.push(`Next: ${buildStatusNextStep(status, report)}`);
|
|
518
1518
|
return lines.join("\n");
|
|
519
1519
|
}
|
|
520
1520
|
function requireActivationRoot(input, openclawHome, command) {
|
|
@@ -560,10 +1560,18 @@ function formatScanSessionSummary(result) {
|
|
|
560
1560
|
function formatScanLiveSummary(result, snapshotOutPath) {
|
|
561
1561
|
const materializedPackId = result.snapshot.learner.lastMaterialization?.candidate.summary.packId ?? "none";
|
|
562
1562
|
const materializationReason = result.snapshot.learner.lastMaterialization?.reason ?? "none";
|
|
1563
|
+
const teacherSummary = [
|
|
1564
|
+
`artifacts=${result.snapshot.teacher.artifactCount}`,
|
|
1565
|
+
`freshness=${result.snapshot.teacher.latestFreshness}`,
|
|
1566
|
+
`humanLabels=${result.supervision.humanLabelCount}`
|
|
1567
|
+
];
|
|
1568
|
+
if (result.snapshot.diagnostics.lastNoOpReason !== "none") {
|
|
1569
|
+
teacherSummary.push(`noop=${result.snapshot.diagnostics.lastNoOpReason}`);
|
|
1570
|
+
}
|
|
563
1571
|
return [
|
|
564
1572
|
"SCAN live ok",
|
|
565
1573
|
`source digest=${result.supervision.exportDigest} session=${result.supervision.sessionId ?? "none"} channel=${result.supervision.channel ?? "none"} range=${result.supervision.eventRange.start}-${result.supervision.eventRange.end}/${result.supervision.eventRange.count}`,
|
|
566
|
-
`teacher
|
|
1574
|
+
`teacher ${teacherSummary.join(" ")}`,
|
|
567
1575
|
`labels source=${result.labelFlow.source} human=${result.labelFlow.humanLabelCount ?? "none"} self=${result.labelFlow.selfLabelCount ?? "none"} implicitPositive=${result.labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${result.labelFlow.asyncTeacherArtifactCount ?? "none"}`,
|
|
568
1576
|
`path source=${result.learningPath.source} pg=${result.learningPath.policyGradientVersion} method=${result.learningPath.policyGradientMethod ?? "none"} target=${result.learningPath.targetConstruction ?? "none"} connect=${result.learningPath.connectOpsFired ?? "none"} trajectories=${result.learningPath.reconstructedTrajectoryCount ?? "none"}`,
|
|
569
1577
|
`learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
|
|
@@ -583,12 +1591,12 @@ export function parseOperatorCliArgs(argv) {
|
|
|
583
1591
|
let rootDir = null;
|
|
584
1592
|
let workspacePath = null;
|
|
585
1593
|
let packLabel = null;
|
|
586
|
-
let packRoot = null;
|
|
587
1594
|
let workspaceId = null;
|
|
588
1595
|
let observedAt = null;
|
|
589
1596
|
let snapshotOutPath = null;
|
|
590
1597
|
let openclawHome = null;
|
|
591
1598
|
let shared = false;
|
|
1599
|
+
let skipEmbedderProvision = false;
|
|
592
1600
|
let keepData = false;
|
|
593
1601
|
let purgeData = false;
|
|
594
1602
|
let restart = "safe";
|
|
@@ -992,6 +2000,10 @@ export function parseOperatorCliArgs(argv) {
|
|
|
992
2000
|
shared = true;
|
|
993
2001
|
continue;
|
|
994
2002
|
}
|
|
2003
|
+
if (arg === "--skip-embedder-provision") {
|
|
2004
|
+
skipEmbedderProvision = true;
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
995
2007
|
if (arg === "--keep-data") {
|
|
996
2008
|
keepData = true;
|
|
997
2009
|
continue;
|
|
@@ -1125,14 +2137,6 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1125
2137
|
index += 1;
|
|
1126
2138
|
continue;
|
|
1127
2139
|
}
|
|
1128
|
-
if (arg === "--pack-root") {
|
|
1129
|
-
if (next === undefined) {
|
|
1130
|
-
throw new Error("--pack-root requires a value");
|
|
1131
|
-
}
|
|
1132
|
-
packRoot = next;
|
|
1133
|
-
index += 1;
|
|
1134
|
-
continue;
|
|
1135
|
-
}
|
|
1136
2140
|
if (arg === "--workspace-id") {
|
|
1137
2141
|
if (next === undefined) {
|
|
1138
2142
|
throw new Error("--workspace-id requires a value");
|
|
@@ -1146,13 +2150,64 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1146
2150
|
if (command !== "detach" && command !== "uninstall" && restartExplicitlySet) {
|
|
1147
2151
|
throw new Error("--restart only applies to detach/uninstall");
|
|
1148
2152
|
}
|
|
2153
|
+
if (command !== "install" && command !== "attach" && shared) {
|
|
2154
|
+
throw new Error("--shared only applies to install/attach");
|
|
2155
|
+
}
|
|
2156
|
+
if (command !== "install" && command !== "attach" && skipEmbedderProvision) {
|
|
2157
|
+
throw new Error("--skip-embedder-provision only applies to install/attach");
|
|
2158
|
+
}
|
|
1149
2159
|
if (command !== "uninstall" && keepData) {
|
|
1150
2160
|
throw new Error("--keep-data only applies to uninstall; use detach to preserve activation data");
|
|
1151
2161
|
}
|
|
1152
2162
|
if (command !== "uninstall" && purgeData) {
|
|
1153
2163
|
throw new Error("--purge-data only applies to uninstall");
|
|
1154
2164
|
}
|
|
1155
|
-
if (command
|
|
2165
|
+
if (command !== "install" && command !== "attach" && workspaceId !== null) {
|
|
2166
|
+
throw new Error("--workspace-id only applies to install/attach");
|
|
2167
|
+
}
|
|
2168
|
+
if (command !== "scan" && packLabel !== null) {
|
|
2169
|
+
throw new Error("--pack-label only applies to scan --live");
|
|
2170
|
+
}
|
|
2171
|
+
if ((command === "install" || command === "attach") && brainAttachmentPolicy !== null) {
|
|
2172
|
+
throw new Error(`${command} uses dedicated by default or --shared for shared mode; --brain-attachment-policy only applies to status/rollback inspection`);
|
|
2173
|
+
}
|
|
2174
|
+
if (command === "install") {
|
|
2175
|
+
if (help) {
|
|
2176
|
+
return {
|
|
2177
|
+
command,
|
|
2178
|
+
openclawHome: "",
|
|
2179
|
+
openclawHomeSource: "explicit",
|
|
2180
|
+
activationRoot: "",
|
|
2181
|
+
activationRootSource: "explicit",
|
|
2182
|
+
shared: false,
|
|
2183
|
+
workspaceId: "",
|
|
2184
|
+
workspaceIdSource: "explicit",
|
|
2185
|
+
skipEmbedderProvision: false,
|
|
2186
|
+
skipEmbedderProvisionSource: null,
|
|
2187
|
+
json,
|
|
2188
|
+
help
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
|
|
2192
|
+
const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
|
|
2193
|
+
const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
|
|
2194
|
+
const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
|
|
2195
|
+
return {
|
|
2196
|
+
command,
|
|
2197
|
+
openclawHome: resolvedOpenclawHome.openclawHome,
|
|
2198
|
+
openclawHomeSource: resolvedOpenclawHome.openclawHomeSource,
|
|
2199
|
+
activationRoot: resolvedActivationRoot.activationRoot,
|
|
2200
|
+
activationRootSource: resolvedActivationRoot.source,
|
|
2201
|
+
shared,
|
|
2202
|
+
workspaceId: resolvedWorkspaceId.workspaceId,
|
|
2203
|
+
workspaceIdSource: resolvedWorkspaceId.source,
|
|
2204
|
+
skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
|
|
2205
|
+
skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
|
|
2206
|
+
json,
|
|
2207
|
+
help
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
if (command === "attach") {
|
|
1156
2211
|
if (help) {
|
|
1157
2212
|
return {
|
|
1158
2213
|
command,
|
|
@@ -1163,22 +2218,30 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1163
2218
|
shared: false,
|
|
1164
2219
|
workspaceId: "",
|
|
1165
2220
|
workspaceIdSource: "explicit",
|
|
2221
|
+
skipEmbedderProvision: false,
|
|
2222
|
+
skipEmbedderProvisionSource: null,
|
|
1166
2223
|
json,
|
|
1167
2224
|
help
|
|
1168
2225
|
};
|
|
1169
2226
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
2227
|
+
if (openclawHome === null || openclawHome.trim().length === 0) {
|
|
2228
|
+
throw new Error("--openclaw-home is required for attach; use install for the first-time default path");
|
|
2229
|
+
}
|
|
2230
|
+
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
2231
|
+
const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome, activationRoot);
|
|
2232
|
+
const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome, workspaceId);
|
|
2233
|
+
const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
|
|
1173
2234
|
return {
|
|
1174
2235
|
command,
|
|
1175
|
-
openclawHome: resolvedOpenclawHome
|
|
1176
|
-
openclawHomeSource:
|
|
2236
|
+
openclawHome: resolvedOpenclawHome,
|
|
2237
|
+
openclawHomeSource: "explicit",
|
|
1177
2238
|
activationRoot: resolvedActivationRoot.activationRoot,
|
|
1178
2239
|
activationRootSource: resolvedActivationRoot.source,
|
|
1179
2240
|
shared,
|
|
1180
2241
|
workspaceId: resolvedWorkspaceId.workspaceId,
|
|
1181
2242
|
workspaceIdSource: resolvedWorkspaceId.source,
|
|
2243
|
+
skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
|
|
2244
|
+
skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
|
|
1182
2245
|
json,
|
|
1183
2246
|
help
|
|
1184
2247
|
};
|
|
@@ -1240,30 +2303,6 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1240
2303
|
help
|
|
1241
2304
|
};
|
|
1242
2305
|
}
|
|
1243
|
-
if (command === "attach") {
|
|
1244
|
-
if (help) {
|
|
1245
|
-
return { command, activationRoot: "", packRoot: "", packLabel: "", workspaceId: "", brainAttachmentPolicy: null, json, help };
|
|
1246
|
-
}
|
|
1247
|
-
if (activationRoot === null || activationRoot.trim().length === 0) {
|
|
1248
|
-
throw new Error("--activation-root is required for attach");
|
|
1249
|
-
}
|
|
1250
|
-
const resolvedActivationRoot = path.resolve(activationRoot);
|
|
1251
|
-
const resolvedPackRoot = packRoot !== null
|
|
1252
|
-
? path.resolve(packRoot)
|
|
1253
|
-
: path.resolve(resolvedActivationRoot, "packs", "initial");
|
|
1254
|
-
const resolvedWorkspaceId = workspaceId ?? "workspace";
|
|
1255
|
-
const resolvedPackLabel = packLabel ?? "cli-attach";
|
|
1256
|
-
return {
|
|
1257
|
-
command,
|
|
1258
|
-
activationRoot: resolvedActivationRoot,
|
|
1259
|
-
packRoot: resolvedPackRoot,
|
|
1260
|
-
packLabel: resolvedPackLabel,
|
|
1261
|
-
workspaceId: resolvedWorkspaceId,
|
|
1262
|
-
brainAttachmentPolicy: brainAttachmentPolicy,
|
|
1263
|
-
json,
|
|
1264
|
-
help
|
|
1265
|
-
};
|
|
1266
|
-
}
|
|
1267
2306
|
if (command === "scan") {
|
|
1268
2307
|
if ((sessionPath === null && livePath === null) || (sessionPath !== null && livePath !== null)) {
|
|
1269
2308
|
throw new Error("scan requires exactly one of --session or --live");
|
|
@@ -1347,6 +2386,7 @@ function resolveExtensionRuntimeGuardPath() {
|
|
|
1347
2386
|
path.resolve(__dirname, "..", "..", "extension", "runtime-guard.ts"),
|
|
1348
2387
|
];
|
|
1349
2388
|
const jsCandidates = [
|
|
2389
|
+
path.resolve(__dirname, "..", "dist", "extension", "runtime-guard.js"),
|
|
1350
2390
|
path.resolve(__dirname, "extension", "runtime-guard.js"),
|
|
1351
2391
|
path.resolve(__dirname, "..", "extension", "runtime-guard.js"),
|
|
1352
2392
|
];
|
|
@@ -1473,7 +2513,7 @@ function buildExtensionIndexTs(activationRoot) {
|
|
|
1473
2513
|
function buildExtensionPackageJson() {
|
|
1474
2514
|
const packageMetadata = readOpenClawPackageMetadata();
|
|
1475
2515
|
return JSON.stringify({
|
|
1476
|
-
name: "openclawbrain
|
|
2516
|
+
name: "openclawbrain",
|
|
1477
2517
|
version: packageMetadata.version,
|
|
1478
2518
|
private: true,
|
|
1479
2519
|
type: "module",
|
|
@@ -1576,6 +2616,56 @@ function buildHistoryEntry(record, slot, isActive) {
|
|
|
1576
2616
|
current: isActive
|
|
1577
2617
|
};
|
|
1578
2618
|
}
|
|
2619
|
+
function ensureLifecycleLearnerService(activationRoot) {
|
|
2620
|
+
const outcome = ensureManagedLearnerServiceForActivationRoot(activationRoot);
|
|
2621
|
+
return {
|
|
2622
|
+
state: outcome.state,
|
|
2623
|
+
detail: outcome.detail,
|
|
2624
|
+
plistPath: outcome.inspection.plistPath,
|
|
2625
|
+
logPath: outcome.inspection.logPath,
|
|
2626
|
+
configuredActivationRoot: outcome.inspection.configuredActivationRoot,
|
|
2627
|
+
matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
function resolveCleanupLearnerServiceOutcome(activationRoot, openclawHome) {
|
|
2631
|
+
if (activationRoot === null) {
|
|
2632
|
+
return {
|
|
2633
|
+
state: "unresolved",
|
|
2634
|
+
detail: "Learner service preservation is unresolved because the activation root could not be resolved from the installed profile hook.",
|
|
2635
|
+
plistPath: null,
|
|
2636
|
+
logPath: null,
|
|
2637
|
+
configuredActivationRoot: null,
|
|
2638
|
+
matchesRequestedActivationRoot: null
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
const remainingProfiles = findOtherInstalledHookReferencesForActivationRoot({
|
|
2642
|
+
activationRoot,
|
|
2643
|
+
excludingOpenClawHome: openclawHome
|
|
2644
|
+
});
|
|
2645
|
+
if (remainingProfiles.length > 0) {
|
|
2646
|
+
const inspection = inspectManagedLearnerService(activationRoot);
|
|
2647
|
+
const attachedProfiles = remainingProfiles
|
|
2648
|
+
.map(({ openclawHome: profileHome }) => shortenPath(path.resolve(profileHome)))
|
|
2649
|
+
.join(", ");
|
|
2650
|
+
return {
|
|
2651
|
+
state: "preserved",
|
|
2652
|
+
detail: `Preserved the background learner service for ${path.resolve(activationRoot)} because other attached OpenClaw profiles still share this activation root: ${attachedProfiles}.`,
|
|
2653
|
+
plistPath: inspection.plistPath,
|
|
2654
|
+
logPath: inspection.logPath,
|
|
2655
|
+
configuredActivationRoot: inspection.configuredActivationRoot,
|
|
2656
|
+
matchesRequestedActivationRoot: inspection.matchesRequestedActivationRoot
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
const outcome = removeManagedLearnerServiceForActivationRoot(activationRoot);
|
|
2660
|
+
return {
|
|
2661
|
+
state: outcome.state,
|
|
2662
|
+
detail: outcome.detail,
|
|
2663
|
+
plistPath: outcome.inspection.plistPath,
|
|
2664
|
+
logPath: outcome.inspection.logPath,
|
|
2665
|
+
configuredActivationRoot: outcome.inspection.configuredActivationRoot,
|
|
2666
|
+
matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
1579
2669
|
function formatInspectionFindings(findings) {
|
|
1580
2670
|
return findings.join("; ");
|
|
1581
2671
|
}
|
|
@@ -1736,15 +2826,33 @@ function runHistoryCommand(parsed) {
|
|
|
1736
2826
|
}
|
|
1737
2827
|
return 0;
|
|
1738
2828
|
}
|
|
1739
|
-
function
|
|
2829
|
+
function runProfileHookAttachCommand(parsed) {
|
|
1740
2830
|
const steps = [];
|
|
1741
2831
|
const commandLabel = parsed.command.toUpperCase();
|
|
1742
|
-
|
|
2832
|
+
const isInstall = parsed.command === "install";
|
|
2833
|
+
const targetInspection = inspectOpenClawHome(parsed.openclawHome);
|
|
2834
|
+
const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
|
|
2835
|
+
steps.push(`Target OpenClaw home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
|
|
2836
|
+
steps.push(isInstall
|
|
2837
|
+
? "Lifecycle mode: install is the safe first-time default for wiring one profile to one activation root."
|
|
2838
|
+
: "Lifecycle mode: attach is the explicit reattach/manual profile-hook path; use install for first-time setup.");
|
|
2839
|
+
steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
2840
|
+
steps.push(`Target hook path: ${extensionDir}`);
|
|
1743
2841
|
// 1. Validate --openclaw-home exists and has openclaw.json
|
|
1744
2842
|
validateOpenClawHome(parsed.openclawHome);
|
|
1745
2843
|
// 2. Inspect the activation root before writing profile hook artifacts.
|
|
1746
2844
|
const activationPlan = inspectInstallActivationPlan(parsed);
|
|
1747
|
-
// 3.
|
|
2845
|
+
// 3. Ensure the default embedder exists before bootstrap unless the operator explicitly opts out.
|
|
2846
|
+
const embedderProvision = activationPlan.action === "bootstrap"
|
|
2847
|
+
? ensureInstallEmbedderReady(parsed)
|
|
2848
|
+
: null;
|
|
2849
|
+
if (embedderProvision === null) {
|
|
2850
|
+
steps.push("Skipped bootstrap-time embedder provisioning because attach/install is reusing healthy activation state.");
|
|
2851
|
+
}
|
|
2852
|
+
else {
|
|
2853
|
+
steps.push(embedderProvision.detail);
|
|
2854
|
+
}
|
|
2855
|
+
// 4. Create activation root if needed
|
|
1748
2856
|
if (activationPlan.createActivationRoot) {
|
|
1749
2857
|
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1750
2858
|
steps.push(`Created activation root: ${parsed.activationRoot}`);
|
|
@@ -1753,7 +2861,17 @@ function runInstallCommand(parsed) {
|
|
|
1753
2861
|
steps.push(`Activation root exists: ${parsed.activationRoot}`);
|
|
1754
2862
|
}
|
|
1755
2863
|
steps.push(activationPlan.inspectionStep);
|
|
1756
|
-
//
|
|
2864
|
+
// 5. Persist install-written local provider defaults so watch/learning surfaces do not depend on gateway env wiring.
|
|
2865
|
+
const providerDefaults = shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall)
|
|
2866
|
+
? writeInstallProviderDefaults(parsed)
|
|
2867
|
+
: null;
|
|
2868
|
+
if (providerDefaults === null) {
|
|
2869
|
+
steps.push("Preserved existing provider-defaults.json because explicit attach is reusing existing activation data.");
|
|
2870
|
+
}
|
|
2871
|
+
else {
|
|
2872
|
+
steps.push(providerDefaults.detail);
|
|
2873
|
+
}
|
|
2874
|
+
// 6. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
|
|
1757
2875
|
if (activationPlan.action === "bootstrap") {
|
|
1758
2876
|
const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
|
|
1759
2877
|
mkdirSync(packRoot, { recursive: true });
|
|
@@ -1763,13 +2881,13 @@ function runInstallCommand(parsed) {
|
|
|
1763
2881
|
brainAttachmentPolicy,
|
|
1764
2882
|
activationRoot: parsed.activationRoot,
|
|
1765
2883
|
packRoot,
|
|
1766
|
-
packLabel: "install-cli",
|
|
2884
|
+
packLabel: isInstall ? "install-cli" : "attach-cli",
|
|
1767
2885
|
workspace: {
|
|
1768
2886
|
workspaceId: parsed.workspaceId,
|
|
1769
|
-
snapshotId: `${parsed.workspaceId}
|
|
2887
|
+
snapshotId: `${parsed.workspaceId}@${parsed.command}-${new Date().toISOString().slice(0, 10)}`,
|
|
1770
2888
|
capturedAt: new Date().toISOString(),
|
|
1771
2889
|
rootDir: parsed.openclawHome,
|
|
1772
|
-
revision: "cli-install-v1"
|
|
2890
|
+
revision: isInstall ? "cli-install-v1" : "cli-attach-v1"
|
|
1773
2891
|
},
|
|
1774
2892
|
interactionEvents: [],
|
|
1775
2893
|
feedbackEvents: []
|
|
@@ -1777,10 +2895,11 @@ function runInstallCommand(parsed) {
|
|
|
1777
2895
|
steps.push(`Bootstrapped brain attach: state=${result.currentProfile.brain.state} awaitingFirstExport=${yesNo(result.currentProfile.brainStatus.awaitingFirstExport)}`);
|
|
1778
2896
|
}
|
|
1779
2897
|
else {
|
|
1780
|
-
steps.push(
|
|
2898
|
+
steps.push(isInstall
|
|
2899
|
+
? `Kept inspected activation state: active pack ${activationPlan.activePackId}`
|
|
2900
|
+
: `Reused inspected activation state for explicit attach: active pack ${activationPlan.activePackId}`);
|
|
1781
2901
|
}
|
|
1782
|
-
//
|
|
1783
|
-
const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
|
|
2902
|
+
// 7-10. Write extension files
|
|
1784
2903
|
mkdirSync(extensionDir, { recursive: true });
|
|
1785
2904
|
// 5. Write index.ts
|
|
1786
2905
|
const indexTsPath = path.join(extensionDir, "index.ts");
|
|
@@ -1833,92 +2952,67 @@ function runInstallCommand(parsed) {
|
|
|
1833
2952
|
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
1834
2953
|
writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
|
|
1835
2954
|
steps.push(`Wrote manifest: ${manifestPath}`);
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
const entries = readdirSync(parsed.openclawHome, { withFileTypes: true });
|
|
1851
|
-
const workspaceDirs = entries
|
|
1852
|
-
.filter(e => e.isDirectory() && e.name.startsWith("workspace-"))
|
|
1853
|
-
.map(e => path.join(parsed.openclawHome, e.name));
|
|
1854
|
-
// If no workspace-* dirs found, check if openclawHome itself is a workspace
|
|
1855
|
-
if (workspaceDirs.length === 0) {
|
|
1856
|
-
workspaceDirs.push(parsed.openclawHome);
|
|
1857
|
-
}
|
|
1858
|
-
for (const wsDir of workspaceDirs) {
|
|
1859
|
-
const brainMdPath = path.join(wsDir, "BRAIN.md");
|
|
1860
|
-
writeFileSync(brainMdPath, brainMdContent, "utf8");
|
|
1861
|
-
steps.push(`Wrote BRAIN.md: ${brainMdPath}`);
|
|
1862
|
-
// If AGENTS.md exists, append brain reference to startup sequence
|
|
1863
|
-
const agentsMdPath = path.join(wsDir, "AGENTS.md");
|
|
1864
|
-
if (existsSync(agentsMdPath)) {
|
|
1865
|
-
const agentsContent = readFileSync(agentsMdPath, "utf8");
|
|
1866
|
-
if (!agentsContent.includes("BRAIN.md")) {
|
|
1867
|
-
// Find the startup sequence section and append after the last numbered item
|
|
1868
|
-
const startupMarker = "## Session Startup";
|
|
1869
|
-
if (agentsContent.includes(startupMarker)) {
|
|
1870
|
-
// Find the numbered list in the startup section and append after last item
|
|
1871
|
-
const lines = agentsContent.split("\n");
|
|
1872
|
-
let lastNumberedIdx = -1;
|
|
1873
|
-
let inStartup = false;
|
|
1874
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1875
|
-
const line = lines[i] ?? "";
|
|
1876
|
-
if (line.includes(startupMarker)) {
|
|
1877
|
-
inStartup = true;
|
|
1878
|
-
continue;
|
|
1879
|
-
}
|
|
1880
|
-
if (inStartup && /^\d+\.\s/.test(line.trim())) {
|
|
1881
|
-
lastNumberedIdx = i;
|
|
1882
|
-
}
|
|
1883
|
-
if (inStartup && line.startsWith("## ") && !line.includes(startupMarker)) {
|
|
1884
|
-
break;
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
if (lastNumberedIdx >= 0) {
|
|
1888
|
-
lines.splice(lastNumberedIdx + 1, 0, agentsMdBrainRef.trimEnd());
|
|
1889
|
-
writeFileSync(agentsMdPath, lines.join("\n"), "utf8");
|
|
1890
|
-
steps.push(`Updated AGENTS.md startup sequence: ${agentsMdPath}`);
|
|
1891
|
-
}
|
|
1892
|
-
else {
|
|
1893
|
-
appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
|
|
1894
|
-
steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
else {
|
|
1898
|
-
appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
|
|
1899
|
-
steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
else {
|
|
1903
|
-
steps.push(`AGENTS.md already references BRAIN.md: ${agentsMdPath}`);
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
catch (err) {
|
|
1909
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1910
|
-
steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
|
|
1911
|
-
}
|
|
1912
|
-
const restartGuidance = buildInstallReloadGuidance();
|
|
2955
|
+
const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
|
|
2956
|
+
steps.push(learnerService.detail);
|
|
2957
|
+
const brainFeedback = buildInstallBrainFeedbackSummary({
|
|
2958
|
+
parsed,
|
|
2959
|
+
targetInspection,
|
|
2960
|
+
extensionDir,
|
|
2961
|
+
activationPlan,
|
|
2962
|
+
learnerService,
|
|
2963
|
+
embedderProvision,
|
|
2964
|
+
providerDefaults
|
|
2965
|
+
});
|
|
2966
|
+
const restartGuidance = buildInstallReloadGuidance({
|
|
2967
|
+
targetInspection
|
|
2968
|
+
});
|
|
1913
2969
|
const nextSteps = [
|
|
1914
2970
|
restartGuidance,
|
|
1915
|
-
|
|
2971
|
+
brainFeedback.restart.gatewayStatusCommand === null
|
|
2972
|
+
? null
|
|
2973
|
+
: `Confirm gateway after restart: ${brainFeedback.restart.gatewayStatusCommand}`,
|
|
2974
|
+
`Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`,
|
|
2975
|
+
`Check learner service: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`,
|
|
2976
|
+
embedderProvision !== null && embedderProvision.state === "skipped"
|
|
2977
|
+
? `Provision default embedder later: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`
|
|
2978
|
+
: null
|
|
2979
|
+
].filter((step) => step !== null);
|
|
2980
|
+
const preflightSummary = [
|
|
2981
|
+
`Hook: installed at ${shortenPath(extensionDir)}`,
|
|
2982
|
+
parsed.shared
|
|
2983
|
+
? "Attachment policy: shared activation root declared; same-gateway many-profile load/serve proof is still not checked in"
|
|
2984
|
+
: "Attachment policy: dedicated activation root for this profile/home boundary",
|
|
2985
|
+
activationPlan.action === "bootstrap"
|
|
2986
|
+
? "Attachment: seed/current-profile attach created; restart plus status will prove later serve-path use"
|
|
2987
|
+
: `Attachment: existing active pack ${activationPlan.activePackId} kept in place; restart plus status will prove later serve-path use`,
|
|
2988
|
+
embedderProvision === null
|
|
2989
|
+
? "Embedder: unchanged because no bootstrap was needed"
|
|
2990
|
+
: embedderProvision.state === "ensured"
|
|
2991
|
+
? `Embedder: default Ollama model ${embedderProvision.model} was ensured before bootstrap`
|
|
2992
|
+
: `Embedder: default Ollama model ${embedderProvision.model} was intentionally skipped`,
|
|
2993
|
+
`Learner: background service ${learnerService.state} for the exact activation root/profile boundary`,
|
|
2994
|
+
`Serve path: install alone does not prove serving; restart the profile and run ${buildInstallStatusCommand(parsed.activationRoot)}`
|
|
1916
2995
|
];
|
|
1917
2996
|
const lifecycleSummary = [
|
|
1918
|
-
|
|
2997
|
+
isInstall
|
|
2998
|
+
? "Lifecycle mode: install (safe first-time/default profile hookup)"
|
|
2999
|
+
: "Lifecycle mode: attach (explicit reattach/manual profile hookup)",
|
|
3000
|
+
`OpenClaw target: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
|
|
3001
|
+
`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`,
|
|
3002
|
+
brainFeedback.profile.exactProfileName === null
|
|
3003
|
+
? "Profile token: current_profile only; this install did not infer an exact --profile token"
|
|
3004
|
+
: `Profile token: use exact OpenClaw profile casing ${quoteShellArg(brainFeedback.profile.exactProfileName)} for host-side restart/status commands`,
|
|
1919
3005
|
`Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
|
|
3006
|
+
`Attachment policy: ${brainFeedback.attachment.policy} (${brainFeedback.attachment.detail})`,
|
|
1920
3007
|
`Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
|
|
3008
|
+
embedderProvision === null
|
|
3009
|
+
? "Embedder: unchanged because no bootstrap was needed"
|
|
3010
|
+
: embedderProvision.state === "ensured"
|
|
3011
|
+
? `Embedder: ensured default Ollama model ${embedderProvision.model} before brain init`
|
|
3012
|
+
: `Embedder: skipped default Ollama model ${embedderProvision.model} via ${parsed.skipEmbedderProvisionSource === "flag" ? "--skip-embedder-provision" : OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}`,
|
|
3013
|
+
...(providerDefaults === null ? [] : [`${providerDefaults.lifecycleSummary} (${shortenPath(providerDefaults.path)})`]),
|
|
1921
3014
|
`Profile hook: installed at ${shortenPath(extensionDir)}`,
|
|
3015
|
+
`Learner service: ${learnerService.state} for ${shortenPath(parsed.activationRoot)}`,
|
|
1922
3016
|
activationPlan.resolution === "new_root"
|
|
1923
3017
|
? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
|
|
1924
3018
|
: activationPlan.resolution === "missing_pointers"
|
|
@@ -1928,11 +3022,13 @@ function runInstallCommand(parsed) {
|
|
|
1928
3022
|
: `Activation data: reused healthy state at ${shortenPath(parsed.activationRoot)}`,
|
|
1929
3023
|
activationPlan.action === "bootstrap"
|
|
1930
3024
|
? activationPlan.resolution === "new_root"
|
|
1931
|
-
? "
|
|
3025
|
+
? `${isInstall ? "Install" : "Attach"}: bootstrapped a seed/current-profile brain`
|
|
1932
3026
|
: activationPlan.resolution === "missing_pointers"
|
|
1933
|
-
? "
|
|
1934
|
-
: "
|
|
1935
|
-
:
|
|
3027
|
+
? `${isInstall ? "Install" : "Attach"}: repaired missing activation pointers and bootstrapped a seed/current-profile brain`
|
|
3028
|
+
: `${isInstall ? "Install" : "Attach"}: repaired empty activation pointers and bootstrapped a seed/current-profile brain`
|
|
3029
|
+
: isInstall
|
|
3030
|
+
? `Install: kept healthy active pack ${activationPlan.activePackId} in place`
|
|
3031
|
+
: `Attach: rewired the profile hook to healthy active pack ${activationPlan.activePackId}`
|
|
1936
3032
|
];
|
|
1937
3033
|
// 9. Print summary
|
|
1938
3034
|
if (parsed.json) {
|
|
@@ -1940,6 +3036,13 @@ function runInstallCommand(parsed) {
|
|
|
1940
3036
|
command: parsed.command,
|
|
1941
3037
|
openclawHome: parsed.openclawHome,
|
|
1942
3038
|
openclawHomeSource: parsed.openclawHomeSource,
|
|
3039
|
+
openclawTarget: {
|
|
3040
|
+
layout: targetInspection.layout,
|
|
3041
|
+
detail: describeOpenClawHomeInspection(targetInspection),
|
|
3042
|
+
profileId: targetInspection.profileId,
|
|
3043
|
+
profileSource: targetInspection.profileSource,
|
|
3044
|
+
configuredProfileIds: targetInspection.configuredProfileIds
|
|
3045
|
+
},
|
|
1943
3046
|
activationRoot: parsed.activationRoot,
|
|
1944
3047
|
resolvedInputs: {
|
|
1945
3048
|
activationRoot: {
|
|
@@ -1953,8 +3056,52 @@ function runInstallCommand(parsed) {
|
|
|
1953
3056
|
},
|
|
1954
3057
|
workspaceId: parsed.workspaceId,
|
|
1955
3058
|
shared: parsed.shared,
|
|
3059
|
+
embedderProvision: embedderProvision === null
|
|
3060
|
+
? null
|
|
3061
|
+
: {
|
|
3062
|
+
skipped: parsed.skipEmbedderProvision,
|
|
3063
|
+
source: parsed.skipEmbedderProvisionSource,
|
|
3064
|
+
model: embedderProvision.model,
|
|
3065
|
+
baseUrl: embedderProvision.baseUrl
|
|
3066
|
+
},
|
|
3067
|
+
providerDefaults: providerDefaults === null
|
|
3068
|
+
? null
|
|
3069
|
+
: {
|
|
3070
|
+
path: providerDefaults.path,
|
|
3071
|
+
teacher: providerDefaults.defaults.teacher === undefined
|
|
3072
|
+
? null
|
|
3073
|
+
: {
|
|
3074
|
+
provider: providerDefaults.defaults.teacher.provider ?? null,
|
|
3075
|
+
model: providerDefaults.defaults.teacher.model ?? null,
|
|
3076
|
+
detectedLocally: providerDefaults.defaults.teacher.detectedLocally ?? false
|
|
3077
|
+
},
|
|
3078
|
+
embedder: providerDefaults.defaults.embedder === undefined
|
|
3079
|
+
? null
|
|
3080
|
+
: {
|
|
3081
|
+
provider: providerDefaults.defaults.embedder.provider ?? null,
|
|
3082
|
+
model: providerDefaults.defaults.embedder.model ?? null
|
|
3083
|
+
},
|
|
3084
|
+
teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
|
|
3085
|
+
embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
|
|
3086
|
+
},
|
|
3087
|
+
learnerService,
|
|
3088
|
+
brainFeedback: {
|
|
3089
|
+
hookPath: brainFeedback.hookPath,
|
|
3090
|
+
providerDefaultsPath: brainFeedback.providerDefaultsPath,
|
|
3091
|
+
profile: brainFeedback.profile,
|
|
3092
|
+
attachment: brainFeedback.attachment,
|
|
3093
|
+
restart: brainFeedback.restart,
|
|
3094
|
+
embedder: brainFeedback.embedder,
|
|
3095
|
+
teacher: brainFeedback.teacher,
|
|
3096
|
+
learnerService: brainFeedback.learnerService,
|
|
3097
|
+
startup: brainFeedback.startup,
|
|
3098
|
+
provedNow: brainFeedback.provedNow,
|
|
3099
|
+
notYetProved: brainFeedback.notYetProved,
|
|
3100
|
+
lines: brainFeedback.lines
|
|
3101
|
+
},
|
|
1956
3102
|
extensionDir,
|
|
1957
3103
|
lifecycleSummary,
|
|
3104
|
+
preflightSummary,
|
|
1958
3105
|
restartGuidance,
|
|
1959
3106
|
nextSteps,
|
|
1960
3107
|
steps
|
|
@@ -1962,19 +3109,28 @@ function runInstallCommand(parsed) {
|
|
|
1962
3109
|
}
|
|
1963
3110
|
else {
|
|
1964
3111
|
console.log(`${commandLabel} complete\n`);
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1968
|
-
console.log("");
|
|
1969
|
-
console.log("Lifecycle:");
|
|
1970
|
-
for (const line of lifecycleSummary) {
|
|
3112
|
+
console.log("Brain feedback:");
|
|
3113
|
+
for (const line of brainFeedback.lines) {
|
|
1971
3114
|
console.log(` ${line}`);
|
|
1972
3115
|
}
|
|
1973
|
-
console.log(`
|
|
3116
|
+
console.log(`Restart: ${restartGuidance}`);
|
|
3117
|
+
if (brainFeedback.restart.gatewayStatusCommand !== null) {
|
|
3118
|
+
console.log(`Gateway: Confirm OpenClaw after restart: ${brainFeedback.restart.gatewayStatusCommand}`);
|
|
3119
|
+
}
|
|
1974
3120
|
console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
|
|
3121
|
+
console.log(`Learner: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`);
|
|
3122
|
+
if (embedderProvision !== null && embedderProvision.state === "skipped") {
|
|
3123
|
+
console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
|
|
3124
|
+
}
|
|
1975
3125
|
}
|
|
1976
3126
|
return 0;
|
|
1977
3127
|
}
|
|
3128
|
+
function runInstallCommand(parsed) {
|
|
3129
|
+
return runProfileHookAttachCommand(parsed);
|
|
3130
|
+
}
|
|
3131
|
+
function runAttachCommand(parsed) {
|
|
3132
|
+
return runProfileHookAttachCommand(parsed);
|
|
3133
|
+
}
|
|
1978
3134
|
function validateOpenClawHome(openclawHome) {
|
|
1979
3135
|
if (!existsSync(openclawHome)) {
|
|
1980
3136
|
throw new Error(`--openclaw-home directory does not exist: ${openclawHome}`);
|
|
@@ -1984,6 +3140,85 @@ function validateOpenClawHome(openclawHome) {
|
|
|
1984
3140
|
throw new Error(`openclaw.json not found in ${openclawHome}`);
|
|
1985
3141
|
}
|
|
1986
3142
|
}
|
|
3143
|
+
function readJsonObjectRecord(value) {
|
|
3144
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
return value;
|
|
3148
|
+
}
|
|
3149
|
+
function readOpenClawJsonConfig(openclawHome) {
|
|
3150
|
+
const openclawJsonPath = path.join(openclawHome, "openclaw.json");
|
|
3151
|
+
let parsed;
|
|
3152
|
+
try {
|
|
3153
|
+
parsed = JSON.parse(readFileSync(openclawJsonPath, "utf8"));
|
|
3154
|
+
}
|
|
3155
|
+
catch (error) {
|
|
3156
|
+
throw new Error(`Failed to read ${openclawJsonPath}: ${toErrorMessage(error)}`);
|
|
3157
|
+
}
|
|
3158
|
+
const config = readJsonObjectRecord(parsed);
|
|
3159
|
+
if (config === null) {
|
|
3160
|
+
throw new Error(`Failed to read ${openclawJsonPath}: openclaw.json must contain a top-level object`);
|
|
3161
|
+
}
|
|
3162
|
+
return {
|
|
3163
|
+
path: openclawJsonPath,
|
|
3164
|
+
config
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
function scrubOpenClawBrainPluginConfig(openclawHome) {
|
|
3168
|
+
const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
|
|
3169
|
+
const plugins = readJsonObjectRecord(config.plugins);
|
|
3170
|
+
if (plugins === null) {
|
|
3171
|
+
return {
|
|
3172
|
+
path: openclawJsonPath,
|
|
3173
|
+
changed: false,
|
|
3174
|
+
detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
const changes = [];
|
|
3178
|
+
let changed = false;
|
|
3179
|
+
if (Array.isArray(plugins.allow)) {
|
|
3180
|
+
const filteredAllow = plugins.allow.filter((entry) => entry !== "openclawbrain");
|
|
3181
|
+
if (filteredAllow.length !== plugins.allow.length) {
|
|
3182
|
+
changed = true;
|
|
3183
|
+
changes.push("removed plugins.allow entry");
|
|
3184
|
+
if (filteredAllow.length > 0) {
|
|
3185
|
+
plugins.allow = filteredAllow;
|
|
3186
|
+
}
|
|
3187
|
+
else {
|
|
3188
|
+
delete plugins.allow;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
const entries = readJsonObjectRecord(plugins.entries);
|
|
3193
|
+
if (entries !== null && Object.prototype.hasOwnProperty.call(entries, "openclawbrain")) {
|
|
3194
|
+
delete entries.openclawbrain;
|
|
3195
|
+
changed = true;
|
|
3196
|
+
changes.push("removed plugins.entries.openclawbrain");
|
|
3197
|
+
}
|
|
3198
|
+
if (entries !== null && Object.keys(entries).length === 0 && Object.prototype.hasOwnProperty.call(plugins, "entries")) {
|
|
3199
|
+
delete plugins.entries;
|
|
3200
|
+
changed = true;
|
|
3201
|
+
changes.push("removed empty plugins.entries container");
|
|
3202
|
+
}
|
|
3203
|
+
if (Object.keys(plugins).length === 0 && Object.prototype.hasOwnProperty.call(config, "plugins")) {
|
|
3204
|
+
delete config.plugins;
|
|
3205
|
+
changed = true;
|
|
3206
|
+
changes.push("removed empty plugins container");
|
|
3207
|
+
}
|
|
3208
|
+
if (changed) {
|
|
3209
|
+
writeFileSync(openclawJsonPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3210
|
+
return {
|
|
3211
|
+
path: openclawJsonPath,
|
|
3212
|
+
changed: true,
|
|
3213
|
+
detail: `Scrubbed stale openclawbrain plugin config in ${openclawJsonPath}: ${changes.join(", ")}`
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
return {
|
|
3217
|
+
path: openclawJsonPath,
|
|
3218
|
+
changed: false,
|
|
3219
|
+
detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
1987
3222
|
function resolveCleanupActivationRoot(openclawHome, explicitActivationRoot) {
|
|
1988
3223
|
if (explicitActivationRoot !== null) {
|
|
1989
3224
|
return path.resolve(explicitActivationRoot);
|
|
@@ -2024,25 +3259,50 @@ function buildRestartGuidance(restart) {
|
|
|
2024
3259
|
function runDetachCommand(parsed) {
|
|
2025
3260
|
const steps = [];
|
|
2026
3261
|
validateOpenClawHome(parsed.openclawHome);
|
|
3262
|
+
const targetInspection = inspectOpenClawHome(parsed.openclawHome);
|
|
3263
|
+
steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
2027
3264
|
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
3265
|
+
const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
|
|
3266
|
+
const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
|
|
2028
3267
|
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
3268
|
+
const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
|
|
2029
3269
|
const activationData = summarizeKeptActivationData(activationRoot);
|
|
2030
3270
|
const restartGuidance = buildRestartGuidance(parsed.restart);
|
|
2031
3271
|
const nextSteps = [
|
|
2032
3272
|
restartGuidance,
|
|
2033
3273
|
activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
|
|
2034
|
-
`
|
|
3274
|
+
activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
|
|
3275
|
+
`Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
|
|
2035
3276
|
].filter((step) => step !== null);
|
|
3277
|
+
steps.push(pluginConfigCleanup.detail);
|
|
3278
|
+
if (legacyResidue.removedNotes.length > 0) {
|
|
3279
|
+
steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
|
|
3280
|
+
}
|
|
3281
|
+
if (legacyResidue.updatedAgents.length > 0) {
|
|
3282
|
+
steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
|
|
3283
|
+
}
|
|
3284
|
+
steps.push(learnerService.detail);
|
|
2036
3285
|
steps.push(activationData.activationDataDetail);
|
|
2037
3286
|
steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
|
|
2038
3287
|
if (parsed.json) {
|
|
2039
3288
|
console.log(JSON.stringify({
|
|
2040
3289
|
command: "detach",
|
|
2041
3290
|
openclawHome: parsed.openclawHome,
|
|
3291
|
+
openclawTarget: {
|
|
3292
|
+
layout: targetInspection.layout,
|
|
3293
|
+
detail: describeOpenClawHomeInspection(targetInspection),
|
|
3294
|
+
profileId: targetInspection.profileId,
|
|
3295
|
+
profileSource: targetInspection.profileSource,
|
|
3296
|
+
configuredProfileIds: targetInspection.configuredProfileIds
|
|
3297
|
+
},
|
|
2042
3298
|
extensionDir,
|
|
2043
3299
|
activationRoot,
|
|
2044
3300
|
dataAction: "kept",
|
|
2045
3301
|
activationDataState: activationData.activationDataState,
|
|
3302
|
+
pluginConfigCleanup,
|
|
3303
|
+
learnerService,
|
|
3304
|
+
removedLegacyNotes: legacyResidue.removedNotes,
|
|
3305
|
+
updatedAgents: legacyResidue.updatedAgents,
|
|
2046
3306
|
restartMode: parsed.restart,
|
|
2047
3307
|
restartGuidance,
|
|
2048
3308
|
nextSteps,
|
|
@@ -2055,26 +3315,47 @@ function runDetachCommand(parsed) {
|
|
|
2055
3315
|
console.log(` ✓ ${step}`);
|
|
2056
3316
|
}
|
|
2057
3317
|
console.log("");
|
|
2058
|
-
console.log(`Lifecycle: OpenClaw
|
|
3318
|
+
console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
|
|
3319
|
+
console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
2059
3320
|
if (activationRoot !== null) {
|
|
2060
3321
|
console.log(`Brain data: ${shortenPath(activationRoot)} remains available for inspection or reattach.`);
|
|
2061
3322
|
}
|
|
2062
3323
|
else {
|
|
2063
3324
|
console.log("Brain data: preserved, but the activation root could not be resolved from the removed hook.");
|
|
2064
3325
|
}
|
|
3326
|
+
console.log(`Config: ${pluginConfigCleanup.detail}`);
|
|
3327
|
+
console.log(`Learner: ${learnerService.detail}`);
|
|
2065
3328
|
console.log(`Next: ${restartGuidance}`);
|
|
2066
3329
|
if (activationRoot !== null) {
|
|
2067
3330
|
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
3331
|
+
console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
|
|
2068
3332
|
}
|
|
2069
|
-
console.log(`Reattach: ${
|
|
3333
|
+
console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
|
|
2070
3334
|
}
|
|
2071
3335
|
return 0;
|
|
2072
3336
|
}
|
|
2073
3337
|
function runUninstallCommand(parsed) {
|
|
2074
3338
|
const steps = [];
|
|
2075
3339
|
validateOpenClawHome(parsed.openclawHome);
|
|
3340
|
+
const targetInspection = inspectOpenClawHome(parsed.openclawHome);
|
|
3341
|
+
steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
2076
3342
|
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
3343
|
+
if (parsed.dataMode === "purge" && activationRoot !== null) {
|
|
3344
|
+
assertActivationRootPurgeIsNotShared({
|
|
3345
|
+
activationRoot,
|
|
3346
|
+
openclawHome: parsed.openclawHome
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
|
|
3350
|
+
const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
|
|
3351
|
+
if (parsed.dataMode === "purge" &&
|
|
3352
|
+
activationRoot !== null &&
|
|
3353
|
+
learnerService.state === "preserved" &&
|
|
3354
|
+
learnerService.matchesRequestedActivationRoot !== false) {
|
|
3355
|
+
throw new Error(`Refusing to purge activation root ${path.resolve(activationRoot)} because the background learner service for this exact root could not be removed. ${learnerService.detail}`);
|
|
3356
|
+
}
|
|
2077
3357
|
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
3358
|
+
const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
|
|
2078
3359
|
let activationData;
|
|
2079
3360
|
if (parsed.dataMode === "purge") {
|
|
2080
3361
|
if (activationRoot === null) {
|
|
@@ -2103,8 +3384,19 @@ function runUninstallCommand(parsed) {
|
|
|
2103
3384
|
const nextSteps = [
|
|
2104
3385
|
restartGuidance,
|
|
2105
3386
|
parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
|
|
2106
|
-
`
|
|
3387
|
+
activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
|
|
3388
|
+
parsed.dataMode === "keep"
|
|
3389
|
+
? `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
|
|
3390
|
+
: `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
|
|
2107
3391
|
].filter((step) => step !== null);
|
|
3392
|
+
steps.push(pluginConfigCleanup.detail);
|
|
3393
|
+
if (legacyResidue.removedNotes.length > 0) {
|
|
3394
|
+
steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
|
|
3395
|
+
}
|
|
3396
|
+
if (legacyResidue.updatedAgents.length > 0) {
|
|
3397
|
+
steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
|
|
3398
|
+
}
|
|
3399
|
+
steps.push(learnerService.detail);
|
|
2108
3400
|
steps.push(activationData.activationDataDetail);
|
|
2109
3401
|
steps.push(parsed.dataMode === "purge"
|
|
2110
3402
|
? "Uninstall removed the OpenClaw profile hook and activation data."
|
|
@@ -2113,10 +3405,21 @@ function runUninstallCommand(parsed) {
|
|
|
2113
3405
|
console.log(JSON.stringify({
|
|
2114
3406
|
command: "uninstall",
|
|
2115
3407
|
openclawHome: parsed.openclawHome,
|
|
3408
|
+
openclawTarget: {
|
|
3409
|
+
layout: targetInspection.layout,
|
|
3410
|
+
detail: describeOpenClawHomeInspection(targetInspection),
|
|
3411
|
+
profileId: targetInspection.profileId,
|
|
3412
|
+
profileSource: targetInspection.profileSource,
|
|
3413
|
+
configuredProfileIds: targetInspection.configuredProfileIds
|
|
3414
|
+
},
|
|
2116
3415
|
extensionDir,
|
|
2117
3416
|
activationRoot,
|
|
2118
3417
|
dataAction: parsed.dataMode,
|
|
2119
3418
|
activationDataState: activationData.activationDataState,
|
|
3419
|
+
pluginConfigCleanup,
|
|
3420
|
+
learnerService,
|
|
3421
|
+
removedLegacyNotes: legacyResidue.removedNotes,
|
|
3422
|
+
updatedAgents: legacyResidue.updatedAgents,
|
|
2120
3423
|
restartMode: parsed.restart,
|
|
2121
3424
|
restartGuidance,
|
|
2122
3425
|
nextSteps,
|
|
@@ -2129,16 +3432,27 @@ function runUninstallCommand(parsed) {
|
|
|
2129
3432
|
console.log(` ✓ ${step}`);
|
|
2130
3433
|
}
|
|
2131
3434
|
console.log("");
|
|
2132
|
-
console.log(`Lifecycle: OpenClaw
|
|
3435
|
+
console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
|
|
3436
|
+
console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
2133
3437
|
console.log(`Data mode: ${parsed.dataMode === "purge" ? "purged" : "kept"}`);
|
|
2134
3438
|
if (activationRoot !== null) {
|
|
2135
3439
|
console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
|
|
2136
3440
|
}
|
|
3441
|
+
console.log(`Config: ${pluginConfigCleanup.detail}`);
|
|
3442
|
+
console.log(`Learner: ${learnerService.detail}`);
|
|
2137
3443
|
console.log(`Next: ${restartGuidance}`);
|
|
2138
3444
|
if (parsed.dataMode === "keep" && activationRoot !== null) {
|
|
2139
3445
|
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
2140
3446
|
}
|
|
2141
|
-
|
|
3447
|
+
if (activationRoot !== null) {
|
|
3448
|
+
console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
|
|
3449
|
+
}
|
|
3450
|
+
if (parsed.dataMode === "keep") {
|
|
3451
|
+
console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
|
|
3452
|
+
}
|
|
3453
|
+
else {
|
|
3454
|
+
console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
|
|
3455
|
+
}
|
|
2142
3456
|
}
|
|
2143
3457
|
return 0;
|
|
2144
3458
|
}
|
|
@@ -2704,13 +4018,62 @@ function exportLocalSessionTailChangesToScanRoot(input) {
|
|
|
2704
4018
|
warnings
|
|
2705
4019
|
};
|
|
2706
4020
|
}
|
|
2707
|
-
function
|
|
2708
|
-
|
|
4021
|
+
function summarizeVectorEmbeddingState(vectors) {
|
|
4022
|
+
if (vectors === null || vectors === undefined) {
|
|
4023
|
+
return {
|
|
4024
|
+
vectorEntryCount: null,
|
|
4025
|
+
numericEmbeddingEntryCount: null,
|
|
4026
|
+
embeddingModels: []
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
const embeddingModels = [...new Set(vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
|
|
4030
|
+
return {
|
|
4031
|
+
vectorEntryCount: vectors.entries.length,
|
|
4032
|
+
numericEmbeddingEntryCount: vectors.entries.filter((entry) => entry.embedding !== undefined).length,
|
|
4033
|
+
embeddingModels
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
function buildWatchEmbedTracePoint(input) {
|
|
4037
|
+
const summary = summarizeVectorEmbeddingState(input.vectors);
|
|
4038
|
+
return {
|
|
4039
|
+
slot: input.slot,
|
|
4040
|
+
packId: input.packId,
|
|
4041
|
+
runtimeEmbedderPresent: input.embedder !== null,
|
|
4042
|
+
runtimeEmbedderModel: input.embedder?.model ?? null,
|
|
4043
|
+
vectorEntryCount: summary.vectorEntryCount,
|
|
4044
|
+
numericEmbeddingEntryCount: summary.numericEmbeddingEntryCount,
|
|
4045
|
+
embeddingModels: summary.embeddingModels,
|
|
4046
|
+
error: input.error ?? null
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
function buildWatchEmbedTracePointFromPack(input) {
|
|
4050
|
+
return buildWatchEmbedTracePoint({
|
|
4051
|
+
slot: input.slot,
|
|
4052
|
+
packId: input.pack?.manifest.packId ?? null,
|
|
4053
|
+
embedder: input.embedder,
|
|
4054
|
+
vectors: input.pack?.vectors,
|
|
4055
|
+
error: input.error ?? null
|
|
4056
|
+
});
|
|
4057
|
+
}
|
|
4058
|
+
function formatWatchEmbedTracePoint(label, point) {
|
|
4059
|
+
const models = point.embeddingModels.length === 0 ? "none" : point.embeddingModels.join("|");
|
|
4060
|
+
const slot = point.slot ?? "build";
|
|
4061
|
+
const packId = point.packId ?? "unknown";
|
|
4062
|
+
const embedderState = point.runtimeEmbedderPresent ? `present:${point.runtimeEmbedderModel ?? "unknown"}` : "null";
|
|
4063
|
+
const counts = point.vectorEntryCount === null || point.numericEmbeddingEntryCount === null
|
|
4064
|
+
? "vectors=unknown numeric=unknown"
|
|
4065
|
+
: `vectors=${point.vectorEntryCount} numeric=${point.numericEmbeddingEntryCount}`;
|
|
4066
|
+
const error = point.error === null ? "" : ` error=${point.error}`;
|
|
4067
|
+
return `embed-trace ${label} slot=${slot} pack=${packId} runtimeEmbedder=${embedderState} ${counts} models=${models}${error}`;
|
|
4068
|
+
}
|
|
4069
|
+
async function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId, embedder, log) {
|
|
4070
|
+
let materialization = snapshot?.learner?.lastMaterialization ?? null;
|
|
2709
4071
|
if (materialization === null) {
|
|
2710
4072
|
return {
|
|
2711
4073
|
lastHandledMaterializationPackId,
|
|
2712
4074
|
logLine: null,
|
|
2713
4075
|
materializedPackId: null,
|
|
4076
|
+
embedInstrumentation: null,
|
|
2714
4077
|
failure: null
|
|
2715
4078
|
};
|
|
2716
4079
|
}
|
|
@@ -2722,10 +4085,38 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2722
4085
|
lastHandledMaterializationPackId,
|
|
2723
4086
|
logLine: null,
|
|
2724
4087
|
materializedPackId: packId,
|
|
4088
|
+
embedInstrumentation: null,
|
|
2725
4089
|
failure: null
|
|
2726
4090
|
};
|
|
2727
4091
|
}
|
|
4092
|
+
if (embedder !== null) {
|
|
4093
|
+
materialization = {
|
|
4094
|
+
...materialization,
|
|
4095
|
+
candidate: await reindexCandidatePackBuildResultWithEmbedder(materialization.candidate, embedder)
|
|
4096
|
+
};
|
|
4097
|
+
if (snapshot?.learner !== undefined && snapshot.learner !== null) {
|
|
4098
|
+
snapshot.learner.lastMaterialization = materialization;
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
2728
4101
|
const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
|
|
4102
|
+
const observedAt = new Date().toISOString();
|
|
4103
|
+
const beforeCandidateMaterialization = buildWatchEmbedTracePoint({
|
|
4104
|
+
slot: null,
|
|
4105
|
+
packId,
|
|
4106
|
+
embedder,
|
|
4107
|
+
vectors: materialization?.candidate?.payloads?.vectors
|
|
4108
|
+
});
|
|
4109
|
+
let embedInstrumentation = {
|
|
4110
|
+
observedAt,
|
|
4111
|
+
candidatePackId: packId,
|
|
4112
|
+
promotionAllowed: null,
|
|
4113
|
+
promotionFindings: [],
|
|
4114
|
+
beforeCandidateMaterialization,
|
|
4115
|
+
afterCandidateMaterialization: null,
|
|
4116
|
+
afterStage: null,
|
|
4117
|
+
afterPromote: null
|
|
4118
|
+
};
|
|
4119
|
+
log?.(formatWatchEmbedTracePoint("before_materialize", beforeCandidateMaterialization));
|
|
2729
4120
|
try {
|
|
2730
4121
|
const candidateRootDir = path.resolve(activationRoot, "packs", packId);
|
|
2731
4122
|
mkdirSync(candidateRootDir, { recursive: true });
|
|
@@ -2737,27 +4128,81 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2737
4128
|
activeBeforePack = null;
|
|
2738
4129
|
}
|
|
2739
4130
|
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
|
|
4131
|
+
embedInstrumentation = {
|
|
4132
|
+
...embedInstrumentation,
|
|
4133
|
+
afterCandidateMaterialization: buildWatchEmbedTracePointFromPack({
|
|
4134
|
+
slot: "candidate",
|
|
4135
|
+
pack: candidateDescriptor,
|
|
4136
|
+
embedder
|
|
4137
|
+
})
|
|
4138
|
+
};
|
|
4139
|
+
if (embedInstrumentation.afterCandidateMaterialization !== null) {
|
|
4140
|
+
log?.(formatWatchEmbedTracePoint("after_materialize", embedInstrumentation.afterCandidateMaterialization));
|
|
4141
|
+
}
|
|
2740
4142
|
appendLearningUpdateLogs({
|
|
2741
4143
|
activationRoot,
|
|
2742
4144
|
materialization,
|
|
2743
4145
|
activeBeforePack,
|
|
2744
4146
|
candidateDescriptor
|
|
2745
4147
|
});
|
|
2746
|
-
const now =
|
|
4148
|
+
const now = observedAt;
|
|
2747
4149
|
stageCandidatePack(activationRoot, candidateRootDir, {
|
|
2748
4150
|
updatedAt: now,
|
|
2749
4151
|
reason: `watch_stage:${materialization.reason}:${materialization.lane}`
|
|
2750
4152
|
});
|
|
2751
4153
|
const inspection = inspectActivationState(activationRoot, now);
|
|
4154
|
+
let stagedPack = null;
|
|
4155
|
+
let stagedPackError = null;
|
|
4156
|
+
try {
|
|
4157
|
+
stagedPack = loadPackFromActivation(activationRoot, "candidate", { requireActivationReady: true });
|
|
4158
|
+
}
|
|
4159
|
+
catch (error) {
|
|
4160
|
+
stagedPackError = formatWatchError(error);
|
|
4161
|
+
}
|
|
4162
|
+
embedInstrumentation = {
|
|
4163
|
+
...embedInstrumentation,
|
|
4164
|
+
promotionAllowed: inspection.promotion.allowed,
|
|
4165
|
+
promotionFindings: [...inspection.promotion.findings],
|
|
4166
|
+
afterStage: buildWatchEmbedTracePointFromPack({
|
|
4167
|
+
slot: "candidate",
|
|
4168
|
+
pack: stagedPack,
|
|
4169
|
+
embedder,
|
|
4170
|
+
error: stagedPackError
|
|
4171
|
+
})
|
|
4172
|
+
};
|
|
4173
|
+
if (embedInstrumentation.afterStage !== null) {
|
|
4174
|
+
log?.(formatWatchEmbedTracePoint("after_stage", embedInstrumentation.afterStage));
|
|
4175
|
+
}
|
|
2752
4176
|
if (inspection.promotion.allowed) {
|
|
2753
4177
|
promoteCandidatePack(activationRoot, {
|
|
2754
4178
|
updatedAt: now,
|
|
2755
4179
|
reason: `watch_promote:${materialization.reason}:${materialization.lane}`
|
|
2756
4180
|
});
|
|
4181
|
+
let promotedPack = null;
|
|
4182
|
+
let promotedPackError = null;
|
|
4183
|
+
try {
|
|
4184
|
+
promotedPack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
|
|
4185
|
+
}
|
|
4186
|
+
catch (error) {
|
|
4187
|
+
promotedPackError = formatWatchError(error);
|
|
4188
|
+
}
|
|
4189
|
+
embedInstrumentation = {
|
|
4190
|
+
...embedInstrumentation,
|
|
4191
|
+
afterPromote: buildWatchEmbedTracePointFromPack({
|
|
4192
|
+
slot: "active",
|
|
4193
|
+
pack: promotedPack,
|
|
4194
|
+
embedder,
|
|
4195
|
+
error: promotedPackError
|
|
4196
|
+
})
|
|
4197
|
+
};
|
|
4198
|
+
if (embedInstrumentation.afterPromote !== null) {
|
|
4199
|
+
log?.(formatWatchEmbedTracePoint("after_promote", embedInstrumentation.afterPromote));
|
|
4200
|
+
}
|
|
2757
4201
|
return {
|
|
2758
4202
|
lastHandledMaterializationPackId: packId,
|
|
2759
4203
|
materializedPackId: packId,
|
|
2760
4204
|
logLine: `Promoted ${shortPackId} → active`,
|
|
4205
|
+
embedInstrumentation,
|
|
2761
4206
|
failure: null
|
|
2762
4207
|
};
|
|
2763
4208
|
}
|
|
@@ -2765,15 +4210,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2765
4210
|
lastHandledMaterializationPackId: packId,
|
|
2766
4211
|
materializedPackId: packId,
|
|
2767
4212
|
logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
|
|
4213
|
+
embedInstrumentation,
|
|
2768
4214
|
failure: null
|
|
2769
4215
|
};
|
|
2770
4216
|
}
|
|
2771
4217
|
catch (error) {
|
|
2772
4218
|
const message = error instanceof Error ? error.message : String(error);
|
|
4219
|
+
embedInstrumentation = {
|
|
4220
|
+
...embedInstrumentation,
|
|
4221
|
+
afterCandidateMaterialization: embedInstrumentation.afterCandidateMaterialization ??
|
|
4222
|
+
buildWatchEmbedTracePoint({
|
|
4223
|
+
slot: "candidate",
|
|
4224
|
+
packId,
|
|
4225
|
+
embedder,
|
|
4226
|
+
vectors: null,
|
|
4227
|
+
error: message
|
|
4228
|
+
})
|
|
4229
|
+
};
|
|
2773
4230
|
return {
|
|
2774
4231
|
lastHandledMaterializationPackId,
|
|
2775
4232
|
materializedPackId: packId,
|
|
2776
4233
|
logLine: `Promotion failed for ${shortPackId}: ${message}`,
|
|
4234
|
+
embedInstrumentation,
|
|
2777
4235
|
failure: {
|
|
2778
4236
|
mode: "materialization_failed",
|
|
2779
4237
|
detail: message,
|
|
@@ -2782,15 +4240,18 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2782
4240
|
};
|
|
2783
4241
|
}
|
|
2784
4242
|
}
|
|
2785
|
-
function resolveWatchTeacherLabelerConfig(input) {
|
|
4243
|
+
function resolveWatchTeacherLabelerConfig(input, activationRoot) {
|
|
2786
4244
|
if (input !== undefined) {
|
|
2787
4245
|
return {
|
|
2788
4246
|
teacherLabeler: input,
|
|
2789
4247
|
warnings: []
|
|
2790
4248
|
};
|
|
2791
4249
|
}
|
|
2792
|
-
const providerConfig =
|
|
2793
|
-
|
|
4250
|
+
const providerConfig = readOpenClawBrainProviderConfigFromSources({
|
|
4251
|
+
env: process.env,
|
|
4252
|
+
activationRoot
|
|
4253
|
+
});
|
|
4254
|
+
const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_|provider defaults/u.test(warning));
|
|
2794
4255
|
if (providerConfig.teacher.provider !== "ollama") {
|
|
2795
4256
|
return {
|
|
2796
4257
|
teacherLabeler: null,
|
|
@@ -2816,12 +4277,178 @@ function resolveWatchTeacherLabelerConfig(input) {
|
|
|
2816
4277
|
warnings
|
|
2817
4278
|
};
|
|
2818
4279
|
}
|
|
4280
|
+
function resolveWatchEmbedderConfig(input, activationRoot) {
|
|
4281
|
+
if (input !== undefined) {
|
|
4282
|
+
return {
|
|
4283
|
+
embedder: input,
|
|
4284
|
+
warnings: []
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
4287
|
+
const defaultsResult = readOpenClawBrainProviderDefaults(activationRoot);
|
|
4288
|
+
const providerConfig = readOpenClawBrainProviderConfigFromSources({
|
|
4289
|
+
env: process.env,
|
|
4290
|
+
activationRoot,
|
|
4291
|
+
defaults: defaultsResult.defaults
|
|
4292
|
+
});
|
|
4293
|
+
const warnings = [...new Set([
|
|
4294
|
+
...defaultsResult.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning)),
|
|
4295
|
+
...providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning))
|
|
4296
|
+
])];
|
|
4297
|
+
const explicitEnv = typeof process.env[OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV] === "string" ||
|
|
4298
|
+
typeof process.env[OPENCLAWBRAIN_EMBEDDER_MODEL_ENV] === "string" ||
|
|
4299
|
+
typeof process.env[OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV] === "string";
|
|
4300
|
+
// Legacy install-written provider-defaults.json files can predate embedder fields entirely.
|
|
4301
|
+
// If a persisted defaults file exists, treat that activation root as explicitly configured and
|
|
4302
|
+
// let provider-config resolution fill in the embedder fallback instead of silently dropping to null.
|
|
4303
|
+
const explicitDefaults = defaultsResult.defaults !== null;
|
|
4304
|
+
if (!explicitEnv && !explicitDefaults) {
|
|
4305
|
+
return {
|
|
4306
|
+
embedder: null,
|
|
4307
|
+
warnings
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
if (providerConfig.embedder.provider !== "ollama") {
|
|
4311
|
+
return {
|
|
4312
|
+
embedder: null,
|
|
4313
|
+
warnings
|
|
4314
|
+
};
|
|
4315
|
+
}
|
|
4316
|
+
return {
|
|
4317
|
+
embedder: createOllamaEmbedder({
|
|
4318
|
+
baseUrl: providerConfig.embedderBaseUrl,
|
|
4319
|
+
model: providerConfig.embedder.model
|
|
4320
|
+
}),
|
|
4321
|
+
warnings
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
function summarizeWatchLatestUserMessage(localPoll) {
|
|
4325
|
+
let latest = null;
|
|
4326
|
+
for (const change of localPoll.changes) {
|
|
4327
|
+
if (change.lastUserMessageAt === null || change.lastUserMessageText === null) {
|
|
4328
|
+
continue;
|
|
4329
|
+
}
|
|
4330
|
+
const candidate = {
|
|
4331
|
+
at: change.lastUserMessageAt,
|
|
4332
|
+
text: change.lastUserMessageText,
|
|
4333
|
+
sessionId: change.sessionId
|
|
4334
|
+
};
|
|
4335
|
+
if (latest === null || Date.parse(candidate.at) >= Date.parse(latest.at)) {
|
|
4336
|
+
latest = candidate;
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
return latest;
|
|
4340
|
+
}
|
|
4341
|
+
function summarizeWatchPackTransition(input) {
|
|
4342
|
+
const beforeActivePackId = input.before?.active?.packId ?? input.before?.pointers.active?.packId ?? null;
|
|
4343
|
+
const afterActivePackId = input.after?.active?.packId ?? input.after?.pointers.active?.packId ?? null;
|
|
4344
|
+
if (afterActivePackId !== null && beforeActivePackId !== afterActivePackId) {
|
|
4345
|
+
return {
|
|
4346
|
+
kind: "promoted_active",
|
|
4347
|
+
fromPackId: beforeActivePackId,
|
|
4348
|
+
toPackId: afterActivePackId
|
|
4349
|
+
};
|
|
4350
|
+
}
|
|
4351
|
+
const beforeCandidatePackId = input.before?.candidate?.packId ?? input.before?.pointers.candidate?.packId ?? null;
|
|
4352
|
+
const afterCandidatePackId = input.after?.candidate?.packId ?? input.after?.pointers.candidate?.packId ?? null;
|
|
4353
|
+
if (afterCandidatePackId !== null && beforeCandidatePackId !== afterCandidatePackId) {
|
|
4354
|
+
return {
|
|
4355
|
+
kind: "staged_candidate",
|
|
4356
|
+
fromPackId: beforeCandidatePackId,
|
|
4357
|
+
toPackId: afterCandidatePackId
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4360
|
+
return null;
|
|
4361
|
+
}
|
|
4362
|
+
function truncateWatchMessage(text, maxLength = 96) {
|
|
4363
|
+
const normalized = text.replace(/\s+/gu, " ").trim();
|
|
4364
|
+
if (normalized.length <= maxLength) {
|
|
4365
|
+
return normalized;
|
|
4366
|
+
}
|
|
4367
|
+
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
4368
|
+
}
|
|
4369
|
+
function buildWatchLastObservedDelta(input) {
|
|
4370
|
+
const exported = input.exported.exportedBundleCount > 0 ||
|
|
4371
|
+
input.exported.exportedEventCount > 0;
|
|
4372
|
+
const labeled = (input.snapshotAfter.diagnostics.emittedArtifactCount ?? 0) >
|
|
4373
|
+
(input.snapshotBefore.diagnostics.emittedArtifactCount ?? 0);
|
|
4374
|
+
const latestPackTransition = summarizeWatchPackTransition({
|
|
4375
|
+
before: input.beforeInspection,
|
|
4376
|
+
after: input.afterInspection
|
|
4377
|
+
});
|
|
4378
|
+
const promoted = latestPackTransition?.kind === "promoted_active";
|
|
4379
|
+
const afterActivePackId = input.afterInspection?.active?.packId ?? input.afterInspection?.pointers.active?.packId ?? null;
|
|
4380
|
+
const served = promoted && afterActivePackId === latestPackTransition?.toPackId && input.afterInspection?.active?.activationReady === true;
|
|
4381
|
+
const latestUserMessage = summarizeWatchLatestUserMessage(input.localPoll);
|
|
4382
|
+
const selectedBackfillOnly = !exported && input.scanResult.selected.length > 0;
|
|
4383
|
+
const cycleDidNothing = !exported && !labeled && !promoted && !served;
|
|
4384
|
+
let explanation;
|
|
4385
|
+
if (latestUserMessage === null) {
|
|
4386
|
+
if (selectedBackfillOnly) {
|
|
4387
|
+
explanation =
|
|
4388
|
+
"No new local user message was exported in this cycle; the learner only revisited stored exports, so this pass does not prove a new last-turn change.";
|
|
4389
|
+
}
|
|
4390
|
+
else if (cycleDidNothing) {
|
|
4391
|
+
explanation = "No new local user message or learner-visible export was observed in this cycle, so nothing changed.";
|
|
4392
|
+
}
|
|
4393
|
+
else if (promoted && latestPackTransition !== null) {
|
|
4394
|
+
explanation =
|
|
4395
|
+
`No new local user message was exported in this cycle; pack ${latestPackTransition.toPackId} moved into ${latestPackTransition.kind === "promoted_active" ? "active serving" : "the candidate slot"} from previously accumulated learner state.`;
|
|
4396
|
+
}
|
|
4397
|
+
else {
|
|
4398
|
+
explanation =
|
|
4399
|
+
"This cycle observed learner activity, but it did not include a new local user message, so the latest last-turn delta cannot be attributed to a fresh user turn.";
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
else {
|
|
4403
|
+
const quotedMessage = `"${truncateWatchMessage(latestUserMessage.text)}"`;
|
|
4404
|
+
if (exported && labeled && promoted && served && latestPackTransition !== null) {
|
|
4405
|
+
explanation =
|
|
4406
|
+
`Latest user message ${quotedMessage} was exported, labeled, promoted into pack ${latestPackTransition.toPackId}, and is now served from the active pack.`;
|
|
4407
|
+
}
|
|
4408
|
+
else if (exported && labeled && !promoted) {
|
|
4409
|
+
explanation =
|
|
4410
|
+
`Latest user message ${quotedMessage} was exported and labeled, but it has not been promoted into the serving pack yet.`;
|
|
4411
|
+
}
|
|
4412
|
+
else if (exported && !labeled && !promoted) {
|
|
4413
|
+
explanation =
|
|
4414
|
+
`Latest user message ${quotedMessage} was exported, but it did not add a new teacher label or change the serving pack in this cycle.`;
|
|
4415
|
+
}
|
|
4416
|
+
else if (exported && !labeled && promoted && latestPackTransition !== null) {
|
|
4417
|
+
explanation =
|
|
4418
|
+
`Latest user message ${quotedMessage} was exported, but this cycle's promotion to pack ${latestPackTransition.toPackId} is not backed by a new teacher label from that message alone.`;
|
|
4419
|
+
}
|
|
4420
|
+
else if (!exported && labeled) {
|
|
4421
|
+
explanation =
|
|
4422
|
+
`Latest user message ${quotedMessage} was already in stored exports; this cycle only labeled or replayed it, without a new local export.`;
|
|
4423
|
+
}
|
|
4424
|
+
else if (cycleDidNothing) {
|
|
4425
|
+
explanation = `Latest user message ${quotedMessage} did not produce a new export, label, or serving-pack change in this cycle.`;
|
|
4426
|
+
}
|
|
4427
|
+
else {
|
|
4428
|
+
explanation =
|
|
4429
|
+
`Latest user message ${quotedMessage} changed learner state this cycle, but the local artifacts do not prove a clean export-to-serve handoff yet.`;
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
return {
|
|
4433
|
+
available: true,
|
|
4434
|
+
observedAt: input.observedAt,
|
|
4435
|
+
exported,
|
|
4436
|
+
labeled,
|
|
4437
|
+
promoted,
|
|
4438
|
+
served,
|
|
4439
|
+
latestPackTransition,
|
|
4440
|
+
explanation
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
2819
4443
|
export async function createWatchCommandRuntime(input) {
|
|
2820
4444
|
const activationRoot = path.resolve(input.activationRoot);
|
|
2821
4445
|
const bootstrapObservedAt = new Date().toISOString();
|
|
2822
4446
|
const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
|
|
2823
4447
|
? path.resolve(input.scanRoot)
|
|
2824
4448
|
: path.resolve(activationRoot, "event-exports");
|
|
4449
|
+
const pollIntervalSeconds = Number.isInteger(input.pollIntervalSeconds) && (input.pollIntervalSeconds ?? 0) > 0
|
|
4450
|
+
? input.pollIntervalSeconds
|
|
4451
|
+
: DEFAULT_WATCH_POLL_INTERVAL_SECONDS;
|
|
2825
4452
|
const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
|
|
2826
4453
|
const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
|
|
2827
4454
|
const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
|
|
@@ -2832,15 +4459,26 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2832
4459
|
log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
|
|
2833
4460
|
log(`Scan root: ${shortenPath(scanRoot)}`);
|
|
2834
4461
|
log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
|
|
2835
|
-
const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler);
|
|
4462
|
+
const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
|
|
4463
|
+
const resolvedEmbedder = resolveWatchEmbedderConfig(input.embedder, activationRoot);
|
|
2836
4464
|
const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
|
|
2837
4465
|
for (const warning of resolvedTeacherLabeler.warnings) {
|
|
2838
|
-
startupWarnings.push(`
|
|
2839
|
-
log(`Teacher
|
|
4466
|
+
startupWarnings.push(`teacher_config_warning:${warning}`);
|
|
4467
|
+
log(`Teacher config warning: ${warning}`);
|
|
4468
|
+
}
|
|
4469
|
+
for (const warning of resolvedEmbedder.warnings) {
|
|
4470
|
+
startupWarnings.push(`embedder_config_warning:${warning}`);
|
|
4471
|
+
log(`Embedder config warning: ${warning}`);
|
|
2840
4472
|
}
|
|
2841
4473
|
if (teacherLabeler?.provider === "ollama") {
|
|
2842
4474
|
log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
|
|
2843
4475
|
}
|
|
4476
|
+
if (resolvedEmbedder.embedder !== null) {
|
|
4477
|
+
log(`Embedder: provider=ollama model=${resolvedEmbedder.embedder.model}`);
|
|
4478
|
+
}
|
|
4479
|
+
else {
|
|
4480
|
+
log("Embedder: numeric pack materialization is not configured; watch will keep keyword/weight vectors only.");
|
|
4481
|
+
}
|
|
2844
4482
|
const scanner = createRuntimeEventExportScanner({ scanRoot });
|
|
2845
4483
|
let lastServeTimeFallbackReason = null;
|
|
2846
4484
|
const baseTeacherLoopInput = {
|
|
@@ -2877,10 +4515,23 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2877
4515
|
};
|
|
2878
4516
|
let teacherLoop;
|
|
2879
4517
|
let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
|
|
4518
|
+
let lastEmbedInstrumentation = restoredTeacherState.embedInstrumentation;
|
|
4519
|
+
let restoredLastObservedDelta = restoredTeacherState.lastObservedDelta;
|
|
2880
4520
|
if (restoredTeacherState.error !== null) {
|
|
2881
4521
|
const message = restoredTeacherState.error;
|
|
2882
4522
|
startupWarnings.push(`teacher_snapshot_reset:${message}`);
|
|
2883
4523
|
lastHandledMaterializationPackId = null;
|
|
4524
|
+
lastEmbedInstrumentation = null;
|
|
4525
|
+
restoredLastObservedDelta = {
|
|
4526
|
+
available: true,
|
|
4527
|
+
observedAt: bootstrapObservedAt,
|
|
4528
|
+
exported: false,
|
|
4529
|
+
labeled: false,
|
|
4530
|
+
promoted: false,
|
|
4531
|
+
served: false,
|
|
4532
|
+
latestPackTransition: null,
|
|
4533
|
+
explanation: "Watch reset an unreadable teacher snapshot, so no prior last-turn delta can be trusted."
|
|
4534
|
+
};
|
|
2884
4535
|
log(`Teacher snapshot reset: ${message}`);
|
|
2885
4536
|
teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
|
|
2886
4537
|
}
|
|
@@ -2895,6 +4546,17 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2895
4546
|
const message = formatWatchError(error);
|
|
2896
4547
|
startupWarnings.push(`teacher_snapshot_reset:${message}`);
|
|
2897
4548
|
lastHandledMaterializationPackId = null;
|
|
4549
|
+
lastEmbedInstrumentation = null;
|
|
4550
|
+
restoredLastObservedDelta = {
|
|
4551
|
+
available: true,
|
|
4552
|
+
observedAt: bootstrapObservedAt,
|
|
4553
|
+
exported: false,
|
|
4554
|
+
labeled: false,
|
|
4555
|
+
promoted: false,
|
|
4556
|
+
served: false,
|
|
4557
|
+
latestPackTransition: null,
|
|
4558
|
+
explanation: "Watch reset an unusable teacher snapshot, so no prior last-turn delta can be trusted."
|
|
4559
|
+
};
|
|
2898
4560
|
log(`Teacher snapshot reset: ${message}`);
|
|
2899
4561
|
teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
|
|
2900
4562
|
}
|
|
@@ -2903,11 +4565,19 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2903
4565
|
const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
|
|
2904
4566
|
log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
|
|
2905
4567
|
}
|
|
4568
|
+
const resolvedProfileRoots = input.profileRoots === undefined
|
|
4569
|
+
? resolveWatchProfileRootsForActivationRoot(activationRoot)
|
|
4570
|
+
: [...new Set(input.profileRoots.map((root) => path.resolve(root)))];
|
|
4571
|
+
if (input.profileRoots === undefined && resolvedProfileRoots !== undefined) {
|
|
4572
|
+
log(`Session tail scope: attached OpenClaw home${resolvedProfileRoots.length === 1 ? "" : "s"} ${resolvedProfileRoots
|
|
4573
|
+
.map((root) => shortenPath(root))
|
|
4574
|
+
.join(", ")}`);
|
|
4575
|
+
}
|
|
2906
4576
|
let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
|
|
2907
4577
|
let localSessionTail;
|
|
2908
4578
|
try {
|
|
2909
4579
|
localSessionTail = createOpenClawLocalSessionTail({
|
|
2910
|
-
...(
|
|
4580
|
+
...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
|
|
2911
4581
|
cursor: restoredCursor,
|
|
2912
4582
|
emitExistingOnFirstPoll: restoredCursor.length === 0
|
|
2913
4583
|
});
|
|
@@ -2917,7 +4587,7 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2917
4587
|
log(`Session tail cursor reset: ${message}`);
|
|
2918
4588
|
restoredCursor = [];
|
|
2919
4589
|
localSessionTail = createOpenClawLocalSessionTail({
|
|
2920
|
-
...(
|
|
4590
|
+
...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
|
|
2921
4591
|
emitExistingOnFirstPoll: true
|
|
2922
4592
|
});
|
|
2923
4593
|
persistWatchSessionTailCursor(sessionTailCursorPath, []);
|
|
@@ -2938,8 +4608,11 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2938
4608
|
log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
|
|
2939
4609
|
}
|
|
2940
4610
|
let bootstrapSnapshot = teacherLoop.snapshot();
|
|
2941
|
-
const replayPromotion = applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId);
|
|
4611
|
+
const replayPromotion = await applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId, resolvedEmbedder.embedder, log);
|
|
2942
4612
|
lastHandledMaterializationPackId = replayPromotion.lastHandledMaterializationPackId;
|
|
4613
|
+
if (replayPromotion.embedInstrumentation !== null) {
|
|
4614
|
+
lastEmbedInstrumentation = replayPromotion.embedInstrumentation;
|
|
4615
|
+
}
|
|
2943
4616
|
if (replayPromotion.logLine !== null) {
|
|
2944
4617
|
log(replayPromotion.logLine);
|
|
2945
4618
|
bootstrapSnapshot = teacherLoop.snapshot();
|
|
@@ -2947,6 +4620,7 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2947
4620
|
const bootstrapCursor = localSessionTail.snapshot();
|
|
2948
4621
|
persistWatchTeacherSnapshot(teacherSnapshotPath, {
|
|
2949
4622
|
lastRunAt: bootstrapObservedAt,
|
|
4623
|
+
pollIntervalSeconds,
|
|
2950
4624
|
scanRoot,
|
|
2951
4625
|
sessionTailCursorPath,
|
|
2952
4626
|
sessionTailCursorUpdatedAt: bootstrapObservedAt,
|
|
@@ -2962,26 +4636,44 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2962
4636
|
lastTeacherError: null,
|
|
2963
4637
|
localSessionTailNoopReason: null,
|
|
2964
4638
|
lastHandledMaterializationPackId,
|
|
4639
|
+
lastObservedDelta: restoredLastObservedDelta.available
|
|
4640
|
+
? restoredLastObservedDelta
|
|
4641
|
+
: {
|
|
4642
|
+
available: true,
|
|
4643
|
+
observedAt: bootstrapObservedAt,
|
|
4644
|
+
exported: false,
|
|
4645
|
+
labeled: false,
|
|
4646
|
+
promoted: false,
|
|
4647
|
+
served: false,
|
|
4648
|
+
latestPackTransition: null,
|
|
4649
|
+
explanation: "Watch bootstrapped its state, but no new local user-message delta has been observed yet."
|
|
4650
|
+
},
|
|
4651
|
+
embedInstrumentation: lastEmbedInstrumentation,
|
|
2965
4652
|
failure: replayPromotion.failure,
|
|
2966
4653
|
snapshot: bootstrapSnapshot
|
|
2967
4654
|
});
|
|
2968
4655
|
return {
|
|
2969
4656
|
activationRoot,
|
|
2970
4657
|
scanRoot,
|
|
4658
|
+
pollIntervalSeconds,
|
|
2971
4659
|
sessionTailCursorPath,
|
|
2972
4660
|
teacherSnapshotPath,
|
|
2973
4661
|
startupWarnings,
|
|
2974
4662
|
lastTeacherError: null,
|
|
2975
4663
|
replayState,
|
|
2976
4664
|
lastHandledMaterializationPackId,
|
|
4665
|
+
lastEmbedInstrumentation,
|
|
2977
4666
|
scanner,
|
|
2978
4667
|
teacherLoop,
|
|
2979
|
-
localSessionTail
|
|
4668
|
+
localSessionTail,
|
|
4669
|
+
embedder: resolvedEmbedder.embedder
|
|
2980
4670
|
};
|
|
2981
4671
|
}
|
|
2982
4672
|
export async function runWatchCommandPass(runtime, options = {}) {
|
|
2983
4673
|
const log = options.log ?? watchLog;
|
|
2984
4674
|
const observedAt = options.observedAt ?? new Date().toISOString();
|
|
4675
|
+
const snapshotBefore = runtime.teacherLoop.snapshot();
|
|
4676
|
+
const beforeInspection = inspectActivationState(runtime.activationRoot, observedAt);
|
|
2985
4677
|
const localPoll = runtime.localSessionTail.pollOnce({
|
|
2986
4678
|
observedAt
|
|
2987
4679
|
});
|
|
@@ -3015,9 +4707,12 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
3015
4707
|
const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
|
|
3016
4708
|
runtime.lastTeacherError = null;
|
|
3017
4709
|
snapshot = ingestResult.snapshot;
|
|
3018
|
-
const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
|
|
4710
|
+
const promotion = await applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId, runtime.embedder, log);
|
|
3019
4711
|
runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
|
|
3020
4712
|
materializedPackId = promotion.materializedPackId;
|
|
4713
|
+
if (promotion.embedInstrumentation !== null) {
|
|
4714
|
+
runtime.lastEmbedInstrumentation = promotion.embedInstrumentation;
|
|
4715
|
+
}
|
|
3021
4716
|
failure = promotion.failure;
|
|
3022
4717
|
if (promotion.logLine !== null) {
|
|
3023
4718
|
log(promotion.logLine);
|
|
@@ -3049,8 +4744,20 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
3049
4744
|
snapshot = runtime.teacherLoop.snapshot();
|
|
3050
4745
|
}
|
|
3051
4746
|
}
|
|
4747
|
+
const afterInspection = inspectActivationState(runtime.activationRoot, observedAt);
|
|
4748
|
+
const lastObservedDelta = buildWatchLastObservedDelta({
|
|
4749
|
+
observedAt,
|
|
4750
|
+
localPoll,
|
|
4751
|
+
exported,
|
|
4752
|
+
scanResult,
|
|
4753
|
+
snapshotBefore,
|
|
4754
|
+
snapshotAfter: snapshot,
|
|
4755
|
+
beforeInspection,
|
|
4756
|
+
afterInspection
|
|
4757
|
+
});
|
|
3052
4758
|
persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
|
|
3053
4759
|
lastRunAt: observedAt,
|
|
4760
|
+
pollIntervalSeconds: runtime.pollIntervalSeconds,
|
|
3054
4761
|
scanRoot: runtime.scanRoot,
|
|
3055
4762
|
sessionTailCursorPath: runtime.sessionTailCursorPath,
|
|
3056
4763
|
sessionTailCursorUpdatedAt: observedAt,
|
|
@@ -3066,6 +4773,8 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
3066
4773
|
lastTeacherError: runtime.lastTeacherError,
|
|
3067
4774
|
localSessionTailNoopReason: localPoll.noopReason,
|
|
3068
4775
|
lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
|
|
4776
|
+
lastObservedDelta,
|
|
4777
|
+
embedInstrumentation: runtime.lastEmbedInstrumentation,
|
|
3069
4778
|
failure,
|
|
3070
4779
|
snapshot
|
|
3071
4780
|
});
|
|
@@ -3086,6 +4795,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
3086
4795
|
scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
|
|
3087
4796
|
scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
|
|
3088
4797
|
materialized: materializedPackId,
|
|
4798
|
+
lastObservedDelta,
|
|
3089
4799
|
diagnostics: snapshot.diagnostics ?? null,
|
|
3090
4800
|
localSessionTailNoopReason: localPoll.noopReason
|
|
3091
4801
|
}));
|
|
@@ -3103,6 +4813,7 @@ async function runWatchCommand(parsed) {
|
|
|
3103
4813
|
const runtime = await createWatchCommandRuntime({
|
|
3104
4814
|
activationRoot: parsed.activationRoot,
|
|
3105
4815
|
scanRoot: parsed.scanRoot,
|
|
4816
|
+
pollIntervalSeconds: parsed.interval,
|
|
3106
4817
|
log: watchLog
|
|
3107
4818
|
});
|
|
3108
4819
|
watchLog(`Interval: ${parsed.interval}s`);
|
|
@@ -3330,31 +5041,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
3330
5041
|
return runUninstallCommand(parsed);
|
|
3331
5042
|
}
|
|
3332
5043
|
if (parsed.command === "attach") {
|
|
3333
|
-
|
|
3334
|
-
mkdirSync(parsed.packRoot, { recursive: true });
|
|
3335
|
-
const result = bootstrapRuntimeAttach({
|
|
3336
|
-
profileSelector: "current_profile",
|
|
3337
|
-
...(parsed.brainAttachmentPolicy != null ? { brainAttachmentPolicy: parsed.brainAttachmentPolicy } : {}),
|
|
3338
|
-
activationRoot: parsed.activationRoot,
|
|
3339
|
-
packRoot: parsed.packRoot,
|
|
3340
|
-
packLabel: parsed.packLabel,
|
|
3341
|
-
workspace: {
|
|
3342
|
-
workspaceId: parsed.workspaceId,
|
|
3343
|
-
snapshotId: `${parsed.workspaceId}@bootstrap-${new Date().toISOString().slice(0, 10)}`,
|
|
3344
|
-
capturedAt: new Date().toISOString(),
|
|
3345
|
-
rootDir: process.cwd(),
|
|
3346
|
-
revision: "cli-bootstrap-v1"
|
|
3347
|
-
},
|
|
3348
|
-
interactionEvents: [],
|
|
3349
|
-
feedbackEvents: []
|
|
3350
|
-
});
|
|
3351
|
-
if (parsed.json) {
|
|
3352
|
-
console.log(JSON.stringify(result, null, 2));
|
|
3353
|
-
}
|
|
3354
|
-
else {
|
|
3355
|
-
console.log(formatBootstrapRuntimeAttachReport(result));
|
|
3356
|
-
}
|
|
3357
|
-
return 0;
|
|
5044
|
+
return runAttachCommand(parsed);
|
|
3358
5045
|
}
|
|
3359
5046
|
if (parsed.command === "scan") {
|
|
3360
5047
|
if (parsed.sessionPath !== null) {
|
|
@@ -3392,6 +5079,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
3392
5079
|
// At this point only status/rollback commands remain
|
|
3393
5080
|
const statusOrRollback = parsed;
|
|
3394
5081
|
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
|
|
5082
|
+
const targetInspection = statusOrRollback.openclawHome === null ? null : inspectOpenClawHome(statusOrRollback.openclawHome);
|
|
3395
5083
|
if (statusOrRollback.command === "rollback") {
|
|
3396
5084
|
const result = rollbackRuntimeAttach({
|
|
3397
5085
|
activationRoot,
|
|
@@ -3417,11 +5105,21 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
3417
5105
|
}
|
|
3418
5106
|
else {
|
|
3419
5107
|
const report = buildOperatorSurfaceReport(operatorInput);
|
|
5108
|
+
const providerConfig = readOpenClawBrainProviderConfigFromSources({
|
|
5109
|
+
env: process.env,
|
|
5110
|
+
activationRoot
|
|
5111
|
+
});
|
|
3420
5112
|
if (statusOrRollback.detailed) {
|
|
3421
|
-
console.log(formatCurrentProfileStatusSummary(status, report
|
|
5113
|
+
console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
|
|
5114
|
+
openclawHome: statusOrRollback.openclawHome,
|
|
5115
|
+
providerConfig
|
|
5116
|
+
}));
|
|
3422
5117
|
}
|
|
3423
5118
|
else {
|
|
3424
|
-
console.log(formatHumanFriendlyStatus(status, report
|
|
5119
|
+
console.log(formatHumanFriendlyStatus(status, report, targetInspection, {
|
|
5120
|
+
openclawHome: statusOrRollback.openclawHome,
|
|
5121
|
+
providerConfig
|
|
5122
|
+
}));
|
|
3425
5123
|
}
|
|
3426
5124
|
}
|
|
3427
5125
|
return 0;
|