@openclawbrain/openclaw 0.2.3 → 0.3.1
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 +2 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +81 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/extension/runtime-guard.d.ts +61 -0
- package/dist/extension/runtime-guard.js +230 -0
- package/dist/extension/runtime-guard.js.map +1 -0
- package/dist/src/cli.d.ts +19 -9
- package/dist/src/cli.js +1464 -380
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +7 -4
- package/dist/src/daemon.js +275 -47
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/index.d.ts +150 -2
- package/dist/src/index.js +769 -139
- package/dist/src/index.js.map +1 -1
- package/dist/src/learning-spine.d.ts +2 -1
- package/dist/src/learning-spine.js +8 -0
- package/dist/src/learning-spine.js.map +1 -1
- package/dist/src/ollama-client.d.ts +46 -0
- package/dist/src/ollama-client.js +231 -0
- package/dist/src/ollama-client.js.map +1 -0
- 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 +64 -0
- package/dist/src/provider-config.js +306 -0
- package/dist/src/provider-config.js.map +1 -0
- package/dist/src/resolve-activation-root.d.ts +5 -5
- package/dist/src/resolve-activation-root.js +76 -34
- package/dist/src/resolve-activation-root.js.map +1 -1
- package/dist/src/session-store.js +16 -5
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/session-tail.js +14 -2
- package/dist/src/session-tail.js.map +1 -1
- package/dist/src/shadow-extension-proof.d.ts +40 -0
- package/dist/src/shadow-extension-proof.js +214 -0
- package/dist/src/shadow-extension-proof.js.map +1 -0
- package/dist/src/teacher-labeler.d.ts +50 -0
- package/dist/src/teacher-labeler.js +424 -0
- package/dist/src/teacher-labeler.js.map +1 -0
- package/extension/index.ts +22 -1
- package/extension/runtime-guard.ts +17 -2
- package/package.json +8 -7
package/dist/src/cli.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync,
|
|
2
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
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 { DEFAULT_OLLAMA_EMBEDDING_MODEL } from "@openclawbrain/compiler";
|
|
8
9
|
import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
|
|
9
10
|
import { exportBrain, importBrain } from "./import-export.js";
|
|
10
11
|
import { buildNormalizedEventExport } from "@openclawbrain/contracts";
|
|
11
12
|
import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
|
|
12
|
-
import { inspectActivationState, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
|
|
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 { 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";
|
|
17
|
+
import { appendLearningUpdateLogs } from "./learning-spine.js";
|
|
15
18
|
import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
|
|
16
19
|
import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
20
|
+
import { readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
|
|
21
|
+
const OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV = "OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION";
|
|
22
|
+
const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
|
|
23
|
+
"qwen3.5:9b",
|
|
24
|
+
"qwen3.5:8b",
|
|
25
|
+
"qwen3:8b",
|
|
26
|
+
"qwen2.5:7b"
|
|
27
|
+
];
|
|
17
28
|
function quoteShellArg(value) {
|
|
18
29
|
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
19
30
|
}
|
|
@@ -24,37 +35,32 @@ function normalizeOptionalCliString(value) {
|
|
|
24
35
|
const trimmed = value.trim();
|
|
25
36
|
return trimmed.length > 0 ? trimmed : null;
|
|
26
37
|
}
|
|
38
|
+
function readTruthyEnvFlag(name, env = process.env) {
|
|
39
|
+
const value = normalizeOptionalCliString(env[name]);
|
|
40
|
+
if (value === null) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
44
|
+
}
|
|
27
45
|
function getCliHomeDir() {
|
|
28
46
|
return process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
29
47
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
let entries;
|
|
33
|
-
try {
|
|
34
|
-
entries = readdirSync(resolvedHomeDir, { withFileTypes: true });
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
return entries
|
|
40
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith(".openclaw-"))
|
|
41
|
-
.map((entry) => path.join(resolvedHomeDir, entry.name))
|
|
42
|
-
.filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
|
|
43
|
-
.sort((left, right) => left.localeCompare(right));
|
|
48
|
+
function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
|
|
49
|
+
return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
|
|
44
50
|
}
|
|
45
|
-
function
|
|
51
|
+
function formatInstallOpenClawHomeSource(source) {
|
|
46
52
|
switch (source) {
|
|
47
53
|
case "explicit":
|
|
48
54
|
return "--openclaw-home";
|
|
49
55
|
case "env":
|
|
50
56
|
return "OPENCLAW_HOME";
|
|
51
|
-
case "
|
|
52
|
-
return "single discovered
|
|
57
|
+
case "discovered_single_home":
|
|
58
|
+
return "single discovered install target";
|
|
53
59
|
default:
|
|
54
60
|
return source;
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
|
-
function
|
|
63
|
+
function resolveInstallOpenClawHome(explicitOpenclawHome) {
|
|
58
64
|
const normalizedExplicitHome = normalizeOptionalCliString(explicitOpenclawHome);
|
|
59
65
|
if (normalizedExplicitHome !== null) {
|
|
60
66
|
return {
|
|
@@ -69,30 +75,30 @@ function resolveSetupOpenClawHome(explicitOpenclawHome) {
|
|
|
69
75
|
openclawHomeSource: "env"
|
|
70
76
|
};
|
|
71
77
|
}
|
|
72
|
-
const discoveredHomes =
|
|
78
|
+
const discoveredHomes = discoverInstallCandidateOpenClawHomes();
|
|
73
79
|
if (discoveredHomes.length === 1) {
|
|
74
80
|
return {
|
|
75
81
|
openclawHome: path.resolve(discoveredHomes[0]),
|
|
76
|
-
openclawHomeSource: "
|
|
82
|
+
openclawHomeSource: "discovered_single_home"
|
|
77
83
|
};
|
|
78
84
|
}
|
|
79
85
|
if (discoveredHomes.length > 1) {
|
|
80
86
|
const installPrefix = detectConsumerSafeOperatorCliPrefix();
|
|
81
|
-
const targetChoices =
|
|
82
|
-
.map((
|
|
83
|
-
const resolvedCandidate = path.resolve(
|
|
84
|
-
return ` - ${resolvedCandidate}\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
|
|
87
|
+
const targetChoices = discoverOpenClawHomes()
|
|
88
|
+
.map((inspection) => {
|
|
89
|
+
const resolvedCandidate = path.resolve(inspection.openclawHome);
|
|
90
|
+
return ` - ${resolvedCandidate} (${describeOpenClawHomeInspection(inspection)})\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
|
|
85
91
|
})
|
|
86
92
|
.join("\n");
|
|
87
93
|
throw new Error([
|
|
88
|
-
"Refusing ambiguous
|
|
94
|
+
"Refusing ambiguous OpenClaw install targets.",
|
|
89
95
|
targetChoices,
|
|
90
|
-
"Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one
|
|
96
|
+
"Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one OpenClaw home."
|
|
91
97
|
].join("\n"));
|
|
92
98
|
}
|
|
93
|
-
throw new Error("No OpenClaw
|
|
99
|
+
throw new Error("No OpenClaw home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
|
|
94
100
|
}
|
|
95
|
-
function
|
|
101
|
+
function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
|
|
96
102
|
const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
|
|
97
103
|
if (normalizedExplicitActivationRoot !== null) {
|
|
98
104
|
return {
|
|
@@ -105,7 +111,7 @@ function resolveSetupActivationRoot(openclawHome, explicitActivationRoot) {
|
|
|
105
111
|
source: "default_from_openclaw_home"
|
|
106
112
|
};
|
|
107
113
|
}
|
|
108
|
-
function
|
|
114
|
+
function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
109
115
|
const normalizedExplicitWorkspaceId = normalizeOptionalCliString(explicitWorkspaceId);
|
|
110
116
|
if (normalizedExplicitWorkspaceId !== null) {
|
|
111
117
|
return {
|
|
@@ -113,18 +119,24 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
|
113
119
|
source: "explicit"
|
|
114
120
|
};
|
|
115
121
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
const inspection = inspectOpenClawHome(openclawHome);
|
|
123
|
+
if (inspection.profileId !== null) {
|
|
124
|
+
return {
|
|
125
|
+
workspaceId: inspection.profileId,
|
|
126
|
+
source: inspection.profileSource === "directory_name"
|
|
127
|
+
? "openclaw_home_dir"
|
|
128
|
+
: inspection.profileSource === "openclaw_json_profile"
|
|
129
|
+
? "openclaw_json_profile"
|
|
130
|
+
: inspection.profileSource === "openclaw_json_single_profile_key"
|
|
131
|
+
? "openclaw_json_single_profile_key"
|
|
132
|
+
: "fallback"
|
|
133
|
+
};
|
|
125
134
|
}
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
if (inspection.layout === "shared_home_profiles_in_config" || inspection.layout === "single_openclaw_home") {
|
|
136
|
+
return {
|
|
137
|
+
workspaceId: "current_profile",
|
|
138
|
+
source: "current_profile_boundary"
|
|
139
|
+
};
|
|
128
140
|
}
|
|
129
141
|
const dirName = path.basename(openclawHome);
|
|
130
142
|
if (dirName === ".openclaw") {
|
|
@@ -145,18 +157,40 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
|
145
157
|
source: "fallback"
|
|
146
158
|
};
|
|
147
159
|
}
|
|
148
|
-
function
|
|
160
|
+
function resolveInstallEmbedderProvisionSkip(explicitSkip) {
|
|
161
|
+
if (explicitSkip) {
|
|
162
|
+
return {
|
|
163
|
+
skipEmbedderProvision: true,
|
|
164
|
+
skipEmbedderProvisionSource: "flag"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (readTruthyEnvFlag(OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV)) {
|
|
168
|
+
return {
|
|
169
|
+
skipEmbedderProvision: true,
|
|
170
|
+
skipEmbedderProvisionSource: "env"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
skipEmbedderProvision: false,
|
|
175
|
+
skipEmbedderProvisionSource: null
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function formatInstallActivationRootSource(source) {
|
|
149
179
|
if (source === "explicit") {
|
|
150
180
|
return "explicit --activation-root";
|
|
151
181
|
}
|
|
152
182
|
return "default beside --openclaw-home";
|
|
153
183
|
}
|
|
154
|
-
function
|
|
184
|
+
function formatInstallWorkspaceIdSource(source) {
|
|
155
185
|
switch (source) {
|
|
156
186
|
case "explicit":
|
|
157
187
|
return "explicit --workspace-id";
|
|
158
188
|
case "openclaw_json_profile":
|
|
159
189
|
return "from openclaw.json profile";
|
|
190
|
+
case "openclaw_json_single_profile_key":
|
|
191
|
+
return "from the only openclaw.json profiles entry";
|
|
192
|
+
case "current_profile_boundary":
|
|
193
|
+
return "current_profile boundary for a shared OpenClaw home";
|
|
160
194
|
case "openclaw_home_dir":
|
|
161
195
|
return "from OpenClaw home dir";
|
|
162
196
|
default:
|
|
@@ -262,7 +296,7 @@ function buildDoctorDeletedMessage(args) {
|
|
|
262
296
|
const jsonCommand = buildStatusReplacementCommand(replacementInput, true);
|
|
263
297
|
const lines = [
|
|
264
298
|
"`doctor` is no longer a separate operator surface.",
|
|
265
|
-
'Use `openclawbrain status
|
|
299
|
+
'Use `openclawbrain status --activation-root <path>` as the human answer to "How\'s the brain?" and `status --json` for the canonical current-profile object.',
|
|
266
300
|
"Use `describeAttachStatus()` or the proof helpers only when you need deeper activation diagnostics."
|
|
267
301
|
];
|
|
268
302
|
if (json && jsonCommand !== null) {
|
|
@@ -276,36 +310,42 @@ function buildDoctorDeletedMessage(args) {
|
|
|
276
310
|
}
|
|
277
311
|
return lines.join(" ");
|
|
278
312
|
}
|
|
313
|
+
function buildSetupDeletedMessage() {
|
|
314
|
+
return [
|
|
315
|
+
"`setup` has been removed.",
|
|
316
|
+
"Use `openclawbrain install` instead.",
|
|
317
|
+
"The install command still accepts the explicit targeting flags that setup used: `--openclaw-home`, `--activation-root`, `--workspace-id`, and `--shared`."
|
|
318
|
+
].join(" ");
|
|
319
|
+
}
|
|
279
320
|
function operatorCliHelp() {
|
|
280
321
|
return [
|
|
281
322
|
"Usage:",
|
|
282
323
|
" openclawbrain install [--openclaw-home <path>] [options]",
|
|
283
|
-
" openclawbrain
|
|
324
|
+
" openclawbrain attach --openclaw-home <path> [options]",
|
|
325
|
+
" openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
|
|
326
|
+
" openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
|
|
327
|
+
" openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
|
|
284
328
|
" openclawbrain detach --openclaw-home <path> [options]",
|
|
285
329
|
" openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
|
|
286
|
-
" openclawbrain
|
|
287
|
-
" openclawbrain
|
|
288
|
-
" openclawbrain context \"message\" [--activation-root <path>]",
|
|
289
|
-
" openclawbrain history [--activation-root <path>] [--limit N] [--json]",
|
|
330
|
+
" openclawbrain context \"message\" [--activation-root <path>|--openclaw-home <path>]",
|
|
331
|
+
" openclawbrain history [--activation-root <path>|--openclaw-home <path>] [--limit N] [--json]",
|
|
290
332
|
" openclawbrain scan --session <trace.json> --root <path> [options]",
|
|
291
333
|
" openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
|
|
292
|
-
" openclawbrain learn [--activation-root <path>] [--json]",
|
|
293
|
-
" openclawbrain
|
|
294
|
-
" openclawbrain daemon <start|stop|status|logs> [--activation-root <path>]",
|
|
295
|
-
" openclawbrain-ops <status|rollback> --activation-root <path> [options] # compatibility alias",
|
|
334
|
+
" openclawbrain learn [--activation-root <path>|--openclaw-home <path>] [--json]",
|
|
335
|
+
" openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
|
|
296
336
|
" openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
|
|
297
337
|
"",
|
|
298
338
|
"Options:",
|
|
299
|
-
" --openclaw-home <path> OpenClaw
|
|
300
|
-
" --shared Set brain-attachment-policy to shared instead of dedicated (install/
|
|
301
|
-
|
|
339
|
+
" --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.",
|
|
340
|
+
" --shared Set brain-attachment-policy to shared instead of dedicated (install/attach only).",
|
|
341
|
+
` --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.`,
|
|
342
|
+
" --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.",
|
|
302
343
|
" --keep-data Preserve activation data on uninstall; detach always behaves this way.",
|
|
303
344
|
" --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
|
|
304
345
|
" --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
|
|
305
|
-
" --
|
|
306
|
-
" --workspace-id <id> Workspace identifier for attach/install/setup provenance; install/setup defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
|
|
346
|
+
" --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.",
|
|
307
347
|
" --event-export <path> Event-export bundle root or normalized export JSON payload.",
|
|
308
|
-
" --teacher-snapshot <path>
|
|
348
|
+
" --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.",
|
|
309
349
|
" --updated-at <iso> Observation time to use for freshness checks.",
|
|
310
350
|
" --brain-attachment-policy <undeclared|dedicated|shared> Override attachment policy semantics for status inspection.",
|
|
311
351
|
" --detailed Show verbose diagnostic output for status (default is human-friendly summary).",
|
|
@@ -313,7 +353,7 @@ function operatorCliHelp() {
|
|
|
313
353
|
" --session <path> Sanitized recorded-session trace JSON to replay.",
|
|
314
354
|
" --live <path> Runtime event-export bundle root or normalized export JSON to scan once.",
|
|
315
355
|
" --root <path> Output root for scan --session replay artifacts.",
|
|
316
|
-
" --workspace <path> Workspace metadata JSON for scan --live candidate
|
|
356
|
+
" --workspace <path> Workspace metadata JSON for scan --live candidate materialization.",
|
|
317
357
|
" --pack-label <label> Candidate-pack label for scan --live. Defaults to scanner-live-cli.",
|
|
318
358
|
" --observed-at <iso> Observation time for scan --live freshness checks.",
|
|
319
359
|
" --snapshot-out <path> Write the one-shot scan --live snapshot JSON.",
|
|
@@ -323,24 +363,31 @@ function operatorCliHelp() {
|
|
|
323
363
|
" --json Emit machine-readable JSON instead of text.",
|
|
324
364
|
" --help Show this help.",
|
|
325
365
|
"",
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
330
|
-
"
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
"
|
|
336
|
-
"
|
|
337
|
-
"
|
|
366
|
+
"Lifecycle flow:",
|
|
367
|
+
" 1. install openclawbrain install — safe first-time default; pass --openclaw-home when more than one OpenClaw home/layout is present",
|
|
368
|
+
" 2. attach openclawbrain attach --openclaw-home <path> [--activation-root <path>] — explicit reattach/manual hook path for known brain data; use install first",
|
|
369
|
+
" 3. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
|
|
370
|
+
" 4. status --detailed openclawbrain status --activation-root <path> --detailed — explain serve path, freshness, backlog, and failure mode",
|
|
371
|
+
" 5. watch openclawbrain watch --activation-root <path> — run the foreground learning/watch loop",
|
|
372
|
+
" 6. daemon start openclawbrain daemon start --activation-root <path> — keep watch running in the background on macOS",
|
|
373
|
+
" 7. daemon status openclawbrain daemon status --activation-root <path> — inspect the background watch state",
|
|
374
|
+
" 8. detach openclawbrain detach --openclaw-home <path> — remove the profile hookup only and keep brain data",
|
|
375
|
+
" 9. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data — remove the hookup and choose the data outcome explicitly",
|
|
376
|
+
"",
|
|
377
|
+
"Advanced/operator surfaces:",
|
|
378
|
+
" context preview the brain context that would be injected for a message",
|
|
379
|
+
" rollback preview or apply active <- previous, active -> candidate pointer movement",
|
|
380
|
+
" scan inspect one recorded session or live event export without claiming a daemon is running",
|
|
381
|
+
" learn one-shot local-session learning pass against the resolved activation root",
|
|
338
382
|
" status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
|
|
339
|
-
" watch/daemon persist
|
|
383
|
+
" watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
|
|
384
|
+
" 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",
|
|
340
385
|
"",
|
|
341
386
|
"Exit codes:",
|
|
387
|
+
" install: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
|
|
342
388
|
" status: 0 on successful inspection, 1 on input/read failure.",
|
|
343
389
|
" rollback: 0 when ready/applied, 1 when blocked or on input/read failure.",
|
|
390
|
+
" attach: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
|
|
344
391
|
" detach: 0 on successful unhook, 1 on input/read failure.",
|
|
345
392
|
" uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
|
|
346
393
|
" scan: 0 on successful replay/scan, 1 on input/read failure."
|
|
@@ -382,7 +429,8 @@ function formatLearningBuckets(report) {
|
|
|
382
429
|
return `pi:${buckets.principal_immediate},pb:${buckets.principal_backfill},live:${buckets.live},backfill:${buckets.backfill}`;
|
|
383
430
|
}
|
|
384
431
|
function formatLearningWarnings(report) {
|
|
385
|
-
|
|
432
|
+
const warnings = report.learning.warningStates.filter((warning) => warning !== "teacher_snapshot_unavailable");
|
|
433
|
+
return warnings.length === 0 ? "none" : warnings.join("|");
|
|
386
434
|
}
|
|
387
435
|
function formatLabelFlowSummary(labelFlow) {
|
|
388
436
|
return `source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`;
|
|
@@ -390,14 +438,322 @@ function formatLabelFlowSummary(labelFlow) {
|
|
|
390
438
|
function formatLearningPathSummary(learningPath) {
|
|
391
439
|
return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
|
|
392
440
|
}
|
|
393
|
-
function
|
|
441
|
+
function formatTeacherLoopSummary(report) {
|
|
442
|
+
const parts = [
|
|
443
|
+
`snapshot=${report.teacherLoop.sourcePath ?? "none"}`,
|
|
444
|
+
`kind=${report.teacherLoop.sourceKind}`,
|
|
445
|
+
`lastRun=${report.teacherLoop.lastRunAt ?? "none"}`,
|
|
446
|
+
`artifacts=${report.teacherLoop.artifactCount ?? "none"}`,
|
|
447
|
+
`freshness=${report.teacherLoop.latestFreshness}`,
|
|
448
|
+
`queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"}`,
|
|
449
|
+
`running=${yesNo(report.teacherLoop.running)}`
|
|
450
|
+
];
|
|
451
|
+
if (report.teacherLoop.lastNoOpReason !== "none") {
|
|
452
|
+
parts.push(`noOp=${report.teacherLoop.lastNoOpReason}`);
|
|
453
|
+
}
|
|
454
|
+
if (report.teacherLoop.failureMode !== "none") {
|
|
455
|
+
const failureDetail = report.teacherLoop.failureDetail === null
|
|
456
|
+
? report.teacherLoop.failureMode
|
|
457
|
+
: `${report.teacherLoop.failureMode}(${report.teacherLoop.failureDetail})`;
|
|
458
|
+
parts.push(`failure=${failureDetail}`);
|
|
459
|
+
}
|
|
460
|
+
return parts.join(" ");
|
|
461
|
+
}
|
|
462
|
+
function formatCompactValue(value, maxLength = 64) {
|
|
463
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}...`;
|
|
464
|
+
}
|
|
465
|
+
function formatCompactList(values, maxItems = 2, maxLength = 64) {
|
|
466
|
+
if (values.length === 0) {
|
|
467
|
+
return "none";
|
|
468
|
+
}
|
|
469
|
+
const visible = values.slice(0, maxItems).map((value) => formatCompactValue(value, maxLength));
|
|
470
|
+
return values.length > maxItems ? `${visible.join("|")}+${values.length - maxItems}more` : visible.join("|");
|
|
471
|
+
}
|
|
472
|
+
const SERVICE_RISK_FINDING_CODES = new Set([
|
|
473
|
+
"activation_broken_install",
|
|
474
|
+
"activation_stale_incomplete",
|
|
475
|
+
"active_missing",
|
|
476
|
+
"active_unhealthy",
|
|
477
|
+
"learned_route_missing",
|
|
478
|
+
"serve_path_fail_open",
|
|
479
|
+
"serve_path_hard_fail",
|
|
480
|
+
"serve_path_route_evidence_missing"
|
|
481
|
+
]);
|
|
482
|
+
const DEGRADED_BRAIN_FINDING_CODES = new Set([
|
|
483
|
+
"bootstrap_waiting_for_first_export",
|
|
484
|
+
"serve_path_unprobed",
|
|
485
|
+
"brain_context_kernel_only",
|
|
486
|
+
"candidate_unhealthy",
|
|
487
|
+
"promotion_blocked",
|
|
488
|
+
"supervision_not_flowing",
|
|
489
|
+
"scan_surfaces_missing"
|
|
490
|
+
]);
|
|
491
|
+
const COSMETIC_FINDING_CODES = new Set([
|
|
492
|
+
"last_promotion_unknown",
|
|
493
|
+
"rollback_blocked",
|
|
494
|
+
"supervision_unavailable",
|
|
495
|
+
"turn_attribution_partial",
|
|
496
|
+
"teacher_snapshot_unavailable"
|
|
497
|
+
]);
|
|
498
|
+
const LEARNING_WARNING_MESSAGES = {
|
|
499
|
+
awaiting_first_export: "awaiting first export",
|
|
500
|
+
principal_live_backlog: "principal live backlog is ahead of serving",
|
|
501
|
+
principal_backfill_pending: "principal backfill is still queued",
|
|
502
|
+
active_pack_behind_latest_principal: "active pack is behind the latest principal correction",
|
|
503
|
+
passive_backfill_pending: "passive backfill remains queued",
|
|
504
|
+
teacher_queue_full: "teacher queue is full",
|
|
505
|
+
teacher_labels_stale: "teacher labels are stale",
|
|
506
|
+
teacher_no_artifacts: "teacher produced no artifacts",
|
|
507
|
+
teacher_snapshot_unavailable: "teacher snapshot is unavailable"
|
|
508
|
+
};
|
|
509
|
+
function summarizeStatusInstallHook(openclawHome) {
|
|
510
|
+
if (openclawHome === null) {
|
|
511
|
+
return {
|
|
512
|
+
state: "unknown",
|
|
513
|
+
detail: "profile hook state is unknown from activation-root-only status; pin --openclaw-home to prove install state"
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const extensionDir = path.join(path.resolve(openclawHome), "extensions", "openclawbrain");
|
|
517
|
+
const indexPath = path.join(extensionDir, "index.ts");
|
|
518
|
+
const runtimeGuardPath = path.join(extensionDir, "runtime-guard.js");
|
|
519
|
+
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
520
|
+
if (existsSync(indexPath) && existsSync(runtimeGuardPath) && existsSync(manifestPath)) {
|
|
521
|
+
return {
|
|
522
|
+
state: "installed",
|
|
523
|
+
detail: `profile hook is installed at ${shortenPath(extensionDir)}`
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
state: "not_installed",
|
|
528
|
+
detail: `profile hook is not present at ${shortenPath(extensionDir)}`
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function runOllamaProbe(args, baseUrl) {
|
|
532
|
+
try {
|
|
533
|
+
execFileSync("ollama", [...args], {
|
|
534
|
+
stdio: "pipe",
|
|
535
|
+
timeout: 2_000,
|
|
536
|
+
env: {
|
|
537
|
+
...process.env,
|
|
538
|
+
OLLAMA_HOST: baseUrl
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
detected: true,
|
|
543
|
+
detail: `ollama responded to ${args.join(" ")} at ${baseUrl}`
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
548
|
+
return {
|
|
549
|
+
detected: false,
|
|
550
|
+
detail: "ollama CLI was not found on PATH"
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
detected: true,
|
|
555
|
+
detail: describeExecFailure(error)
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function summarizeStatusEmbeddings(report, providerConfig) {
|
|
560
|
+
let embeddedEntryCount = null;
|
|
561
|
+
let totalEntryCount = null;
|
|
562
|
+
let models = [];
|
|
563
|
+
let liveState = "unknown";
|
|
564
|
+
let liveDetail = "no activation-ready active pack is available for embedding inspection";
|
|
565
|
+
if (report.active !== null && report.active.activationReady) {
|
|
566
|
+
try {
|
|
567
|
+
const activePack = loadPackFromActivation(report.activationRoot, "active", {
|
|
568
|
+
requireActivationReady: true
|
|
569
|
+
});
|
|
570
|
+
if (activePack !== null) {
|
|
571
|
+
totalEntryCount = activePack.vectors.entries.length;
|
|
572
|
+
embeddedEntryCount = activePack.vectors.entries.filter((entry) => entry.embedding !== undefined).length;
|
|
573
|
+
models = [...new Set(activePack.vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
|
|
574
|
+
liveState = embeddedEntryCount > 0 ? "yes" : "no";
|
|
575
|
+
liveDetail = `active pack stores ${embeddedEntryCount}/${totalEntryCount} numeric embeddings`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
liveDetail = `embedding inspection failed: ${toErrorMessage(error)}`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (providerConfig.embedder.provider === "off") {
|
|
583
|
+
return {
|
|
584
|
+
provider: providerConfig.embedder.provider,
|
|
585
|
+
model: providerConfig.embedder.model,
|
|
586
|
+
provisionedState: "off",
|
|
587
|
+
liveState,
|
|
588
|
+
embeddedEntryCount,
|
|
589
|
+
totalEntryCount,
|
|
590
|
+
models,
|
|
591
|
+
detail: `${liveDetail}; embedder provider is off`
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
if (providerConfig.embedder.provider === "keywords") {
|
|
595
|
+
return {
|
|
596
|
+
provider: providerConfig.embedder.provider,
|
|
597
|
+
model: providerConfig.embedder.model,
|
|
598
|
+
provisionedState: "builtin",
|
|
599
|
+
liveState,
|
|
600
|
+
embeddedEntryCount,
|
|
601
|
+
totalEntryCount,
|
|
602
|
+
models,
|
|
603
|
+
detail: `${liveDetail}; keyword embedder needs no Ollama model provision`
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const modelProbe = runOllamaProbe(["show", providerConfig.embedder.model], providerConfig.embedderBaseUrl);
|
|
607
|
+
return {
|
|
608
|
+
provider: providerConfig.embedder.provider,
|
|
609
|
+
model: providerConfig.embedder.model,
|
|
610
|
+
provisionedState: modelProbe.detected && /responded to/.test(modelProbe.detail) ? "confirmed" : "not_confirmed",
|
|
611
|
+
liveState,
|
|
612
|
+
embeddedEntryCount,
|
|
613
|
+
totalEntryCount,
|
|
614
|
+
models,
|
|
615
|
+
detail: `${liveDetail}; ollama model check: ${modelProbe.detail}`
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function summarizeStatusLocalLlm(providerConfig) {
|
|
619
|
+
const detection = runOllamaProbe(["--version"], providerConfig.teacherBaseUrl);
|
|
620
|
+
const enabled = providerConfig.teacher.provider === "ollama";
|
|
621
|
+
if (enabled) {
|
|
622
|
+
return {
|
|
623
|
+
detected: detection.detected,
|
|
624
|
+
enabled,
|
|
625
|
+
provider: providerConfig.teacher.provider,
|
|
626
|
+
model: providerConfig.teacher.model,
|
|
627
|
+
detail: detection.detected
|
|
628
|
+
? `teacher provider is ollama and the local LLM surface answered at ${providerConfig.teacherBaseUrl}`
|
|
629
|
+
: `teacher provider is ollama but the local LLM surface was not detected (${detection.detail})`
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
detected: detection.detected,
|
|
634
|
+
enabled,
|
|
635
|
+
provider: providerConfig.teacher.provider,
|
|
636
|
+
model: providerConfig.teacher.model,
|
|
637
|
+
detail: detection.detected
|
|
638
|
+
? `local Ollama is detectable, but teacher labeling is ${providerConfig.teacher.provider}`
|
|
639
|
+
: `teacher labeling is ${providerConfig.teacher.provider}; no local Ollama CLI was detected`
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function pushUniqueAlert(target, value) {
|
|
643
|
+
const normalized = value.trim();
|
|
644
|
+
if (normalized.length === 0) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (target.includes(normalized) === false) {
|
|
648
|
+
target.push(normalized);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function summarizeStatusAlerts(report, providerConfig, embeddings, localLlm) {
|
|
652
|
+
const buckets = {
|
|
653
|
+
serviceRisk: [],
|
|
654
|
+
degradedBrain: [],
|
|
655
|
+
cosmeticNoise: []
|
|
656
|
+
};
|
|
657
|
+
for (const finding of report.findings) {
|
|
658
|
+
if (finding.severity === "pass") {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (SERVICE_RISK_FINDING_CODES.has(finding.code)) {
|
|
662
|
+
pushUniqueAlert(buckets.serviceRisk, finding.summary);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (DEGRADED_BRAIN_FINDING_CODES.has(finding.code)) {
|
|
666
|
+
pushUniqueAlert(buckets.degradedBrain, finding.summary);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (COSMETIC_FINDING_CODES.has(finding.code)) {
|
|
670
|
+
pushUniqueAlert(buckets.cosmeticNoise, finding.summary);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
pushUniqueAlert(finding.severity === "fail" ? buckets.serviceRisk : buckets.degradedBrain, finding.summary);
|
|
674
|
+
}
|
|
675
|
+
for (const warningState of report.learning.warningStates) {
|
|
676
|
+
const message = LEARNING_WARNING_MESSAGES[warningState];
|
|
677
|
+
if (message === undefined) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (warningState === "teacher_snapshot_unavailable") {
|
|
681
|
+
pushUniqueAlert(buckets.cosmeticNoise, message);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
pushUniqueAlert(buckets.degradedBrain, message);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (providerConfig.warnings.length > 0) {
|
|
688
|
+
pushUniqueAlert(buckets.cosmeticNoise, "provider env warnings forced fallback defaults");
|
|
689
|
+
}
|
|
690
|
+
if (localLlm.enabled && !localLlm.detected) {
|
|
691
|
+
pushUniqueAlert(buckets.degradedBrain, "local LLM is enabled but not detected");
|
|
692
|
+
}
|
|
693
|
+
if (embeddings.provider === "ollama" && embeddings.provisionedState !== "confirmed") {
|
|
694
|
+
pushUniqueAlert(buckets.degradedBrain, `embedder model ${embeddings.model} is not confirmed on Ollama`);
|
|
695
|
+
}
|
|
696
|
+
if (embeddings.provider === "ollama" && embeddings.liveState === "no") {
|
|
697
|
+
pushUniqueAlert(buckets.degradedBrain, "embedder is provisioned but the active pack has no live numeric embeddings");
|
|
698
|
+
}
|
|
699
|
+
return buckets;
|
|
700
|
+
}
|
|
701
|
+
function summarizeStatusWatchState(report) {
|
|
702
|
+
if (!report.teacherLoop.available || report.teacherLoop.sourceKind !== "watch_snapshot") {
|
|
703
|
+
return "not_visible";
|
|
704
|
+
}
|
|
705
|
+
return report.teacherLoop.running === true ? "running" : "snapshot_only";
|
|
706
|
+
}
|
|
707
|
+
function summarizeStatusServeReality(status) {
|
|
708
|
+
if (status.brainStatus.serveState === "serving_active_pack") {
|
|
709
|
+
return "proven_active_pack";
|
|
710
|
+
}
|
|
711
|
+
return status.brainStatus.serveState;
|
|
712
|
+
}
|
|
713
|
+
function formatStatusAlertLine(values) {
|
|
714
|
+
const normalized = values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
715
|
+
return normalized.length === 0 ? "none" : formatCompactList(normalized, 2, 64);
|
|
716
|
+
}
|
|
717
|
+
function summarizeStatusStartupToken(status) {
|
|
718
|
+
if (status.attachment.state !== "attached") {
|
|
719
|
+
return "BRAIN_NOT_YET_LOADED";
|
|
720
|
+
}
|
|
721
|
+
if (status.brainStatus.activationState === "broken_install" || status.brainStatus.activationState === "stale_incomplete" || status.brainStatus.activationState === "detached") {
|
|
722
|
+
return "BRAIN_NOT_YET_LOADED";
|
|
723
|
+
}
|
|
724
|
+
return status.brainStatus.serveState === "serving_active_pack" ? "BRAIN_LOADED" : "BRAIN_NOT_YET_LOADED";
|
|
725
|
+
}
|
|
726
|
+
function buildCompactStatusHeader(status, report, options) {
|
|
727
|
+
const installHook = summarizeStatusInstallHook(options.openclawHome);
|
|
728
|
+
const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
|
|
729
|
+
const localLlm = summarizeStatusLocalLlm(options.providerConfig);
|
|
730
|
+
const alerts = summarizeStatusAlerts(report, options.providerConfig, embeddings, localLlm);
|
|
731
|
+
const promoted = status.brain.state === "pg_promoted_pack_authoritative" ? "yes" : "no";
|
|
732
|
+
const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
|
|
733
|
+
return [
|
|
734
|
+
`reality hook=${installHook.state} attach=${status.attachment.state} watch=${summarizeStatusWatchState(report)} promoted=${promoted} serve=${summarizeStatusServeReality(status)}`,
|
|
735
|
+
`startup ${summarizeStatusStartupToken(status)} init=${status.brainStatus.activationState} proof=status_probe`,
|
|
736
|
+
`explain ${status.brain.summary}`,
|
|
737
|
+
`embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
|
|
738
|
+
`localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
|
|
739
|
+
`alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
|
|
740
|
+
];
|
|
741
|
+
}
|
|
742
|
+
function formatCurrentProfileStatusSummary(status, report, targetInspection, options) {
|
|
394
743
|
const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
|
|
744
|
+
const targetLine = targetInspection === null
|
|
745
|
+
? `target activation=${status.host.activationRoot} source=activation_root_only`
|
|
746
|
+
: `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${shortenPath(path.join(targetInspection.openclawHome, "extensions", "openclawbrain", "index.ts"))}`;
|
|
395
747
|
return [
|
|
396
748
|
`STATUS ${status.brainStatus.status}`,
|
|
749
|
+
...buildCompactStatusHeader(status, report, options),
|
|
397
750
|
`answer ${status.brain.summary}`,
|
|
751
|
+
targetLine,
|
|
752
|
+
...(targetInspection === null ? [] : [`preflight ${formatOpenClawTargetExplanation(targetInspection)}`]),
|
|
398
753
|
`host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
|
|
399
754
|
`profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
|
|
400
755
|
`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)}`,
|
|
756
|
+
`activation state=${status.brainStatus.activationState} detail=${status.brain.detail}`,
|
|
401
757
|
`brain pack=${status.brain.activePackId ?? "none"} state=${status.brain.state} init=${status.brain.initMode ?? "unknown"} routeFreshness=${status.brain.routeFreshness} lastPromotion=${status.brain.lastPromotionAt ?? "none"} router=${status.brain.routerIdentity ?? "none"}`,
|
|
402
758
|
`serve state=${status.brainStatus.serveState} failOpen=${yesNo(status.brainStatus.failOpen)} hardFail=${yesNo(report.servePath.hardRequirementViolated)} usedRouteFn=${yesNo(status.brainStatus.usedLearnedRouteFn)} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)} detail=${status.brainStatus.detail}`,
|
|
403
759
|
`route router=${report.servePath.routerIdentity ?? status.brain.routerIdentity ?? "none"} supervision=${report.servePath.refreshStatus ?? status.brain.routeFreshness} freshness=${report.servePath.freshnessChecksum ?? "none"}`,
|
|
@@ -409,7 +765,8 @@ function formatCurrentProfileStatusSummary(status, report) {
|
|
|
409
765
|
`graph source=${report.graph.runtimePlasticitySource ?? "none"} ops=${formatStructuralOps(report)} changed=${yesNo(report.graph.changed)} pruned=${report.graph.prunedBlockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} summary=${report.graph.operatorSummary ?? report.graph.detail}`,
|
|
410
766
|
`path ${formatLearningPathSummary(report.learningPath)}`,
|
|
411
767
|
`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}`,
|
|
412
|
-
`teacher
|
|
768
|
+
`teacher ${formatTeacherLoopSummary(report)}`,
|
|
769
|
+
`passive cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} slices=${report.teacherLoop.liveSlicesPerCycle ?? "none"}/${report.teacherLoop.backfillSlicesPerCycle ?? "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"}`,
|
|
413
770
|
`rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
414
771
|
`proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
|
|
415
772
|
`logs root=${status.brain.logRoot ?? "none"}`,
|
|
@@ -426,15 +783,322 @@ function shortenPath(fullPath) {
|
|
|
426
783
|
}
|
|
427
784
|
return fullPath;
|
|
428
785
|
}
|
|
786
|
+
function formatOpenClawTargetLine(inspection) {
|
|
787
|
+
const profilePart = inspection.profileId === null
|
|
788
|
+
? "profile=current_profile"
|
|
789
|
+
: `profile=${inspection.profileId} via ${formatOpenClawHomeProfileSource(inspection.profileSource)}`;
|
|
790
|
+
return `home=${shortenPath(inspection.openclawHome)} layout=${formatOpenClawHomeLayout(inspection.layout)} ${profilePart}`;
|
|
791
|
+
}
|
|
792
|
+
function formatOpenClawTargetExplanation(inspection) {
|
|
793
|
+
return describeOpenClawHomeInspection(inspection);
|
|
794
|
+
}
|
|
429
795
|
function buildInstallStatusCommand(activationRoot) {
|
|
430
796
|
return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
|
|
431
797
|
}
|
|
432
798
|
function buildInstallCommand(openclawHome) {
|
|
433
799
|
return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
|
|
434
800
|
}
|
|
801
|
+
function buildAttachCommand(openclawHome, activationRoot = null) {
|
|
802
|
+
const parts = ["openclawbrain", "attach", "--openclaw-home", quoteShellArg(openclawHome)];
|
|
803
|
+
if (activationRoot !== null) {
|
|
804
|
+
parts.push("--activation-root", quoteShellArg(activationRoot));
|
|
805
|
+
}
|
|
806
|
+
return parts.join(" ");
|
|
807
|
+
}
|
|
808
|
+
function buildInstallEmbedderProvisionCommand(baseUrl, model) {
|
|
809
|
+
return `OLLAMA_HOST=${quoteShellArg(baseUrl)} ollama pull ${quoteShellArg(model)}`;
|
|
810
|
+
}
|
|
811
|
+
function describeExecOutput(value) {
|
|
812
|
+
if (typeof value === "string") {
|
|
813
|
+
const normalized = value.trim();
|
|
814
|
+
return normalized.length > 0 ? normalized : null;
|
|
815
|
+
}
|
|
816
|
+
if (value instanceof Buffer) {
|
|
817
|
+
const normalized = value.toString("utf8").trim();
|
|
818
|
+
return normalized.length > 0 ? normalized : null;
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
function describeExecFailure(error) {
|
|
823
|
+
if (error instanceof Error) {
|
|
824
|
+
const childError = error;
|
|
825
|
+
if (childError.code === "ENOENT") {
|
|
826
|
+
return "ollama was not found on PATH";
|
|
827
|
+
}
|
|
828
|
+
const stderr = describeExecOutput(childError.stderr);
|
|
829
|
+
if (stderr !== null) {
|
|
830
|
+
return stderr;
|
|
831
|
+
}
|
|
832
|
+
const stdout = describeExecOutput(childError.stdout);
|
|
833
|
+
if (stdout !== null) {
|
|
834
|
+
return stdout;
|
|
835
|
+
}
|
|
836
|
+
const message = childError.message.trim();
|
|
837
|
+
if (message.length > 0) {
|
|
838
|
+
return message;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return String(error);
|
|
842
|
+
}
|
|
843
|
+
function toErrorMessage(error) {
|
|
844
|
+
return error instanceof Error ? error.message : String(error);
|
|
845
|
+
}
|
|
846
|
+
function ensureInstallEmbedderReady(parsed) {
|
|
847
|
+
const providerConfig = readOpenClawBrainProviderConfig(process.env);
|
|
848
|
+
const model = DEFAULT_OLLAMA_EMBEDDING_MODEL;
|
|
849
|
+
const baseUrl = providerConfig.embedderBaseUrl;
|
|
850
|
+
if (parsed.skipEmbedderProvision) {
|
|
851
|
+
const skipReason = parsed.skipEmbedderProvisionSource === "flag"
|
|
852
|
+
? "--skip-embedder-provision"
|
|
853
|
+
: `${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1`;
|
|
854
|
+
return {
|
|
855
|
+
state: "skipped",
|
|
856
|
+
model,
|
|
857
|
+
baseUrl,
|
|
858
|
+
detail: `Skipped default embedder provisioning (${skipReason}); ${parsed.command} continued only because the operator explicitly opted out. ` +
|
|
859
|
+
`Provision it later with ${buildInstallEmbedderProvisionCommand(baseUrl, model)}.`
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
execFileSync("ollama", ["pull", model], {
|
|
864
|
+
stdio: "pipe",
|
|
865
|
+
env: {
|
|
866
|
+
...process.env,
|
|
867
|
+
OLLAMA_HOST: baseUrl
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
const detail = describeExecFailure(error);
|
|
873
|
+
throw new Error(`Default embedder provisioning failed before brain init. Tried ${buildInstallEmbedderProvisionCommand(baseUrl, model)}. ` +
|
|
874
|
+
`${parsed.command === "install" ? "Install" : "Attach"} stops here so the bootstrap path does not quietly continue without ${model}. ` +
|
|
875
|
+
`Fix Ollama and rerun ${parsed.command}, or explicitly skip with --skip-embedder-provision or ${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1. ` +
|
|
876
|
+
`Detail: ${detail}`);
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
state: "ensured",
|
|
880
|
+
model,
|
|
881
|
+
baseUrl,
|
|
882
|
+
detail: `Ensured default embedder before brain bootstrap: ${buildInstallEmbedderProvisionCommand(baseUrl, model)}`
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function parseOllamaListModelNames(output) {
|
|
886
|
+
return output
|
|
887
|
+
.split(/\r?\n/u)
|
|
888
|
+
.map((line) => line.trim())
|
|
889
|
+
.filter((line) => line.length > 0 && !/^name\s+/iu.test(line))
|
|
890
|
+
.map((line) => line.split(/\s+/u)[0] ?? "")
|
|
891
|
+
.filter((name) => name.length > 0);
|
|
892
|
+
}
|
|
893
|
+
function selectCompatibleLocalTeacherModel(models) {
|
|
894
|
+
const normalized = models.map((model) => model.trim()).filter((model) => model.length > 0);
|
|
895
|
+
for (const prefix of INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES) {
|
|
896
|
+
const exact = normalized.find((model) => model === prefix);
|
|
897
|
+
if (exact !== undefined) {
|
|
898
|
+
return exact;
|
|
899
|
+
}
|
|
900
|
+
const variant = normalized.find((model) => model.startsWith(`${prefix}-`) ||
|
|
901
|
+
model.startsWith(`${prefix}_`) ||
|
|
902
|
+
model.startsWith(`${prefix}.`));
|
|
903
|
+
if (variant !== undefined) {
|
|
904
|
+
return variant;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
function detectInstallTeacherDefaults(baseUrl) {
|
|
910
|
+
try {
|
|
911
|
+
const output = execFileSync("ollama", ["list"], {
|
|
912
|
+
stdio: "pipe",
|
|
913
|
+
env: {
|
|
914
|
+
...process.env,
|
|
915
|
+
OLLAMA_HOST: baseUrl
|
|
916
|
+
}
|
|
917
|
+
}).toString("utf8");
|
|
918
|
+
const availableModels = parseOllamaListModelNames(output);
|
|
919
|
+
const model = selectCompatibleLocalTeacherModel(availableModels);
|
|
920
|
+
if (model === null) {
|
|
921
|
+
return {
|
|
922
|
+
provider: "heuristic",
|
|
923
|
+
model: null,
|
|
924
|
+
baseUrl,
|
|
925
|
+
availableModels,
|
|
926
|
+
detectionDetail: availableModels.length === 0
|
|
927
|
+
? `No compatible local Ollama teacher model detected on ${baseUrl}; watch keeps heuristic teacher defaults.`
|
|
928
|
+
: `No compatible local Ollama teacher model detected on ${baseUrl}; saw ${availableModels.join(", ")} and kept heuristic teacher defaults.`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
provider: "ollama",
|
|
933
|
+
model,
|
|
934
|
+
baseUrl,
|
|
935
|
+
availableModels,
|
|
936
|
+
detectionDetail: `Detected compatible local Ollama teacher model ${model} on ${baseUrl}; watch will enable it by default from the installed activation root.`
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
const detail = describeExecFailure(error);
|
|
941
|
+
return {
|
|
942
|
+
provider: "heuristic",
|
|
943
|
+
model: null,
|
|
944
|
+
baseUrl,
|
|
945
|
+
availableModels: [],
|
|
946
|
+
detectionDetail: `Local Ollama teacher autodetect failed on ${baseUrl}; kept heuristic teacher defaults. Detail: ${detail}`
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function writeInstallProviderDefaults(parsed) {
|
|
951
|
+
const providerConfig = readOpenClawBrainProviderConfig(process.env);
|
|
952
|
+
const teacherDetection = detectInstallTeacherDefaults(providerConfig.teacherBaseUrl);
|
|
953
|
+
const defaultsPath = resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot);
|
|
954
|
+
const defaults = {
|
|
955
|
+
contract: "openclawbrain_provider_defaults.v1",
|
|
956
|
+
writtenAt: new Date().toISOString(),
|
|
957
|
+
source: "install",
|
|
958
|
+
teacherBaseUrl: providerConfig.teacherBaseUrl,
|
|
959
|
+
embedderBaseUrl: providerConfig.embedderBaseUrl,
|
|
960
|
+
teacher: {
|
|
961
|
+
provider: teacherDetection.provider,
|
|
962
|
+
model: teacherDetection.model,
|
|
963
|
+
detectedLocally: teacherDetection.provider === "ollama",
|
|
964
|
+
detectedFromModel: teacherDetection.model
|
|
965
|
+
},
|
|
966
|
+
embedder: {
|
|
967
|
+
provider: "ollama",
|
|
968
|
+
model: DEFAULT_OLLAMA_EMBEDDING_MODEL
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + "\n", "utf8");
|
|
972
|
+
return {
|
|
973
|
+
path: defaultsPath,
|
|
974
|
+
defaults,
|
|
975
|
+
detail: `Wrote local provider defaults: ${teacherDetection.detectionDetail}`,
|
|
976
|
+
lifecycleSummary: teacherDetection.provider === "ollama" && teacherDetection.model !== null
|
|
977
|
+
? `Teacher: auto-enabled local Ollama model ${teacherDetection.model} from install-written defaults`
|
|
978
|
+
: "Teacher: no compatible local Ollama model detected; watch stays heuristic unless explicitly overridden"
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function buildInstallBrainFeedbackSummary(input) {
|
|
982
|
+
const providerDefaultsPath = resolveOpenClawBrainProviderDefaultsPath(input.parsed.activationRoot);
|
|
983
|
+
const embedderState = input.embedderProvision === null ? "unchanged" : input.embedderProvision.state;
|
|
984
|
+
const teacherDefaults = input.providerDefaults?.defaults.teacher;
|
|
985
|
+
const teacherProvider = teacherDefaults?.provider ?? "unknown";
|
|
986
|
+
const teacherModel = teacherDefaults?.model ?? null;
|
|
987
|
+
const detectedLocalLlm = teacherDefaults?.detectedLocally ?? null;
|
|
988
|
+
const provedNow = input.activationPlan.action === "bootstrap"
|
|
989
|
+
? `hook written, activation root ready, seed/current-profile attach bootstrapped, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
|
|
990
|
+
: `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved${input.providerDefaults === null ? "" : ", provider defaults written"}`;
|
|
991
|
+
const notYetProved = input.activationPlan.action === "bootstrap"
|
|
992
|
+
? `OpenClaw has not reloaded this hook yet; restart plus status still must prove live startup/load and the first exported turn`
|
|
993
|
+
: `OpenClaw has not reloaded this hook yet; this ${input.parsed.command} run does not itself prove live startup/load after restart`;
|
|
994
|
+
return {
|
|
995
|
+
hookPath: input.extensionDir,
|
|
996
|
+
providerDefaultsPath,
|
|
997
|
+
embedder: {
|
|
998
|
+
provider: "ollama",
|
|
999
|
+
model: DEFAULT_OLLAMA_EMBEDDING_MODEL,
|
|
1000
|
+
state: embedderState
|
|
1001
|
+
},
|
|
1002
|
+
teacher: {
|
|
1003
|
+
provider: teacherProvider,
|
|
1004
|
+
model: teacherModel,
|
|
1005
|
+
detectedLocalLlm
|
|
1006
|
+
},
|
|
1007
|
+
startup: {
|
|
1008
|
+
token: "BRAIN_NOT_YET_LOADED",
|
|
1009
|
+
proof: "restart_required"
|
|
1010
|
+
},
|
|
1011
|
+
provedNow,
|
|
1012
|
+
notYetProved,
|
|
1013
|
+
lines: [
|
|
1014
|
+
`target ${formatOpenClawTargetLine(input.targetInspection)} source=${formatInstallOpenClawHomeSource(input.parsed.openclawHomeSource)}`,
|
|
1015
|
+
`hook written=${shortenPath(input.extensionDir)}`,
|
|
1016
|
+
`activation root=${shortenPath(input.parsed.activationRoot)} source=${formatInstallActivationRootSource(input.parsed.activationRootSource)}`,
|
|
1017
|
+
`defaults provider-defaults=${shortenPath(providerDefaultsPath)} state=${input.providerDefaults === null ? "unchanged" : "written"}`,
|
|
1018
|
+
`embedder provider=ollama model=${DEFAULT_OLLAMA_EMBEDDING_MODEL} state=${embedderState}`,
|
|
1019
|
+
`teacher provider=${teacherProvider} model=${teacherModel ?? "none"} localLLM=${detectedLocalLlm === null ? "unknown" : yesNo(detectedLocalLlm)}`,
|
|
1020
|
+
"startup BRAIN_NOT_YET_LOADED proof=restart_required",
|
|
1021
|
+
`provedNow ${provedNow}`,
|
|
1022
|
+
`notYet ${notYetProved}`
|
|
1023
|
+
]
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
435
1026
|
function buildInstallReloadGuidance() {
|
|
436
1027
|
return "If this OpenClaw profile is currently running, restart it before expecting the new brain hook to load. If it is stopped, the next launch will pick it up.";
|
|
437
1028
|
}
|
|
1029
|
+
const LEGACY_PROFILE_NOTE_FILENAMES = ["BRAIN.md", "brain.md"];
|
|
1030
|
+
const LEGACY_BRAIN_AGENTS_LINE = "5. Read `BRAIN.md` — your learning brain context";
|
|
1031
|
+
function isLegacyBrainAdvisoryContent(content) {
|
|
1032
|
+
return content.includes("## OpenClawBrain")
|
|
1033
|
+
&& content.includes("You have a learning brain attached at ")
|
|
1034
|
+
&& content.includes("openclawbrain status --activation-root")
|
|
1035
|
+
&& content.includes("openclawbrain rollback --activation-root");
|
|
1036
|
+
}
|
|
1037
|
+
function writeUpdatedTextFile(filePath, nextText, previousText) {
|
|
1038
|
+
const normalizedNextText = previousText.endsWith("\n") ? `${nextText}\n` : nextText;
|
|
1039
|
+
writeFileSync(filePath, normalizedNextText, "utf8");
|
|
1040
|
+
}
|
|
1041
|
+
function collectProfileResidueDirs(openclawHome) {
|
|
1042
|
+
const directories = [path.resolve(openclawHome)];
|
|
1043
|
+
try {
|
|
1044
|
+
const entries = readdirSync(openclawHome, { withFileTypes: true });
|
|
1045
|
+
for (const entry of entries) {
|
|
1046
|
+
if (entry.isDirectory() && entry.name.startsWith("workspace-")) {
|
|
1047
|
+
directories.push(path.join(openclawHome, entry.name));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
catch {
|
|
1052
|
+
// Residue cleanup stays best-effort.
|
|
1053
|
+
}
|
|
1054
|
+
return directories;
|
|
1055
|
+
}
|
|
1056
|
+
function removeLegacyProfileResidue(openclawHome) {
|
|
1057
|
+
const removedNotes = [];
|
|
1058
|
+
const updatedAgents = [];
|
|
1059
|
+
for (const directory of collectProfileResidueDirs(openclawHome)) {
|
|
1060
|
+
for (const fileName of LEGACY_PROFILE_NOTE_FILENAMES) {
|
|
1061
|
+
const notePath = path.join(directory, fileName);
|
|
1062
|
+
if (!existsSync(notePath)) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const content = readFileSync(notePath, "utf8");
|
|
1067
|
+
if (!isLegacyBrainAdvisoryContent(content)) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
rmSync(notePath, { force: true });
|
|
1075
|
+
removedNotes.push(notePath);
|
|
1076
|
+
}
|
|
1077
|
+
const agentsPath = path.join(directory, "AGENTS.md");
|
|
1078
|
+
if (!existsSync(agentsPath)) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
let agentsContent;
|
|
1082
|
+
try {
|
|
1083
|
+
agentsContent = readFileSync(agentsPath, "utf8");
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const nextContent = agentsContent
|
|
1089
|
+
.split("\n")
|
|
1090
|
+
.filter((line) => line.trim() !== LEGACY_BRAIN_AGENTS_LINE)
|
|
1091
|
+
.join("\n");
|
|
1092
|
+
if (nextContent !== agentsContent) {
|
|
1093
|
+
writeUpdatedTextFile(agentsPath, nextContent, agentsContent);
|
|
1094
|
+
updatedAgents.push(agentsPath);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
removedNotes,
|
|
1099
|
+
updatedAgents
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
438
1102
|
function buildCleanupRestartGuidance(restart) {
|
|
439
1103
|
if (restart === "never") {
|
|
440
1104
|
return "No restart requested. If this OpenClaw profile is currently running, it may keep the previous hook state until the next restart.";
|
|
@@ -445,64 +1109,56 @@ function buildCleanupRestartGuidance(restart) {
|
|
|
445
1109
|
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.";
|
|
446
1110
|
}
|
|
447
1111
|
function buildStatusNextStep(status, report) {
|
|
1112
|
+
const activationRootArg = quoteShellArg(status.host.activationRoot);
|
|
1113
|
+
if (status.brainStatus.activationState === "broken_install") {
|
|
1114
|
+
return "Repair or replace the activation root before trusting serve-path status again.";
|
|
1115
|
+
}
|
|
1116
|
+
if (status.brainStatus.activationState === "stale_incomplete") {
|
|
1117
|
+
return "Clean up or repair the retained activation state before reattaching or promoting packs.";
|
|
1118
|
+
}
|
|
448
1119
|
if (status.brainStatus.status === "fail") {
|
|
449
|
-
return
|
|
1120
|
+
return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
|
|
450
1121
|
}
|
|
451
1122
|
if (status.brainStatus.awaitingFirstExport) {
|
|
452
|
-
return
|
|
1123
|
+
return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
|
|
453
1124
|
}
|
|
454
1125
|
if (report.learning.warningStates.includes("principal_live_backlog") ||
|
|
455
1126
|
report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
|
|
456
1127
|
return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
|
|
457
1128
|
}
|
|
458
1129
|
if (report.rollback.allowed) {
|
|
459
|
-
return
|
|
460
|
-
}
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
function formatHumanFriendlyStatus(status, report) {
|
|
464
|
-
// Brain status line
|
|
465
|
-
const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
|
|
466
|
-
const brainIcon = brainActive ? "Active ✓" : status.brainStatus.status === "fail" ? "Inactive ✗" : `${status.brainStatus.status}`;
|
|
467
|
-
// Pack line
|
|
468
|
-
const packId = status.brain.activePackId ?? "none";
|
|
469
|
-
const packShort = packId.length > 9 ? packId.slice(0, 9) : packId;
|
|
470
|
-
const state = status.brain.state ?? "unknown";
|
|
471
|
-
// Activation root
|
|
472
|
-
const activationPath = shortenPath(status.host.activationRoot);
|
|
473
|
-
// Policy
|
|
474
|
-
const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
|
|
475
|
-
const principalFrontier = formatPrincipalCheckpointFrontier(report);
|
|
476
|
-
const pendingLive = String(report.learning.pendingLive ?? "none");
|
|
477
|
-
const pendingBackfill = String(report.learning.pendingBackfill ?? "none");
|
|
478
|
-
const nextLane = report.learning.nextPriorityLane ?? "none";
|
|
479
|
-
const nextBucket = report.learning.nextPriorityBucket ?? "none";
|
|
1130
|
+
return `Use \`openclawbrain rollback --activation-root ${activationRootArg} --dry-run\` before restoring the previous pack.`;
|
|
1131
|
+
}
|
|
1132
|
+
return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
|
|
1133
|
+
}
|
|
1134
|
+
function formatHumanFriendlyStatus(status, report, targetInspection, options) {
|
|
480
1135
|
const lines = [
|
|
481
|
-
`
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
`
|
|
488
|
-
`Labels: ${formatLabelFlowSummary(report.labelFlow)}`,
|
|
489
|
-
`Learning: ${formatLearningPathSummary(report.learningPath)}`
|
|
1136
|
+
`STATUS ${status.brainStatus.status}`,
|
|
1137
|
+
...buildCompactStatusHeader(status, report, options),
|
|
1138
|
+
...(targetInspection === null ? [] : [
|
|
1139
|
+
`target ${formatOpenClawTargetLine(targetInspection)}`,
|
|
1140
|
+
`preflight ${formatOpenClawTargetExplanation(targetInspection)}`
|
|
1141
|
+
]),
|
|
1142
|
+
`next ${buildStatusNextStep(status, report)}`
|
|
490
1143
|
];
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1144
|
+
return lines.join("\n");
|
|
1145
|
+
}
|
|
1146
|
+
function requireActivationRoot(input, openclawHome, command) {
|
|
1147
|
+
const explicitActivationRoot = input.activationRoot.trim().length > 0 ? input.activationRoot : null;
|
|
1148
|
+
if (explicitActivationRoot !== null) {
|
|
1149
|
+
return path.resolve(explicitActivationRoot);
|
|
494
1150
|
}
|
|
495
|
-
if (
|
|
496
|
-
|
|
1151
|
+
if (openclawHome !== null) {
|
|
1152
|
+
return resolveActivationRoot({
|
|
1153
|
+
openclawHome
|
|
1154
|
+
});
|
|
497
1155
|
}
|
|
498
|
-
|
|
499
|
-
return lines.join("\n");
|
|
1156
|
+
throw new Error(`${command} requires --activation-root <path> or --openclaw-home <path>; unpinned host auto-resolution is no longer supported for ${command}.`);
|
|
500
1157
|
}
|
|
501
|
-
function
|
|
502
|
-
// Use the shared auto-detect chain for ALL commands:
|
|
503
|
-
// explicit flag → ~/.openclawbrain/activation → extension scan → clear error
|
|
1158
|
+
function resolveCliActivationRoot(explicitActivationRoot, openclawHome) {
|
|
504
1159
|
return resolveActivationRoot({
|
|
505
|
-
explicit:
|
|
1160
|
+
explicit: explicitActivationRoot,
|
|
1161
|
+
openclawHome
|
|
506
1162
|
});
|
|
507
1163
|
}
|
|
508
1164
|
function readJsonFile(filePath) {
|
|
@@ -530,10 +1186,18 @@ function formatScanSessionSummary(result) {
|
|
|
530
1186
|
function formatScanLiveSummary(result, snapshotOutPath) {
|
|
531
1187
|
const materializedPackId = result.snapshot.learner.lastMaterialization?.candidate.summary.packId ?? "none";
|
|
532
1188
|
const materializationReason = result.snapshot.learner.lastMaterialization?.reason ?? "none";
|
|
1189
|
+
const teacherSummary = [
|
|
1190
|
+
`artifacts=${result.snapshot.teacher.artifactCount}`,
|
|
1191
|
+
`freshness=${result.snapshot.teacher.latestFreshness}`,
|
|
1192
|
+
`humanLabels=${result.supervision.humanLabelCount}`
|
|
1193
|
+
];
|
|
1194
|
+
if (result.snapshot.diagnostics.lastNoOpReason !== "none") {
|
|
1195
|
+
teacherSummary.push(`noop=${result.snapshot.diagnostics.lastNoOpReason}`);
|
|
1196
|
+
}
|
|
533
1197
|
return [
|
|
534
1198
|
"SCAN live ok",
|
|
535
1199
|
`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}`,
|
|
536
|
-
`teacher
|
|
1200
|
+
`teacher ${teacherSummary.join(" ")}`,
|
|
537
1201
|
`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"}`,
|
|
538
1202
|
`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"}`,
|
|
539
1203
|
`learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
|
|
@@ -553,12 +1217,12 @@ export function parseOperatorCliArgs(argv) {
|
|
|
553
1217
|
let rootDir = null;
|
|
554
1218
|
let workspacePath = null;
|
|
555
1219
|
let packLabel = null;
|
|
556
|
-
let packRoot = null;
|
|
557
1220
|
let workspaceId = null;
|
|
558
1221
|
let observedAt = null;
|
|
559
1222
|
let snapshotOutPath = null;
|
|
560
1223
|
let openclawHome = null;
|
|
561
1224
|
let shared = false;
|
|
1225
|
+
let skipEmbedderProvision = false;
|
|
562
1226
|
let keepData = false;
|
|
563
1227
|
let purgeData = false;
|
|
564
1228
|
let restart = "safe";
|
|
@@ -571,11 +1235,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
571
1235
|
if (args[0] === "doctor") {
|
|
572
1236
|
throw new Error(buildDoctorDeletedMessage(args.slice(1)));
|
|
573
1237
|
}
|
|
1238
|
+
if (args[0] === "setup") {
|
|
1239
|
+
throw new Error(buildSetupDeletedMessage());
|
|
1240
|
+
}
|
|
574
1241
|
if (args[0] === "daemon") {
|
|
575
1242
|
args.shift();
|
|
576
1243
|
return parseDaemonArgs(args);
|
|
577
1244
|
}
|
|
578
|
-
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "
|
|
1245
|
+
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "detach" || args[0] === "uninstall" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
|
|
579
1246
|
command = args.shift();
|
|
580
1247
|
}
|
|
581
1248
|
if (command === "learn") {
|
|
@@ -598,6 +1265,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
598
1265
|
index += 1;
|
|
599
1266
|
continue;
|
|
600
1267
|
}
|
|
1268
|
+
if (arg === "--openclaw-home") {
|
|
1269
|
+
const next = args[index + 1];
|
|
1270
|
+
if (next === undefined) {
|
|
1271
|
+
throw new Error("--openclaw-home requires a value");
|
|
1272
|
+
}
|
|
1273
|
+
openclawHome = next;
|
|
1274
|
+
index += 1;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
601
1277
|
if (arg.startsWith("--")) {
|
|
602
1278
|
throw new Error(`unknown argument for learn: ${arg}`);
|
|
603
1279
|
}
|
|
@@ -607,7 +1283,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
607
1283
|
}
|
|
608
1284
|
return {
|
|
609
1285
|
command,
|
|
610
|
-
activationRoot:
|
|
1286
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
611
1287
|
json,
|
|
612
1288
|
help
|
|
613
1289
|
};
|
|
@@ -663,9 +1339,12 @@ export function parseOperatorCliArgs(argv) {
|
|
|
663
1339
|
if (help) {
|
|
664
1340
|
return { command, activationRoot: "", scanRoot: null, interval: 30, json, help };
|
|
665
1341
|
}
|
|
1342
|
+
if (activationRoot === null || activationRoot.trim().length === 0) {
|
|
1343
|
+
throw new Error("watch requires --activation-root <path>");
|
|
1344
|
+
}
|
|
666
1345
|
return {
|
|
667
1346
|
command,
|
|
668
|
-
activationRoot:
|
|
1347
|
+
activationRoot: path.resolve(activationRoot),
|
|
669
1348
|
scanRoot: watchScanRoot,
|
|
670
1349
|
interval: watchInterval,
|
|
671
1350
|
json,
|
|
@@ -693,6 +1372,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
693
1372
|
index += 1;
|
|
694
1373
|
continue;
|
|
695
1374
|
}
|
|
1375
|
+
if (arg === "--openclaw-home") {
|
|
1376
|
+
const next = args[index + 1];
|
|
1377
|
+
if (next === undefined) {
|
|
1378
|
+
throw new Error("--openclaw-home requires a value");
|
|
1379
|
+
}
|
|
1380
|
+
openclawHome = next;
|
|
1381
|
+
index += 1;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
696
1384
|
if (arg.startsWith("--")) {
|
|
697
1385
|
throw new Error(`unknown argument for context: ${arg}`);
|
|
698
1386
|
}
|
|
@@ -707,7 +1395,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
707
1395
|
return {
|
|
708
1396
|
command,
|
|
709
1397
|
message: messageParts.join(" "),
|
|
710
|
-
activationRoot:
|
|
1398
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
711
1399
|
json,
|
|
712
1400
|
help
|
|
713
1401
|
};
|
|
@@ -733,6 +1421,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
733
1421
|
index += 1;
|
|
734
1422
|
continue;
|
|
735
1423
|
}
|
|
1424
|
+
if (arg === "--openclaw-home") {
|
|
1425
|
+
const next = args[index + 1];
|
|
1426
|
+
if (next === undefined) {
|
|
1427
|
+
throw new Error("--openclaw-home requires a value");
|
|
1428
|
+
}
|
|
1429
|
+
openclawHome = next;
|
|
1430
|
+
index += 1;
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
736
1433
|
if (arg === "--limit") {
|
|
737
1434
|
const next = args[index + 1];
|
|
738
1435
|
if (next === undefined) {
|
|
@@ -755,7 +1452,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
755
1452
|
}
|
|
756
1453
|
return {
|
|
757
1454
|
command,
|
|
758
|
-
activationRoot:
|
|
1455
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
759
1456
|
limit: historyLimit,
|
|
760
1457
|
json,
|
|
761
1458
|
help
|
|
@@ -781,6 +1478,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
781
1478
|
index += 1;
|
|
782
1479
|
continue;
|
|
783
1480
|
}
|
|
1481
|
+
if (arg === "--openclaw-home") {
|
|
1482
|
+
const next = args[index + 1];
|
|
1483
|
+
if (next === undefined)
|
|
1484
|
+
throw new Error("--openclaw-home requires a value");
|
|
1485
|
+
openclawHome = next;
|
|
1486
|
+
index += 1;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
784
1489
|
if (arg === "-o" || arg === "--output") {
|
|
785
1490
|
const next = args[index + 1];
|
|
786
1491
|
if (next === undefined)
|
|
@@ -798,7 +1503,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
798
1503
|
throw new Error("export requires -o <output.tar.gz>");
|
|
799
1504
|
return {
|
|
800
1505
|
command,
|
|
801
|
-
activationRoot:
|
|
1506
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
802
1507
|
outputPath: path.resolve(outputPath),
|
|
803
1508
|
json,
|
|
804
1509
|
help,
|
|
@@ -829,6 +1534,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
829
1534
|
index += 1;
|
|
830
1535
|
continue;
|
|
831
1536
|
}
|
|
1537
|
+
if (arg === "--openclaw-home") {
|
|
1538
|
+
const next = args[index + 1];
|
|
1539
|
+
if (next === undefined)
|
|
1540
|
+
throw new Error("--openclaw-home requires a value");
|
|
1541
|
+
openclawHome = next;
|
|
1542
|
+
index += 1;
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
832
1545
|
if (arg.startsWith("--"))
|
|
833
1546
|
throw new Error(`unknown argument for import: ${arg}`);
|
|
834
1547
|
if (archivePath === null) {
|
|
@@ -845,7 +1558,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
845
1558
|
return {
|
|
846
1559
|
command,
|
|
847
1560
|
archivePath: path.resolve(archivePath),
|
|
848
|
-
activationRoot:
|
|
1561
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
849
1562
|
force,
|
|
850
1563
|
json,
|
|
851
1564
|
help,
|
|
@@ -875,13 +1588,21 @@ export function parseOperatorCliArgs(argv) {
|
|
|
875
1588
|
index += 1;
|
|
876
1589
|
continue;
|
|
877
1590
|
}
|
|
1591
|
+
if (arg === "--openclaw-home") {
|
|
1592
|
+
const next = args[index + 1];
|
|
1593
|
+
if (next === undefined)
|
|
1594
|
+
throw new Error("--openclaw-home requires a value");
|
|
1595
|
+
openclawHome = next;
|
|
1596
|
+
index += 1;
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
878
1599
|
throw new Error(`unknown argument for reset: ${arg}`);
|
|
879
1600
|
}
|
|
880
1601
|
if (help)
|
|
881
1602
|
return { command, activationRoot: "", yes: false, json, help };
|
|
882
1603
|
return {
|
|
883
1604
|
command,
|
|
884
|
-
activationRoot:
|
|
1605
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
885
1606
|
yes,
|
|
886
1607
|
json,
|
|
887
1608
|
help
|
|
@@ -905,6 +1626,10 @@ export function parseOperatorCliArgs(argv) {
|
|
|
905
1626
|
shared = true;
|
|
906
1627
|
continue;
|
|
907
1628
|
}
|
|
1629
|
+
if (arg === "--skip-embedder-provision") {
|
|
1630
|
+
skipEmbedderProvision = true;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
908
1633
|
if (arg === "--keep-data") {
|
|
909
1634
|
keepData = true;
|
|
910
1635
|
continue;
|
|
@@ -1038,14 +1763,6 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1038
1763
|
index += 1;
|
|
1039
1764
|
continue;
|
|
1040
1765
|
}
|
|
1041
|
-
if (arg === "--pack-root") {
|
|
1042
|
-
if (next === undefined) {
|
|
1043
|
-
throw new Error("--pack-root requires a value");
|
|
1044
|
-
}
|
|
1045
|
-
packRoot = next;
|
|
1046
|
-
index += 1;
|
|
1047
|
-
continue;
|
|
1048
|
-
}
|
|
1049
1766
|
if (arg === "--workspace-id") {
|
|
1050
1767
|
if (next === undefined) {
|
|
1051
1768
|
throw new Error("--workspace-id requires a value");
|
|
@@ -1059,13 +1776,28 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1059
1776
|
if (command !== "detach" && command !== "uninstall" && restartExplicitlySet) {
|
|
1060
1777
|
throw new Error("--restart only applies to detach/uninstall");
|
|
1061
1778
|
}
|
|
1779
|
+
if (command !== "install" && command !== "attach" && shared) {
|
|
1780
|
+
throw new Error("--shared only applies to install/attach");
|
|
1781
|
+
}
|
|
1782
|
+
if (command !== "install" && command !== "attach" && skipEmbedderProvision) {
|
|
1783
|
+
throw new Error("--skip-embedder-provision only applies to install/attach");
|
|
1784
|
+
}
|
|
1062
1785
|
if (command !== "uninstall" && keepData) {
|
|
1063
1786
|
throw new Error("--keep-data only applies to uninstall; use detach to preserve activation data");
|
|
1064
1787
|
}
|
|
1065
1788
|
if (command !== "uninstall" && purgeData) {
|
|
1066
1789
|
throw new Error("--purge-data only applies to uninstall");
|
|
1067
1790
|
}
|
|
1068
|
-
if (command
|
|
1791
|
+
if (command !== "install" && command !== "attach" && workspaceId !== null) {
|
|
1792
|
+
throw new Error("--workspace-id only applies to install/attach");
|
|
1793
|
+
}
|
|
1794
|
+
if (command !== "scan" && packLabel !== null) {
|
|
1795
|
+
throw new Error("--pack-label only applies to scan --live");
|
|
1796
|
+
}
|
|
1797
|
+
if ((command === "install" || command === "attach") && brainAttachmentPolicy !== null) {
|
|
1798
|
+
throw new Error(`${command} uses dedicated by default or --shared for shared mode; --brain-attachment-policy only applies to status/rollback inspection`);
|
|
1799
|
+
}
|
|
1800
|
+
if (command === "install") {
|
|
1069
1801
|
if (help) {
|
|
1070
1802
|
return {
|
|
1071
1803
|
command,
|
|
@@ -1076,13 +1808,16 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1076
1808
|
shared: false,
|
|
1077
1809
|
workspaceId: "",
|
|
1078
1810
|
workspaceIdSource: "explicit",
|
|
1811
|
+
skipEmbedderProvision: false,
|
|
1812
|
+
skipEmbedderProvisionSource: null,
|
|
1079
1813
|
json,
|
|
1080
1814
|
help
|
|
1081
1815
|
};
|
|
1082
1816
|
}
|
|
1083
|
-
const resolvedOpenclawHome =
|
|
1084
|
-
const resolvedActivationRoot =
|
|
1085
|
-
const resolvedWorkspaceId =
|
|
1817
|
+
const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
|
|
1818
|
+
const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
|
|
1819
|
+
const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
|
|
1820
|
+
const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
|
|
1086
1821
|
return {
|
|
1087
1822
|
command,
|
|
1088
1823
|
openclawHome: resolvedOpenclawHome.openclawHome,
|
|
@@ -1092,6 +1827,47 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1092
1827
|
shared,
|
|
1093
1828
|
workspaceId: resolvedWorkspaceId.workspaceId,
|
|
1094
1829
|
workspaceIdSource: resolvedWorkspaceId.source,
|
|
1830
|
+
skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
|
|
1831
|
+
skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
|
|
1832
|
+
json,
|
|
1833
|
+
help
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
if (command === "attach") {
|
|
1837
|
+
if (help) {
|
|
1838
|
+
return {
|
|
1839
|
+
command,
|
|
1840
|
+
openclawHome: "",
|
|
1841
|
+
openclawHomeSource: "explicit",
|
|
1842
|
+
activationRoot: "",
|
|
1843
|
+
activationRootSource: "explicit",
|
|
1844
|
+
shared: false,
|
|
1845
|
+
workspaceId: "",
|
|
1846
|
+
workspaceIdSource: "explicit",
|
|
1847
|
+
skipEmbedderProvision: false,
|
|
1848
|
+
skipEmbedderProvisionSource: null,
|
|
1849
|
+
json,
|
|
1850
|
+
help
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
if (openclawHome === null || openclawHome.trim().length === 0) {
|
|
1854
|
+
throw new Error("--openclaw-home is required for attach; use install for the first-time default path");
|
|
1855
|
+
}
|
|
1856
|
+
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
1857
|
+
const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome, activationRoot);
|
|
1858
|
+
const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome, workspaceId);
|
|
1859
|
+
const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
|
|
1860
|
+
return {
|
|
1861
|
+
command,
|
|
1862
|
+
openclawHome: resolvedOpenclawHome,
|
|
1863
|
+
openclawHomeSource: "explicit",
|
|
1864
|
+
activationRoot: resolvedActivationRoot.activationRoot,
|
|
1865
|
+
activationRootSource: resolvedActivationRoot.source,
|
|
1866
|
+
shared,
|
|
1867
|
+
workspaceId: resolvedWorkspaceId.workspaceId,
|
|
1868
|
+
workspaceIdSource: resolvedWorkspaceId.source,
|
|
1869
|
+
skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
|
|
1870
|
+
skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
|
|
1095
1871
|
json,
|
|
1096
1872
|
help
|
|
1097
1873
|
};
|
|
@@ -1153,30 +1929,6 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1153
1929
|
help
|
|
1154
1930
|
};
|
|
1155
1931
|
}
|
|
1156
|
-
if (command === "attach") {
|
|
1157
|
-
if (help) {
|
|
1158
|
-
return { command, activationRoot: "", packRoot: "", packLabel: "", workspaceId: "", brainAttachmentPolicy: null, json, help };
|
|
1159
|
-
}
|
|
1160
|
-
if (activationRoot === null || activationRoot.trim().length === 0) {
|
|
1161
|
-
throw new Error("--activation-root is required for attach");
|
|
1162
|
-
}
|
|
1163
|
-
const resolvedActivationRoot = path.resolve(activationRoot);
|
|
1164
|
-
const resolvedPackRoot = packRoot !== null
|
|
1165
|
-
? path.resolve(packRoot)
|
|
1166
|
-
: path.resolve(resolvedActivationRoot, "packs", "initial");
|
|
1167
|
-
const resolvedWorkspaceId = workspaceId ?? "workspace";
|
|
1168
|
-
const resolvedPackLabel = packLabel ?? "cli-attach";
|
|
1169
|
-
return {
|
|
1170
|
-
command,
|
|
1171
|
-
activationRoot: resolvedActivationRoot,
|
|
1172
|
-
packRoot: resolvedPackRoot,
|
|
1173
|
-
packLabel: resolvedPackLabel,
|
|
1174
|
-
workspaceId: resolvedWorkspaceId,
|
|
1175
|
-
brainAttachmentPolicy: brainAttachmentPolicy,
|
|
1176
|
-
json,
|
|
1177
|
-
help
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
1932
|
if (command === "scan") {
|
|
1181
1933
|
if ((sessionPath === null && livePath === null) || (sessionPath !== null && livePath !== null)) {
|
|
1182
1934
|
throw new Error("scan requires exactly one of --session or --live");
|
|
@@ -1219,6 +1971,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1219
1971
|
updatedAt,
|
|
1220
1972
|
brainAttachmentPolicy
|
|
1221
1973
|
},
|
|
1974
|
+
openclawHome: normalizeOptionalCliString(openclawHome),
|
|
1222
1975
|
json,
|
|
1223
1976
|
help,
|
|
1224
1977
|
dryRun,
|
|
@@ -1259,6 +2012,7 @@ function resolveExtensionRuntimeGuardPath() {
|
|
|
1259
2012
|
path.resolve(__dirname, "..", "..", "extension", "runtime-guard.ts"),
|
|
1260
2013
|
];
|
|
1261
2014
|
const jsCandidates = [
|
|
2015
|
+
path.resolve(__dirname, "..", "dist", "extension", "runtime-guard.js"),
|
|
1262
2016
|
path.resolve(__dirname, "extension", "runtime-guard.js"),
|
|
1263
2017
|
path.resolve(__dirname, "..", "extension", "runtime-guard.js"),
|
|
1264
2018
|
];
|
|
@@ -1282,6 +2036,36 @@ const LOCAL_WORKSPACE_EXTENSION_PACKAGES = [
|
|
|
1282
2036
|
"provenance",
|
|
1283
2037
|
"workspace-metadata"
|
|
1284
2038
|
];
|
|
2039
|
+
const OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV = "OPENCLAWBRAIN_EXTENSION_TARBALL_DIR";
|
|
2040
|
+
function resolveNpmCommand() {
|
|
2041
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
2042
|
+
}
|
|
2043
|
+
function resolveExtensionInstallReleaseTarballs() {
|
|
2044
|
+
const configuredDir = normalizeOptionalCliString(process.env[OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV]);
|
|
2045
|
+
if (configuredDir === null) {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
const artifactDir = path.resolve(configuredDir);
|
|
2049
|
+
let entries;
|
|
2050
|
+
try {
|
|
2051
|
+
entries = readdirSync(artifactDir, { withFileTypes: true });
|
|
2052
|
+
}
|
|
2053
|
+
catch (error) {
|
|
2054
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2055
|
+
throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} is unreadable: ${artifactDir} (${detail})`);
|
|
2056
|
+
}
|
|
2057
|
+
const tarballs = entries
|
|
2058
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
|
|
2059
|
+
.map((entry) => path.join(artifactDir, entry.name))
|
|
2060
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2061
|
+
if (tarballs.length === 0) {
|
|
2062
|
+
throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} has no .tgz release artifacts: ${artifactDir}`);
|
|
2063
|
+
}
|
|
2064
|
+
return {
|
|
2065
|
+
artifactDir,
|
|
2066
|
+
tarballs
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
1285
2069
|
function resolveLocalWorkspaceRootForExtensionInstall() {
|
|
1286
2070
|
const candidates = [
|
|
1287
2071
|
path.resolve(__dirname, "..", "..", "..", ".."),
|
|
@@ -1458,6 +2242,85 @@ function buildHistoryEntry(record, slot, isActive) {
|
|
|
1458
2242
|
current: isActive
|
|
1459
2243
|
};
|
|
1460
2244
|
}
|
|
2245
|
+
function formatInspectionFindings(findings) {
|
|
2246
|
+
return findings.join("; ");
|
|
2247
|
+
}
|
|
2248
|
+
function buildInstallRefusalError(parsed, detail) {
|
|
2249
|
+
const purgeCommand = `openclawbrain uninstall --openclaw-home ${quoteShellArg(parsed.openclawHome)} ` +
|
|
2250
|
+
`--activation-root ${quoteShellArg(parsed.activationRoot)} --purge-data`;
|
|
2251
|
+
return new Error(`Refusing to reuse activation root ${path.resolve(parsed.activationRoot)}: ${detail}. ` +
|
|
2252
|
+
"Install only repairs an empty first-state root; it will not overwrite populated or broken activation state. " +
|
|
2253
|
+
`Inspect: ${buildInstallStatusCommand(parsed.activationRoot)}. ` +
|
|
2254
|
+
`Reset: ${purgeCommand}.`);
|
|
2255
|
+
}
|
|
2256
|
+
function inspectInstallActivationPlan(parsed) {
|
|
2257
|
+
const resolvedActivationRoot = path.resolve(parsed.activationRoot);
|
|
2258
|
+
const activationPointersPath = path.join(resolvedActivationRoot, "activation-pointers.json");
|
|
2259
|
+
if (!existsSync(resolvedActivationRoot)) {
|
|
2260
|
+
return {
|
|
2261
|
+
createActivationRoot: true,
|
|
2262
|
+
action: "bootstrap",
|
|
2263
|
+
resolution: "new_root",
|
|
2264
|
+
inspectionStep: "Activation state inspection: activation root is missing; bootstrapping first state.",
|
|
2265
|
+
activePackId: null
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
const activationRootStats = statSync(resolvedActivationRoot);
|
|
2269
|
+
if (!activationRootStats.isDirectory()) {
|
|
2270
|
+
throw buildInstallRefusalError(parsed, "activation root path exists but is not a directory");
|
|
2271
|
+
}
|
|
2272
|
+
if (!existsSync(activationPointersPath)) {
|
|
2273
|
+
return {
|
|
2274
|
+
createActivationRoot: false,
|
|
2275
|
+
action: "bootstrap",
|
|
2276
|
+
resolution: "missing_pointers",
|
|
2277
|
+
inspectionStep: "Activation state inspection: activation root exists but activation-pointers.json is missing; bootstrapping first state.",
|
|
2278
|
+
activePackId: null
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
let inspection;
|
|
2282
|
+
try {
|
|
2283
|
+
inspection = inspectActivationState(resolvedActivationRoot, new Date().toISOString());
|
|
2284
|
+
}
|
|
2285
|
+
catch (error) {
|
|
2286
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2287
|
+
throw buildInstallRefusalError(parsed, `activation pointers could not be inspected (${detail})`);
|
|
2288
|
+
}
|
|
2289
|
+
if (inspection.active === null && inspection.candidate === null && inspection.previous === null) {
|
|
2290
|
+
return {
|
|
2291
|
+
createActivationRoot: false,
|
|
2292
|
+
action: "bootstrap",
|
|
2293
|
+
resolution: "empty_pointers",
|
|
2294
|
+
inspectionStep: "Activation state inspection: activation pointers are present but all slots are empty; bootstrapping first state.",
|
|
2295
|
+
activePackId: null
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
const unhealthySlots = [inspection.active, inspection.candidate, inspection.previous]
|
|
2299
|
+
.filter((slot) => slot !== null && !slot.activationReady)
|
|
2300
|
+
.map((slot) => `${slot.slot}: ${formatInspectionFindings(slot.findings)}`);
|
|
2301
|
+
if (unhealthySlots.length > 0) {
|
|
2302
|
+
throw buildInstallRefusalError(parsed, `activation state contains unhealthy slots (${unhealthySlots.join(" | ")})`);
|
|
2303
|
+
}
|
|
2304
|
+
if (inspection.active === null) {
|
|
2305
|
+
const populatedSlots = [inspection.candidate, inspection.previous]
|
|
2306
|
+
.filter((slot) => slot !== null)
|
|
2307
|
+
.map((slot) => slot.slot);
|
|
2308
|
+
throw buildInstallRefusalError(parsed, `activation state is populated without an active pack (${populatedSlots.join(", ")})`);
|
|
2309
|
+
}
|
|
2310
|
+
if (inspection.candidate !== null && !inspection.promotion.allowed) {
|
|
2311
|
+
throw buildInstallRefusalError(parsed, `candidate slot is stale or incoherent (${formatInspectionFindings(inspection.promotion.findings)})`);
|
|
2312
|
+
}
|
|
2313
|
+
if (inspection.previous !== null && !inspection.rollback.allowed) {
|
|
2314
|
+
throw buildInstallRefusalError(parsed, `previous slot is stale or incoherent (${formatInspectionFindings(inspection.rollback.findings)})`);
|
|
2315
|
+
}
|
|
2316
|
+
return {
|
|
2317
|
+
createActivationRoot: false,
|
|
2318
|
+
action: "keep",
|
|
2319
|
+
resolution: "healthy_existing",
|
|
2320
|
+
inspectionStep: `Activation state inspection: active pack ${inspection.active.packId} is healthy; keeping existing activation state.`,
|
|
2321
|
+
activePackId: inspection.active.packId
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
1461
2324
|
function runHistoryCommand(parsed) {
|
|
1462
2325
|
const activationRoot = parsed.activationRoot;
|
|
1463
2326
|
const pointersPath = path.join(activationRoot, "activation-pointers.json");
|
|
@@ -1539,29 +2402,53 @@ function runHistoryCommand(parsed) {
|
|
|
1539
2402
|
}
|
|
1540
2403
|
return 0;
|
|
1541
2404
|
}
|
|
1542
|
-
function
|
|
2405
|
+
function runProfileHookAttachCommand(parsed) {
|
|
1543
2406
|
const steps = [];
|
|
1544
2407
|
const commandLabel = parsed.command.toUpperCase();
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
|
|
2408
|
+
const isInstall = parsed.command === "install";
|
|
2409
|
+
const targetInspection = inspectOpenClawHome(parsed.openclawHome);
|
|
2410
|
+
const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
|
|
2411
|
+
steps.push(`Target OpenClaw home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
|
|
2412
|
+
steps.push(isInstall
|
|
2413
|
+
? "Lifecycle mode: install is the safe first-time default for wiring one profile to one activation root."
|
|
2414
|
+
: "Lifecycle mode: attach is the explicit reattach/manual profile-hook path; use install for first-time setup.");
|
|
2415
|
+
steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
2416
|
+
steps.push(`Target hook path: ${extensionDir}`);
|
|
1548
2417
|
// 1. Validate --openclaw-home exists and has openclaw.json
|
|
1549
2418
|
validateOpenClawHome(parsed.openclawHome);
|
|
1550
|
-
// 2.
|
|
1551
|
-
|
|
2419
|
+
// 2. Inspect the activation root before writing profile hook artifacts.
|
|
2420
|
+
const activationPlan = inspectInstallActivationPlan(parsed);
|
|
2421
|
+
// 3. Ensure the default embedder exists before bootstrap unless the operator explicitly opts out.
|
|
2422
|
+
const embedderProvision = activationPlan.action === "bootstrap"
|
|
2423
|
+
? ensureInstallEmbedderReady(parsed)
|
|
2424
|
+
: null;
|
|
2425
|
+
if (embedderProvision === null) {
|
|
2426
|
+
steps.push("Skipped bootstrap-time embedder provisioning because attach/install is reusing healthy activation state.");
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
steps.push(embedderProvision.detail);
|
|
2430
|
+
}
|
|
2431
|
+
// 4. Create activation root if needed
|
|
2432
|
+
if (activationPlan.createActivationRoot) {
|
|
1552
2433
|
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1553
2434
|
steps.push(`Created activation root: ${parsed.activationRoot}`);
|
|
1554
2435
|
}
|
|
1555
2436
|
else {
|
|
1556
2437
|
steps.push(`Activation root exists: ${parsed.activationRoot}`);
|
|
1557
2438
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2439
|
+
steps.push(activationPlan.inspectionStep);
|
|
2440
|
+
// 5. Persist install-written local provider defaults so watch/learning surfaces do not depend on gateway env wiring.
|
|
2441
|
+
const providerDefaults = isInstall || activationPlan.action === "bootstrap"
|
|
2442
|
+
? writeInstallProviderDefaults(parsed)
|
|
2443
|
+
: null;
|
|
2444
|
+
if (providerDefaults === null) {
|
|
2445
|
+
steps.push("Skipped provider-default refresh because explicit attach is reusing existing activation data.");
|
|
1562
2446
|
}
|
|
1563
2447
|
else {
|
|
1564
|
-
|
|
2448
|
+
steps.push(providerDefaults.detail);
|
|
2449
|
+
}
|
|
2450
|
+
// 6. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
|
|
2451
|
+
if (activationPlan.action === "bootstrap") {
|
|
1565
2452
|
const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
|
|
1566
2453
|
mkdirSync(packRoot, { recursive: true });
|
|
1567
2454
|
const brainAttachmentPolicy = parsed.shared ? "shared" : "dedicated";
|
|
@@ -1570,27 +2457,31 @@ function runSetupCommand(parsed) {
|
|
|
1570
2457
|
brainAttachmentPolicy,
|
|
1571
2458
|
activationRoot: parsed.activationRoot,
|
|
1572
2459
|
packRoot,
|
|
1573
|
-
packLabel: "
|
|
2460
|
+
packLabel: isInstall ? "install-cli" : "attach-cli",
|
|
1574
2461
|
workspace: {
|
|
1575
2462
|
workspaceId: parsed.workspaceId,
|
|
1576
|
-
snapshotId: `${parsed.workspaceId}
|
|
2463
|
+
snapshotId: `${parsed.workspaceId}@${parsed.command}-${new Date().toISOString().slice(0, 10)}`,
|
|
1577
2464
|
capturedAt: new Date().toISOString(),
|
|
1578
2465
|
rootDir: parsed.openclawHome,
|
|
1579
|
-
revision: "cli-
|
|
2466
|
+
revision: isInstall ? "cli-install-v1" : "cli-attach-v1"
|
|
1580
2467
|
},
|
|
1581
2468
|
interactionEvents: [],
|
|
1582
2469
|
feedbackEvents: []
|
|
1583
2470
|
});
|
|
1584
|
-
steps.push(`Bootstrapped brain attach:
|
|
2471
|
+
steps.push(`Bootstrapped brain attach: state=${result.currentProfile.brain.state} awaitingFirstExport=${yesNo(result.currentProfile.brainStatus.awaitingFirstExport)}`);
|
|
1585
2472
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
2473
|
+
else {
|
|
2474
|
+
steps.push(isInstall
|
|
2475
|
+
? `Kept inspected activation state: active pack ${activationPlan.activePackId}`
|
|
2476
|
+
: `Reused inspected activation state for explicit attach: active pack ${activationPlan.activePackId}`);
|
|
2477
|
+
}
|
|
2478
|
+
// 7-10. Write extension files
|
|
1588
2479
|
mkdirSync(extensionDir, { recursive: true });
|
|
1589
|
-
//
|
|
2480
|
+
// 5. Write index.ts
|
|
1590
2481
|
const indexTsPath = path.join(extensionDir, "index.ts");
|
|
1591
2482
|
writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
|
|
1592
2483
|
steps.push(`Wrote extension: ${indexTsPath}`);
|
|
1593
|
-
//
|
|
2484
|
+
// 5b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
|
|
1594
2485
|
const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
|
|
1595
2486
|
if (runtimeGuardPaths.ts !== null) {
|
|
1596
2487
|
const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
|
|
@@ -1600,17 +2491,31 @@ function runSetupCommand(parsed) {
|
|
|
1600
2491
|
const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
|
|
1601
2492
|
writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
|
|
1602
2493
|
steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
|
|
1603
|
-
//
|
|
2494
|
+
// 6. Write package.json
|
|
1604
2495
|
const packageJsonPath = path.join(extensionDir, "package.json");
|
|
1605
2496
|
writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
|
|
1606
2497
|
steps.push(`Wrote package.json: ${packageJsonPath}`);
|
|
1607
|
-
//
|
|
2498
|
+
// 7. npm install
|
|
2499
|
+
const releaseTarballInstall = resolveExtensionInstallReleaseTarballs();
|
|
1608
2500
|
try {
|
|
1609
|
-
|
|
1610
|
-
|
|
2501
|
+
if (releaseTarballInstall !== null) {
|
|
2502
|
+
execFileSync(resolveNpmCommand(), ["install", "--ignore-scripts", "--no-save", ...releaseTarballInstall.tarballs], { cwd: extensionDir, stdio: "pipe" });
|
|
2503
|
+
steps.push(`Installed extension dependencies from release artifacts: ${releaseTarballInstall.tarballs.length} tarballs from ${releaseTarballInstall.artifactDir}`);
|
|
2504
|
+
}
|
|
2505
|
+
else {
|
|
2506
|
+
execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
|
|
2507
|
+
steps.push("Ran npm install --ignore-scripts");
|
|
2508
|
+
const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
|
|
2509
|
+
if (linkedPackages !== null) {
|
|
2510
|
+
steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
1611
2513
|
}
|
|
1612
2514
|
catch (err) {
|
|
1613
2515
|
const message = err instanceof Error ? err.message : String(err);
|
|
2516
|
+
if (releaseTarballInstall !== null) {
|
|
2517
|
+
throw new Error(`Extension dependency install from release artifacts failed: ${message}`);
|
|
2518
|
+
}
|
|
1614
2519
|
const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
|
|
1615
2520
|
if (linkedPackages !== null) {
|
|
1616
2521
|
steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
|
|
@@ -1619,109 +2524,83 @@ function runSetupCommand(parsed) {
|
|
|
1619
2524
|
steps.push(`npm install failed (non-fatal): ${message}`);
|
|
1620
2525
|
}
|
|
1621
2526
|
}
|
|
1622
|
-
//
|
|
2527
|
+
// 8. Write plugin manifest
|
|
1623
2528
|
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
1624
2529
|
writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
|
|
1625
2530
|
steps.push(`Wrote manifest: ${manifestPath}`);
|
|
1626
|
-
// 8. Write BRAIN.md to workspace directories
|
|
1627
|
-
const brainMdContent = [
|
|
1628
|
-
"## OpenClawBrain",
|
|
1629
|
-
`You have a learning brain attached at ${parsed.activationRoot}.`,
|
|
1630
|
-
"- It learns automatically from your conversations",
|
|
1631
|
-
'- Corrections matter — "no, actually X" teaches the brain X',
|
|
1632
|
-
"- You don't manage it — background daemon handles learning",
|
|
1633
|
-
"- Check: `openclawbrain status`",
|
|
1634
|
-
"- Rollback: `openclawbrain rollback`",
|
|
1635
|
-
'- See what brain knows: `openclawbrain context "your question"`',
|
|
1636
|
-
""
|
|
1637
|
-
].join("\n");
|
|
1638
|
-
const agentsMdBrainRef = "\n5. Read `BRAIN.md` — your learning brain context\n";
|
|
1639
|
-
try {
|
|
1640
|
-
const entries = readdirSync(parsed.openclawHome, { withFileTypes: true });
|
|
1641
|
-
const workspaceDirs = entries
|
|
1642
|
-
.filter(e => e.isDirectory() && e.name.startsWith("workspace-"))
|
|
1643
|
-
.map(e => path.join(parsed.openclawHome, e.name));
|
|
1644
|
-
// If no workspace-* dirs found, check if openclawHome itself is a workspace
|
|
1645
|
-
if (workspaceDirs.length === 0) {
|
|
1646
|
-
workspaceDirs.push(parsed.openclawHome);
|
|
1647
|
-
}
|
|
1648
|
-
for (const wsDir of workspaceDirs) {
|
|
1649
|
-
const brainMdPath = path.join(wsDir, "BRAIN.md");
|
|
1650
|
-
writeFileSync(brainMdPath, brainMdContent, "utf8");
|
|
1651
|
-
steps.push(`Wrote BRAIN.md: ${brainMdPath}`);
|
|
1652
|
-
// If AGENTS.md exists, append brain reference to startup sequence
|
|
1653
|
-
const agentsMdPath = path.join(wsDir, "AGENTS.md");
|
|
1654
|
-
if (existsSync(agentsMdPath)) {
|
|
1655
|
-
const agentsContent = readFileSync(agentsMdPath, "utf8");
|
|
1656
|
-
if (!agentsContent.includes("BRAIN.md")) {
|
|
1657
|
-
// Find the startup sequence section and append after the last numbered item
|
|
1658
|
-
const startupMarker = "## Session Startup";
|
|
1659
|
-
if (agentsContent.includes(startupMarker)) {
|
|
1660
|
-
// Find the numbered list in the startup section and append after last item
|
|
1661
|
-
const lines = agentsContent.split("\n");
|
|
1662
|
-
let lastNumberedIdx = -1;
|
|
1663
|
-
let inStartup = false;
|
|
1664
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1665
|
-
const line = lines[i] ?? "";
|
|
1666
|
-
if (line.includes(startupMarker)) {
|
|
1667
|
-
inStartup = true;
|
|
1668
|
-
continue;
|
|
1669
|
-
}
|
|
1670
|
-
if (inStartup && /^\d+\.\s/.test(line.trim())) {
|
|
1671
|
-
lastNumberedIdx = i;
|
|
1672
|
-
}
|
|
1673
|
-
if (inStartup && line.startsWith("## ") && !line.includes(startupMarker)) {
|
|
1674
|
-
break;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
if (lastNumberedIdx >= 0) {
|
|
1678
|
-
lines.splice(lastNumberedIdx + 1, 0, agentsMdBrainRef.trimEnd());
|
|
1679
|
-
writeFileSync(agentsMdPath, lines.join("\n"), "utf8");
|
|
1680
|
-
steps.push(`Updated AGENTS.md startup sequence: ${agentsMdPath}`);
|
|
1681
|
-
}
|
|
1682
|
-
else {
|
|
1683
|
-
appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
|
|
1684
|
-
steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
else {
|
|
1688
|
-
appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
|
|
1689
|
-
steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
else {
|
|
1693
|
-
steps.push(`AGENTS.md already references BRAIN.md: ${agentsMdPath}`);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
catch (err) {
|
|
1699
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1700
|
-
steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
|
|
1701
|
-
}
|
|
1702
2531
|
const restartGuidance = buildInstallReloadGuidance();
|
|
1703
2532
|
const nextSteps = [
|
|
1704
2533
|
restartGuidance,
|
|
1705
|
-
`Check status: ${buildInstallStatusCommand(parsed.activationRoot)}
|
|
2534
|
+
`Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`,
|
|
2535
|
+
embedderProvision !== null && embedderProvision.state === "skipped"
|
|
2536
|
+
? `Provision default embedder later: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`
|
|
2537
|
+
: null
|
|
2538
|
+
].filter((step) => step !== null);
|
|
2539
|
+
const preflightSummary = [
|
|
2540
|
+
`Hook: installed at ${shortenPath(extensionDir)}`,
|
|
2541
|
+
activationPlan.action === "bootstrap"
|
|
2542
|
+
? "Attachment: seed/current-profile attach created; restart plus status will prove later serve-path use"
|
|
2543
|
+
: `Attachment: existing active pack ${activationPlan.activePackId} kept in place; restart plus status will prove later serve-path use`,
|
|
2544
|
+
embedderProvision === null
|
|
2545
|
+
? "Embedder: unchanged because no bootstrap was needed"
|
|
2546
|
+
: embedderProvision.state === "ensured"
|
|
2547
|
+
? `Embedder: default Ollama model ${embedderProvision.model} was ensured before bootstrap`
|
|
2548
|
+
: `Embedder: default Ollama model ${embedderProvision.model} was intentionally skipped`,
|
|
2549
|
+
`Serve path: install alone does not prove serving; restart the profile and run ${buildInstallStatusCommand(parsed.activationRoot)}`
|
|
1706
2550
|
];
|
|
1707
2551
|
const lifecycleSummary = [
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2552
|
+
isInstall
|
|
2553
|
+
? "Lifecycle mode: install (safe first-time/default profile hookup)"
|
|
2554
|
+
: "Lifecycle mode: attach (explicit reattach/manual profile hookup)",
|
|
2555
|
+
`OpenClaw target: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
|
|
2556
|
+
`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`,
|
|
2557
|
+
`Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
|
|
2558
|
+
`Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
|
|
2559
|
+
embedderProvision === null
|
|
2560
|
+
? "Embedder: unchanged because no bootstrap was needed"
|
|
2561
|
+
: embedderProvision.state === "ensured"
|
|
2562
|
+
? `Embedder: ensured default Ollama model ${embedderProvision.model} before brain init`
|
|
2563
|
+
: `Embedder: skipped default Ollama model ${embedderProvision.model} via ${parsed.skipEmbedderProvisionSource === "flag" ? "--skip-embedder-provision" : OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}`,
|
|
2564
|
+
...(providerDefaults === null ? [] : [`${providerDefaults.lifecycleSummary} (${shortenPath(providerDefaults.path)})`]),
|
|
1711
2565
|
`Profile hook: installed at ${shortenPath(extensionDir)}`,
|
|
1712
|
-
|
|
2566
|
+
activationPlan.resolution === "new_root"
|
|
1713
2567
|
? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
|
|
1714
|
-
:
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2568
|
+
: activationPlan.resolution === "missing_pointers"
|
|
2569
|
+
? `Activation data: repaired missing pointers at ${shortenPath(parsed.activationRoot)}`
|
|
2570
|
+
: activationPlan.resolution === "empty_pointers"
|
|
2571
|
+
? `Activation data: repaired empty pointers at ${shortenPath(parsed.activationRoot)}`
|
|
2572
|
+
: `Activation data: reused healthy state at ${shortenPath(parsed.activationRoot)}`,
|
|
2573
|
+
activationPlan.action === "bootstrap"
|
|
2574
|
+
? activationPlan.resolution === "new_root"
|
|
2575
|
+
? `${isInstall ? "Install" : "Attach"}: bootstrapped a seed/current-profile brain`
|
|
2576
|
+
: activationPlan.resolution === "missing_pointers"
|
|
2577
|
+
? `${isInstall ? "Install" : "Attach"}: repaired missing activation pointers and bootstrapped a seed/current-profile brain`
|
|
2578
|
+
: `${isInstall ? "Install" : "Attach"}: repaired empty activation pointers and bootstrapped a seed/current-profile brain`
|
|
2579
|
+
: isInstall
|
|
2580
|
+
? `Install: kept healthy active pack ${activationPlan.activePackId} in place`
|
|
2581
|
+
: `Attach: rewired the profile hook to healthy active pack ${activationPlan.activePackId}`
|
|
1718
2582
|
];
|
|
2583
|
+
const brainFeedback = buildInstallBrainFeedbackSummary({
|
|
2584
|
+
parsed,
|
|
2585
|
+
targetInspection,
|
|
2586
|
+
extensionDir,
|
|
2587
|
+
activationPlan,
|
|
2588
|
+
embedderProvision,
|
|
2589
|
+
providerDefaults
|
|
2590
|
+
});
|
|
1719
2591
|
// 9. Print summary
|
|
1720
2592
|
if (parsed.json) {
|
|
1721
2593
|
console.log(JSON.stringify({
|
|
1722
2594
|
command: parsed.command,
|
|
1723
2595
|
openclawHome: parsed.openclawHome,
|
|
1724
2596
|
openclawHomeSource: parsed.openclawHomeSource,
|
|
2597
|
+
openclawTarget: {
|
|
2598
|
+
layout: targetInspection.layout,
|
|
2599
|
+
detail: describeOpenClawHomeInspection(targetInspection),
|
|
2600
|
+
profileId: targetInspection.profileId,
|
|
2601
|
+
profileSource: targetInspection.profileSource,
|
|
2602
|
+
configuredProfileIds: targetInspection.configuredProfileIds
|
|
2603
|
+
},
|
|
1725
2604
|
activationRoot: parsed.activationRoot,
|
|
1726
2605
|
resolvedInputs: {
|
|
1727
2606
|
activationRoot: {
|
|
@@ -1735,8 +2614,47 @@ function runSetupCommand(parsed) {
|
|
|
1735
2614
|
},
|
|
1736
2615
|
workspaceId: parsed.workspaceId,
|
|
1737
2616
|
shared: parsed.shared,
|
|
2617
|
+
embedderProvision: embedderProvision === null
|
|
2618
|
+
? null
|
|
2619
|
+
: {
|
|
2620
|
+
skipped: parsed.skipEmbedderProvision,
|
|
2621
|
+
source: parsed.skipEmbedderProvisionSource,
|
|
2622
|
+
model: embedderProvision.model,
|
|
2623
|
+
baseUrl: embedderProvision.baseUrl
|
|
2624
|
+
},
|
|
2625
|
+
providerDefaults: providerDefaults === null
|
|
2626
|
+
? null
|
|
2627
|
+
: {
|
|
2628
|
+
path: providerDefaults.path,
|
|
2629
|
+
teacher: providerDefaults.defaults.teacher === undefined
|
|
2630
|
+
? null
|
|
2631
|
+
: {
|
|
2632
|
+
provider: providerDefaults.defaults.teacher.provider ?? null,
|
|
2633
|
+
model: providerDefaults.defaults.teacher.model ?? null,
|
|
2634
|
+
detectedLocally: providerDefaults.defaults.teacher.detectedLocally ?? false
|
|
2635
|
+
},
|
|
2636
|
+
embedder: providerDefaults.defaults.embedder === undefined
|
|
2637
|
+
? null
|
|
2638
|
+
: {
|
|
2639
|
+
provider: providerDefaults.defaults.embedder.provider ?? null,
|
|
2640
|
+
model: providerDefaults.defaults.embedder.model ?? null
|
|
2641
|
+
},
|
|
2642
|
+
teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
|
|
2643
|
+
embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
|
|
2644
|
+
},
|
|
2645
|
+
brainFeedback: {
|
|
2646
|
+
hookPath: brainFeedback.hookPath,
|
|
2647
|
+
providerDefaultsPath: brainFeedback.providerDefaultsPath,
|
|
2648
|
+
embedder: brainFeedback.embedder,
|
|
2649
|
+
teacher: brainFeedback.teacher,
|
|
2650
|
+
startup: brainFeedback.startup,
|
|
2651
|
+
provedNow: brainFeedback.provedNow,
|
|
2652
|
+
notYetProved: brainFeedback.notYetProved,
|
|
2653
|
+
lines: brainFeedback.lines
|
|
2654
|
+
},
|
|
1738
2655
|
extensionDir,
|
|
1739
2656
|
lifecycleSummary,
|
|
2657
|
+
preflightSummary,
|
|
1740
2658
|
restartGuidance,
|
|
1741
2659
|
nextSteps,
|
|
1742
2660
|
steps
|
|
@@ -1744,19 +2662,24 @@ function runSetupCommand(parsed) {
|
|
|
1744
2662
|
}
|
|
1745
2663
|
else {
|
|
1746
2664
|
console.log(`${commandLabel} complete\n`);
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
}
|
|
1750
|
-
console.log("");
|
|
1751
|
-
console.log("Lifecycle:");
|
|
1752
|
-
for (const line of lifecycleSummary) {
|
|
2665
|
+
console.log("Brain feedback:");
|
|
2666
|
+
for (const line of brainFeedback.lines) {
|
|
1753
2667
|
console.log(` ${line}`);
|
|
1754
2668
|
}
|
|
1755
2669
|
console.log(`Next: ${restartGuidance}`);
|
|
1756
2670
|
console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
|
|
2671
|
+
if (embedderProvision !== null && embedderProvision.state === "skipped") {
|
|
2672
|
+
console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
|
|
2673
|
+
}
|
|
1757
2674
|
}
|
|
1758
2675
|
return 0;
|
|
1759
2676
|
}
|
|
2677
|
+
function runInstallCommand(parsed) {
|
|
2678
|
+
return runProfileHookAttachCommand(parsed);
|
|
2679
|
+
}
|
|
2680
|
+
function runAttachCommand(parsed) {
|
|
2681
|
+
return runProfileHookAttachCommand(parsed);
|
|
2682
|
+
}
|
|
1760
2683
|
function validateOpenClawHome(openclawHome) {
|
|
1761
2684
|
if (!existsSync(openclawHome)) {
|
|
1762
2685
|
throw new Error(`--openclaw-home directory does not exist: ${openclawHome}`);
|
|
@@ -1806,25 +2729,43 @@ function buildRestartGuidance(restart) {
|
|
|
1806
2729
|
function runDetachCommand(parsed) {
|
|
1807
2730
|
const steps = [];
|
|
1808
2731
|
validateOpenClawHome(parsed.openclawHome);
|
|
2732
|
+
const targetInspection = inspectOpenClawHome(parsed.openclawHome);
|
|
2733
|
+
steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
1809
2734
|
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
1810
2735
|
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
2736
|
+
const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
|
|
1811
2737
|
const activationData = summarizeKeptActivationData(activationRoot);
|
|
1812
2738
|
const restartGuidance = buildRestartGuidance(parsed.restart);
|
|
1813
2739
|
const nextSteps = [
|
|
1814
2740
|
restartGuidance,
|
|
1815
2741
|
activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
|
|
1816
|
-
`Reattach later: ${
|
|
2742
|
+
`Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
|
|
1817
2743
|
].filter((step) => step !== null);
|
|
2744
|
+
if (legacyResidue.removedNotes.length > 0) {
|
|
2745
|
+
steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
|
|
2746
|
+
}
|
|
2747
|
+
if (legacyResidue.updatedAgents.length > 0) {
|
|
2748
|
+
steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
|
|
2749
|
+
}
|
|
1818
2750
|
steps.push(activationData.activationDataDetail);
|
|
1819
2751
|
steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
|
|
1820
2752
|
if (parsed.json) {
|
|
1821
2753
|
console.log(JSON.stringify({
|
|
1822
2754
|
command: "detach",
|
|
1823
2755
|
openclawHome: parsed.openclawHome,
|
|
2756
|
+
openclawTarget: {
|
|
2757
|
+
layout: targetInspection.layout,
|
|
2758
|
+
detail: describeOpenClawHomeInspection(targetInspection),
|
|
2759
|
+
profileId: targetInspection.profileId,
|
|
2760
|
+
profileSource: targetInspection.profileSource,
|
|
2761
|
+
configuredProfileIds: targetInspection.configuredProfileIds
|
|
2762
|
+
},
|
|
1824
2763
|
extensionDir,
|
|
1825
2764
|
activationRoot,
|
|
1826
2765
|
dataAction: "kept",
|
|
1827
2766
|
activationDataState: activationData.activationDataState,
|
|
2767
|
+
removedLegacyNotes: legacyResidue.removedNotes,
|
|
2768
|
+
updatedAgents: legacyResidue.updatedAgents,
|
|
1828
2769
|
restartMode: parsed.restart,
|
|
1829
2770
|
restartGuidance,
|
|
1830
2771
|
nextSteps,
|
|
@@ -1837,7 +2778,8 @@ function runDetachCommand(parsed) {
|
|
|
1837
2778
|
console.log(` ✓ ${step}`);
|
|
1838
2779
|
}
|
|
1839
2780
|
console.log("");
|
|
1840
|
-
console.log(`Lifecycle: OpenClaw
|
|
2781
|
+
console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
|
|
2782
|
+
console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
1841
2783
|
if (activationRoot !== null) {
|
|
1842
2784
|
console.log(`Brain data: ${shortenPath(activationRoot)} remains available for inspection or reattach.`);
|
|
1843
2785
|
}
|
|
@@ -1848,15 +2790,18 @@ function runDetachCommand(parsed) {
|
|
|
1848
2790
|
if (activationRoot !== null) {
|
|
1849
2791
|
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
1850
2792
|
}
|
|
1851
|
-
console.log(`Reattach: ${
|
|
2793
|
+
console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
|
|
1852
2794
|
}
|
|
1853
2795
|
return 0;
|
|
1854
2796
|
}
|
|
1855
2797
|
function runUninstallCommand(parsed) {
|
|
1856
2798
|
const steps = [];
|
|
1857
2799
|
validateOpenClawHome(parsed.openclawHome);
|
|
2800
|
+
const targetInspection = inspectOpenClawHome(parsed.openclawHome);
|
|
2801
|
+
steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
1858
2802
|
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
1859
2803
|
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
2804
|
+
const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
|
|
1860
2805
|
let activationData;
|
|
1861
2806
|
if (parsed.dataMode === "purge") {
|
|
1862
2807
|
if (activationRoot === null) {
|
|
@@ -1885,8 +2830,16 @@ function runUninstallCommand(parsed) {
|
|
|
1885
2830
|
const nextSteps = [
|
|
1886
2831
|
restartGuidance,
|
|
1887
2832
|
parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
|
|
1888
|
-
|
|
2833
|
+
parsed.dataMode === "keep"
|
|
2834
|
+
? `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
|
|
2835
|
+
: `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
|
|
1889
2836
|
].filter((step) => step !== null);
|
|
2837
|
+
if (legacyResidue.removedNotes.length > 0) {
|
|
2838
|
+
steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
|
|
2839
|
+
}
|
|
2840
|
+
if (legacyResidue.updatedAgents.length > 0) {
|
|
2841
|
+
steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
|
|
2842
|
+
}
|
|
1890
2843
|
steps.push(activationData.activationDataDetail);
|
|
1891
2844
|
steps.push(parsed.dataMode === "purge"
|
|
1892
2845
|
? "Uninstall removed the OpenClaw profile hook and activation data."
|
|
@@ -1895,10 +2848,19 @@ function runUninstallCommand(parsed) {
|
|
|
1895
2848
|
console.log(JSON.stringify({
|
|
1896
2849
|
command: "uninstall",
|
|
1897
2850
|
openclawHome: parsed.openclawHome,
|
|
2851
|
+
openclawTarget: {
|
|
2852
|
+
layout: targetInspection.layout,
|
|
2853
|
+
detail: describeOpenClawHomeInspection(targetInspection),
|
|
2854
|
+
profileId: targetInspection.profileId,
|
|
2855
|
+
profileSource: targetInspection.profileSource,
|
|
2856
|
+
configuredProfileIds: targetInspection.configuredProfileIds
|
|
2857
|
+
},
|
|
1898
2858
|
extensionDir,
|
|
1899
2859
|
activationRoot,
|
|
1900
2860
|
dataAction: parsed.dataMode,
|
|
1901
2861
|
activationDataState: activationData.activationDataState,
|
|
2862
|
+
removedLegacyNotes: legacyResidue.removedNotes,
|
|
2863
|
+
updatedAgents: legacyResidue.updatedAgents,
|
|
1902
2864
|
restartMode: parsed.restart,
|
|
1903
2865
|
restartGuidance,
|
|
1904
2866
|
nextSteps,
|
|
@@ -1911,7 +2873,8 @@ function runUninstallCommand(parsed) {
|
|
|
1911
2873
|
console.log(` ✓ ${step}`);
|
|
1912
2874
|
}
|
|
1913
2875
|
console.log("");
|
|
1914
|
-
console.log(`Lifecycle: OpenClaw
|
|
2876
|
+
console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
|
|
2877
|
+
console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
|
|
1915
2878
|
console.log(`Data mode: ${parsed.dataMode === "purge" ? "purged" : "kept"}`);
|
|
1916
2879
|
if (activationRoot !== null) {
|
|
1917
2880
|
console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
|
|
@@ -1920,7 +2883,12 @@ function runUninstallCommand(parsed) {
|
|
|
1920
2883
|
if (parsed.dataMode === "keep" && activationRoot !== null) {
|
|
1921
2884
|
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
1922
2885
|
}
|
|
1923
|
-
|
|
2886
|
+
if (parsed.dataMode === "keep") {
|
|
2887
|
+
console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
|
|
2888
|
+
}
|
|
2889
|
+
else {
|
|
2890
|
+
console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
|
|
2891
|
+
}
|
|
1924
2892
|
}
|
|
1925
2893
|
return 0;
|
|
1926
2894
|
}
|
|
@@ -2341,16 +3309,9 @@ function formatTimestamp() {
|
|
|
2341
3309
|
function watchLog(message) {
|
|
2342
3310
|
console.log(`${formatTimestamp()} ${message}`);
|
|
2343
3311
|
}
|
|
2344
|
-
function
|
|
2345
|
-
|
|
2346
|
-
return explicitPath;
|
|
2347
|
-
}
|
|
2348
|
-
const asyncSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(activationRoot);
|
|
2349
|
-
return existsSync(asyncSnapshotPath) ? asyncSnapshotPath : null;
|
|
3312
|
+
function formatWatchError(error) {
|
|
3313
|
+
return error instanceof Error ? error.message : String(error);
|
|
2350
3314
|
}
|
|
2351
|
-
const WATCH_STATE_DIRNAME = "watch";
|
|
2352
|
-
const WATCH_SESSION_TAIL_CURSOR_BASENAME = "session-tail-cursor.json";
|
|
2353
|
-
const WATCH_TEACHER_SNAPSHOT_BASENAME = "teacher-snapshot.json";
|
|
2354
3315
|
function sanitizeWatchPathSegment(value) {
|
|
2355
3316
|
const sanitized = value
|
|
2356
3317
|
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
@@ -2358,15 +3319,6 @@ function sanitizeWatchPathSegment(value) {
|
|
|
2358
3319
|
.slice(0, 96);
|
|
2359
3320
|
return sanitized.length > 0 ? sanitized : "session";
|
|
2360
3321
|
}
|
|
2361
|
-
function resolveWatchStateRoot(activationRoot) {
|
|
2362
|
-
return path.resolve(activationRoot, WATCH_STATE_DIRNAME);
|
|
2363
|
-
}
|
|
2364
|
-
function resolveWatchSessionTailCursorPath(activationRoot) {
|
|
2365
|
-
return path.join(resolveWatchStateRoot(activationRoot), WATCH_SESSION_TAIL_CURSOR_BASENAME);
|
|
2366
|
-
}
|
|
2367
|
-
function resolveWatchTeacherSnapshotPath(activationRoot) {
|
|
2368
|
-
return path.join(resolveWatchStateRoot(activationRoot), WATCH_TEACHER_SNAPSHOT_BASENAME);
|
|
2369
|
-
}
|
|
2370
3322
|
function readOptionalJsonFile(filePath) {
|
|
2371
3323
|
if (!existsSync(filePath)) {
|
|
2372
3324
|
return null;
|
|
@@ -2400,28 +3352,8 @@ function persistWatchSessionTailCursor(cursorPath, cursor) {
|
|
|
2400
3352
|
cursor
|
|
2401
3353
|
});
|
|
2402
3354
|
}
|
|
2403
|
-
function
|
|
2404
|
-
|
|
2405
|
-
return {
|
|
2406
|
-
lastHandledMaterializationPackId: parsed !== null && typeof parsed.lastHandledMaterializationPackId === "string"
|
|
2407
|
-
? parsed.lastHandledMaterializationPackId
|
|
2408
|
-
: null
|
|
2409
|
-
};
|
|
2410
|
-
}
|
|
2411
|
-
function persistWatchTeacherSnapshot(snapshotPath, input) {
|
|
2412
|
-
writeJsonFile(snapshotPath, {
|
|
2413
|
-
contract: "openclaw_watch_teacher_snapshot.v1",
|
|
2414
|
-
runtimeOwner: "openclaw",
|
|
2415
|
-
updatedAt: new Date().toISOString(),
|
|
2416
|
-
scanRoot: input.scanRoot,
|
|
2417
|
-
replayedBundleCount: input.replayedBundleCount,
|
|
2418
|
-
replayedEventCount: input.replayedEventCount,
|
|
2419
|
-
exportedBundleCount: input.exportedBundleCount,
|
|
2420
|
-
exportedEventCount: input.exportedEventCount,
|
|
2421
|
-
localSessionTailNoopReason: input.localSessionTailNoopReason,
|
|
2422
|
-
lastHandledMaterializationPackId: input.lastHandledMaterializationPackId,
|
|
2423
|
-
snapshot: input.snapshot
|
|
2424
|
-
});
|
|
3355
|
+
function countWatchCursorBridgedEvents(cursor) {
|
|
3356
|
+
return cursor.reduce((sum, entry) => sum + entry.bridgedEventCount, 0);
|
|
2425
3357
|
}
|
|
2426
3358
|
function listWatchRuntimeEventExportBundleRoots(scanRoot) {
|
|
2427
3359
|
if (!existsSync(scanRoot)) {
|
|
@@ -2528,7 +3460,8 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2528
3460
|
return {
|
|
2529
3461
|
lastHandledMaterializationPackId,
|
|
2530
3462
|
logLine: null,
|
|
2531
|
-
materializedPackId: null
|
|
3463
|
+
materializedPackId: null,
|
|
3464
|
+
failure: null
|
|
2532
3465
|
};
|
|
2533
3466
|
}
|
|
2534
3467
|
const packId = typeof materialization?.candidate?.summary?.packId === "string"
|
|
@@ -2538,14 +3471,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2538
3471
|
return {
|
|
2539
3472
|
lastHandledMaterializationPackId,
|
|
2540
3473
|
logLine: null,
|
|
2541
|
-
materializedPackId: packId
|
|
3474
|
+
materializedPackId: packId,
|
|
3475
|
+
failure: null
|
|
2542
3476
|
};
|
|
2543
3477
|
}
|
|
2544
3478
|
const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
|
|
2545
3479
|
try {
|
|
2546
3480
|
const candidateRootDir = path.resolve(activationRoot, "packs", packId);
|
|
2547
3481
|
mkdirSync(candidateRootDir, { recursive: true });
|
|
2548
|
-
|
|
3482
|
+
let activeBeforePack = null;
|
|
3483
|
+
try {
|
|
3484
|
+
activeBeforePack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
|
|
3485
|
+
}
|
|
3486
|
+
catch {
|
|
3487
|
+
activeBeforePack = null;
|
|
3488
|
+
}
|
|
3489
|
+
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
|
|
3490
|
+
appendLearningUpdateLogs({
|
|
3491
|
+
activationRoot,
|
|
3492
|
+
materialization,
|
|
3493
|
+
activeBeforePack,
|
|
3494
|
+
candidateDescriptor
|
|
3495
|
+
});
|
|
2549
3496
|
const now = new Date().toISOString();
|
|
2550
3497
|
stageCandidatePack(activationRoot, candidateRootDir, {
|
|
2551
3498
|
updatedAt: now,
|
|
@@ -2560,13 +3507,15 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2560
3507
|
return {
|
|
2561
3508
|
lastHandledMaterializationPackId: packId,
|
|
2562
3509
|
materializedPackId: packId,
|
|
2563
|
-
logLine: `Promoted ${shortPackId} → active
|
|
3510
|
+
logLine: `Promoted ${shortPackId} → active`,
|
|
3511
|
+
failure: null
|
|
2564
3512
|
};
|
|
2565
3513
|
}
|
|
2566
3514
|
return {
|
|
2567
3515
|
lastHandledMaterializationPackId: packId,
|
|
2568
3516
|
materializedPackId: packId,
|
|
2569
|
-
logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})
|
|
3517
|
+
logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
|
|
3518
|
+
failure: null
|
|
2570
3519
|
};
|
|
2571
3520
|
}
|
|
2572
3521
|
catch (error) {
|
|
@@ -2574,26 +3523,80 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2574
3523
|
return {
|
|
2575
3524
|
lastHandledMaterializationPackId,
|
|
2576
3525
|
materializedPackId: packId,
|
|
2577
|
-
logLine: `Promotion failed for ${shortPackId}: ${message}
|
|
3526
|
+
logLine: `Promotion failed for ${shortPackId}: ${message}`,
|
|
3527
|
+
failure: {
|
|
3528
|
+
mode: "materialization_failed",
|
|
3529
|
+
detail: message,
|
|
3530
|
+
at: new Date().toISOString()
|
|
3531
|
+
}
|
|
2578
3532
|
};
|
|
2579
3533
|
}
|
|
2580
3534
|
}
|
|
3535
|
+
function resolveWatchTeacherLabelerConfig(input, activationRoot) {
|
|
3536
|
+
if (input !== undefined) {
|
|
3537
|
+
return {
|
|
3538
|
+
teacherLabeler: input,
|
|
3539
|
+
warnings: []
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
const providerConfig = readOpenClawBrainProviderConfigFromSources({
|
|
3543
|
+
env: process.env,
|
|
3544
|
+
activationRoot
|
|
3545
|
+
});
|
|
3546
|
+
const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_|provider defaults/u.test(warning));
|
|
3547
|
+
if (providerConfig.teacher.provider !== "ollama") {
|
|
3548
|
+
return {
|
|
3549
|
+
teacherLabeler: null,
|
|
3550
|
+
warnings
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
return {
|
|
3554
|
+
teacherLabeler: {
|
|
3555
|
+
provider: "ollama",
|
|
3556
|
+
baseUrl: providerConfig.teacherBaseUrl,
|
|
3557
|
+
model: providerConfig.teacher.model,
|
|
3558
|
+
...(providerConfig.teacher.timeoutMs === undefined ? {} : { timeoutMs: providerConfig.teacher.timeoutMs }),
|
|
3559
|
+
...(providerConfig.teacher.maxPromptChars === undefined ? {} : { maxPromptChars: providerConfig.teacher.maxPromptChars }),
|
|
3560
|
+
...(providerConfig.teacher.maxResponseChars === undefined ? {} : { maxResponseChars: providerConfig.teacher.maxResponseChars }),
|
|
3561
|
+
...(providerConfig.teacher.maxOutputTokens === undefined ? {} : { maxOutputTokens: providerConfig.teacher.maxOutputTokens }),
|
|
3562
|
+
...(providerConfig.teacher.maxArtifactsPerExport === undefined
|
|
3563
|
+
? {}
|
|
3564
|
+
: { maxArtifactsPerExport: providerConfig.teacher.maxArtifactsPerExport }),
|
|
3565
|
+
...(providerConfig.teacher.maxInteractionsPerExport === undefined
|
|
3566
|
+
? {}
|
|
3567
|
+
: { maxInteractionsPerExport: providerConfig.teacher.maxInteractionsPerExport })
|
|
3568
|
+
},
|
|
3569
|
+
warnings
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
2581
3572
|
export async function createWatchCommandRuntime(input) {
|
|
2582
3573
|
const activationRoot = path.resolve(input.activationRoot);
|
|
3574
|
+
const bootstrapObservedAt = new Date().toISOString();
|
|
2583
3575
|
const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
|
|
2584
3576
|
? path.resolve(input.scanRoot)
|
|
2585
3577
|
: path.resolve(activationRoot, "event-exports");
|
|
2586
3578
|
const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
|
|
2587
3579
|
const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
|
|
3580
|
+
const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
|
|
2588
3581
|
const log = input.log ?? watchLog;
|
|
3582
|
+
const startupWarnings = [];
|
|
2589
3583
|
mkdirSync(scanRoot, { recursive: true });
|
|
2590
3584
|
mkdirSync(resolveWatchStateRoot(activationRoot), { recursive: true });
|
|
2591
3585
|
log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
|
|
2592
3586
|
log(`Scan root: ${shortenPath(scanRoot)}`);
|
|
2593
3587
|
log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
|
|
3588
|
+
const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
|
|
3589
|
+
const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
|
|
3590
|
+
for (const warning of resolvedTeacherLabeler.warnings) {
|
|
3591
|
+
startupWarnings.push(`teacher_config_warning:${warning}`);
|
|
3592
|
+
log(`Teacher config warning: ${warning}`);
|
|
3593
|
+
}
|
|
3594
|
+
if (teacherLabeler?.provider === "ollama") {
|
|
3595
|
+
log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
|
|
3596
|
+
}
|
|
2594
3597
|
const scanner = createRuntimeEventExportScanner({ scanRoot });
|
|
2595
3598
|
let lastServeTimeFallbackReason = null;
|
|
2596
|
-
const
|
|
3599
|
+
const baseTeacherLoopInput = {
|
|
2597
3600
|
packLabel: "watch-cli",
|
|
2598
3601
|
workspace: {
|
|
2599
3602
|
workspaceId: "watch-cli",
|
|
@@ -2603,6 +3606,7 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2603
3606
|
revision: "watch-cli-v2"
|
|
2604
3607
|
},
|
|
2605
3608
|
learnedRouting: true,
|
|
3609
|
+
...(teacherLabeler !== null ? { teacherLabeler } : {}),
|
|
2606
3610
|
resolveLearnedRoutingState: () => {
|
|
2607
3611
|
const resolved = resolveServeTimeLearningRuntimeInput(activationRoot);
|
|
2608
3612
|
if (resolved.fallbackReason !== null && resolved.fallbackReason !== lastServeTimeFallbackReason) {
|
|
@@ -2620,11 +3624,38 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2620
3624
|
persistBaseline(activationRoot, state);
|
|
2621
3625
|
}
|
|
2622
3626
|
catch (error) {
|
|
2623
|
-
|
|
2624
|
-
log(`Baseline persist failed: ${message}`);
|
|
3627
|
+
log(`Baseline persist failed: ${formatWatchError(error)}`);
|
|
2625
3628
|
}
|
|
2626
3629
|
}
|
|
2627
|
-
}
|
|
3630
|
+
};
|
|
3631
|
+
let teacherLoop;
|
|
3632
|
+
let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
|
|
3633
|
+
if (restoredTeacherState.error !== null) {
|
|
3634
|
+
const message = restoredTeacherState.error;
|
|
3635
|
+
startupWarnings.push(`teacher_snapshot_reset:${message}`);
|
|
3636
|
+
lastHandledMaterializationPackId = null;
|
|
3637
|
+
log(`Teacher snapshot reset: ${message}`);
|
|
3638
|
+
teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
|
|
3639
|
+
}
|
|
3640
|
+
else {
|
|
3641
|
+
try {
|
|
3642
|
+
teacherLoop = createAsyncTeacherLiveLoop({
|
|
3643
|
+
...baseTeacherLoopInput,
|
|
3644
|
+
...(restoredTeacherState.snapshot !== null ? { resumeFromSnapshot: restoredTeacherState.snapshot } : {})
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
catch (error) {
|
|
3648
|
+
const message = formatWatchError(error);
|
|
3649
|
+
startupWarnings.push(`teacher_snapshot_reset:${message}`);
|
|
3650
|
+
lastHandledMaterializationPackId = null;
|
|
3651
|
+
log(`Teacher snapshot reset: ${message}`);
|
|
3652
|
+
teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
if (restoredTeacherState.snapshot !== null && startupWarnings.length === 0) {
|
|
3656
|
+
const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
|
|
3657
|
+
log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
|
|
3658
|
+
}
|
|
2628
3659
|
let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
|
|
2629
3660
|
let localSessionTail;
|
|
2630
3661
|
try {
|
|
@@ -2644,8 +3675,18 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2644
3675
|
});
|
|
2645
3676
|
persistWatchSessionTailCursor(sessionTailCursorPath, []);
|
|
2646
3677
|
}
|
|
2647
|
-
let
|
|
2648
|
-
|
|
3678
|
+
let replayState = {
|
|
3679
|
+
replayedBundleCount: 0,
|
|
3680
|
+
replayedEventCount: 0
|
|
3681
|
+
};
|
|
3682
|
+
try {
|
|
3683
|
+
replayState = await replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot);
|
|
3684
|
+
}
|
|
3685
|
+
catch (error) {
|
|
3686
|
+
const message = formatWatchError(error);
|
|
3687
|
+
startupWarnings.push(`teacher_replay_failed:${message}`);
|
|
3688
|
+
log(`Async teacher replay fail-open: ${message}`);
|
|
3689
|
+
}
|
|
2649
3690
|
if (replayState.replayedBundleCount > 0) {
|
|
2650
3691
|
log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
|
|
2651
3692
|
}
|
|
@@ -2656,14 +3697,25 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2656
3697
|
log(replayPromotion.logLine);
|
|
2657
3698
|
bootstrapSnapshot = teacherLoop.snapshot();
|
|
2658
3699
|
}
|
|
3700
|
+
const bootstrapCursor = localSessionTail.snapshot();
|
|
2659
3701
|
persistWatchTeacherSnapshot(teacherSnapshotPath, {
|
|
3702
|
+
lastRunAt: bootstrapObservedAt,
|
|
2660
3703
|
scanRoot,
|
|
3704
|
+
sessionTailCursorPath,
|
|
3705
|
+
sessionTailCursorUpdatedAt: bootstrapObservedAt,
|
|
3706
|
+
sessionTailSessionsTracked: bootstrapCursor.length,
|
|
3707
|
+
sessionTailBridgedEventCount: countWatchCursorBridgedEvents(bootstrapCursor),
|
|
3708
|
+
scannerCheckpointPath: scanner.checkpointPath,
|
|
3709
|
+
scannerCheckpoint: scanner.snapshot(),
|
|
2661
3710
|
replayedBundleCount: replayState.replayedBundleCount,
|
|
2662
3711
|
replayedEventCount: replayState.replayedEventCount,
|
|
2663
3712
|
exportedBundleCount: 0,
|
|
2664
3713
|
exportedEventCount: 0,
|
|
3714
|
+
startupWarnings,
|
|
3715
|
+
lastTeacherError: null,
|
|
2665
3716
|
localSessionTailNoopReason: null,
|
|
2666
3717
|
lastHandledMaterializationPackId,
|
|
3718
|
+
failure: replayPromotion.failure,
|
|
2667
3719
|
snapshot: bootstrapSnapshot
|
|
2668
3720
|
});
|
|
2669
3721
|
return {
|
|
@@ -2671,6 +3723,8 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2671
3723
|
scanRoot,
|
|
2672
3724
|
sessionTailCursorPath,
|
|
2673
3725
|
teacherSnapshotPath,
|
|
3726
|
+
startupWarnings,
|
|
3727
|
+
lastTeacherError: null,
|
|
2674
3728
|
replayState,
|
|
2675
3729
|
lastHandledMaterializationPackId,
|
|
2676
3730
|
scanner,
|
|
@@ -2684,6 +3738,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
2684
3738
|
const localPoll = runtime.localSessionTail.pollOnce({
|
|
2685
3739
|
observedAt
|
|
2686
3740
|
});
|
|
3741
|
+
const scannerCheckpointBeforeScan = runtime.scanner.snapshot();
|
|
2687
3742
|
const exported = exportLocalSessionTailChangesToScanRoot({
|
|
2688
3743
|
scanRoot: runtime.scanRoot,
|
|
2689
3744
|
polledAt: localPoll.polledAt,
|
|
@@ -2703,31 +3758,71 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
2703
3758
|
const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
|
|
2704
3759
|
let snapshot = runtime.teacherLoop.snapshot();
|
|
2705
3760
|
let materializedPackId = null;
|
|
3761
|
+
let failure = null;
|
|
2706
3762
|
if (totalSelected === 0) {
|
|
2707
3763
|
log("Scanning... no changes");
|
|
2708
3764
|
}
|
|
2709
3765
|
else {
|
|
2710
3766
|
log(`Scanning... ${totalSelected} export bundle${totalSelected === 1 ? "" : "s"} selected, ${totalEvents} event${totalEvents === 1 ? "" : "s"}`);
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
3767
|
+
try {
|
|
3768
|
+
const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
|
|
3769
|
+
runtime.lastTeacherError = null;
|
|
3770
|
+
snapshot = ingestResult.snapshot;
|
|
3771
|
+
const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
|
|
3772
|
+
runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
|
|
3773
|
+
materializedPackId = promotion.materializedPackId;
|
|
3774
|
+
failure = promotion.failure;
|
|
3775
|
+
if (promotion.logLine !== null) {
|
|
3776
|
+
log(promotion.logLine);
|
|
3777
|
+
snapshot = runtime.teacherLoop.snapshot();
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
catch (error) {
|
|
3781
|
+
const message = formatWatchError(error);
|
|
3782
|
+
runtime.lastTeacherError = message;
|
|
3783
|
+
failure = {
|
|
3784
|
+
mode: "teacher_fail_open",
|
|
3785
|
+
detail: message,
|
|
3786
|
+
at: observedAt
|
|
3787
|
+
};
|
|
3788
|
+
log(`Async teacher fail-open: ${message}`);
|
|
3789
|
+
try {
|
|
3790
|
+
runtime.scanner.restoreCheckpoint(scannerCheckpointBeforeScan);
|
|
3791
|
+
}
|
|
3792
|
+
catch (restoreError) {
|
|
3793
|
+
const restoreMessage = formatWatchError(restoreError);
|
|
3794
|
+
runtime.lastTeacherError = `${message}; scanner checkpoint restore failed: ${restoreMessage}`;
|
|
3795
|
+
failure = {
|
|
3796
|
+
mode: "teacher_fail_open",
|
|
3797
|
+
detail: runtime.lastTeacherError,
|
|
3798
|
+
at: observedAt
|
|
3799
|
+
};
|
|
3800
|
+
log(`Scanner checkpoint restore failed: ${restoreMessage}`);
|
|
3801
|
+
}
|
|
2718
3802
|
snapshot = runtime.teacherLoop.snapshot();
|
|
2719
3803
|
}
|
|
2720
3804
|
}
|
|
2721
3805
|
persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
|
|
3806
|
+
lastRunAt: observedAt,
|
|
2722
3807
|
scanRoot: runtime.scanRoot,
|
|
3808
|
+
sessionTailCursorPath: runtime.sessionTailCursorPath,
|
|
3809
|
+
sessionTailCursorUpdatedAt: observedAt,
|
|
3810
|
+
sessionTailSessionsTracked: localPoll.cursor.length,
|
|
3811
|
+
sessionTailBridgedEventCount: countWatchCursorBridgedEvents(localPoll.cursor),
|
|
3812
|
+
scannerCheckpointPath: runtime.scanner.checkpointPath,
|
|
3813
|
+
scannerCheckpoint: runtime.scanner.snapshot(),
|
|
2723
3814
|
replayedBundleCount: runtime.replayState.replayedBundleCount,
|
|
2724
3815
|
replayedEventCount: runtime.replayState.replayedEventCount,
|
|
2725
3816
|
exportedBundleCount: exported.exportedBundleCount,
|
|
2726
3817
|
exportedEventCount: exported.exportedEventCount,
|
|
3818
|
+
startupWarnings: runtime.startupWarnings,
|
|
3819
|
+
lastTeacherError: runtime.lastTeacherError,
|
|
2727
3820
|
localSessionTailNoopReason: localPoll.noopReason,
|
|
2728
3821
|
lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
|
|
3822
|
+
failure,
|
|
2729
3823
|
snapshot
|
|
2730
3824
|
});
|
|
3825
|
+
const persistedScannerCheckpoint = runtime.scanner.snapshot();
|
|
2731
3826
|
if (options.json) {
|
|
2732
3827
|
console.log(JSON.stringify({
|
|
2733
3828
|
timestamp: observedAt,
|
|
@@ -2739,6 +3834,10 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
2739
3834
|
events: totalEvents,
|
|
2740
3835
|
live: scanResult.live.length,
|
|
2741
3836
|
backfill: scanResult.backfill.length,
|
|
3837
|
+
sessionTailSessionsTracked: localPoll.cursor.length,
|
|
3838
|
+
sessionTailBridgedEvents: countWatchCursorBridgedEvents(localPoll.cursor),
|
|
3839
|
+
scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
|
|
3840
|
+
scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
|
|
2742
3841
|
materialized: materializedPackId,
|
|
2743
3842
|
diagnostics: snapshot.diagnostics ?? null,
|
|
2744
3843
|
localSessionTailNoopReason: localPoll.noopReason
|
|
@@ -2842,12 +3941,13 @@ function resetActivationRoot(activationRoot) {
|
|
|
2842
3941
|
function runResetCommand(parsed) {
|
|
2843
3942
|
if (parsed.help) {
|
|
2844
3943
|
console.log([
|
|
2845
|
-
"Usage: openclawbrain reset [--activation-root <path>] [--yes] [--json]",
|
|
3944
|
+
"Usage: openclawbrain reset [--activation-root <path>|--openclaw-home <path>] [--yes] [--json]",
|
|
2846
3945
|
"",
|
|
2847
3946
|
"Wipes all learned state and returns the brain to seed state.",
|
|
2848
3947
|
"",
|
|
2849
3948
|
"Options:",
|
|
2850
3949
|
" --activation-root <path> Activation root (auto-detected if omitted)",
|
|
3950
|
+
" --openclaw-home <path> Pin auto-detection to one installed OpenClaw profile",
|
|
2851
3951
|
" --yes, -y Skip confirmation prompt",
|
|
2852
3952
|
" --json Emit machine-readable JSON output",
|
|
2853
3953
|
" --help Show this help"
|
|
@@ -2898,7 +3998,7 @@ function runResetCommand(parsed) {
|
|
|
2898
3998
|
}
|
|
2899
3999
|
console.log(" Activation pointers reset to seed state.");
|
|
2900
4000
|
console.log(`\nBrain at ${shortenPath(activationRoot)} is now in seed state.`);
|
|
2901
|
-
console.log(
|
|
4001
|
+
console.log(`Run \`openclawbrain status --activation-root ${quoteShellArg(activationRoot)}\` to verify.`);
|
|
2902
4002
|
}
|
|
2903
4003
|
return 0;
|
|
2904
4004
|
}
|
|
@@ -2973,8 +4073,8 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
2973
4073
|
});
|
|
2974
4074
|
return 0;
|
|
2975
4075
|
}
|
|
2976
|
-
if (parsed.command === "install"
|
|
2977
|
-
return
|
|
4076
|
+
if (parsed.command === "install") {
|
|
4077
|
+
return runInstallCommand(parsed);
|
|
2978
4078
|
}
|
|
2979
4079
|
if (parsed.command === "detach") {
|
|
2980
4080
|
return runDetachCommand(parsed);
|
|
@@ -2983,31 +4083,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
2983
4083
|
return runUninstallCommand(parsed);
|
|
2984
4084
|
}
|
|
2985
4085
|
if (parsed.command === "attach") {
|
|
2986
|
-
|
|
2987
|
-
mkdirSync(parsed.packRoot, { recursive: true });
|
|
2988
|
-
const result = bootstrapRuntimeAttach({
|
|
2989
|
-
profileSelector: "current_profile",
|
|
2990
|
-
...(parsed.brainAttachmentPolicy != null ? { brainAttachmentPolicy: parsed.brainAttachmentPolicy } : {}),
|
|
2991
|
-
activationRoot: parsed.activationRoot,
|
|
2992
|
-
packRoot: parsed.packRoot,
|
|
2993
|
-
packLabel: parsed.packLabel,
|
|
2994
|
-
workspace: {
|
|
2995
|
-
workspaceId: parsed.workspaceId,
|
|
2996
|
-
snapshotId: `${parsed.workspaceId}@bootstrap-${new Date().toISOString().slice(0, 10)}`,
|
|
2997
|
-
capturedAt: new Date().toISOString(),
|
|
2998
|
-
rootDir: process.cwd(),
|
|
2999
|
-
revision: "cli-bootstrap-v1"
|
|
3000
|
-
},
|
|
3001
|
-
interactionEvents: [],
|
|
3002
|
-
feedbackEvents: []
|
|
3003
|
-
});
|
|
3004
|
-
if (parsed.json) {
|
|
3005
|
-
console.log(JSON.stringify(result, null, 2));
|
|
3006
|
-
}
|
|
3007
|
-
else {
|
|
3008
|
-
console.log(formatBootstrapRuntimeAttachReport(result));
|
|
3009
|
-
}
|
|
3010
|
-
return 0;
|
|
4086
|
+
return runAttachCommand(parsed);
|
|
3011
4087
|
}
|
|
3012
4088
|
if (parsed.command === "scan") {
|
|
3013
4089
|
if (parsed.sessionPath !== null) {
|
|
@@ -3044,7 +4120,8 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
3044
4120
|
}
|
|
3045
4121
|
// At this point only status/rollback commands remain
|
|
3046
4122
|
const statusOrRollback = parsed;
|
|
3047
|
-
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.command);
|
|
4123
|
+
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
|
|
4124
|
+
const targetInspection = statusOrRollback.openclawHome === null ? null : inspectOpenClawHome(statusOrRollback.openclawHome);
|
|
3048
4125
|
if (statusOrRollback.command === "rollback") {
|
|
3049
4126
|
const result = rollbackRuntimeAttach({
|
|
3050
4127
|
activationRoot,
|
|
@@ -3070,11 +4147,18 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
3070
4147
|
}
|
|
3071
4148
|
else {
|
|
3072
4149
|
const report = buildOperatorSurfaceReport(operatorInput);
|
|
4150
|
+
const providerConfig = readOpenClawBrainProviderConfig(process.env);
|
|
3073
4151
|
if (statusOrRollback.detailed) {
|
|
3074
|
-
console.log(formatCurrentProfileStatusSummary(status, report
|
|
4152
|
+
console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
|
|
4153
|
+
openclawHome: statusOrRollback.openclawHome,
|
|
4154
|
+
providerConfig
|
|
4155
|
+
}));
|
|
3075
4156
|
}
|
|
3076
4157
|
else {
|
|
3077
|
-
console.log(formatHumanFriendlyStatus(status, report
|
|
4158
|
+
console.log(formatHumanFriendlyStatus(status, report, targetInspection, {
|
|
4159
|
+
openclawHome: statusOrRollback.openclawHome,
|
|
4160
|
+
providerConfig
|
|
4161
|
+
}));
|
|
3078
4162
|
}
|
|
3079
4163
|
}
|
|
3080
4164
|
return 0;
|