@openclawbrain/openclaw 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +10 -0
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +73 -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 +66 -4
- package/dist/src/cli.js +1845 -241
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +7 -4
- package/dist/src/daemon.js +311 -28
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/index.d.ts +213 -4
- package/dist/src/index.js +1151 -157
- 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/local-session-passive-learning.d.ts +1 -0
- package/dist/src/local-session-passive-learning.js +97 -7
- package/dist/src/local-session-passive-learning.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/provider-config.d.ts +28 -0
- package/dist/src/provider-config.js +150 -0
- package/dist/src/provider-config.js.map +1 -0
- package/dist/src/resolve-activation-root.d.ts +3 -3
- package/dist/src/resolve-activation-root.js +105 -35
- package/dist/src/resolve-activation-root.js.map +1 -1
- package/dist/src/session-store.d.ts +18 -0
- package/dist/src/session-store.js +40 -0
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/session-tail.d.ts +6 -3
- package/dist/src/session-tail.js +35 -4
- 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 +74 -35
- package/extension/runtime-guard.ts +353 -0
- package/package.json +13 -13
package/dist/src/cli.js
CHANGED
|
@@ -1,21 +1,170 @@
|
|
|
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, appendFileSync } from "node:fs";
|
|
2
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync, 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
8
|
import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
|
|
9
9
|
import { exportBrain, importBrain } from "./import-export.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { buildNormalizedEventExport } from "@openclawbrain/contracts";
|
|
11
|
+
import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
|
|
12
|
+
import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
|
|
12
13
|
import { resolveActivationRoot } from "./resolve-activation-root.js";
|
|
13
|
-
import { bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadRuntimeEventExportBundle, rollbackRuntimeAttach, scanLiveEventExport, scanRecordedSession } from "./index.js";
|
|
14
|
+
import { buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
|
|
15
|
+
import { appendLearningUpdateLogs } from "./learning-spine.js";
|
|
14
16
|
import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
|
|
15
|
-
import {
|
|
17
|
+
import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
18
|
+
import { readOpenClawBrainProviderConfig } from "./provider-config.js";
|
|
16
19
|
function quoteShellArg(value) {
|
|
17
20
|
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
18
21
|
}
|
|
22
|
+
function normalizeOptionalCliString(value) {
|
|
23
|
+
if (typeof value !== "string") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
28
|
+
}
|
|
29
|
+
function getCliHomeDir() {
|
|
30
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
31
|
+
}
|
|
32
|
+
function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
|
|
33
|
+
const resolvedHomeDir = path.resolve(homeDir);
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = readdirSync(resolvedHomeDir, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return entries
|
|
42
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(".openclaw-"))
|
|
43
|
+
.map((entry) => path.join(resolvedHomeDir, entry.name))
|
|
44
|
+
.filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
|
|
45
|
+
.sort((left, right) => left.localeCompare(right));
|
|
46
|
+
}
|
|
47
|
+
function formatInstallOpenClawHomeSource(source) {
|
|
48
|
+
switch (source) {
|
|
49
|
+
case "explicit":
|
|
50
|
+
return "--openclaw-home";
|
|
51
|
+
case "env":
|
|
52
|
+
return "OPENCLAW_HOME";
|
|
53
|
+
case "discovered_single_profile":
|
|
54
|
+
return "single discovered live profile";
|
|
55
|
+
default:
|
|
56
|
+
return source;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function resolveInstallOpenClawHome(explicitOpenclawHome) {
|
|
60
|
+
const normalizedExplicitHome = normalizeOptionalCliString(explicitOpenclawHome);
|
|
61
|
+
if (normalizedExplicitHome !== null) {
|
|
62
|
+
return {
|
|
63
|
+
openclawHome: path.resolve(normalizedExplicitHome),
|
|
64
|
+
openclawHomeSource: "explicit"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const envOpenClawHome = normalizeOptionalCliString(process.env.OPENCLAW_HOME);
|
|
68
|
+
if (envOpenClawHome !== null) {
|
|
69
|
+
return {
|
|
70
|
+
openclawHome: path.resolve(envOpenClawHome),
|
|
71
|
+
openclawHomeSource: "env"
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const discoveredHomes = discoverInstallCandidateOpenClawHomes();
|
|
75
|
+
if (discoveredHomes.length === 1) {
|
|
76
|
+
return {
|
|
77
|
+
openclawHome: path.resolve(discoveredHomes[0]),
|
|
78
|
+
openclawHomeSource: "discovered_single_profile"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (discoveredHomes.length > 1) {
|
|
82
|
+
const installPrefix = detectConsumerSafeOperatorCliPrefix();
|
|
83
|
+
const targetChoices = discoveredHomes
|
|
84
|
+
.map((candidate) => {
|
|
85
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
86
|
+
return ` - ${resolvedCandidate}\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
|
|
87
|
+
})
|
|
88
|
+
.join("\n");
|
|
89
|
+
throw new Error([
|
|
90
|
+
"Refusing ambiguous live OpenClaw targets for install.",
|
|
91
|
+
targetChoices,
|
|
92
|
+
"Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one profile."
|
|
93
|
+
].join("\n"));
|
|
94
|
+
}
|
|
95
|
+
throw new Error("No OpenClaw profile home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
|
|
96
|
+
}
|
|
97
|
+
function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
|
|
98
|
+
const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
|
|
99
|
+
if (normalizedExplicitActivationRoot !== null) {
|
|
100
|
+
return {
|
|
101
|
+
activationRoot: path.resolve(normalizedExplicitActivationRoot),
|
|
102
|
+
source: "explicit"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
activationRoot: path.resolve(path.dirname(openclawHome), ".openclawbrain", "activation"),
|
|
107
|
+
source: "default_from_openclaw_home"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
111
|
+
const normalizedExplicitWorkspaceId = normalizeOptionalCliString(explicitWorkspaceId);
|
|
112
|
+
if (normalizedExplicitWorkspaceId !== null) {
|
|
113
|
+
return {
|
|
114
|
+
workspaceId: normalizedExplicitWorkspaceId,
|
|
115
|
+
source: "explicit"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const openclawConfigPath = path.join(openclawHome, "openclaw.json");
|
|
120
|
+
const openclawConfig = JSON.parse(readFileSync(openclawConfigPath, "utf8"));
|
|
121
|
+
if (typeof openclawConfig.profile === "string" && openclawConfig.profile.trim().length > 0) {
|
|
122
|
+
return {
|
|
123
|
+
workspaceId: openclawConfig.profile.trim(),
|
|
124
|
+
source: "openclaw_json_profile"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Fall back to the profile-home name when install is pointed at an incomplete or not-yet-readable profile.
|
|
130
|
+
}
|
|
131
|
+
const dirName = path.basename(openclawHome);
|
|
132
|
+
if (dirName === ".openclaw") {
|
|
133
|
+
return {
|
|
134
|
+
workspaceId: "default",
|
|
135
|
+
source: "openclaw_home_dir"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const derivedWorkspaceId = dirName.startsWith(".openclaw-") ? dirName.slice(".openclaw-".length) : dirName;
|
|
139
|
+
if (derivedWorkspaceId.trim().length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
workspaceId: derivedWorkspaceId,
|
|
142
|
+
source: "openclaw_home_dir"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
workspaceId: "workspace",
|
|
147
|
+
source: "fallback"
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function formatInstallActivationRootSource(source) {
|
|
151
|
+
if (source === "explicit") {
|
|
152
|
+
return "explicit --activation-root";
|
|
153
|
+
}
|
|
154
|
+
return "default beside --openclaw-home";
|
|
155
|
+
}
|
|
156
|
+
function formatInstallWorkspaceIdSource(source) {
|
|
157
|
+
switch (source) {
|
|
158
|
+
case "explicit":
|
|
159
|
+
return "explicit --workspace-id";
|
|
160
|
+
case "openclaw_json_profile":
|
|
161
|
+
return "from openclaw.json profile";
|
|
162
|
+
case "openclaw_home_dir":
|
|
163
|
+
return "from OpenClaw home dir";
|
|
164
|
+
default:
|
|
165
|
+
return "fallback default";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
19
168
|
function detectConsumerSafeOperatorCliPrefix() {
|
|
20
169
|
const npmExecPath = (process.env.npm_execpath ?? "").toLowerCase();
|
|
21
170
|
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
@@ -115,7 +264,7 @@ function buildDoctorDeletedMessage(args) {
|
|
|
115
264
|
const jsonCommand = buildStatusReplacementCommand(replacementInput, true);
|
|
116
265
|
const lines = [
|
|
117
266
|
"`doctor` is no longer a separate operator surface.",
|
|
118
|
-
'Use `openclawbrain status
|
|
267
|
+
'Use `openclawbrain status --activation-root <path>` as the human answer to "How\'s the brain?" and `status --json` for the canonical current-profile object.',
|
|
119
268
|
"Use `describeAttachStatus()` or the proof helpers only when you need deeper activation diagnostics."
|
|
120
269
|
];
|
|
121
270
|
if (json && jsonCommand !== null) {
|
|
@@ -129,30 +278,42 @@ function buildDoctorDeletedMessage(args) {
|
|
|
129
278
|
}
|
|
130
279
|
return lines.join(" ");
|
|
131
280
|
}
|
|
281
|
+
function buildSetupDeletedMessage() {
|
|
282
|
+
return [
|
|
283
|
+
"`setup` has been removed.",
|
|
284
|
+
"Use `openclawbrain install` instead.",
|
|
285
|
+
"The install command still accepts the explicit targeting flags that setup used: `--openclaw-home`, `--activation-root`, `--workspace-id`, and `--shared`."
|
|
286
|
+
].join(" ");
|
|
287
|
+
}
|
|
132
288
|
function operatorCliHelp() {
|
|
133
289
|
return [
|
|
134
290
|
"Usage:",
|
|
135
|
-
" openclawbrain
|
|
291
|
+
" openclawbrain install [--openclaw-home <path>] [options]",
|
|
292
|
+
" openclawbrain detach --openclaw-home <path> [options]",
|
|
293
|
+
" openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
|
|
136
294
|
" openclawbrain attach --activation-root <path> [options]",
|
|
137
|
-
" openclawbrain <status|rollback> --activation-root <path> [options]",
|
|
138
|
-
" openclawbrain context \"message\" [--activation-root <path>]",
|
|
139
|
-
" openclawbrain history [--activation-root <path>] [--limit N] [--json]",
|
|
295
|
+
" openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
|
|
296
|
+
" openclawbrain context \"message\" [--activation-root <path>|--openclaw-home <path>]",
|
|
297
|
+
" openclawbrain history [--activation-root <path>|--openclaw-home <path>] [--limit N] [--json]",
|
|
140
298
|
" openclawbrain scan --session <trace.json> --root <path> [options]",
|
|
141
299
|
" openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
|
|
142
|
-
" openclawbrain learn [--activation-root <path>] [--json]",
|
|
143
|
-
" openclawbrain watch
|
|
144
|
-
" openclawbrain daemon <start|stop|status|logs>
|
|
145
|
-
" openclawbrain-ops <status|rollback> --activation-root <path> [options] # compatibility alias",
|
|
300
|
+
" openclawbrain learn [--activation-root <path>|--openclaw-home <path>] [--json]",
|
|
301
|
+
" openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
|
|
302
|
+
" openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
|
|
303
|
+
" openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
|
|
146
304
|
" openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
|
|
147
305
|
"",
|
|
148
306
|
"Options:",
|
|
149
|
-
" --openclaw-home <path> OpenClaw profile home dir for
|
|
150
|
-
" --shared Set brain-attachment-policy to shared instead of dedicated (
|
|
151
|
-
" --activation-root <path>
|
|
307
|
+
" --openclaw-home <path> OpenClaw profile home dir for install/detach/uninstall (e.g. ~/.openclaw-Tern). Also pins status/rollback/context/history/learn to that installed profile when applicable.",
|
|
308
|
+
" --shared Set brain-attachment-policy to shared instead of dedicated (install only).",
|
|
309
|
+
" --activation-root <path> Explicit activation root for attach/watch/daemon and other stateful commands; install defaults to sibling .openclawbrain/activation next to the selected OpenClaw home.",
|
|
310
|
+
" --keep-data Preserve activation data on uninstall; detach always behaves this way.",
|
|
311
|
+
" --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
|
|
312
|
+
" --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
|
|
152
313
|
" --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
|
|
153
|
-
" --workspace-id <id> Workspace identifier for attach provenance
|
|
314
|
+
" --workspace-id <id> Workspace identifier for attach/install provenance; install defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
|
|
154
315
|
" --event-export <path> Event-export bundle root or normalized export JSON payload.",
|
|
155
|
-
" --teacher-snapshot <path>
|
|
316
|
+
" --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.",
|
|
156
317
|
" --updated-at <iso> Observation time to use for freshness checks.",
|
|
157
318
|
" --brain-attachment-policy <undeclared|dedicated|shared> Override attachment policy semantics for status inspection.",
|
|
158
319
|
" --detailed Show verbose diagnostic output for status (default is human-friendly summary).",
|
|
@@ -171,19 +332,26 @@ function operatorCliHelp() {
|
|
|
171
332
|
" --help Show this help.",
|
|
172
333
|
"",
|
|
173
334
|
"Common flow:",
|
|
335
|
+
" 0. install openclawbrain install — attach the brain with sane defaults; pass --openclaw-home for explicit targeting on many-profile hosts",
|
|
336
|
+
" 0. detach openclawbrain detach --openclaw-home <path> — remove the profile hook only; activation data stays in place",
|
|
337
|
+
" 0. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data — remove the profile hook and choose the data outcome explicitly",
|
|
174
338
|
" 0. context openclawbrain context \"hello\" — preview the brain context that would be injected for a message",
|
|
175
339
|
" 0. attach openclawbrain attach --activation-root <path>",
|
|
176
|
-
" 1. status answer \"How's the brain?\" for
|
|
177
|
-
" 2. status --json read the canonical current_profile_brain_status.v1 object
|
|
178
|
-
" 3. rollback --dry-run preview active <- previous, active -> candidate",
|
|
179
|
-
" 4. rollback apply the rollback when the preview says ready",
|
|
340
|
+
" 1. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
|
|
341
|
+
" 2. status --json openclawbrain status --activation-root <path> --json — read the canonical current_profile_brain_status.v1 object",
|
|
342
|
+
" 3. rollback --dry-run openclawbrain rollback --activation-root <path> --dry-run — preview active <- previous, active -> candidate",
|
|
343
|
+
" 4. rollback openclawbrain rollback --activation-root <path> — apply the rollback when the preview says ready",
|
|
180
344
|
" 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
|
|
181
345
|
" 6. scan --live scan one live event export into teacher/learner state without claiming a daemon is running",
|
|
182
346
|
" status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
|
|
347
|
+
" watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
|
|
348
|
+
" watch Ollama teacher labels: set OPENCLAWBRAIN_TEACHER_PROVIDER=ollama plus optional OPENCLAWBRAIN_TEACHER_BASE_URL, OPENCLAWBRAIN_TEACHER_MODEL, and OPENCLAWBRAIN_TEACHER_* budget vars",
|
|
183
349
|
"",
|
|
184
350
|
"Exit codes:",
|
|
185
351
|
" status: 0 on successful inspection, 1 on input/read failure.",
|
|
186
352
|
" rollback: 0 when ready/applied, 1 when blocked or on input/read failure.",
|
|
353
|
+
" detach: 0 on successful unhook, 1 on input/read failure.",
|
|
354
|
+
" uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
|
|
187
355
|
" scan: 0 on successful replay/scan, 1 on input/read failure."
|
|
188
356
|
].join("\n");
|
|
189
357
|
}
|
|
@@ -225,6 +393,12 @@ function formatLearningBuckets(report) {
|
|
|
225
393
|
function formatLearningWarnings(report) {
|
|
226
394
|
return report.learning.warningStates.length === 0 ? "none" : report.learning.warningStates.join("|");
|
|
227
395
|
}
|
|
396
|
+
function formatLabelFlowSummary(labelFlow) {
|
|
397
|
+
return `source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`;
|
|
398
|
+
}
|
|
399
|
+
function formatLearningPathSummary(learningPath) {
|
|
400
|
+
return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
|
|
401
|
+
}
|
|
228
402
|
function formatCurrentProfileStatusSummary(status, report) {
|
|
229
403
|
const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
|
|
230
404
|
return [
|
|
@@ -233,6 +407,7 @@ function formatCurrentProfileStatusSummary(status, report) {
|
|
|
233
407
|
`host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
|
|
234
408
|
`profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
|
|
235
409
|
`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)}`,
|
|
410
|
+
`activation state=${status.brainStatus.activationState} detail=${status.brain.detail}`,
|
|
236
411
|
`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"}`,
|
|
237
412
|
`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}`,
|
|
238
413
|
`route router=${report.servePath.routerIdentity ?? status.brain.routerIdentity ?? "none"} supervision=${report.servePath.refreshStatus ?? status.brain.routeFreshness} freshness=${report.servePath.freshnessChecksum ?? "none"}`,
|
|
@@ -240,8 +415,12 @@ function formatCurrentProfileStatusSummary(status, report) {
|
|
|
240
415
|
`decision ${status.brainStatus.structuralDecision.detail}`,
|
|
241
416
|
`principal latest=${formatPrincipalLatest(report)} pending=${report.principal.pendingCount ?? report.learning.pendingPrincipalCount ?? "none"} checkpoint=${formatPrincipalCheckpointFrontier(report)} downstream=${yesNo(report.principal.servingDownstreamOfLatestCorrection)} lag=${report.learning.principalLagToPromotion.sequenceLag ?? "none"}`,
|
|
242
417
|
`scanner flowing=${yesNo(report.supervision.flowing)} scan=${report.supervision.scanPolicy ?? "none"} surfaces=${formatScannerSurfaces(report)} labels=${report.supervision.humanLabelCount ?? "none"}/${report.supervision.selfLabelCount ?? "none"} attributable=${report.supervision.attributedEventCount ?? "none"}/${report.supervision.totalEventCount ?? "none"} digests=${report.supervision.selectionDigestCount ?? "none"}`,
|
|
418
|
+
`labels ${formatLabelFlowSummary(report.labelFlow)}`,
|
|
243
419
|
`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}`,
|
|
420
|
+
`path ${formatLearningPathSummary(report.learningPath)}`,
|
|
244
421
|
`learning state=${report.learning.backlogState} bootstrapped=${yesNo(report.learning.bootstrapped)} mode=${report.learning.mode} next=${report.learning.nextPriorityLane} priority=${report.learning.nextPriorityBucket} pending=${report.learning.pendingLive ?? "none"}/${report.learning.pendingBackfill ?? "none"} buckets=${formatLearningBuckets(report)} warn=${formatLearningWarnings(report)} lastPack=${report.learning.lastMaterializedPackId ?? "none"} detail=${report.learning.detail}`,
|
|
422
|
+
`teacher snapshot=${report.teacherLoop.sourcePath ?? "none"} kind=${report.teacherLoop.sourceKind} lastRun=${report.teacherLoop.lastRunAt ?? "none"} artifacts=${report.teacherLoop.artifactCount ?? "none"} freshness=${report.teacherLoop.latestFreshness} queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"} running=${yesNo(report.teacherLoop.running)} noOp=${report.teacherLoop.lastNoOpReason} failure=${report.teacherLoop.failureMode}${report.teacherLoop.failureDetail === null ? "" : `(${report.teacherLoop.failureDetail})`}`,
|
|
423
|
+
`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"}`,
|
|
245
424
|
`rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
246
425
|
`proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
|
|
247
426
|
`logs root=${status.brain.logRoot ?? "none"}`,
|
|
@@ -258,6 +437,47 @@ function shortenPath(fullPath) {
|
|
|
258
437
|
}
|
|
259
438
|
return fullPath;
|
|
260
439
|
}
|
|
440
|
+
function buildInstallStatusCommand(activationRoot) {
|
|
441
|
+
return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
|
|
442
|
+
}
|
|
443
|
+
function buildInstallCommand(openclawHome) {
|
|
444
|
+
return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
|
|
445
|
+
}
|
|
446
|
+
function buildInstallReloadGuidance() {
|
|
447
|
+
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.";
|
|
448
|
+
}
|
|
449
|
+
function buildCleanupRestartGuidance(restart) {
|
|
450
|
+
if (restart === "never") {
|
|
451
|
+
return "No restart requested. If this OpenClaw profile is currently running, it may keep the previous hook state until the next restart.";
|
|
452
|
+
}
|
|
453
|
+
if (restart === "external") {
|
|
454
|
+
return "Restart this OpenClaw profile externally if it is currently running. If it is stopped, the next launch will pick up the new hook state.";
|
|
455
|
+
}
|
|
456
|
+
return "If this OpenClaw profile is currently running, restart it before expecting the new hook state to take effect. If it is stopped, the next launch will pick it up.";
|
|
457
|
+
}
|
|
458
|
+
function buildStatusNextStep(status, report) {
|
|
459
|
+
const activationRootArg = quoteShellArg(status.host.activationRoot);
|
|
460
|
+
if (status.brainStatus.activationState === "broken_install") {
|
|
461
|
+
return "Repair or replace the activation root before trusting serve-path status again.";
|
|
462
|
+
}
|
|
463
|
+
if (status.brainStatus.activationState === "stale_incomplete") {
|
|
464
|
+
return "Clean up or repair the retained activation state before reattaching or promoting packs.";
|
|
465
|
+
}
|
|
466
|
+
if (status.brainStatus.status === "fail") {
|
|
467
|
+
return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
|
|
468
|
+
}
|
|
469
|
+
if (status.brainStatus.awaitingFirstExport) {
|
|
470
|
+
return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
|
|
471
|
+
}
|
|
472
|
+
if (report.learning.warningStates.includes("principal_live_backlog") ||
|
|
473
|
+
report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
|
|
474
|
+
return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
|
|
475
|
+
}
|
|
476
|
+
if (report.rollback.allowed) {
|
|
477
|
+
return `Use \`openclawbrain rollback --activation-root ${activationRootArg} --dry-run\` before restoring the previous pack.`;
|
|
478
|
+
}
|
|
479
|
+
return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
|
|
480
|
+
}
|
|
261
481
|
function formatHumanFriendlyStatus(status, report) {
|
|
262
482
|
// Brain status line
|
|
263
483
|
const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
|
|
@@ -270,11 +490,22 @@ function formatHumanFriendlyStatus(status, report) {
|
|
|
270
490
|
const activationPath = shortenPath(status.host.activationRoot);
|
|
271
491
|
// Policy
|
|
272
492
|
const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
|
|
493
|
+
const principalFrontier = formatPrincipalCheckpointFrontier(report);
|
|
494
|
+
const pendingLive = String(report.learning.pendingLive ?? "none");
|
|
495
|
+
const pendingBackfill = String(report.learning.pendingBackfill ?? "none");
|
|
496
|
+
const nextLane = report.learning.nextPriorityLane ?? "none";
|
|
497
|
+
const nextBucket = report.learning.nextPriorityBucket ?? "none";
|
|
273
498
|
const lines = [
|
|
274
499
|
`Brain: ${brainIcon}`,
|
|
275
500
|
`Pack: ${packShort} (${state})`,
|
|
276
501
|
`Activation: ${activationPath}`,
|
|
277
|
-
`Policy: ${policy}
|
|
502
|
+
`Policy: ${policy}`,
|
|
503
|
+
`Lifecycle: activation=${status.brainStatus.activationState} attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
|
|
504
|
+
`Rollback: state=${report.rollback.state} ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
505
|
+
`Backlog: principal=${principalFrontier} live=${pendingLive} backfill=${pendingBackfill} next=${nextLane}/${nextBucket}`,
|
|
506
|
+
`Labels: ${formatLabelFlowSummary(report.labelFlow)}`,
|
|
507
|
+
`Teacher: lastRun=${report.teacherLoop.lastRunAt ?? "none"} artifacts=${report.teacherLoop.artifactCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} cadence=${report.teacherLoop.learningCadence}/${report.teacherLoop.scanPolicy} failure=${report.teacherLoop.failureMode}`,
|
|
508
|
+
`Learning: ${formatLearningPathSummary(report.learningPath)}`
|
|
278
509
|
];
|
|
279
510
|
// Add learning/serve warnings if relevant
|
|
280
511
|
if (report.learning.warningStates.length > 0) {
|
|
@@ -283,13 +514,25 @@ function formatHumanFriendlyStatus(status, report) {
|
|
|
283
514
|
if (status.brainStatus.awaitingFirstExport) {
|
|
284
515
|
lines.push(`Note: Awaiting first event export`);
|
|
285
516
|
}
|
|
517
|
+
lines.push(`Next: ${buildStatusNextStep(status, report)}`);
|
|
286
518
|
return lines.join("\n");
|
|
287
519
|
}
|
|
288
|
-
function requireActivationRoot(input,
|
|
289
|
-
|
|
290
|
-
|
|
520
|
+
function requireActivationRoot(input, openclawHome, command) {
|
|
521
|
+
const explicitActivationRoot = input.activationRoot.trim().length > 0 ? input.activationRoot : null;
|
|
522
|
+
if (explicitActivationRoot !== null) {
|
|
523
|
+
return path.resolve(explicitActivationRoot);
|
|
524
|
+
}
|
|
525
|
+
if (openclawHome !== null) {
|
|
526
|
+
return resolveActivationRoot({
|
|
527
|
+
openclawHome
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
throw new Error(`${command} requires --activation-root <path> or --openclaw-home <path>; unpinned host auto-resolution is no longer supported for ${command}.`);
|
|
531
|
+
}
|
|
532
|
+
function resolveCliActivationRoot(explicitActivationRoot, openclawHome) {
|
|
291
533
|
return resolveActivationRoot({
|
|
292
|
-
explicit:
|
|
534
|
+
explicit: explicitActivationRoot,
|
|
535
|
+
openclawHome
|
|
293
536
|
});
|
|
294
537
|
}
|
|
295
538
|
function readJsonFile(filePath) {
|
|
@@ -321,6 +564,8 @@ function formatScanLiveSummary(result, snapshotOutPath) {
|
|
|
321
564
|
"SCAN live ok",
|
|
322
565
|
`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}`,
|
|
323
566
|
`teacher artifacts=${result.snapshot.teacher.artifactCount} freshness=${result.snapshot.teacher.latestFreshness} humanLabels=${result.supervision.humanLabelCount} noop=${result.snapshot.diagnostics.lastNoOpReason}`,
|
|
567
|
+
`labels source=${result.labelFlow.source} human=${result.labelFlow.humanLabelCount ?? "none"} self=${result.labelFlow.selfLabelCount ?? "none"} implicitPositive=${result.labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${result.labelFlow.asyncTeacherArtifactCount ?? "none"}`,
|
|
568
|
+
`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"}`,
|
|
324
569
|
`learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
|
|
325
570
|
`observed ${result.observedAt}`,
|
|
326
571
|
`snapshot ${snapshotOutPath ?? "none"}`
|
|
@@ -344,6 +589,10 @@ export function parseOperatorCliArgs(argv) {
|
|
|
344
589
|
let snapshotOutPath = null;
|
|
345
590
|
let openclawHome = null;
|
|
346
591
|
let shared = false;
|
|
592
|
+
let keepData = false;
|
|
593
|
+
let purgeData = false;
|
|
594
|
+
let restart = "safe";
|
|
595
|
+
let restartExplicitlySet = false;
|
|
347
596
|
let json = false;
|
|
348
597
|
let help = false;
|
|
349
598
|
let dryRun = false;
|
|
@@ -352,11 +601,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
352
601
|
if (args[0] === "doctor") {
|
|
353
602
|
throw new Error(buildDoctorDeletedMessage(args.slice(1)));
|
|
354
603
|
}
|
|
604
|
+
if (args[0] === "setup") {
|
|
605
|
+
throw new Error(buildSetupDeletedMessage());
|
|
606
|
+
}
|
|
355
607
|
if (args[0] === "daemon") {
|
|
356
608
|
args.shift();
|
|
357
609
|
return parseDaemonArgs(args);
|
|
358
610
|
}
|
|
359
|
-
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "
|
|
611
|
+
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") {
|
|
360
612
|
command = args.shift();
|
|
361
613
|
}
|
|
362
614
|
if (command === "learn") {
|
|
@@ -379,6 +631,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
379
631
|
index += 1;
|
|
380
632
|
continue;
|
|
381
633
|
}
|
|
634
|
+
if (arg === "--openclaw-home") {
|
|
635
|
+
const next = args[index + 1];
|
|
636
|
+
if (next === undefined) {
|
|
637
|
+
throw new Error("--openclaw-home requires a value");
|
|
638
|
+
}
|
|
639
|
+
openclawHome = next;
|
|
640
|
+
index += 1;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
382
643
|
if (arg.startsWith("--")) {
|
|
383
644
|
throw new Error(`unknown argument for learn: ${arg}`);
|
|
384
645
|
}
|
|
@@ -388,7 +649,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
388
649
|
}
|
|
389
650
|
return {
|
|
390
651
|
command,
|
|
391
|
-
activationRoot:
|
|
652
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
392
653
|
json,
|
|
393
654
|
help
|
|
394
655
|
};
|
|
@@ -444,9 +705,12 @@ export function parseOperatorCliArgs(argv) {
|
|
|
444
705
|
if (help) {
|
|
445
706
|
return { command, activationRoot: "", scanRoot: null, interval: 30, json, help };
|
|
446
707
|
}
|
|
708
|
+
if (activationRoot === null || activationRoot.trim().length === 0) {
|
|
709
|
+
throw new Error("watch requires --activation-root <path>");
|
|
710
|
+
}
|
|
447
711
|
return {
|
|
448
712
|
command,
|
|
449
|
-
activationRoot:
|
|
713
|
+
activationRoot: path.resolve(activationRoot),
|
|
450
714
|
scanRoot: watchScanRoot,
|
|
451
715
|
interval: watchInterval,
|
|
452
716
|
json,
|
|
@@ -474,6 +738,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
474
738
|
index += 1;
|
|
475
739
|
continue;
|
|
476
740
|
}
|
|
741
|
+
if (arg === "--openclaw-home") {
|
|
742
|
+
const next = args[index + 1];
|
|
743
|
+
if (next === undefined) {
|
|
744
|
+
throw new Error("--openclaw-home requires a value");
|
|
745
|
+
}
|
|
746
|
+
openclawHome = next;
|
|
747
|
+
index += 1;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
477
750
|
if (arg.startsWith("--")) {
|
|
478
751
|
throw new Error(`unknown argument for context: ${arg}`);
|
|
479
752
|
}
|
|
@@ -488,7 +761,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
488
761
|
return {
|
|
489
762
|
command,
|
|
490
763
|
message: messageParts.join(" "),
|
|
491
|
-
activationRoot:
|
|
764
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
492
765
|
json,
|
|
493
766
|
help
|
|
494
767
|
};
|
|
@@ -514,6 +787,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
514
787
|
index += 1;
|
|
515
788
|
continue;
|
|
516
789
|
}
|
|
790
|
+
if (arg === "--openclaw-home") {
|
|
791
|
+
const next = args[index + 1];
|
|
792
|
+
if (next === undefined) {
|
|
793
|
+
throw new Error("--openclaw-home requires a value");
|
|
794
|
+
}
|
|
795
|
+
openclawHome = next;
|
|
796
|
+
index += 1;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
517
799
|
if (arg === "--limit") {
|
|
518
800
|
const next = args[index + 1];
|
|
519
801
|
if (next === undefined) {
|
|
@@ -536,7 +818,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
536
818
|
}
|
|
537
819
|
return {
|
|
538
820
|
command,
|
|
539
|
-
activationRoot:
|
|
821
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
540
822
|
limit: historyLimit,
|
|
541
823
|
json,
|
|
542
824
|
help
|
|
@@ -562,6 +844,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
562
844
|
index += 1;
|
|
563
845
|
continue;
|
|
564
846
|
}
|
|
847
|
+
if (arg === "--openclaw-home") {
|
|
848
|
+
const next = args[index + 1];
|
|
849
|
+
if (next === undefined)
|
|
850
|
+
throw new Error("--openclaw-home requires a value");
|
|
851
|
+
openclawHome = next;
|
|
852
|
+
index += 1;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
565
855
|
if (arg === "-o" || arg === "--output") {
|
|
566
856
|
const next = args[index + 1];
|
|
567
857
|
if (next === undefined)
|
|
@@ -579,7 +869,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
579
869
|
throw new Error("export requires -o <output.tar.gz>");
|
|
580
870
|
return {
|
|
581
871
|
command,
|
|
582
|
-
activationRoot:
|
|
872
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
583
873
|
outputPath: path.resolve(outputPath),
|
|
584
874
|
json,
|
|
585
875
|
help,
|
|
@@ -610,6 +900,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
610
900
|
index += 1;
|
|
611
901
|
continue;
|
|
612
902
|
}
|
|
903
|
+
if (arg === "--openclaw-home") {
|
|
904
|
+
const next = args[index + 1];
|
|
905
|
+
if (next === undefined)
|
|
906
|
+
throw new Error("--openclaw-home requires a value");
|
|
907
|
+
openclawHome = next;
|
|
908
|
+
index += 1;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
613
911
|
if (arg.startsWith("--"))
|
|
614
912
|
throw new Error(`unknown argument for import: ${arg}`);
|
|
615
913
|
if (archivePath === null) {
|
|
@@ -626,7 +924,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
626
924
|
return {
|
|
627
925
|
command,
|
|
628
926
|
archivePath: path.resolve(archivePath),
|
|
629
|
-
activationRoot:
|
|
927
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
630
928
|
force,
|
|
631
929
|
json,
|
|
632
930
|
help,
|
|
@@ -656,13 +954,21 @@ export function parseOperatorCliArgs(argv) {
|
|
|
656
954
|
index += 1;
|
|
657
955
|
continue;
|
|
658
956
|
}
|
|
957
|
+
if (arg === "--openclaw-home") {
|
|
958
|
+
const next = args[index + 1];
|
|
959
|
+
if (next === undefined)
|
|
960
|
+
throw new Error("--openclaw-home requires a value");
|
|
961
|
+
openclawHome = next;
|
|
962
|
+
index += 1;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
659
965
|
throw new Error(`unknown argument for reset: ${arg}`);
|
|
660
966
|
}
|
|
661
967
|
if (help)
|
|
662
968
|
return { command, activationRoot: "", yes: false, json, help };
|
|
663
969
|
return {
|
|
664
970
|
command,
|
|
665
|
-
activationRoot:
|
|
971
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
666
972
|
yes,
|
|
667
973
|
json,
|
|
668
974
|
help
|
|
@@ -686,6 +992,27 @@ export function parseOperatorCliArgs(argv) {
|
|
|
686
992
|
shared = true;
|
|
687
993
|
continue;
|
|
688
994
|
}
|
|
995
|
+
if (arg === "--keep-data") {
|
|
996
|
+
keepData = true;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (arg === "--purge-data") {
|
|
1000
|
+
purgeData = true;
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (arg === "--restart") {
|
|
1004
|
+
const next = args[index + 1];
|
|
1005
|
+
if (next === undefined) {
|
|
1006
|
+
throw new Error("--restart requires a value");
|
|
1007
|
+
}
|
|
1008
|
+
if (next !== "never" && next !== "safe" && next !== "external") {
|
|
1009
|
+
throw new Error(`invalid --restart value: ${next}`);
|
|
1010
|
+
}
|
|
1011
|
+
restart = next;
|
|
1012
|
+
restartExplicitlySet = true;
|
|
1013
|
+
index += 1;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
689
1016
|
if (arg === "--detailed") {
|
|
690
1017
|
detailed = true;
|
|
691
1018
|
continue;
|
|
@@ -816,25 +1143,99 @@ export function parseOperatorCliArgs(argv) {
|
|
|
816
1143
|
}
|
|
817
1144
|
throw new Error(`unknown argument: ${arg}`);
|
|
818
1145
|
}
|
|
819
|
-
if (command
|
|
1146
|
+
if (command !== "detach" && command !== "uninstall" && restartExplicitlySet) {
|
|
1147
|
+
throw new Error("--restart only applies to detach/uninstall");
|
|
1148
|
+
}
|
|
1149
|
+
if (command !== "uninstall" && keepData) {
|
|
1150
|
+
throw new Error("--keep-data only applies to uninstall; use detach to preserve activation data");
|
|
1151
|
+
}
|
|
1152
|
+
if (command !== "uninstall" && purgeData) {
|
|
1153
|
+
throw new Error("--purge-data only applies to uninstall");
|
|
1154
|
+
}
|
|
1155
|
+
if (command === "install") {
|
|
1156
|
+
if (help) {
|
|
1157
|
+
return {
|
|
1158
|
+
command,
|
|
1159
|
+
openclawHome: "",
|
|
1160
|
+
openclawHomeSource: "explicit",
|
|
1161
|
+
activationRoot: "",
|
|
1162
|
+
activationRootSource: "explicit",
|
|
1163
|
+
shared: false,
|
|
1164
|
+
workspaceId: "",
|
|
1165
|
+
workspaceIdSource: "explicit",
|
|
1166
|
+
json,
|
|
1167
|
+
help
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
|
|
1171
|
+
const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
|
|
1172
|
+
const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
|
|
1173
|
+
return {
|
|
1174
|
+
command,
|
|
1175
|
+
openclawHome: resolvedOpenclawHome.openclawHome,
|
|
1176
|
+
openclawHomeSource: resolvedOpenclawHome.openclawHomeSource,
|
|
1177
|
+
activationRoot: resolvedActivationRoot.activationRoot,
|
|
1178
|
+
activationRootSource: resolvedActivationRoot.source,
|
|
1179
|
+
shared,
|
|
1180
|
+
workspaceId: resolvedWorkspaceId.workspaceId,
|
|
1181
|
+
workspaceIdSource: resolvedWorkspaceId.source,
|
|
1182
|
+
json,
|
|
1183
|
+
help
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (command === "detach") {
|
|
820
1187
|
if (help) {
|
|
821
|
-
return { command, openclawHome: "", activationRoot:
|
|
1188
|
+
return { command, openclawHome: "", activationRoot: null, restart: "safe", json, help };
|
|
822
1189
|
}
|
|
823
1190
|
if (openclawHome === null || openclawHome.trim().length === 0) {
|
|
824
|
-
throw new Error("--openclaw-home is required for
|
|
1191
|
+
throw new Error("--openclaw-home is required for detach");
|
|
1192
|
+
}
|
|
1193
|
+
if (purgeData) {
|
|
1194
|
+
throw new Error("detach always preserves activation data; use uninstall --purge-data to remove it");
|
|
825
1195
|
}
|
|
826
1196
|
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1197
|
+
const resolvedActivationRoot = resolveActivationRoot({
|
|
1198
|
+
explicit: activationRoot,
|
|
1199
|
+
openclawHome: resolvedOpenclawHome,
|
|
1200
|
+
quiet: true
|
|
1201
|
+
});
|
|
832
1202
|
return {
|
|
833
1203
|
command,
|
|
834
1204
|
openclawHome: resolvedOpenclawHome,
|
|
835
|
-
activationRoot: resolvedActivationRoot,
|
|
836
|
-
|
|
837
|
-
|
|
1205
|
+
activationRoot: resolvedActivationRoot.trim().length === 0 ? null : path.resolve(resolvedActivationRoot),
|
|
1206
|
+
restart,
|
|
1207
|
+
json,
|
|
1208
|
+
help
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
if (command === "uninstall") {
|
|
1212
|
+
if (help) {
|
|
1213
|
+
return { command, openclawHome: "", activationRoot: null, dataMode: "keep", restart: "safe", json, help };
|
|
1214
|
+
}
|
|
1215
|
+
if (openclawHome === null || openclawHome.trim().length === 0) {
|
|
1216
|
+
throw new Error("--openclaw-home is required for uninstall");
|
|
1217
|
+
}
|
|
1218
|
+
if (!keepData && !purgeData) {
|
|
1219
|
+
throw new Error("uninstall requires exactly one of --keep-data or --purge-data");
|
|
1220
|
+
}
|
|
1221
|
+
if (keepData && purgeData) {
|
|
1222
|
+
throw new Error("--keep-data and --purge-data are mutually exclusive");
|
|
1223
|
+
}
|
|
1224
|
+
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
1225
|
+
const resolvedActivationRoot = resolveActivationRoot({
|
|
1226
|
+
explicit: activationRoot,
|
|
1227
|
+
openclawHome: resolvedOpenclawHome,
|
|
1228
|
+
quiet: true
|
|
1229
|
+
});
|
|
1230
|
+
if (purgeData && resolvedActivationRoot.trim().length === 0) {
|
|
1231
|
+
throw new Error("--purge-data requires a resolvable activation root from the installed profile hook or --activation-root <path>");
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
command,
|
|
1235
|
+
openclawHome: resolvedOpenclawHome,
|
|
1236
|
+
activationRoot: resolvedActivationRoot.trim().length === 0 ? null : path.resolve(resolvedActivationRoot),
|
|
1237
|
+
dataMode: purgeData ? "purge" : "keep",
|
|
1238
|
+
restart,
|
|
838
1239
|
json,
|
|
839
1240
|
help
|
|
840
1241
|
};
|
|
@@ -905,6 +1306,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
905
1306
|
updatedAt,
|
|
906
1307
|
brainAttachmentPolicy
|
|
907
1308
|
},
|
|
1309
|
+
openclawHome: normalizeOptionalCliString(openclawHome),
|
|
908
1310
|
json,
|
|
909
1311
|
help,
|
|
910
1312
|
dryRun,
|
|
@@ -939,28 +1341,162 @@ function resolveExtensionTemplatePath() {
|
|
|
939
1341
|
throw new Error("Pre-built extension template not found. Searched:\n" +
|
|
940
1342
|
candidates.map((c) => ` - ${c}`).join("\n"));
|
|
941
1343
|
}
|
|
1344
|
+
function resolveExtensionRuntimeGuardPath() {
|
|
1345
|
+
const tsCandidates = [
|
|
1346
|
+
path.resolve(__dirname, "..", "extension", "runtime-guard.ts"),
|
|
1347
|
+
path.resolve(__dirname, "..", "..", "extension", "runtime-guard.ts"),
|
|
1348
|
+
];
|
|
1349
|
+
const jsCandidates = [
|
|
1350
|
+
path.resolve(__dirname, "extension", "runtime-guard.js"),
|
|
1351
|
+
path.resolve(__dirname, "..", "extension", "runtime-guard.js"),
|
|
1352
|
+
];
|
|
1353
|
+
const tsPath = tsCandidates.find((c) => existsSync(c)) ?? null;
|
|
1354
|
+
const jsPath = jsCandidates.find((c) => existsSync(c));
|
|
1355
|
+
if (!jsPath) {
|
|
1356
|
+
throw new Error("Pre-built extension runtime-guard.js not found. Searched:\n" +
|
|
1357
|
+
jsCandidates.map((c) => ` - ${c}`).join("\n"));
|
|
1358
|
+
}
|
|
1359
|
+
return { ts: tsPath, js: jsPath };
|
|
1360
|
+
}
|
|
1361
|
+
const LOCAL_WORKSPACE_EXTENSION_PACKAGES = [
|
|
1362
|
+
"activation",
|
|
1363
|
+
"compiler",
|
|
1364
|
+
"contracts",
|
|
1365
|
+
"event-export",
|
|
1366
|
+
"events",
|
|
1367
|
+
"learner",
|
|
1368
|
+
"openclaw",
|
|
1369
|
+
"pack-format",
|
|
1370
|
+
"provenance",
|
|
1371
|
+
"workspace-metadata"
|
|
1372
|
+
];
|
|
1373
|
+
const OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV = "OPENCLAWBRAIN_EXTENSION_TARBALL_DIR";
|
|
1374
|
+
function resolveNpmCommand() {
|
|
1375
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1376
|
+
}
|
|
1377
|
+
function resolveExtensionInstallReleaseTarballs() {
|
|
1378
|
+
const configuredDir = normalizeOptionalCliString(process.env[OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV]);
|
|
1379
|
+
if (configuredDir === null) {
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
const artifactDir = path.resolve(configuredDir);
|
|
1383
|
+
let entries;
|
|
1384
|
+
try {
|
|
1385
|
+
entries = readdirSync(artifactDir, { withFileTypes: true });
|
|
1386
|
+
}
|
|
1387
|
+
catch (error) {
|
|
1388
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1389
|
+
throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} is unreadable: ${artifactDir} (${detail})`);
|
|
1390
|
+
}
|
|
1391
|
+
const tarballs = entries
|
|
1392
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
|
|
1393
|
+
.map((entry) => path.join(artifactDir, entry.name))
|
|
1394
|
+
.sort((left, right) => left.localeCompare(right));
|
|
1395
|
+
if (tarballs.length === 0) {
|
|
1396
|
+
throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} has no .tgz release artifacts: ${artifactDir}`);
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
artifactDir,
|
|
1400
|
+
tarballs
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function resolveLocalWorkspaceRootForExtensionInstall() {
|
|
1404
|
+
const candidates = [
|
|
1405
|
+
path.resolve(__dirname, "..", "..", "..", ".."),
|
|
1406
|
+
path.resolve(__dirname, "..", "..", "..")
|
|
1407
|
+
];
|
|
1408
|
+
for (const candidate of candidates) {
|
|
1409
|
+
const packageRoot = path.join(candidate, "packages", "openclaw");
|
|
1410
|
+
const distEntry = path.join(packageRoot, "dist", "src", "index.js");
|
|
1411
|
+
if (existsSync(packageRoot) && existsSync(distEntry)) {
|
|
1412
|
+
return candidate;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
function installExtensionFromLocalWorkspaceBuild(extensionDir) {
|
|
1418
|
+
const workspaceRoot = resolveLocalWorkspaceRootForExtensionInstall();
|
|
1419
|
+
if (workspaceRoot === null) {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
const nodeModulesRoot = path.join(extensionDir, "node_modules", "@openclawbrain");
|
|
1423
|
+
mkdirSync(nodeModulesRoot, { recursive: true });
|
|
1424
|
+
for (const packageName of LOCAL_WORKSPACE_EXTENSION_PACKAGES) {
|
|
1425
|
+
const packageDir = path.join(workspaceRoot, "packages", packageName);
|
|
1426
|
+
const packageDistEntry = path.join(packageDir, "dist", "src", "index.js");
|
|
1427
|
+
if (!existsSync(packageDir) || !existsSync(packageDistEntry)) {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
for (const packageName of LOCAL_WORKSPACE_EXTENSION_PACKAGES) {
|
|
1432
|
+
const packageDir = path.join(workspaceRoot, "packages", packageName);
|
|
1433
|
+
const linkPath = path.join(nodeModulesRoot, packageName);
|
|
1434
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
1435
|
+
symlinkSync(packageDir, linkPath, "dir");
|
|
1436
|
+
}
|
|
1437
|
+
return [...LOCAL_WORKSPACE_EXTENSION_PACKAGES];
|
|
1438
|
+
}
|
|
1439
|
+
let cachedOpenClawPackageMetadata = null;
|
|
1440
|
+
function resolveOpenClawPackageManifestPath() {
|
|
1441
|
+
const candidates = [
|
|
1442
|
+
path.resolve(__dirname, "..", "package.json"),
|
|
1443
|
+
path.resolve(__dirname, "..", "..", "package.json"),
|
|
1444
|
+
];
|
|
1445
|
+
for (const candidate of candidates) {
|
|
1446
|
+
if (existsSync(candidate)) {
|
|
1447
|
+
return candidate;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
throw new Error("OpenClawBrain package manifest not found. Searched:\n" +
|
|
1451
|
+
candidates.map((candidate) => ` - ${candidate}`).join("\n"));
|
|
1452
|
+
}
|
|
1453
|
+
function readOpenClawPackageMetadata() {
|
|
1454
|
+
if (cachedOpenClawPackageMetadata !== null) {
|
|
1455
|
+
return cachedOpenClawPackageMetadata;
|
|
1456
|
+
}
|
|
1457
|
+
const manifestPath = resolveOpenClawPackageManifestPath();
|
|
1458
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1459
|
+
const name = typeof manifest.name === "string" && manifest.name.trim().length > 0
|
|
1460
|
+
? manifest.name.trim()
|
|
1461
|
+
: "@openclawbrain/openclaw";
|
|
1462
|
+
const version = typeof manifest.version === "string" && manifest.version.trim().length > 0
|
|
1463
|
+
? manifest.version.trim()
|
|
1464
|
+
: "0.0.0";
|
|
1465
|
+
cachedOpenClawPackageMetadata = { name, version };
|
|
1466
|
+
return cachedOpenClawPackageMetadata;
|
|
1467
|
+
}
|
|
942
1468
|
function buildExtensionIndexTs(activationRoot) {
|
|
943
1469
|
const templatePath = resolveExtensionTemplatePath();
|
|
944
1470
|
const template = readFileSync(templatePath, "utf8");
|
|
945
1471
|
return template.replace(/const ACTIVATION_ROOT = "__ACTIVATION_ROOT__";/, `const ACTIVATION_ROOT = ${JSON.stringify(activationRoot)};`);
|
|
946
1472
|
}
|
|
947
1473
|
function buildExtensionPackageJson() {
|
|
1474
|
+
const packageMetadata = readOpenClawPackageMetadata();
|
|
948
1475
|
return JSON.stringify({
|
|
949
1476
|
name: "openclawbrain-extension",
|
|
950
|
-
version:
|
|
1477
|
+
version: packageMetadata.version,
|
|
951
1478
|
private: true,
|
|
952
1479
|
type: "module",
|
|
1480
|
+
openclaw: {
|
|
1481
|
+
extensions: ["index.ts"]
|
|
1482
|
+
},
|
|
953
1483
|
dependencies: {
|
|
954
|
-
|
|
1484
|
+
[packageMetadata.name]: packageMetadata.version
|
|
955
1485
|
}
|
|
956
1486
|
}, null, 2) + "\n";
|
|
957
1487
|
}
|
|
958
1488
|
function buildExtensionPluginManifest() {
|
|
1489
|
+
const packageMetadata = readOpenClawPackageMetadata();
|
|
959
1490
|
return JSON.stringify({
|
|
960
1491
|
id: "openclawbrain",
|
|
961
1492
|
name: "OpenClawBrain",
|
|
962
1493
|
description: "Learned memory and context from OpenClawBrain",
|
|
963
|
-
version:
|
|
1494
|
+
version: packageMetadata.version,
|
|
1495
|
+
configSchema: {
|
|
1496
|
+
type: "object",
|
|
1497
|
+
additionalProperties: false,
|
|
1498
|
+
properties: {}
|
|
1499
|
+
}
|
|
964
1500
|
}, null, 2) + "\n";
|
|
965
1501
|
}
|
|
966
1502
|
function formatContextForHuman(result) {
|
|
@@ -1040,15 +1576,94 @@ function buildHistoryEntry(record, slot, isActive) {
|
|
|
1040
1576
|
current: isActive
|
|
1041
1577
|
};
|
|
1042
1578
|
}
|
|
1579
|
+
function formatInspectionFindings(findings) {
|
|
1580
|
+
return findings.join("; ");
|
|
1581
|
+
}
|
|
1582
|
+
function buildInstallRefusalError(parsed, detail) {
|
|
1583
|
+
const purgeCommand = `openclawbrain uninstall --openclaw-home ${quoteShellArg(parsed.openclawHome)} ` +
|
|
1584
|
+
`--activation-root ${quoteShellArg(parsed.activationRoot)} --purge-data`;
|
|
1585
|
+
return new Error(`Refusing to reuse activation root ${path.resolve(parsed.activationRoot)}: ${detail}. ` +
|
|
1586
|
+
"Install only repairs an empty first-state root; it will not overwrite populated or broken activation state. " +
|
|
1587
|
+
`Inspect: ${buildInstallStatusCommand(parsed.activationRoot)}. ` +
|
|
1588
|
+
`Reset: ${purgeCommand}.`);
|
|
1589
|
+
}
|
|
1590
|
+
function inspectInstallActivationPlan(parsed) {
|
|
1591
|
+
const resolvedActivationRoot = path.resolve(parsed.activationRoot);
|
|
1592
|
+
const activationPointersPath = path.join(resolvedActivationRoot, "activation-pointers.json");
|
|
1593
|
+
if (!existsSync(resolvedActivationRoot)) {
|
|
1594
|
+
return {
|
|
1595
|
+
createActivationRoot: true,
|
|
1596
|
+
action: "bootstrap",
|
|
1597
|
+
resolution: "new_root",
|
|
1598
|
+
inspectionStep: "Activation state inspection: activation root is missing; bootstrapping first state.",
|
|
1599
|
+
activePackId: null
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
const activationRootStats = statSync(resolvedActivationRoot);
|
|
1603
|
+
if (!activationRootStats.isDirectory()) {
|
|
1604
|
+
throw buildInstallRefusalError(parsed, "activation root path exists but is not a directory");
|
|
1605
|
+
}
|
|
1606
|
+
if (!existsSync(activationPointersPath)) {
|
|
1607
|
+
return {
|
|
1608
|
+
createActivationRoot: false,
|
|
1609
|
+
action: "bootstrap",
|
|
1610
|
+
resolution: "missing_pointers",
|
|
1611
|
+
inspectionStep: "Activation state inspection: activation root exists but activation-pointers.json is missing; bootstrapping first state.",
|
|
1612
|
+
activePackId: null
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
let inspection;
|
|
1616
|
+
try {
|
|
1617
|
+
inspection = inspectActivationState(resolvedActivationRoot, new Date().toISOString());
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1621
|
+
throw buildInstallRefusalError(parsed, `activation pointers could not be inspected (${detail})`);
|
|
1622
|
+
}
|
|
1623
|
+
if (inspection.active === null && inspection.candidate === null && inspection.previous === null) {
|
|
1624
|
+
return {
|
|
1625
|
+
createActivationRoot: false,
|
|
1626
|
+
action: "bootstrap",
|
|
1627
|
+
resolution: "empty_pointers",
|
|
1628
|
+
inspectionStep: "Activation state inspection: activation pointers are present but all slots are empty; bootstrapping first state.",
|
|
1629
|
+
activePackId: null
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
const unhealthySlots = [inspection.active, inspection.candidate, inspection.previous]
|
|
1633
|
+
.filter((slot) => slot !== null && !slot.activationReady)
|
|
1634
|
+
.map((slot) => `${slot.slot}: ${formatInspectionFindings(slot.findings)}`);
|
|
1635
|
+
if (unhealthySlots.length > 0) {
|
|
1636
|
+
throw buildInstallRefusalError(parsed, `activation state contains unhealthy slots (${unhealthySlots.join(" | ")})`);
|
|
1637
|
+
}
|
|
1638
|
+
if (inspection.active === null) {
|
|
1639
|
+
const populatedSlots = [inspection.candidate, inspection.previous]
|
|
1640
|
+
.filter((slot) => slot !== null)
|
|
1641
|
+
.map((slot) => slot.slot);
|
|
1642
|
+
throw buildInstallRefusalError(parsed, `activation state is populated without an active pack (${populatedSlots.join(", ")})`);
|
|
1643
|
+
}
|
|
1644
|
+
if (inspection.candidate !== null && !inspection.promotion.allowed) {
|
|
1645
|
+
throw buildInstallRefusalError(parsed, `candidate slot is stale or incoherent (${formatInspectionFindings(inspection.promotion.findings)})`);
|
|
1646
|
+
}
|
|
1647
|
+
if (inspection.previous !== null && !inspection.rollback.allowed) {
|
|
1648
|
+
throw buildInstallRefusalError(parsed, `previous slot is stale or incoherent (${formatInspectionFindings(inspection.rollback.findings)})`);
|
|
1649
|
+
}
|
|
1650
|
+
return {
|
|
1651
|
+
createActivationRoot: false,
|
|
1652
|
+
action: "keep",
|
|
1653
|
+
resolution: "healthy_existing",
|
|
1654
|
+
inspectionStep: `Activation state inspection: active pack ${inspection.active.packId} is healthy; keeping existing activation state.`,
|
|
1655
|
+
activePackId: inspection.active.packId
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1043
1658
|
function runHistoryCommand(parsed) {
|
|
1044
1659
|
const activationRoot = parsed.activationRoot;
|
|
1045
1660
|
const pointersPath = path.join(activationRoot, "activation-pointers.json");
|
|
1046
1661
|
if (!existsSync(pointersPath)) {
|
|
1047
1662
|
if (parsed.json) {
|
|
1048
|
-
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain
|
|
1663
|
+
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain install" }, null, 2));
|
|
1049
1664
|
}
|
|
1050
1665
|
else {
|
|
1051
|
-
console.log("No history yet. Run: openclawbrain
|
|
1666
|
+
console.log("No history yet. Run: openclawbrain install");
|
|
1052
1667
|
}
|
|
1053
1668
|
return 0;
|
|
1054
1669
|
}
|
|
@@ -1081,10 +1696,10 @@ function runHistoryCommand(parsed) {
|
|
|
1081
1696
|
}
|
|
1082
1697
|
if (entries.length === 0) {
|
|
1083
1698
|
if (parsed.json) {
|
|
1084
|
-
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain
|
|
1699
|
+
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain install" }, null, 2));
|
|
1085
1700
|
}
|
|
1086
1701
|
else {
|
|
1087
|
-
console.log("No history yet. Run: openclawbrain
|
|
1702
|
+
console.log("No history yet. Run: openclawbrain install");
|
|
1088
1703
|
}
|
|
1089
1704
|
return 0;
|
|
1090
1705
|
}
|
|
@@ -1121,30 +1736,25 @@ function runHistoryCommand(parsed) {
|
|
|
1121
1736
|
}
|
|
1122
1737
|
return 0;
|
|
1123
1738
|
}
|
|
1124
|
-
function
|
|
1739
|
+
function runInstallCommand(parsed) {
|
|
1125
1740
|
const steps = [];
|
|
1741
|
+
const commandLabel = parsed.command.toUpperCase();
|
|
1742
|
+
steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
|
|
1126
1743
|
// 1. Validate --openclaw-home exists and has openclaw.json
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (
|
|
1132
|
-
throw new Error(`openclaw.json not found in ${parsed.openclawHome}`);
|
|
1133
|
-
}
|
|
1134
|
-
// 2. Create activation root if needed
|
|
1135
|
-
if (!existsSync(parsed.activationRoot)) {
|
|
1744
|
+
validateOpenClawHome(parsed.openclawHome);
|
|
1745
|
+
// 2. Inspect the activation root before writing profile hook artifacts.
|
|
1746
|
+
const activationPlan = inspectInstallActivationPlan(parsed);
|
|
1747
|
+
// 3. Create activation root if needed
|
|
1748
|
+
if (activationPlan.createActivationRoot) {
|
|
1136
1749
|
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1137
1750
|
steps.push(`Created activation root: ${parsed.activationRoot}`);
|
|
1138
1751
|
}
|
|
1139
1752
|
else {
|
|
1140
1753
|
steps.push(`Activation root exists: ${parsed.activationRoot}`);
|
|
1141
1754
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
if (
|
|
1145
|
-
steps.push("Brain already attached (activation-pointers.json exists), skipping bootstrap.");
|
|
1146
|
-
}
|
|
1147
|
-
else {
|
|
1755
|
+
steps.push(activationPlan.inspectionStep);
|
|
1756
|
+
// 4. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
|
|
1757
|
+
if (activationPlan.action === "bootstrap") {
|
|
1148
1758
|
const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
|
|
1149
1759
|
mkdirSync(packRoot, { recursive: true });
|
|
1150
1760
|
const brainAttachmentPolicy = parsed.shared ? "shared" : "dedicated";
|
|
@@ -1153,52 +1763,85 @@ function runSetupCommand(parsed) {
|
|
|
1153
1763
|
brainAttachmentPolicy,
|
|
1154
1764
|
activationRoot: parsed.activationRoot,
|
|
1155
1765
|
packRoot,
|
|
1156
|
-
packLabel: "
|
|
1766
|
+
packLabel: "install-cli",
|
|
1157
1767
|
workspace: {
|
|
1158
1768
|
workspaceId: parsed.workspaceId,
|
|
1159
|
-
snapshotId: `${parsed.workspaceId}@
|
|
1769
|
+
snapshotId: `${parsed.workspaceId}@install-${new Date().toISOString().slice(0, 10)}`,
|
|
1160
1770
|
capturedAt: new Date().toISOString(),
|
|
1161
1771
|
rootDir: parsed.openclawHome,
|
|
1162
|
-
revision: "cli-
|
|
1772
|
+
revision: "cli-install-v1"
|
|
1163
1773
|
},
|
|
1164
1774
|
interactionEvents: [],
|
|
1165
1775
|
feedbackEvents: []
|
|
1166
1776
|
});
|
|
1167
|
-
steps.push(`Bootstrapped brain attach:
|
|
1777
|
+
steps.push(`Bootstrapped brain attach: state=${result.currentProfile.brain.state} awaitingFirstExport=${yesNo(result.currentProfile.brainStatus.awaitingFirstExport)}`);
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
steps.push(`Kept inspected activation state: active pack ${activationPlan.activePackId}`);
|
|
1168
1781
|
}
|
|
1169
|
-
//
|
|
1782
|
+
// 5-8. Write extension files
|
|
1170
1783
|
const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
|
|
1171
1784
|
mkdirSync(extensionDir, { recursive: true });
|
|
1172
|
-
//
|
|
1785
|
+
// 5. Write index.ts
|
|
1173
1786
|
const indexTsPath = path.join(extensionDir, "index.ts");
|
|
1174
1787
|
writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
|
|
1175
1788
|
steps.push(`Wrote extension: ${indexTsPath}`);
|
|
1176
|
-
//
|
|
1789
|
+
// 5b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
|
|
1790
|
+
const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
|
|
1791
|
+
if (runtimeGuardPaths.ts !== null) {
|
|
1792
|
+
const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
|
|
1793
|
+
writeFileSync(runtimeGuardTsPath, readFileSync(runtimeGuardPaths.ts, "utf8"), "utf8");
|
|
1794
|
+
steps.push(`Wrote extension runtime-guard source: ${runtimeGuardTsPath}`);
|
|
1795
|
+
}
|
|
1796
|
+
const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
|
|
1797
|
+
writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
|
|
1798
|
+
steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
|
|
1799
|
+
// 6. Write package.json
|
|
1177
1800
|
const packageJsonPath = path.join(extensionDir, "package.json");
|
|
1178
1801
|
writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
|
|
1179
1802
|
steps.push(`Wrote package.json: ${packageJsonPath}`);
|
|
1180
|
-
//
|
|
1803
|
+
// 7. npm install
|
|
1804
|
+
const releaseTarballInstall = resolveExtensionInstallReleaseTarballs();
|
|
1181
1805
|
try {
|
|
1182
|
-
|
|
1183
|
-
|
|
1806
|
+
if (releaseTarballInstall !== null) {
|
|
1807
|
+
execFileSync(resolveNpmCommand(), ["install", "--ignore-scripts", "--no-save", ...releaseTarballInstall.tarballs], { cwd: extensionDir, stdio: "pipe" });
|
|
1808
|
+
steps.push(`Installed extension dependencies from release artifacts: ${releaseTarballInstall.tarballs.length} tarballs from ${releaseTarballInstall.artifactDir}`);
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
|
|
1812
|
+
steps.push("Ran npm install --ignore-scripts");
|
|
1813
|
+
const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
|
|
1814
|
+
if (linkedPackages !== null) {
|
|
1815
|
+
steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1184
1818
|
}
|
|
1185
1819
|
catch (err) {
|
|
1186
1820
|
const message = err instanceof Error ? err.message : String(err);
|
|
1187
|
-
|
|
1821
|
+
if (releaseTarballInstall !== null) {
|
|
1822
|
+
throw new Error(`Extension dependency install from release artifacts failed: ${message}`);
|
|
1823
|
+
}
|
|
1824
|
+
const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
|
|
1825
|
+
if (linkedPackages !== null) {
|
|
1826
|
+
steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
steps.push(`npm install failed (non-fatal): ${message}`);
|
|
1830
|
+
}
|
|
1188
1831
|
}
|
|
1189
|
-
//
|
|
1832
|
+
// 8. Write plugin manifest
|
|
1190
1833
|
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
1191
1834
|
writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
|
|
1192
1835
|
steps.push(`Wrote manifest: ${manifestPath}`);
|
|
1193
|
-
//
|
|
1836
|
+
// 9. Write BRAIN.md to workspace directories
|
|
1194
1837
|
const brainMdContent = [
|
|
1195
1838
|
"## OpenClawBrain",
|
|
1196
1839
|
`You have a learning brain attached at ${parsed.activationRoot}.`,
|
|
1197
1840
|
"- It learns automatically from your conversations",
|
|
1198
1841
|
'- Corrections matter — "no, actually X" teaches the brain X',
|
|
1199
1842
|
"- You don't manage it — background daemon handles learning",
|
|
1200
|
-
|
|
1201
|
-
|
|
1843
|
+
`- Check: \`openclawbrain status --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
|
|
1844
|
+
`- Rollback: \`openclawbrain rollback --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
|
|
1202
1845
|
'- See what brain knows: `openclawbrain context "your question"`',
|
|
1203
1846
|
""
|
|
1204
1847
|
].join("\n");
|
|
@@ -1266,58 +1909,361 @@ function runSetupCommand(parsed) {
|
|
|
1266
1909
|
const message = err instanceof Error ? err.message : String(err);
|
|
1267
1910
|
steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
|
|
1268
1911
|
}
|
|
1912
|
+
const restartGuidance = buildInstallReloadGuidance();
|
|
1913
|
+
const nextSteps = [
|
|
1914
|
+
restartGuidance,
|
|
1915
|
+
`Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`
|
|
1916
|
+
];
|
|
1917
|
+
const lifecycleSummary = [
|
|
1918
|
+
`OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
|
|
1919
|
+
`Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
|
|
1920
|
+
`Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
|
|
1921
|
+
`Profile hook: installed at ${shortenPath(extensionDir)}`,
|
|
1922
|
+
activationPlan.resolution === "new_root"
|
|
1923
|
+
? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
|
|
1924
|
+
: activationPlan.resolution === "missing_pointers"
|
|
1925
|
+
? `Activation data: repaired missing pointers at ${shortenPath(parsed.activationRoot)}`
|
|
1926
|
+
: activationPlan.resolution === "empty_pointers"
|
|
1927
|
+
? `Activation data: repaired empty pointers at ${shortenPath(parsed.activationRoot)}`
|
|
1928
|
+
: `Activation data: reused healthy state at ${shortenPath(parsed.activationRoot)}`,
|
|
1929
|
+
activationPlan.action === "bootstrap"
|
|
1930
|
+
? activationPlan.resolution === "new_root"
|
|
1931
|
+
? "Brain attach: bootstrapped a seed/current-profile attach"
|
|
1932
|
+
: activationPlan.resolution === "missing_pointers"
|
|
1933
|
+
? "Brain attach: repaired missing activation pointers and bootstrapped a seed/current-profile attach"
|
|
1934
|
+
: "Brain attach: repaired empty activation pointers and bootstrapped a seed/current-profile attach"
|
|
1935
|
+
: `Brain attach: kept healthy active pack ${activationPlan.activePackId} in place`
|
|
1936
|
+
];
|
|
1269
1937
|
// 9. Print summary
|
|
1270
1938
|
if (parsed.json) {
|
|
1271
1939
|
console.log(JSON.stringify({
|
|
1272
|
-
command:
|
|
1940
|
+
command: parsed.command,
|
|
1273
1941
|
openclawHome: parsed.openclawHome,
|
|
1942
|
+
openclawHomeSource: parsed.openclawHomeSource,
|
|
1274
1943
|
activationRoot: parsed.activationRoot,
|
|
1944
|
+
resolvedInputs: {
|
|
1945
|
+
activationRoot: {
|
|
1946
|
+
value: parsed.activationRoot,
|
|
1947
|
+
source: parsed.activationRootSource
|
|
1948
|
+
},
|
|
1949
|
+
workspaceId: {
|
|
1950
|
+
value: parsed.workspaceId,
|
|
1951
|
+
source: parsed.workspaceIdSource
|
|
1952
|
+
}
|
|
1953
|
+
},
|
|
1275
1954
|
workspaceId: parsed.workspaceId,
|
|
1276
1955
|
shared: parsed.shared,
|
|
1277
1956
|
extensionDir,
|
|
1957
|
+
lifecycleSummary,
|
|
1958
|
+
restartGuidance,
|
|
1959
|
+
nextSteps,
|
|
1278
1960
|
steps
|
|
1279
1961
|
}, null, 2));
|
|
1280
1962
|
}
|
|
1281
1963
|
else {
|
|
1282
|
-
console.log(
|
|
1964
|
+
console.log(`${commandLabel} complete\n`);
|
|
1283
1965
|
for (const step of steps) {
|
|
1284
1966
|
console.log(` ✓ ${step}`);
|
|
1285
1967
|
}
|
|
1286
1968
|
console.log("");
|
|
1287
|
-
console.log(
|
|
1288
|
-
|
|
1969
|
+
console.log("Lifecycle:");
|
|
1970
|
+
for (const line of lifecycleSummary) {
|
|
1971
|
+
console.log(` ${line}`);
|
|
1972
|
+
}
|
|
1973
|
+
console.log(`Next: ${restartGuidance}`);
|
|
1974
|
+
console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
|
|
1289
1975
|
}
|
|
1290
1976
|
return 0;
|
|
1291
1977
|
}
|
|
1292
|
-
function
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const stores = discoverOpenClawMainSessionStores();
|
|
1296
|
-
if (stores.length === 0) {
|
|
1297
|
-
if (parsed.json) {
|
|
1298
|
-
console.log(JSON.stringify({ command: "learn", activationRoot, scannedSessions: 0, newEvents: 0, materialized: null, promoted: false, message: "No local session stores found." }));
|
|
1299
|
-
}
|
|
1300
|
-
else {
|
|
1301
|
-
console.log("No new session data. Brain is up to date.");
|
|
1302
|
-
}
|
|
1303
|
-
return 0;
|
|
1978
|
+
function validateOpenClawHome(openclawHome) {
|
|
1979
|
+
if (!existsSync(openclawHome)) {
|
|
1980
|
+
throw new Error(`--openclaw-home directory does not exist: ${openclawHome}`);
|
|
1304
1981
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1982
|
+
const openclawJsonPath = path.join(openclawHome, "openclaw.json");
|
|
1983
|
+
if (!existsSync(openclawJsonPath)) {
|
|
1984
|
+
throw new Error(`openclaw.json not found in ${openclawHome}`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
function resolveCleanupActivationRoot(openclawHome, explicitActivationRoot) {
|
|
1988
|
+
if (explicitActivationRoot !== null) {
|
|
1989
|
+
return path.resolve(explicitActivationRoot);
|
|
1990
|
+
}
|
|
1991
|
+
const resolved = resolveActivationRoot({
|
|
1992
|
+
openclawHome,
|
|
1993
|
+
quiet: true
|
|
1994
|
+
});
|
|
1995
|
+
return resolved.trim().length === 0 ? null : path.resolve(resolved);
|
|
1996
|
+
}
|
|
1997
|
+
function removeProfileHookup(openclawHome, steps) {
|
|
1998
|
+
const extensionDir = path.join(openclawHome, "extensions", "openclawbrain");
|
|
1999
|
+
if (!existsSync(extensionDir)) {
|
|
2000
|
+
steps.push(`Profile hookup already absent: ${extensionDir}`);
|
|
2001
|
+
return extensionDir;
|
|
2002
|
+
}
|
|
2003
|
+
rmSync(extensionDir, { recursive: true, force: true });
|
|
2004
|
+
steps.push(`Removed profile hookup: ${extensionDir}`);
|
|
2005
|
+
return extensionDir;
|
|
2006
|
+
}
|
|
2007
|
+
function summarizeKeptActivationData(activationRoot) {
|
|
2008
|
+
if (activationRoot === null) {
|
|
2009
|
+
return {
|
|
2010
|
+
activationRoot: null,
|
|
2011
|
+
activationDataState: "unresolved",
|
|
2012
|
+
activationDataDetail: "Activation data preserved, but the activation root could not be resolved from the profile hook."
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
return {
|
|
2016
|
+
activationRoot,
|
|
2017
|
+
activationDataState: "kept",
|
|
2018
|
+
activationDataDetail: `Activation data preserved at ${activationRoot}`
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
function buildRestartGuidance(restart) {
|
|
2022
|
+
return buildCleanupRestartGuidance(restart);
|
|
2023
|
+
}
|
|
2024
|
+
function runDetachCommand(parsed) {
|
|
2025
|
+
const steps = [];
|
|
2026
|
+
validateOpenClawHome(parsed.openclawHome);
|
|
2027
|
+
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
2028
|
+
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
2029
|
+
const activationData = summarizeKeptActivationData(activationRoot);
|
|
2030
|
+
const restartGuidance = buildRestartGuidance(parsed.restart);
|
|
2031
|
+
const nextSteps = [
|
|
2032
|
+
restartGuidance,
|
|
2033
|
+
activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
|
|
2034
|
+
`Reattach later: ${buildInstallCommand(parsed.openclawHome)}`
|
|
2035
|
+
].filter((step) => step !== null);
|
|
2036
|
+
steps.push(activationData.activationDataDetail);
|
|
2037
|
+
steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
|
|
2038
|
+
if (parsed.json) {
|
|
2039
|
+
console.log(JSON.stringify({
|
|
2040
|
+
command: "detach",
|
|
2041
|
+
openclawHome: parsed.openclawHome,
|
|
2042
|
+
extensionDir,
|
|
2043
|
+
activationRoot,
|
|
2044
|
+
dataAction: "kept",
|
|
2045
|
+
activationDataState: activationData.activationDataState,
|
|
2046
|
+
restartMode: parsed.restart,
|
|
2047
|
+
restartGuidance,
|
|
2048
|
+
nextSteps,
|
|
2049
|
+
steps
|
|
2050
|
+
}, null, 2));
|
|
2051
|
+
}
|
|
2052
|
+
else {
|
|
2053
|
+
console.log("DETACH complete\n");
|
|
2054
|
+
for (const step of steps) {
|
|
2055
|
+
console.log(` ✓ ${step}`);
|
|
2056
|
+
}
|
|
2057
|
+
console.log("");
|
|
2058
|
+
console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
|
|
2059
|
+
if (activationRoot !== null) {
|
|
2060
|
+
console.log(`Brain data: ${shortenPath(activationRoot)} remains available for inspection or reattach.`);
|
|
2061
|
+
}
|
|
2062
|
+
else {
|
|
2063
|
+
console.log("Brain data: preserved, but the activation root could not be resolved from the removed hook.");
|
|
2064
|
+
}
|
|
2065
|
+
console.log(`Next: ${restartGuidance}`);
|
|
2066
|
+
if (activationRoot !== null) {
|
|
2067
|
+
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
2068
|
+
}
|
|
2069
|
+
console.log(`Reattach: ${buildInstallCommand(parsed.openclawHome)}`);
|
|
2070
|
+
}
|
|
2071
|
+
return 0;
|
|
2072
|
+
}
|
|
2073
|
+
function runUninstallCommand(parsed) {
|
|
2074
|
+
const steps = [];
|
|
2075
|
+
validateOpenClawHome(parsed.openclawHome);
|
|
2076
|
+
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
2077
|
+
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
2078
|
+
let activationData;
|
|
2079
|
+
if (parsed.dataMode === "purge") {
|
|
2080
|
+
if (activationRoot === null) {
|
|
2081
|
+
throw new Error("--purge-data requires a resolvable activation root from the installed profile hook or --activation-root <path>");
|
|
2082
|
+
}
|
|
2083
|
+
if (existsSync(activationRoot)) {
|
|
2084
|
+
rmSync(activationRoot, { recursive: true, force: true });
|
|
2085
|
+
activationData = {
|
|
2086
|
+
activationRoot,
|
|
2087
|
+
activationDataState: "removed",
|
|
2088
|
+
activationDataDetail: `Activation data removed at ${activationRoot}`
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
else {
|
|
2092
|
+
activationData = {
|
|
2093
|
+
activationRoot,
|
|
2094
|
+
activationDataState: "already_absent",
|
|
2095
|
+
activationDataDetail: `Activation data already absent at ${activationRoot}`
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
else {
|
|
2100
|
+
activationData = summarizeKeptActivationData(activationRoot);
|
|
2101
|
+
}
|
|
2102
|
+
const restartGuidance = buildRestartGuidance(parsed.restart);
|
|
2103
|
+
const nextSteps = [
|
|
2104
|
+
restartGuidance,
|
|
2105
|
+
parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
|
|
2106
|
+
`Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
|
|
2107
|
+
].filter((step) => step !== null);
|
|
2108
|
+
steps.push(activationData.activationDataDetail);
|
|
2109
|
+
steps.push(parsed.dataMode === "purge"
|
|
2110
|
+
? "Uninstall removed the OpenClaw profile hook and activation data."
|
|
2111
|
+
: "Uninstall removed the OpenClaw profile hook and kept activation data explicitly.");
|
|
2112
|
+
if (parsed.json) {
|
|
2113
|
+
console.log(JSON.stringify({
|
|
2114
|
+
command: "uninstall",
|
|
2115
|
+
openclawHome: parsed.openclawHome,
|
|
2116
|
+
extensionDir,
|
|
2117
|
+
activationRoot,
|
|
2118
|
+
dataAction: parsed.dataMode,
|
|
2119
|
+
activationDataState: activationData.activationDataState,
|
|
2120
|
+
restartMode: parsed.restart,
|
|
2121
|
+
restartGuidance,
|
|
2122
|
+
nextSteps,
|
|
2123
|
+
steps
|
|
2124
|
+
}, null, 2));
|
|
2125
|
+
}
|
|
2126
|
+
else {
|
|
2127
|
+
console.log("UNINSTALL complete\n");
|
|
2128
|
+
for (const step of steps) {
|
|
2129
|
+
console.log(` ✓ ${step}`);
|
|
2130
|
+
}
|
|
2131
|
+
console.log("");
|
|
2132
|
+
console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
|
|
2133
|
+
console.log(`Data mode: ${parsed.dataMode === "purge" ? "purged" : "kept"}`);
|
|
2134
|
+
if (activationRoot !== null) {
|
|
2135
|
+
console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
|
|
2136
|
+
}
|
|
2137
|
+
console.log(`Next: ${restartGuidance}`);
|
|
2138
|
+
if (parsed.dataMode === "keep" && activationRoot !== null) {
|
|
2139
|
+
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
2140
|
+
}
|
|
2141
|
+
console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
|
|
2142
|
+
}
|
|
2143
|
+
return 0;
|
|
2144
|
+
}
|
|
2145
|
+
function resolveServeTimeLearningRuntimeInput(activationRoot) {
|
|
2146
|
+
let serveTimeDecisions = [];
|
|
2147
|
+
let fallbackReason = null;
|
|
2148
|
+
try {
|
|
2149
|
+
serveTimeDecisions = readLearningSpineLogEntries(activationRoot, "serveTimeRouteDecisions");
|
|
2150
|
+
}
|
|
2151
|
+
catch {
|
|
2152
|
+
fallbackReason = "serve_time_decision_log_read_failed";
|
|
2153
|
+
}
|
|
2154
|
+
const decisionLogCount = serveTimeDecisions.length;
|
|
2155
|
+
const pgVersion = decisionLogCount > 0 ? "v2" : "v1";
|
|
2156
|
+
return {
|
|
2157
|
+
pgVersion,
|
|
2158
|
+
serveTimeDecisions,
|
|
2159
|
+
decisionLogCount,
|
|
2160
|
+
baselineState: pgVersion === "v2" ? loadOrInitBaseline(activationRoot) : undefined,
|
|
2161
|
+
fallbackReason
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function runLearnCommand(parsed) {
|
|
2165
|
+
const learnStatePath = path.join(parsed.activationRoot, "learn-cli-state.json");
|
|
2166
|
+
const teacherSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(parsed.activationRoot);
|
|
2167
|
+
function isLearnRuntimeStateLike(value) {
|
|
2168
|
+
if (typeof value !== "object" || value === null) {
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
const candidate = value;
|
|
2172
|
+
return (candidate.runtimeOwner === "openclaw" &&
|
|
2173
|
+
typeof candidate.cursor === "object" &&
|
|
2174
|
+
candidate.cursor !== null &&
|
|
2175
|
+
typeof candidate.pending === "object" &&
|
|
2176
|
+
candidate.pending !== null &&
|
|
2177
|
+
Array.isArray(candidate.pending.live) &&
|
|
2178
|
+
Array.isArray(candidate.pending.backfill) &&
|
|
2179
|
+
typeof candidate.materializationCount === "number" &&
|
|
2180
|
+
typeof candidate.sparseFeedback === "object" &&
|
|
2181
|
+
candidate.sparseFeedback !== null);
|
|
2182
|
+
}
|
|
2183
|
+
function loadPersistedLearnCliState() {
|
|
2184
|
+
if (!existsSync(learnStatePath)) {
|
|
2185
|
+
return {
|
|
2186
|
+
state: createAlwaysOnLearningRuntimeState(),
|
|
2187
|
+
loaded: false,
|
|
2188
|
+
resetReason: null
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
try {
|
|
2192
|
+
const persisted = readJsonFile(learnStatePath);
|
|
2193
|
+
if (persisted.contract !== "openclaw.learn_cli_state.v1" || !isLearnRuntimeStateLike(persisted.state)) {
|
|
2194
|
+
throw new Error("persisted learn state shape is invalid");
|
|
2195
|
+
}
|
|
2196
|
+
return {
|
|
2197
|
+
state: persisted.state,
|
|
2198
|
+
loaded: true,
|
|
2199
|
+
resetReason: null
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
catch (error) {
|
|
2203
|
+
return {
|
|
2204
|
+
state: createAlwaysOnLearningRuntimeState(),
|
|
2205
|
+
loaded: false,
|
|
2206
|
+
resetReason: error instanceof Error ? error.message : "persisted learn state could not be parsed"
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
function persistLearnCliState(state, updatedAt) {
|
|
2211
|
+
const payload = {
|
|
2212
|
+
contract: "openclaw.learn_cli_state.v1",
|
|
2213
|
+
updatedAt,
|
|
2214
|
+
state
|
|
2215
|
+
};
|
|
2216
|
+
mkdirSync(path.dirname(learnStatePath), { recursive: true });
|
|
2217
|
+
writeFileSync(learnStatePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
2218
|
+
}
|
|
2219
|
+
const activationRoot = parsed.activationRoot;
|
|
2220
|
+
const persistedState = loadPersistedLearnCliState();
|
|
2221
|
+
const stores = discoverOpenClawSessionStores();
|
|
2222
|
+
if (stores.length === 0) {
|
|
2223
|
+
const labelFlow = {
|
|
2224
|
+
source: "missing",
|
|
2225
|
+
humanLabelCount: 0,
|
|
2226
|
+
selfLabelCount: 0,
|
|
2227
|
+
asyncTeacherArtifactCount: 0,
|
|
2228
|
+
implicitPositiveCount: 0,
|
|
2229
|
+
detail: "no local session stores were found"
|
|
2230
|
+
};
|
|
2231
|
+
const learningPath = summarizeLearningPathFromMaterialization(null);
|
|
2232
|
+
if (parsed.json) {
|
|
2233
|
+
console.log(JSON.stringify({
|
|
2234
|
+
command: "learn",
|
|
2235
|
+
activationRoot,
|
|
2236
|
+
scannedSessions: 0,
|
|
2237
|
+
newEvents: 0,
|
|
2238
|
+
loadedState: persistedState.loaded,
|
|
2239
|
+
statePath: learnStatePath,
|
|
2240
|
+
stateResetReason: persistedState.resetReason,
|
|
2241
|
+
materialized: null,
|
|
2242
|
+
promoted: false,
|
|
2243
|
+
graph: null,
|
|
2244
|
+
labelFlow,
|
|
2245
|
+
learningPath,
|
|
2246
|
+
message: "No local session stores found."
|
|
2247
|
+
}));
|
|
2248
|
+
}
|
|
2249
|
+
else {
|
|
2250
|
+
console.log("No new session data. Brain is up to date.");
|
|
2251
|
+
}
|
|
2252
|
+
return 0;
|
|
2253
|
+
}
|
|
2254
|
+
let totalSessions = 0;
|
|
2255
|
+
const allInteractionEvents = [];
|
|
2256
|
+
const allFeedbackEvents = [];
|
|
2257
|
+
let nextSequence = 1;
|
|
2258
|
+
const discoveredSessions = stores
|
|
2259
|
+
.flatMap((store) => {
|
|
2260
|
+
const sessionIndex = loadOpenClawSessionIndex(store.indexPath);
|
|
2261
|
+
return Object.entries(sessionIndex).map(([sessionKey, entry]) => ({
|
|
2262
|
+
store,
|
|
2263
|
+
sessionKey,
|
|
2264
|
+
entry
|
|
2265
|
+
}));
|
|
2266
|
+
})
|
|
1321
2267
|
.sort((left, right) => {
|
|
1322
2268
|
if (left.entry.updatedAt !== right.entry.updatedAt) {
|
|
1323
2269
|
return left.entry.updatedAt - right.entry.updatedAt;
|
|
@@ -1326,7 +2272,20 @@ function runLearnCommand(parsed) {
|
|
|
1326
2272
|
return left.store.indexPath.localeCompare(right.store.indexPath);
|
|
1327
2273
|
}
|
|
1328
2274
|
return left.sessionKey.localeCompare(right.sessionKey);
|
|
1329
|
-
})
|
|
2275
|
+
})
|
|
2276
|
+
.filter((() => {
|
|
2277
|
+
const seenSessionIds = new Set();
|
|
2278
|
+
return (session) => {
|
|
2279
|
+
const sessionId = session.entry.sessionId;
|
|
2280
|
+
if (sessionId !== undefined && seenSessionIds.has(sessionId)) {
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
if (sessionId !== undefined) {
|
|
2284
|
+
seenSessionIds.add(sessionId);
|
|
2285
|
+
}
|
|
2286
|
+
return true;
|
|
2287
|
+
};
|
|
2288
|
+
})());
|
|
1330
2289
|
for (const session of discoveredSessions) {
|
|
1331
2290
|
const sessionFile = session.entry.sessionFile;
|
|
1332
2291
|
const records = typeof sessionFile !== "string" || sessionFile.trim().length === 0
|
|
@@ -1348,24 +2307,75 @@ function runLearnCommand(parsed) {
|
|
|
1348
2307
|
});
|
|
1349
2308
|
nextSequence = sessionExport.nextSequence;
|
|
1350
2309
|
totalSessions += 1;
|
|
1351
|
-
totalInteractionEvents += sessionExport.interactionEvents.length;
|
|
1352
|
-
totalFeedbackEvents += sessionExport.feedbackEvents.length;
|
|
1353
2310
|
allInteractionEvents.push(...sessionExport.interactionEvents);
|
|
1354
2311
|
allFeedbackEvents.push(...sessionExport.feedbackEvents);
|
|
1355
2312
|
}
|
|
1356
|
-
const
|
|
2313
|
+
const seenInteractionIds = new Set();
|
|
2314
|
+
const dedupedInteractionEvents = [];
|
|
2315
|
+
for (const event of allInteractionEvents) {
|
|
2316
|
+
if (!seenInteractionIds.has(event.eventId)) {
|
|
2317
|
+
seenInteractionIds.add(event.eventId);
|
|
2318
|
+
dedupedInteractionEvents.push(event);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const seenFeedbackIds = new Set();
|
|
2322
|
+
const dedupedFeedbackEvents = [];
|
|
2323
|
+
for (const event of allFeedbackEvents) {
|
|
2324
|
+
if (!seenFeedbackIds.has(event.eventId)) {
|
|
2325
|
+
seenFeedbackIds.add(event.eventId);
|
|
2326
|
+
dedupedFeedbackEvents.push(event);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const totalEvents = dedupedInteractionEvents.length + dedupedFeedbackEvents.length;
|
|
2330
|
+
const now = new Date().toISOString();
|
|
2331
|
+
const normalizedEventExport = totalEvents === 0
|
|
2332
|
+
? null
|
|
2333
|
+
: buildNormalizedEventExport({
|
|
2334
|
+
interactionEvents: dedupedInteractionEvents,
|
|
2335
|
+
feedbackEvents: dedupedFeedbackEvents
|
|
2336
|
+
});
|
|
2337
|
+
const teacherArtifacts = normalizedEventExport === null
|
|
2338
|
+
? []
|
|
2339
|
+
: buildTeacherSupervisionArtifactsFromNormalizedEventExport({
|
|
2340
|
+
normalizedEventExport,
|
|
2341
|
+
observedAt: now
|
|
2342
|
+
});
|
|
2343
|
+
const labelFlow = normalizedEventExport === null
|
|
2344
|
+
? {
|
|
2345
|
+
source: "missing",
|
|
2346
|
+
humanLabelCount: 0,
|
|
2347
|
+
selfLabelCount: 0,
|
|
2348
|
+
asyncTeacherArtifactCount: 0,
|
|
2349
|
+
implicitPositiveCount: 0,
|
|
2350
|
+
detail: "no normalized learning export was built"
|
|
2351
|
+
}
|
|
2352
|
+
: summarizeNormalizedEventExportLabelFlow(normalizedEventExport, teacherArtifacts.length);
|
|
1357
2353
|
if (totalEvents === 0) {
|
|
1358
2354
|
if (parsed.json) {
|
|
1359
|
-
console.log(JSON.stringify({
|
|
2355
|
+
console.log(JSON.stringify({
|
|
2356
|
+
command: "learn",
|
|
2357
|
+
activationRoot,
|
|
2358
|
+
scannedSessions: totalSessions,
|
|
2359
|
+
newEvents: 0,
|
|
2360
|
+
loadedState: persistedState.loaded,
|
|
2361
|
+
statePath: learnStatePath,
|
|
2362
|
+
stateResetReason: persistedState.resetReason,
|
|
2363
|
+
materialized: null,
|
|
2364
|
+
promoted: false,
|
|
2365
|
+
graph: null,
|
|
2366
|
+
labelFlow,
|
|
2367
|
+
learningPath: summarizeLearningPathFromMaterialization(null),
|
|
2368
|
+
message: "No new session data. Brain is up to date."
|
|
2369
|
+
}));
|
|
1360
2370
|
}
|
|
1361
2371
|
else {
|
|
1362
2372
|
console.log("No new session data. Brain is up to date.");
|
|
1363
2373
|
}
|
|
1364
2374
|
return 0;
|
|
1365
2375
|
}
|
|
1366
|
-
|
|
1367
|
-
const
|
|
1368
|
-
const learnerResult =
|
|
2376
|
+
const learningExport = normalizedEventExport;
|
|
2377
|
+
const serveTimeLearning = resolveServeTimeLearningRuntimeInput(activationRoot);
|
|
2378
|
+
const learnerResult = drainAlwaysOnLearningRuntime({
|
|
1369
2379
|
packLabel: "learn-cli",
|
|
1370
2380
|
workspace: {
|
|
1371
2381
|
workspaceId: "learn-cli",
|
|
@@ -1374,17 +2384,58 @@ function runLearnCommand(parsed) {
|
|
|
1374
2384
|
rootDir: activationRoot,
|
|
1375
2385
|
revision: "learn-cli-v1"
|
|
1376
2386
|
},
|
|
1377
|
-
interactionEvents:
|
|
1378
|
-
feedbackEvents:
|
|
2387
|
+
interactionEvents: dedupedInteractionEvents,
|
|
2388
|
+
feedbackEvents: dedupedFeedbackEvents,
|
|
2389
|
+
teacherSupervisionArtifacts: teacherArtifacts,
|
|
1379
2390
|
learnedRouting: true,
|
|
1380
|
-
state:
|
|
1381
|
-
builtAt: now
|
|
2391
|
+
state: persistedState.state,
|
|
2392
|
+
builtAt: now,
|
|
2393
|
+
maxCycles: 16,
|
|
2394
|
+
pgVersion: serveTimeLearning.pgVersion,
|
|
2395
|
+
...(serveTimeLearning.decisionLogCount > 0 ? { serveTimeDecisions: serveTimeLearning.serveTimeDecisions } : {}),
|
|
2396
|
+
...(serveTimeLearning.baselineState !== undefined ? { baselineState: serveTimeLearning.baselineState } : {})
|
|
1382
2397
|
});
|
|
1383
|
-
|
|
1384
|
-
|
|
2398
|
+
const lastMaterialization = learnerResult.materializations.at(-1) ?? null;
|
|
2399
|
+
const plan = describeAlwaysOnLearningRuntimeState(learnerResult.state, lastMaterialization);
|
|
2400
|
+
const learningPath = summarizeLearningPathFromMaterialization(lastMaterialization);
|
|
2401
|
+
const supervisionCount = lastMaterialization?.candidate.summary.learnedRouter.supervisionCount ?? 0;
|
|
2402
|
+
const routerUpdateCount = lastMaterialization?.candidate.summary.learnedRouter.updateCount ?? 0;
|
|
2403
|
+
const routerNoOpReason = lastMaterialization?.candidate.summary.learnedRouter.noOpReason ?? null;
|
|
2404
|
+
const graphEvolution = lastMaterialization?.candidate.payloads.graph.evolution;
|
|
2405
|
+
const graphSummary = graphEvolution === undefined
|
|
2406
|
+
? null
|
|
2407
|
+
: {
|
|
2408
|
+
structuralOps: graphEvolution.structuralOps,
|
|
2409
|
+
connectDiagnostics: graphEvolution.connectDiagnostics ?? null
|
|
2410
|
+
};
|
|
2411
|
+
const connectSummary = graphSummary?.connectDiagnostics === null || graphSummary?.connectDiagnostics === undefined
|
|
2412
|
+
? ""
|
|
2413
|
+
: ` connect candidates=${graphSummary.connectDiagnostics.candidatePairCount} applied=${graphSummary.connectDiagnostics.appliedPairCount} edges=${graphSummary.connectDiagnostics.createdEdgeCount}.`;
|
|
2414
|
+
const routingBuild = lastMaterialization?.candidate.routingBuild ?? {
|
|
2415
|
+
learnedRoutingPath: serveTimeLearning.pgVersion === "v2" ? "policy_gradient_v2" : "policy_gradient_v1",
|
|
2416
|
+
pgVersionRequested: serveTimeLearning.pgVersion,
|
|
2417
|
+
pgVersionUsed: serveTimeLearning.pgVersion,
|
|
2418
|
+
decisionLogCount: serveTimeLearning.decisionLogCount,
|
|
2419
|
+
fallbackReason: serveTimeLearning.pgVersion === "v1" ? serveTimeLearning.fallbackReason ?? "no_serve_time_decisions" : null,
|
|
2420
|
+
updatedBaseline: null
|
|
2421
|
+
};
|
|
2422
|
+
const learnPathReport = {
|
|
2423
|
+
...routingBuild,
|
|
2424
|
+
fallbackReason: routingBuild.fallbackReason ??
|
|
2425
|
+
(routingBuild.pgVersionUsed === "v1" ? serveTimeLearning.fallbackReason ?? "no_serve_time_decisions" : null)
|
|
2426
|
+
};
|
|
2427
|
+
let promoted = false;
|
|
2428
|
+
let materializedPackId = null;
|
|
2429
|
+
let baselinePersisted = false;
|
|
2430
|
+
const latestTeacherFreshness = teacherArtifacts.length === 0
|
|
2431
|
+
? "none"
|
|
2432
|
+
: teacherArtifacts.some((artifact) => artifact.freshness.status === "fresh")
|
|
2433
|
+
? "fresh"
|
|
2434
|
+
: "stale";
|
|
2435
|
+
if (lastMaterialization !== null) {
|
|
1385
2436
|
const candidatePackRoot = path.join(activationRoot, "packs", `learn-cli-${Date.now()}`);
|
|
1386
2437
|
mkdirSync(candidatePackRoot, { recursive: true });
|
|
1387
|
-
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidatePackRoot,
|
|
2438
|
+
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidatePackRoot, lastMaterialization);
|
|
1388
2439
|
stageCandidatePack(activationRoot, candidatePackRoot, {
|
|
1389
2440
|
updatedAt: now,
|
|
1390
2441
|
reason: "learn_cli_stage"
|
|
@@ -1393,37 +2444,111 @@ function runLearnCommand(parsed) {
|
|
|
1393
2444
|
updatedAt: now,
|
|
1394
2445
|
reason: "learn_cli_promote"
|
|
1395
2446
|
});
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2447
|
+
if (learnPathReport.pgVersionUsed === "v2" && learnPathReport.updatedBaseline !== null) {
|
|
2448
|
+
persistBaseline(activationRoot, learnPathReport.updatedBaseline);
|
|
2449
|
+
baselinePersisted = true;
|
|
2450
|
+
}
|
|
2451
|
+
materializedPackId = candidateDescriptor.manifest.packId;
|
|
2452
|
+
promoted = true;
|
|
2453
|
+
}
|
|
2454
|
+
persistLearnCliState(learnerResult.state, now);
|
|
2455
|
+
writeJsonFile(teacherSnapshotPath, {
|
|
2456
|
+
runtimeOwner: "openclaw",
|
|
2457
|
+
queue: {
|
|
2458
|
+
capacity: 1,
|
|
2459
|
+
depth: 0,
|
|
2460
|
+
running: false
|
|
2461
|
+
},
|
|
2462
|
+
teacher: {
|
|
2463
|
+
artifactCount: teacherArtifacts.length,
|
|
2464
|
+
artifacts: teacherArtifacts,
|
|
2465
|
+
latestFreshness: latestTeacherFreshness
|
|
2466
|
+
},
|
|
2467
|
+
learner: {
|
|
2468
|
+
state: learnerResult.state,
|
|
2469
|
+
lastMaterialization
|
|
2470
|
+
},
|
|
2471
|
+
diagnostics: {
|
|
2472
|
+
acceptedExportCount: 1,
|
|
2473
|
+
processedExportCount: 1,
|
|
2474
|
+
duplicateExportCount: 0,
|
|
2475
|
+
droppedExportCount: 0,
|
|
2476
|
+
emittedArtifactCount: teacherArtifacts.length,
|
|
2477
|
+
dedupedArtifactCount: 0,
|
|
2478
|
+
lastProcessedAt: now,
|
|
2479
|
+
latestFreshness: latestTeacherFreshness,
|
|
2480
|
+
lastNoOpReason: teacherArtifacts.length === 0 ? "no_teacher_artifacts" : "none",
|
|
2481
|
+
notes: [
|
|
2482
|
+
`learn-cli export=${learningExport.provenance.exportDigest} range=${learningExport.range.start}-${learningExport.range.end}/${learningExport.range.count}`,
|
|
2483
|
+
`teacher artifacts=${teacherArtifacts.length} freshness=${latestTeacherFreshness}`,
|
|
2484
|
+
`last materialized pack=${materializedPackId ?? "none"}`
|
|
2485
|
+
]
|
|
2486
|
+
},
|
|
2487
|
+
state: {
|
|
2488
|
+
interactionEvents: learningExport.interactionEvents,
|
|
2489
|
+
feedbackEvents: learningExport.feedbackEvents,
|
|
2490
|
+
seenExportDigests: [learningExport.provenance.exportDigest]
|
|
2491
|
+
},
|
|
2492
|
+
runtime: {
|
|
2493
|
+
startedAt: now,
|
|
2494
|
+
lastHeartbeatAt: now,
|
|
2495
|
+
lastScanAt: now,
|
|
2496
|
+
scanRoot: null,
|
|
2497
|
+
lastAppliedMaterializationJobId: lastMaterialization?.jobId ?? null
|
|
1410
2498
|
}
|
|
2499
|
+
});
|
|
2500
|
+
const summaryMessage = materializedPackId === null
|
|
2501
|
+
? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no candidate materialized, no promotion.`
|
|
2502
|
+
: `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary}`;
|
|
2503
|
+
if (parsed.json) {
|
|
2504
|
+
console.log(JSON.stringify({
|
|
2505
|
+
command: "learn",
|
|
2506
|
+
activationRoot,
|
|
2507
|
+
scannedSessions: totalSessions,
|
|
2508
|
+
newEvents: totalEvents,
|
|
2509
|
+
loadedState: persistedState.loaded,
|
|
2510
|
+
statePath: learnStatePath,
|
|
2511
|
+
stateResetReason: persistedState.resetReason,
|
|
2512
|
+
drain: {
|
|
2513
|
+
cyclesRun: learnerResult.cycles.length,
|
|
2514
|
+
stopReason: learnerResult.stopReason,
|
|
2515
|
+
drained: learnerResult.drained,
|
|
2516
|
+
materializationCount: learnerResult.materializations.length
|
|
2517
|
+
},
|
|
2518
|
+
learner: {
|
|
2519
|
+
teacherBudget: learnerResult.state.sparseFeedback.teacherBudget,
|
|
2520
|
+
eligibleFeedbackCount: learnerResult.state.sparseFeedback.eligibleFeedbackCount,
|
|
2521
|
+
budgetedOutFeedbackCount: learnerResult.state.sparseFeedback.budgetedOutFeedbackCount,
|
|
2522
|
+
supervisionCount,
|
|
2523
|
+
routerUpdateCount,
|
|
2524
|
+
routerNoOpReason,
|
|
2525
|
+
pending: plan.pending,
|
|
2526
|
+
learnedRange: plan.learnedRange
|
|
2527
|
+
},
|
|
2528
|
+
materialized: materializedPackId,
|
|
2529
|
+
promoted,
|
|
2530
|
+
graph: graphSummary,
|
|
2531
|
+
labelFlow,
|
|
2532
|
+
learningPath,
|
|
2533
|
+
learnedRoutingPath: learnPathReport.learnedRoutingPath,
|
|
2534
|
+
pgVersionRequested: learnPathReport.pgVersionRequested,
|
|
2535
|
+
pgVersionUsed: learnPathReport.pgVersionUsed,
|
|
2536
|
+
decisionLogCount: learnPathReport.decisionLogCount,
|
|
2537
|
+
fallbackReason: learnPathReport.fallbackReason,
|
|
2538
|
+
baselinePersisted,
|
|
2539
|
+
message: summaryMessage
|
|
2540
|
+
}, null, 2));
|
|
1411
2541
|
}
|
|
1412
2542
|
else {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
}, null, 2));
|
|
1423
|
-
}
|
|
1424
|
-
else {
|
|
1425
|
-
console.log("No new session data. Brain is up to date.");
|
|
1426
|
-
}
|
|
2543
|
+
const text = materializedPackId === null
|
|
2544
|
+
? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no promotion. cycles=${learnerResult.cycles.length} stop=${learnerResult.stopReason} supervision=${supervisionCount}.`
|
|
2545
|
+
: `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary} cycles=${learnerResult.cycles.length} supervision=${supervisionCount}.`;
|
|
2546
|
+
console.log(text);
|
|
2547
|
+
console.log(`labels: source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`);
|
|
2548
|
+
console.log(`path: source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`);
|
|
2549
|
+
console.log(`learned routing: path=${learnPathReport.learnedRoutingPath} pg=${learnPathReport.pgVersionUsed ?? "n/a"} decisions=${learnPathReport.decisionLogCount}` +
|
|
2550
|
+
`${learnPathReport.fallbackReason === null ? "" : ` fallback=${learnPathReport.fallbackReason}`}` +
|
|
2551
|
+
`${learnPathReport.pgVersionUsed === "v2" ? ` baseline=${baselinePersisted ? "persisted" : "unchanged"}` : ""}`);
|
|
1427
2552
|
}
|
|
1428
2553
|
return 0;
|
|
1429
2554
|
}
|
|
@@ -1434,26 +2559,553 @@ function formatTimestamp() {
|
|
|
1434
2559
|
function watchLog(message) {
|
|
1435
2560
|
console.log(`${formatTimestamp()} ${message}`);
|
|
1436
2561
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
2562
|
+
function formatWatchError(error) {
|
|
2563
|
+
return error instanceof Error ? error.message : String(error);
|
|
2564
|
+
}
|
|
2565
|
+
function sanitizeWatchPathSegment(value) {
|
|
2566
|
+
const sanitized = value
|
|
2567
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
2568
|
+
.replace(/^-+|-+$/g, "")
|
|
2569
|
+
.slice(0, 96);
|
|
2570
|
+
return sanitized.length > 0 ? sanitized : "session";
|
|
2571
|
+
}
|
|
2572
|
+
function readOptionalJsonFile(filePath) {
|
|
2573
|
+
if (!existsSync(filePath)) {
|
|
2574
|
+
return null;
|
|
2575
|
+
}
|
|
2576
|
+
try {
|
|
2577
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
2578
|
+
}
|
|
2579
|
+
catch {
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
function writeJsonFile(filePath, value) {
|
|
2584
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2585
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
2586
|
+
}
|
|
2587
|
+
function loadWatchSessionTailCursor(cursorPath) {
|
|
2588
|
+
const parsed = readOptionalJsonFile(cursorPath);
|
|
2589
|
+
if (Array.isArray(parsed)) {
|
|
2590
|
+
return parsed;
|
|
2591
|
+
}
|
|
2592
|
+
if (parsed !== null && Array.isArray(parsed.cursor)) {
|
|
2593
|
+
return parsed.cursor;
|
|
2594
|
+
}
|
|
2595
|
+
return [];
|
|
2596
|
+
}
|
|
2597
|
+
function persistWatchSessionTailCursor(cursorPath, cursor) {
|
|
2598
|
+
writeJsonFile(cursorPath, {
|
|
2599
|
+
contract: "openclaw_watch_session_tail_cursor.v1",
|
|
2600
|
+
runtimeOwner: "openclaw",
|
|
2601
|
+
updatedAt: new Date().toISOString(),
|
|
2602
|
+
cursor
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
function countWatchCursorBridgedEvents(cursor) {
|
|
2606
|
+
return cursor.reduce((sum, entry) => sum + entry.bridgedEventCount, 0);
|
|
2607
|
+
}
|
|
2608
|
+
function listWatchRuntimeEventExportBundleRoots(scanRoot) {
|
|
2609
|
+
if (!existsSync(scanRoot)) {
|
|
2610
|
+
return [];
|
|
2611
|
+
}
|
|
2612
|
+
return readdirSync(scanRoot, { withFileTypes: true })
|
|
2613
|
+
.filter((entry) => entry.isDirectory())
|
|
2614
|
+
.map((entry) => path.join(scanRoot, entry.name))
|
|
2615
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2616
|
+
}
|
|
2617
|
+
async function replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot) {
|
|
2618
|
+
const seenExportDigests = new Set();
|
|
2619
|
+
const bundles = listWatchRuntimeEventExportBundleRoots(scanRoot)
|
|
2620
|
+
.map((rootDir) => {
|
|
2621
|
+
try {
|
|
2622
|
+
return loadRuntimeEventExportBundle(rootDir);
|
|
2623
|
+
}
|
|
2624
|
+
catch {
|
|
2625
|
+
return null;
|
|
2626
|
+
}
|
|
2627
|
+
})
|
|
2628
|
+
.filter((bundle) => bundle !== null)
|
|
2629
|
+
.sort((left, right) => {
|
|
2630
|
+
const exportedAtCompare = left.manifest.exportedAt.localeCompare(right.manifest.exportedAt);
|
|
2631
|
+
if (exportedAtCompare !== 0) {
|
|
2632
|
+
return exportedAtCompare;
|
|
2633
|
+
}
|
|
2634
|
+
if (left.normalizedEventExport.range.start !== right.normalizedEventExport.range.start) {
|
|
2635
|
+
return left.normalizedEventExport.range.start - right.normalizedEventExport.range.start;
|
|
2636
|
+
}
|
|
2637
|
+
if (left.normalizedEventExport.range.end !== right.normalizedEventExport.range.end) {
|
|
2638
|
+
return left.normalizedEventExport.range.end - right.normalizedEventExport.range.end;
|
|
2639
|
+
}
|
|
2640
|
+
return left.normalizedEventExport.provenance.exportDigest.localeCompare(right.normalizedEventExport.provenance.exportDigest);
|
|
2641
|
+
});
|
|
2642
|
+
let replayedBundleCount = 0;
|
|
2643
|
+
let replayedEventCount = 0;
|
|
2644
|
+
for (const bundle of bundles) {
|
|
2645
|
+
const exportDigest = bundle.normalizedEventExport.provenance.exportDigest;
|
|
2646
|
+
if (seenExportDigests.has(exportDigest)) {
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
2649
|
+
seenExportDigests.add(exportDigest);
|
|
2650
|
+
let enqueue = teacherLoop.enqueueNormalizedEventExport(bundle.normalizedEventExport, {
|
|
2651
|
+
observedAt: bundle.manifest.exportedAt
|
|
2652
|
+
});
|
|
2653
|
+
if (!enqueue.accepted && enqueue.reason === "queue_full") {
|
|
2654
|
+
await teacherLoop.flush();
|
|
2655
|
+
enqueue = teacherLoop.enqueueNormalizedEventExport(bundle.normalizedEventExport, {
|
|
2656
|
+
observedAt: bundle.manifest.exportedAt
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
if (!enqueue.accepted) {
|
|
2660
|
+
continue;
|
|
2661
|
+
}
|
|
2662
|
+
replayedBundleCount += 1;
|
|
2663
|
+
replayedEventCount += bundle.normalizedEventExport.range.count;
|
|
2664
|
+
}
|
|
2665
|
+
if (replayedBundleCount > 0) {
|
|
2666
|
+
await teacherLoop.flush();
|
|
2667
|
+
}
|
|
2668
|
+
return {
|
|
2669
|
+
replayedBundleCount,
|
|
2670
|
+
replayedEventCount
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
function exportLocalSessionTailChangesToScanRoot(input) {
|
|
2674
|
+
let exportedBundleCount = 0;
|
|
2675
|
+
let exportedEventCount = 0;
|
|
2676
|
+
const warnings = [];
|
|
2677
|
+
for (const change of input.changes) {
|
|
2678
|
+
if (change.scannedEventExport === null) {
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
const built = buildNormalizedEventExportFromScannedEvents(change.scannedEventExport);
|
|
2682
|
+
if (!built.ok) {
|
|
2683
|
+
warnings.push(`${change.sessionKey}: ${built.error}`);
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
const exportDigest = built.normalizedEventExport.provenance.exportDigest.replace(/^sha256-/u, "");
|
|
2687
|
+
const exportName = `session-tail-${sanitizeWatchPathSegment(change.sessionKey)}-${built.normalizedEventExport.range.start}-${built.normalizedEventExport.range.end}-${exportDigest.slice(0, 12)}`;
|
|
2688
|
+
const result = writeScannedEventExportBundle({
|
|
2689
|
+
rootDir: path.join(input.scanRoot, exportName),
|
|
2690
|
+
exportName,
|
|
2691
|
+
exportedAt: input.polledAt,
|
|
2692
|
+
scannedEventExport: change.scannedEventExport
|
|
2693
|
+
});
|
|
2694
|
+
if (!result.ok) {
|
|
2695
|
+
warnings.push(`${change.sessionKey}: ${result.error}`);
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2698
|
+
exportedBundleCount += 1;
|
|
2699
|
+
exportedEventCount += result.normalizedEventExport.range.count;
|
|
2700
|
+
}
|
|
2701
|
+
return {
|
|
2702
|
+
exportedBundleCount,
|
|
2703
|
+
exportedEventCount,
|
|
2704
|
+
warnings
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId) {
|
|
2708
|
+
const materialization = snapshot?.learner?.lastMaterialization ?? null;
|
|
2709
|
+
if (materialization === null) {
|
|
2710
|
+
return {
|
|
2711
|
+
lastHandledMaterializationPackId,
|
|
2712
|
+
logLine: null,
|
|
2713
|
+
materializedPackId: null,
|
|
2714
|
+
failure: null
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
const packId = typeof materialization?.candidate?.summary?.packId === "string"
|
|
2718
|
+
? materialization.candidate.summary.packId
|
|
2719
|
+
: null;
|
|
2720
|
+
if (packId === null || packId === lastHandledMaterializationPackId) {
|
|
2721
|
+
return {
|
|
2722
|
+
lastHandledMaterializationPackId,
|
|
2723
|
+
logLine: null,
|
|
2724
|
+
materializedPackId: packId,
|
|
2725
|
+
failure: null
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
|
|
2729
|
+
try {
|
|
2730
|
+
const candidateRootDir = path.resolve(activationRoot, "packs", packId);
|
|
2731
|
+
mkdirSync(candidateRootDir, { recursive: true });
|
|
2732
|
+
let activeBeforePack = null;
|
|
2733
|
+
try {
|
|
2734
|
+
activeBeforePack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
|
|
2735
|
+
}
|
|
2736
|
+
catch {
|
|
2737
|
+
activeBeforePack = null;
|
|
2738
|
+
}
|
|
2739
|
+
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
|
|
2740
|
+
appendLearningUpdateLogs({
|
|
2741
|
+
activationRoot,
|
|
2742
|
+
materialization,
|
|
2743
|
+
activeBeforePack,
|
|
2744
|
+
candidateDescriptor
|
|
2745
|
+
});
|
|
2746
|
+
const now = new Date().toISOString();
|
|
2747
|
+
stageCandidatePack(activationRoot, candidateRootDir, {
|
|
2748
|
+
updatedAt: now,
|
|
2749
|
+
reason: `watch_stage:${materialization.reason}:${materialization.lane}`
|
|
2750
|
+
});
|
|
2751
|
+
const inspection = inspectActivationState(activationRoot, now);
|
|
2752
|
+
if (inspection.promotion.allowed) {
|
|
2753
|
+
promoteCandidatePack(activationRoot, {
|
|
2754
|
+
updatedAt: now,
|
|
2755
|
+
reason: `watch_promote:${materialization.reason}:${materialization.lane}`
|
|
2756
|
+
});
|
|
2757
|
+
return {
|
|
2758
|
+
lastHandledMaterializationPackId: packId,
|
|
2759
|
+
materializedPackId: packId,
|
|
2760
|
+
logLine: `Promoted ${shortPackId} → active`,
|
|
2761
|
+
failure: null
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
return {
|
|
2765
|
+
lastHandledMaterializationPackId: packId,
|
|
2766
|
+
materializedPackId: packId,
|
|
2767
|
+
logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
|
|
2768
|
+
failure: null
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
catch (error) {
|
|
2772
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2773
|
+
return {
|
|
2774
|
+
lastHandledMaterializationPackId,
|
|
2775
|
+
materializedPackId: packId,
|
|
2776
|
+
logLine: `Promotion failed for ${shortPackId}: ${message}`,
|
|
2777
|
+
failure: {
|
|
2778
|
+
mode: "materialization_failed",
|
|
2779
|
+
detail: message,
|
|
2780
|
+
at: new Date().toISOString()
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
function resolveWatchTeacherLabelerConfig(input) {
|
|
2786
|
+
if (input !== undefined) {
|
|
2787
|
+
return {
|
|
2788
|
+
teacherLabeler: input,
|
|
2789
|
+
warnings: []
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
const providerConfig = readOpenClawBrainProviderConfig(process.env);
|
|
2793
|
+
const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_/u.test(warning));
|
|
2794
|
+
if (providerConfig.teacher.provider !== "ollama") {
|
|
2795
|
+
return {
|
|
2796
|
+
teacherLabeler: null,
|
|
2797
|
+
warnings
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
return {
|
|
2801
|
+
teacherLabeler: {
|
|
2802
|
+
provider: "ollama",
|
|
2803
|
+
baseUrl: providerConfig.teacherBaseUrl,
|
|
2804
|
+
model: providerConfig.teacher.model,
|
|
2805
|
+
...(providerConfig.teacher.timeoutMs === undefined ? {} : { timeoutMs: providerConfig.teacher.timeoutMs }),
|
|
2806
|
+
...(providerConfig.teacher.maxPromptChars === undefined ? {} : { maxPromptChars: providerConfig.teacher.maxPromptChars }),
|
|
2807
|
+
...(providerConfig.teacher.maxResponseChars === undefined ? {} : { maxResponseChars: providerConfig.teacher.maxResponseChars }),
|
|
2808
|
+
...(providerConfig.teacher.maxOutputTokens === undefined ? {} : { maxOutputTokens: providerConfig.teacher.maxOutputTokens }),
|
|
2809
|
+
...(providerConfig.teacher.maxArtifactsPerExport === undefined
|
|
2810
|
+
? {}
|
|
2811
|
+
: { maxArtifactsPerExport: providerConfig.teacher.maxArtifactsPerExport }),
|
|
2812
|
+
...(providerConfig.teacher.maxInteractionsPerExport === undefined
|
|
2813
|
+
? {}
|
|
2814
|
+
: { maxInteractionsPerExport: providerConfig.teacher.maxInteractionsPerExport })
|
|
2815
|
+
},
|
|
2816
|
+
warnings
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
export async function createWatchCommandRuntime(input) {
|
|
2820
|
+
const activationRoot = path.resolve(input.activationRoot);
|
|
2821
|
+
const bootstrapObservedAt = new Date().toISOString();
|
|
2822
|
+
const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
|
|
2823
|
+
? path.resolve(input.scanRoot)
|
|
1441
2824
|
: path.resolve(activationRoot, "event-exports");
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
2825
|
+
const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
|
|
2826
|
+
const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
|
|
2827
|
+
const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
|
|
2828
|
+
const log = input.log ?? watchLog;
|
|
2829
|
+
const startupWarnings = [];
|
|
2830
|
+
mkdirSync(scanRoot, { recursive: true });
|
|
2831
|
+
mkdirSync(resolveWatchStateRoot(activationRoot), { recursive: true });
|
|
2832
|
+
log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
|
|
2833
|
+
log(`Scan root: ${shortenPath(scanRoot)}`);
|
|
2834
|
+
log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
|
|
2835
|
+
const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler);
|
|
2836
|
+
const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
|
|
2837
|
+
for (const warning of resolvedTeacherLabeler.warnings) {
|
|
2838
|
+
startupWarnings.push(`teacher_env_warning:${warning}`);
|
|
2839
|
+
log(`Teacher env warning: ${warning}`);
|
|
2840
|
+
}
|
|
2841
|
+
if (teacherLabeler?.provider === "ollama") {
|
|
2842
|
+
log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
|
|
2843
|
+
}
|
|
1445
2844
|
const scanner = createRuntimeEventExportScanner({ scanRoot });
|
|
1446
|
-
|
|
2845
|
+
let lastServeTimeFallbackReason = null;
|
|
2846
|
+
const baseTeacherLoopInput = {
|
|
1447
2847
|
packLabel: "watch-cli",
|
|
1448
2848
|
workspace: {
|
|
1449
2849
|
workspaceId: "watch-cli",
|
|
1450
2850
|
snapshotId: `watch-cli@${new Date().toISOString().slice(0, 10)}`,
|
|
1451
2851
|
capturedAt: new Date().toISOString(),
|
|
1452
2852
|
rootDir: activationRoot,
|
|
1453
|
-
revision: "watch-cli-
|
|
2853
|
+
revision: "watch-cli-v2"
|
|
1454
2854
|
},
|
|
1455
|
-
learnedRouting: true
|
|
2855
|
+
learnedRouting: true,
|
|
2856
|
+
...(teacherLabeler !== null ? { teacherLabeler } : {}),
|
|
2857
|
+
resolveLearnedRoutingState: () => {
|
|
2858
|
+
const resolved = resolveServeTimeLearningRuntimeInput(activationRoot);
|
|
2859
|
+
if (resolved.fallbackReason !== null && resolved.fallbackReason !== lastServeTimeFallbackReason) {
|
|
2860
|
+
log(`Serve-time routing fallback: ${resolved.fallbackReason}`);
|
|
2861
|
+
}
|
|
2862
|
+
lastServeTimeFallbackReason = resolved.fallbackReason;
|
|
2863
|
+
return {
|
|
2864
|
+
pgVersion: resolved.pgVersion,
|
|
2865
|
+
...(resolved.decisionLogCount > 0 ? { serveTimeDecisions: resolved.serveTimeDecisions } : {}),
|
|
2866
|
+
...(resolved.baselineState !== undefined ? { baselineState: resolved.baselineState } : {})
|
|
2867
|
+
};
|
|
2868
|
+
},
|
|
2869
|
+
persistUpdatedBaseline: (state) => {
|
|
2870
|
+
try {
|
|
2871
|
+
persistBaseline(activationRoot, state);
|
|
2872
|
+
}
|
|
2873
|
+
catch (error) {
|
|
2874
|
+
log(`Baseline persist failed: ${formatWatchError(error)}`);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
let teacherLoop;
|
|
2879
|
+
let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
|
|
2880
|
+
if (restoredTeacherState.error !== null) {
|
|
2881
|
+
const message = restoredTeacherState.error;
|
|
2882
|
+
startupWarnings.push(`teacher_snapshot_reset:${message}`);
|
|
2883
|
+
lastHandledMaterializationPackId = null;
|
|
2884
|
+
log(`Teacher snapshot reset: ${message}`);
|
|
2885
|
+
teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
try {
|
|
2889
|
+
teacherLoop = createAsyncTeacherLiveLoop({
|
|
2890
|
+
...baseTeacherLoopInput,
|
|
2891
|
+
...(restoredTeacherState.snapshot !== null ? { resumeFromSnapshot: restoredTeacherState.snapshot } : {})
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
catch (error) {
|
|
2895
|
+
const message = formatWatchError(error);
|
|
2896
|
+
startupWarnings.push(`teacher_snapshot_reset:${message}`);
|
|
2897
|
+
lastHandledMaterializationPackId = null;
|
|
2898
|
+
log(`Teacher snapshot reset: ${message}`);
|
|
2899
|
+
teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (restoredTeacherState.snapshot !== null && startupWarnings.length === 0) {
|
|
2903
|
+
const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
|
|
2904
|
+
log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
|
|
2905
|
+
}
|
|
2906
|
+
let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
|
|
2907
|
+
let localSessionTail;
|
|
2908
|
+
try {
|
|
2909
|
+
localSessionTail = createOpenClawLocalSessionTail({
|
|
2910
|
+
...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
|
|
2911
|
+
cursor: restoredCursor,
|
|
2912
|
+
emitExistingOnFirstPoll: restoredCursor.length === 0
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
catch (error) {
|
|
2916
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2917
|
+
log(`Session tail cursor reset: ${message}`);
|
|
2918
|
+
restoredCursor = [];
|
|
2919
|
+
localSessionTail = createOpenClawLocalSessionTail({
|
|
2920
|
+
...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
|
|
2921
|
+
emitExistingOnFirstPoll: true
|
|
2922
|
+
});
|
|
2923
|
+
persistWatchSessionTailCursor(sessionTailCursorPath, []);
|
|
2924
|
+
}
|
|
2925
|
+
let replayState = {
|
|
2926
|
+
replayedBundleCount: 0,
|
|
2927
|
+
replayedEventCount: 0
|
|
2928
|
+
};
|
|
2929
|
+
try {
|
|
2930
|
+
replayState = await replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot);
|
|
2931
|
+
}
|
|
2932
|
+
catch (error) {
|
|
2933
|
+
const message = formatWatchError(error);
|
|
2934
|
+
startupWarnings.push(`teacher_replay_failed:${message}`);
|
|
2935
|
+
log(`Async teacher replay fail-open: ${message}`);
|
|
2936
|
+
}
|
|
2937
|
+
if (replayState.replayedBundleCount > 0) {
|
|
2938
|
+
log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
|
|
2939
|
+
}
|
|
2940
|
+
let bootstrapSnapshot = teacherLoop.snapshot();
|
|
2941
|
+
const replayPromotion = applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId);
|
|
2942
|
+
lastHandledMaterializationPackId = replayPromotion.lastHandledMaterializationPackId;
|
|
2943
|
+
if (replayPromotion.logLine !== null) {
|
|
2944
|
+
log(replayPromotion.logLine);
|
|
2945
|
+
bootstrapSnapshot = teacherLoop.snapshot();
|
|
2946
|
+
}
|
|
2947
|
+
const bootstrapCursor = localSessionTail.snapshot();
|
|
2948
|
+
persistWatchTeacherSnapshot(teacherSnapshotPath, {
|
|
2949
|
+
lastRunAt: bootstrapObservedAt,
|
|
2950
|
+
scanRoot,
|
|
2951
|
+
sessionTailCursorPath,
|
|
2952
|
+
sessionTailCursorUpdatedAt: bootstrapObservedAt,
|
|
2953
|
+
sessionTailSessionsTracked: bootstrapCursor.length,
|
|
2954
|
+
sessionTailBridgedEventCount: countWatchCursorBridgedEvents(bootstrapCursor),
|
|
2955
|
+
scannerCheckpointPath: scanner.checkpointPath,
|
|
2956
|
+
scannerCheckpoint: scanner.snapshot(),
|
|
2957
|
+
replayedBundleCount: replayState.replayedBundleCount,
|
|
2958
|
+
replayedEventCount: replayState.replayedEventCount,
|
|
2959
|
+
exportedBundleCount: 0,
|
|
2960
|
+
exportedEventCount: 0,
|
|
2961
|
+
startupWarnings,
|
|
2962
|
+
lastTeacherError: null,
|
|
2963
|
+
localSessionTailNoopReason: null,
|
|
2964
|
+
lastHandledMaterializationPackId,
|
|
2965
|
+
failure: replayPromotion.failure,
|
|
2966
|
+
snapshot: bootstrapSnapshot
|
|
2967
|
+
});
|
|
2968
|
+
return {
|
|
2969
|
+
activationRoot,
|
|
2970
|
+
scanRoot,
|
|
2971
|
+
sessionTailCursorPath,
|
|
2972
|
+
teacherSnapshotPath,
|
|
2973
|
+
startupWarnings,
|
|
2974
|
+
lastTeacherError: null,
|
|
2975
|
+
replayState,
|
|
2976
|
+
lastHandledMaterializationPackId,
|
|
2977
|
+
scanner,
|
|
2978
|
+
teacherLoop,
|
|
2979
|
+
localSessionTail
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
export async function runWatchCommandPass(runtime, options = {}) {
|
|
2983
|
+
const log = options.log ?? watchLog;
|
|
2984
|
+
const observedAt = options.observedAt ?? new Date().toISOString();
|
|
2985
|
+
const localPoll = runtime.localSessionTail.pollOnce({
|
|
2986
|
+
observedAt
|
|
2987
|
+
});
|
|
2988
|
+
const scannerCheckpointBeforeScan = runtime.scanner.snapshot();
|
|
2989
|
+
const exported = exportLocalSessionTailChangesToScanRoot({
|
|
2990
|
+
scanRoot: runtime.scanRoot,
|
|
2991
|
+
polledAt: localPoll.polledAt,
|
|
2992
|
+
changes: localPoll.changes
|
|
2993
|
+
});
|
|
2994
|
+
persistWatchSessionTailCursor(runtime.sessionTailCursorPath, localPoll.cursor);
|
|
2995
|
+
for (const warning of [...localPoll.warnings, ...exported.warnings]) {
|
|
2996
|
+
log(`Session tail warning: ${warning}`);
|
|
2997
|
+
}
|
|
2998
|
+
if (exported.exportedBundleCount > 0) {
|
|
2999
|
+
log(`Session tail exported ${exported.exportedBundleCount} bundle${exported.exportedBundleCount === 1 ? "" : "s"} from ${localPoll.changes.length} changed session${localPoll.changes.length === 1 ? "" : "s"}`);
|
|
3000
|
+
}
|
|
3001
|
+
const scanResult = runtime.scanner.scanOnce({
|
|
3002
|
+
scannedAt: observedAt
|
|
1456
3003
|
});
|
|
3004
|
+
const totalSelected = scanResult.selected.length;
|
|
3005
|
+
const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
|
|
3006
|
+
let snapshot = runtime.teacherLoop.snapshot();
|
|
3007
|
+
let materializedPackId = null;
|
|
3008
|
+
let failure = null;
|
|
3009
|
+
if (totalSelected === 0) {
|
|
3010
|
+
log("Scanning... no changes");
|
|
3011
|
+
}
|
|
3012
|
+
else {
|
|
3013
|
+
log(`Scanning... ${totalSelected} export bundle${totalSelected === 1 ? "" : "s"} selected, ${totalEvents} event${totalEvents === 1 ? "" : "s"}`);
|
|
3014
|
+
try {
|
|
3015
|
+
const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
|
|
3016
|
+
runtime.lastTeacherError = null;
|
|
3017
|
+
snapshot = ingestResult.snapshot;
|
|
3018
|
+
const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
|
|
3019
|
+
runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
|
|
3020
|
+
materializedPackId = promotion.materializedPackId;
|
|
3021
|
+
failure = promotion.failure;
|
|
3022
|
+
if (promotion.logLine !== null) {
|
|
3023
|
+
log(promotion.logLine);
|
|
3024
|
+
snapshot = runtime.teacherLoop.snapshot();
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
catch (error) {
|
|
3028
|
+
const message = formatWatchError(error);
|
|
3029
|
+
runtime.lastTeacherError = message;
|
|
3030
|
+
failure = {
|
|
3031
|
+
mode: "teacher_fail_open",
|
|
3032
|
+
detail: message,
|
|
3033
|
+
at: observedAt
|
|
3034
|
+
};
|
|
3035
|
+
log(`Async teacher fail-open: ${message}`);
|
|
3036
|
+
try {
|
|
3037
|
+
runtime.scanner.restoreCheckpoint(scannerCheckpointBeforeScan);
|
|
3038
|
+
}
|
|
3039
|
+
catch (restoreError) {
|
|
3040
|
+
const restoreMessage = formatWatchError(restoreError);
|
|
3041
|
+
runtime.lastTeacherError = `${message}; scanner checkpoint restore failed: ${restoreMessage}`;
|
|
3042
|
+
failure = {
|
|
3043
|
+
mode: "teacher_fail_open",
|
|
3044
|
+
detail: runtime.lastTeacherError,
|
|
3045
|
+
at: observedAt
|
|
3046
|
+
};
|
|
3047
|
+
log(`Scanner checkpoint restore failed: ${restoreMessage}`);
|
|
3048
|
+
}
|
|
3049
|
+
snapshot = runtime.teacherLoop.snapshot();
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
|
|
3053
|
+
lastRunAt: observedAt,
|
|
3054
|
+
scanRoot: runtime.scanRoot,
|
|
3055
|
+
sessionTailCursorPath: runtime.sessionTailCursorPath,
|
|
3056
|
+
sessionTailCursorUpdatedAt: observedAt,
|
|
3057
|
+
sessionTailSessionsTracked: localPoll.cursor.length,
|
|
3058
|
+
sessionTailBridgedEventCount: countWatchCursorBridgedEvents(localPoll.cursor),
|
|
3059
|
+
scannerCheckpointPath: runtime.scanner.checkpointPath,
|
|
3060
|
+
scannerCheckpoint: runtime.scanner.snapshot(),
|
|
3061
|
+
replayedBundleCount: runtime.replayState.replayedBundleCount,
|
|
3062
|
+
replayedEventCount: runtime.replayState.replayedEventCount,
|
|
3063
|
+
exportedBundleCount: exported.exportedBundleCount,
|
|
3064
|
+
exportedEventCount: exported.exportedEventCount,
|
|
3065
|
+
startupWarnings: runtime.startupWarnings,
|
|
3066
|
+
lastTeacherError: runtime.lastTeacherError,
|
|
3067
|
+
localSessionTailNoopReason: localPoll.noopReason,
|
|
3068
|
+
lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
|
|
3069
|
+
failure,
|
|
3070
|
+
snapshot
|
|
3071
|
+
});
|
|
3072
|
+
const persistedScannerCheckpoint = runtime.scanner.snapshot();
|
|
3073
|
+
if (options.json) {
|
|
3074
|
+
console.log(JSON.stringify({
|
|
3075
|
+
timestamp: observedAt,
|
|
3076
|
+
replayedBundles: runtime.replayState.replayedBundleCount,
|
|
3077
|
+
replayedEvents: runtime.replayState.replayedEventCount,
|
|
3078
|
+
exportedBundles: exported.exportedBundleCount,
|
|
3079
|
+
exportedEvents: exported.exportedEventCount,
|
|
3080
|
+
selected: totalSelected,
|
|
3081
|
+
events: totalEvents,
|
|
3082
|
+
live: scanResult.live.length,
|
|
3083
|
+
backfill: scanResult.backfill.length,
|
|
3084
|
+
sessionTailSessionsTracked: localPoll.cursor.length,
|
|
3085
|
+
sessionTailBridgedEvents: countWatchCursorBridgedEvents(localPoll.cursor),
|
|
3086
|
+
scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
|
|
3087
|
+
scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
|
|
3088
|
+
materialized: materializedPackId,
|
|
3089
|
+
diagnostics: snapshot.diagnostics ?? null,
|
|
3090
|
+
localSessionTailNoopReason: localPoll.noopReason
|
|
3091
|
+
}));
|
|
3092
|
+
}
|
|
3093
|
+
return {
|
|
3094
|
+
localPoll,
|
|
3095
|
+
exported,
|
|
3096
|
+
scanResult,
|
|
3097
|
+
snapshot,
|
|
3098
|
+
materializedPackId
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
async function runWatchCommand(parsed) {
|
|
3102
|
+
const intervalMs = parsed.interval * 1000;
|
|
3103
|
+
const runtime = await createWatchCommandRuntime({
|
|
3104
|
+
activationRoot: parsed.activationRoot,
|
|
3105
|
+
scanRoot: parsed.scanRoot,
|
|
3106
|
+
log: watchLog
|
|
3107
|
+
});
|
|
3108
|
+
watchLog(`Interval: ${parsed.interval}s`);
|
|
1457
3109
|
let stopping = false;
|
|
1458
3110
|
const onSignal = () => {
|
|
1459
3111
|
if (stopping) {
|
|
@@ -1466,69 +3118,15 @@ async function runWatchCommand(parsed) {
|
|
|
1466
3118
|
process.on("SIGTERM", onSignal);
|
|
1467
3119
|
while (!stopping) {
|
|
1468
3120
|
try {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
if (totalSelected === 0) {
|
|
1474
|
-
watchLog("Scanning... no changes");
|
|
1475
|
-
}
|
|
1476
|
-
else {
|
|
1477
|
-
const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
|
|
1478
|
-
watchLog(`Scanning... ${totalSelected} session${totalSelected === 1 ? "" : "s"} changed, ${totalEvents} new event${totalEvents === 1 ? "" : "s"}`);
|
|
1479
|
-
// Feed exports into teacher/learner pipeline
|
|
1480
|
-
const ingestResult = await teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
|
|
1481
|
-
const snapshot = ingestResult.snapshot;
|
|
1482
|
-
const materialization = snapshot.learner.lastMaterialization;
|
|
1483
|
-
if (materialization !== null) {
|
|
1484
|
-
const packId = materialization.candidate.summary.packId;
|
|
1485
|
-
const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
|
|
1486
|
-
watchLog(`Learning: materialized ${shortPackId}`);
|
|
1487
|
-
// Attempt stage + promote
|
|
1488
|
-
try {
|
|
1489
|
-
const candidateRootDir = path.resolve(activationRoot, "packs", packId);
|
|
1490
|
-
mkdirSync(candidateRootDir, { recursive: true });
|
|
1491
|
-
materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
|
|
1492
|
-
const now = new Date().toISOString();
|
|
1493
|
-
stageCandidatePack(activationRoot, candidateRootDir, {
|
|
1494
|
-
updatedAt: now,
|
|
1495
|
-
reason: `watch_stage:${materialization.reason}:${materialization.lane}`
|
|
1496
|
-
});
|
|
1497
|
-
const inspection = inspectActivationState(activationRoot, now);
|
|
1498
|
-
if (inspection.promotion.allowed) {
|
|
1499
|
-
promoteCandidatePack(activationRoot, {
|
|
1500
|
-
updatedAt: now,
|
|
1501
|
-
reason: `watch_promote:${materialization.reason}:${materialization.lane}`
|
|
1502
|
-
});
|
|
1503
|
-
watchLog(`Promoted ${shortPackId} → active`);
|
|
1504
|
-
}
|
|
1505
|
-
else {
|
|
1506
|
-
watchLog(`Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
catch (error) {
|
|
1510
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1511
|
-
watchLog(`Promotion failed: ${message}`);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
if (parsed.json) {
|
|
1515
|
-
console.log(JSON.stringify({
|
|
1516
|
-
timestamp: new Date().toISOString(),
|
|
1517
|
-
selected: totalSelected,
|
|
1518
|
-
events: totalEvents,
|
|
1519
|
-
live: liveCount,
|
|
1520
|
-
backfill: backfillCount,
|
|
1521
|
-
materialized: materialization?.candidate.summary.packId ?? null,
|
|
1522
|
-
diagnostics: snapshot.diagnostics
|
|
1523
|
-
}));
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
3121
|
+
await runWatchCommandPass(runtime, {
|
|
3122
|
+
json: parsed.json,
|
|
3123
|
+
log: watchLog
|
|
3124
|
+
});
|
|
1526
3125
|
}
|
|
1527
3126
|
catch (error) {
|
|
1528
3127
|
const message = error instanceof Error ? error.message : String(error);
|
|
1529
3128
|
watchLog(`Error: ${message}`);
|
|
1530
3129
|
}
|
|
1531
|
-
// Wait for the next interval, checking for stop signal periodically
|
|
1532
3130
|
const deadline = Date.now() + intervalMs;
|
|
1533
3131
|
while (!stopping && Date.now() < deadline) {
|
|
1534
3132
|
await new Promise((resolve) => {
|
|
@@ -1590,12 +3188,13 @@ function resetActivationRoot(activationRoot) {
|
|
|
1590
3188
|
function runResetCommand(parsed) {
|
|
1591
3189
|
if (parsed.help) {
|
|
1592
3190
|
console.log([
|
|
1593
|
-
"Usage: openclawbrain reset [--activation-root <path>] [--yes] [--json]",
|
|
3191
|
+
"Usage: openclawbrain reset [--activation-root <path>|--openclaw-home <path>] [--yes] [--json]",
|
|
1594
3192
|
"",
|
|
1595
3193
|
"Wipes all learned state and returns the brain to seed state.",
|
|
1596
3194
|
"",
|
|
1597
3195
|
"Options:",
|
|
1598
3196
|
" --activation-root <path> Activation root (auto-detected if omitted)",
|
|
3197
|
+
" --openclaw-home <path> Pin auto-detection to one installed OpenClaw profile",
|
|
1599
3198
|
" --yes, -y Skip confirmation prompt",
|
|
1600
3199
|
" --json Emit machine-readable JSON output",
|
|
1601
3200
|
" --help Show this help"
|
|
@@ -1646,7 +3245,7 @@ function runResetCommand(parsed) {
|
|
|
1646
3245
|
}
|
|
1647
3246
|
console.log(" Activation pointers reset to seed state.");
|
|
1648
3247
|
console.log(`\nBrain at ${shortenPath(activationRoot)} is now in seed state.`);
|
|
1649
|
-
console.log(
|
|
3248
|
+
console.log(`Run \`openclawbrain status --activation-root ${quoteShellArg(activationRoot)}\` to verify.`);
|
|
1650
3249
|
}
|
|
1651
3250
|
return 0;
|
|
1652
3251
|
}
|
|
@@ -1721,8 +3320,14 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
1721
3320
|
});
|
|
1722
3321
|
return 0;
|
|
1723
3322
|
}
|
|
1724
|
-
if (parsed.command === "
|
|
1725
|
-
return
|
|
3323
|
+
if (parsed.command === "install") {
|
|
3324
|
+
return runInstallCommand(parsed);
|
|
3325
|
+
}
|
|
3326
|
+
if (parsed.command === "detach") {
|
|
3327
|
+
return runDetachCommand(parsed);
|
|
3328
|
+
}
|
|
3329
|
+
if (parsed.command === "uninstall") {
|
|
3330
|
+
return runUninstallCommand(parsed);
|
|
1726
3331
|
}
|
|
1727
3332
|
if (parsed.command === "attach") {
|
|
1728
3333
|
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
@@ -1786,7 +3391,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
1786
3391
|
}
|
|
1787
3392
|
// At this point only status/rollback commands remain
|
|
1788
3393
|
const statusOrRollback = parsed;
|
|
1789
|
-
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.command);
|
|
3394
|
+
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
|
|
1790
3395
|
if (statusOrRollback.command === "rollback") {
|
|
1791
3396
|
const result = rollbackRuntimeAttach({
|
|
1792
3397
|
activationRoot,
|
|
@@ -1801,18 +3406,17 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
1801
3406
|
}
|
|
1802
3407
|
return result.allowed ? 0 : 1;
|
|
1803
3408
|
}
|
|
1804
|
-
const
|
|
3409
|
+
const operatorInput = {
|
|
1805
3410
|
...statusOrRollback.input,
|
|
1806
|
-
activationRoot
|
|
1807
|
-
|
|
3411
|
+
activationRoot,
|
|
3412
|
+
teacherSnapshotPath: resolveOperatorTeacherSnapshotPath(activationRoot, statusOrRollback.input.teacherSnapshotPath)
|
|
3413
|
+
};
|
|
3414
|
+
const status = describeCurrentProfileBrainStatus(operatorInput);
|
|
1808
3415
|
if (statusOrRollback.json) {
|
|
1809
3416
|
console.log(JSON.stringify(status, null, 2));
|
|
1810
3417
|
}
|
|
1811
3418
|
else {
|
|
1812
|
-
const report = buildOperatorSurfaceReport(
|
|
1813
|
-
...statusOrRollback.input,
|
|
1814
|
-
activationRoot
|
|
1815
|
-
});
|
|
3419
|
+
const report = buildOperatorSurfaceReport(operatorInput);
|
|
1816
3420
|
if (statusOrRollback.detailed) {
|
|
1817
3421
|
console.log(formatCurrentProfileStatusSummary(status, report));
|
|
1818
3422
|
}
|