@openclawbrain/openclaw 0.2.3 → 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/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 +8 -4
- package/dist/src/cli.js +501 -154
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +7 -4
- package/dist/src/daemon.js +275 -47
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/index.d.ts +150 -2
- package/dist/src/index.js +769 -139
- package/dist/src/index.js.map +1 -1
- package/dist/src/learning-spine.d.ts +2 -1
- package/dist/src/learning-spine.js +8 -0
- package/dist/src/learning-spine.js.map +1 -1
- package/dist/src/ollama-client.d.ts +46 -0
- package/dist/src/ollama-client.js +231 -0
- package/dist/src/ollama-client.js.map +1 -0
- package/dist/src/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 +68 -21
- package/dist/src/resolve-activation-root.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 +5 -1
- package/extension/runtime-guard.ts +17 -2
- package/package.json +8 -7
package/dist/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
2
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
3
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";
|
|
@@ -9,11 +9,13 @@ import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
|
|
|
9
9
|
import { exportBrain, importBrain } from "./import-export.js";
|
|
10
10
|
import { buildNormalizedEventExport } from "@openclawbrain/contracts";
|
|
11
11
|
import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
|
|
12
|
-
import { inspectActivationState, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
|
|
12
|
+
import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
|
|
13
13
|
import { resolveActivationRoot } from "./resolve-activation-root.js";
|
|
14
|
-
import { buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadRuntimeEventExportBundle, rollbackRuntimeAttach, resolveAsyncTeacherLiveLoopSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } 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";
|
|
15
16
|
import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
|
|
16
17
|
import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
18
|
+
import { readOpenClawBrainProviderConfig } from "./provider-config.js";
|
|
17
19
|
function quoteShellArg(value) {
|
|
18
20
|
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
19
21
|
}
|
|
@@ -27,7 +29,7 @@ function normalizeOptionalCliString(value) {
|
|
|
27
29
|
function getCliHomeDir() {
|
|
28
30
|
return process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
29
31
|
}
|
|
30
|
-
function
|
|
32
|
+
function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
|
|
31
33
|
const resolvedHomeDir = path.resolve(homeDir);
|
|
32
34
|
let entries;
|
|
33
35
|
try {
|
|
@@ -42,7 +44,7 @@ function discoverSetupCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
|
|
|
42
44
|
.filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
|
|
43
45
|
.sort((left, right) => left.localeCompare(right));
|
|
44
46
|
}
|
|
45
|
-
function
|
|
47
|
+
function formatInstallOpenClawHomeSource(source) {
|
|
46
48
|
switch (source) {
|
|
47
49
|
case "explicit":
|
|
48
50
|
return "--openclaw-home";
|
|
@@ -54,7 +56,7 @@ function formatSetupOpenClawHomeSource(source) {
|
|
|
54
56
|
return source;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
function
|
|
59
|
+
function resolveInstallOpenClawHome(explicitOpenclawHome) {
|
|
58
60
|
const normalizedExplicitHome = normalizeOptionalCliString(explicitOpenclawHome);
|
|
59
61
|
if (normalizedExplicitHome !== null) {
|
|
60
62
|
return {
|
|
@@ -69,7 +71,7 @@ function resolveSetupOpenClawHome(explicitOpenclawHome) {
|
|
|
69
71
|
openclawHomeSource: "env"
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
|
-
const discoveredHomes =
|
|
74
|
+
const discoveredHomes = discoverInstallCandidateOpenClawHomes();
|
|
73
75
|
if (discoveredHomes.length === 1) {
|
|
74
76
|
return {
|
|
75
77
|
openclawHome: path.resolve(discoveredHomes[0]),
|
|
@@ -85,14 +87,14 @@ function resolveSetupOpenClawHome(explicitOpenclawHome) {
|
|
|
85
87
|
})
|
|
86
88
|
.join("\n");
|
|
87
89
|
throw new Error([
|
|
88
|
-
"Refusing ambiguous live OpenClaw targets for install
|
|
90
|
+
"Refusing ambiguous live OpenClaw targets for install.",
|
|
89
91
|
targetChoices,
|
|
90
92
|
"Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one profile."
|
|
91
93
|
].join("\n"));
|
|
92
94
|
}
|
|
93
95
|
throw new Error("No OpenClaw profile home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
|
|
94
96
|
}
|
|
95
|
-
function
|
|
97
|
+
function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
|
|
96
98
|
const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
|
|
97
99
|
if (normalizedExplicitActivationRoot !== null) {
|
|
98
100
|
return {
|
|
@@ -105,7 +107,7 @@ function resolveSetupActivationRoot(openclawHome, explicitActivationRoot) {
|
|
|
105
107
|
source: "default_from_openclaw_home"
|
|
106
108
|
};
|
|
107
109
|
}
|
|
108
|
-
function
|
|
110
|
+
function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
109
111
|
const normalizedExplicitWorkspaceId = normalizeOptionalCliString(explicitWorkspaceId);
|
|
110
112
|
if (normalizedExplicitWorkspaceId !== null) {
|
|
111
113
|
return {
|
|
@@ -124,7 +126,7 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
catch {
|
|
127
|
-
// Fall back to the profile-home name when
|
|
129
|
+
// Fall back to the profile-home name when install is pointed at an incomplete or not-yet-readable profile.
|
|
128
130
|
}
|
|
129
131
|
const dirName = path.basename(openclawHome);
|
|
130
132
|
if (dirName === ".openclaw") {
|
|
@@ -145,13 +147,13 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
|
145
147
|
source: "fallback"
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
|
-
function
|
|
150
|
+
function formatInstallActivationRootSource(source) {
|
|
149
151
|
if (source === "explicit") {
|
|
150
152
|
return "explicit --activation-root";
|
|
151
153
|
}
|
|
152
154
|
return "default beside --openclaw-home";
|
|
153
155
|
}
|
|
154
|
-
function
|
|
156
|
+
function formatInstallWorkspaceIdSource(source) {
|
|
155
157
|
switch (source) {
|
|
156
158
|
case "explicit":
|
|
157
159
|
return "explicit --workspace-id";
|
|
@@ -262,7 +264,7 @@ function buildDoctorDeletedMessage(args) {
|
|
|
262
264
|
const jsonCommand = buildStatusReplacementCommand(replacementInput, true);
|
|
263
265
|
const lines = [
|
|
264
266
|
"`doctor` is no longer a separate operator surface.",
|
|
265
|
-
'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.',
|
|
266
268
|
"Use `describeAttachStatus()` or the proof helpers only when you need deeper activation diagnostics."
|
|
267
269
|
];
|
|
268
270
|
if (json && jsonCommand !== null) {
|
|
@@ -276,36 +278,42 @@ function buildDoctorDeletedMessage(args) {
|
|
|
276
278
|
}
|
|
277
279
|
return lines.join(" ");
|
|
278
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
|
+
}
|
|
279
288
|
function operatorCliHelp() {
|
|
280
289
|
return [
|
|
281
290
|
"Usage:",
|
|
282
291
|
" openclawbrain install [--openclaw-home <path>] [options]",
|
|
283
|
-
" openclawbrain setup [--openclaw-home <path>] [options] # compatibility alias",
|
|
284
292
|
" openclawbrain detach --openclaw-home <path> [options]",
|
|
285
293
|
" openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
|
|
286
294
|
" openclawbrain attach --activation-root <path> [options]",
|
|
287
|
-
" openclawbrain <status|rollback> --activation-root <path> [options]",
|
|
288
|
-
" openclawbrain context \"message\" [--activation-root <path>]",
|
|
289
|
-
" 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]",
|
|
290
298
|
" openclawbrain scan --session <trace.json> --root <path> [options]",
|
|
291
299
|
" openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
|
|
292
|
-
" openclawbrain learn [--activation-root <path>] [--json]",
|
|
293
|
-
" openclawbrain watch
|
|
294
|
-
" openclawbrain daemon <start|stop|status|logs>
|
|
295
|
-
" 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",
|
|
296
304
|
" openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
|
|
297
305
|
"",
|
|
298
306
|
"Options:",
|
|
299
|
-
" --openclaw-home <path> OpenClaw profile home dir for install/
|
|
300
|
-
" --shared Set brain-attachment-policy to shared instead of dedicated (install
|
|
301
|
-
" --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.",
|
|
302
310
|
" --keep-data Preserve activation data on uninstall; detach always behaves this way.",
|
|
303
311
|
" --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
|
|
304
312
|
" --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
|
|
305
313
|
" --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
|
|
306
|
-
" --workspace-id <id> Workspace identifier for attach/install
|
|
314
|
+
" --workspace-id <id> Workspace identifier for attach/install provenance; install defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
|
|
307
315
|
" --event-export <path> Event-export bundle root or normalized export JSON payload.",
|
|
308
|
-
" --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.",
|
|
309
317
|
" --updated-at <iso> Observation time to use for freshness checks.",
|
|
310
318
|
" --brain-attachment-policy <undeclared|dedicated|shared> Override attachment policy semantics for status inspection.",
|
|
311
319
|
" --detailed Show verbose diagnostic output for status (default is human-friendly summary).",
|
|
@@ -329,14 +337,15 @@ function operatorCliHelp() {
|
|
|
329
337
|
" 0. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data — remove the profile hook and choose the data outcome explicitly",
|
|
330
338
|
" 0. context openclawbrain context \"hello\" — preview the brain context that would be injected for a message",
|
|
331
339
|
" 0. attach openclawbrain attach --activation-root <path>",
|
|
332
|
-
" 1. status answer \"How's the brain?\" for
|
|
333
|
-
" 2. status --json read the canonical current_profile_brain_status.v1 object
|
|
334
|
-
" 3. rollback --dry-run preview active <- previous, active -> candidate",
|
|
335
|
-
" 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",
|
|
336
344
|
" 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
|
|
337
345
|
" 6. scan --live scan one live event export into teacher/learner state without claiming a daemon is running",
|
|
338
346
|
" status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
|
|
339
|
-
" watch/daemon persist
|
|
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",
|
|
340
349
|
"",
|
|
341
350
|
"Exit codes:",
|
|
342
351
|
" status: 0 on successful inspection, 1 on input/read failure.",
|
|
@@ -398,6 +407,7 @@ function formatCurrentProfileStatusSummary(status, report) {
|
|
|
398
407
|
`host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
|
|
399
408
|
`profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
|
|
400
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}`,
|
|
401
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"}`,
|
|
402
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}`,
|
|
403
413
|
`route router=${report.servePath.routerIdentity ?? status.brain.routerIdentity ?? "none"} supervision=${report.servePath.refreshStatus ?? status.brain.routeFreshness} freshness=${report.servePath.freshnessChecksum ?? "none"}`,
|
|
@@ -409,7 +419,8 @@ function formatCurrentProfileStatusSummary(status, report) {
|
|
|
409
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}`,
|
|
410
420
|
`path ${formatLearningPathSummary(report.learningPath)}`,
|
|
411
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}`,
|
|
412
|
-
`teacher snapshot=${report.teacherLoop.sourcePath ?? "none"}
|
|
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"}`,
|
|
413
424
|
`rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
414
425
|
`proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
|
|
415
426
|
`logs root=${status.brain.logRoot ?? "none"}`,
|
|
@@ -445,20 +456,27 @@ function buildCleanupRestartGuidance(restart) {
|
|
|
445
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.";
|
|
446
457
|
}
|
|
447
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
|
+
}
|
|
448
466
|
if (status.brainStatus.status === "fail") {
|
|
449
|
-
return
|
|
467
|
+
return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
|
|
450
468
|
}
|
|
451
469
|
if (status.brainStatus.awaitingFirstExport) {
|
|
452
|
-
return
|
|
470
|
+
return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
|
|
453
471
|
}
|
|
454
472
|
if (report.learning.warningStates.includes("principal_live_backlog") ||
|
|
455
473
|
report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
|
|
456
474
|
return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
|
|
457
475
|
}
|
|
458
476
|
if (report.rollback.allowed) {
|
|
459
|
-
return
|
|
477
|
+
return `Use \`openclawbrain rollback --activation-root ${activationRootArg} --dry-run\` before restoring the previous pack.`;
|
|
460
478
|
}
|
|
461
|
-
return
|
|
479
|
+
return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
|
|
462
480
|
}
|
|
463
481
|
function formatHumanFriendlyStatus(status, report) {
|
|
464
482
|
// Brain status line
|
|
@@ -482,10 +500,11 @@ function formatHumanFriendlyStatus(status, report) {
|
|
|
482
500
|
`Pack: ${packShort} (${state})`,
|
|
483
501
|
`Activation: ${activationPath}`,
|
|
484
502
|
`Policy: ${policy}`,
|
|
485
|
-
`Lifecycle: attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
|
|
503
|
+
`Lifecycle: activation=${status.brainStatus.activationState} attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
|
|
486
504
|
`Rollback: state=${report.rollback.state} ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
487
505
|
`Backlog: principal=${principalFrontier} live=${pendingLive} backfill=${pendingBackfill} next=${nextLane}/${nextBucket}`,
|
|
488
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}`,
|
|
489
508
|
`Learning: ${formatLearningPathSummary(report.learningPath)}`
|
|
490
509
|
];
|
|
491
510
|
// Add learning/serve warnings if relevant
|
|
@@ -498,11 +517,22 @@ function formatHumanFriendlyStatus(status, report) {
|
|
|
498
517
|
lines.push(`Next: ${buildStatusNextStep(status, report)}`);
|
|
499
518
|
return lines.join("\n");
|
|
500
519
|
}
|
|
501
|
-
function requireActivationRoot(input,
|
|
502
|
-
|
|
503
|
-
|
|
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) {
|
|
504
533
|
return resolveActivationRoot({
|
|
505
|
-
explicit:
|
|
534
|
+
explicit: explicitActivationRoot,
|
|
535
|
+
openclawHome
|
|
506
536
|
});
|
|
507
537
|
}
|
|
508
538
|
function readJsonFile(filePath) {
|
|
@@ -571,11 +601,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
571
601
|
if (args[0] === "doctor") {
|
|
572
602
|
throw new Error(buildDoctorDeletedMessage(args.slice(1)));
|
|
573
603
|
}
|
|
604
|
+
if (args[0] === "setup") {
|
|
605
|
+
throw new Error(buildSetupDeletedMessage());
|
|
606
|
+
}
|
|
574
607
|
if (args[0] === "daemon") {
|
|
575
608
|
args.shift();
|
|
576
609
|
return parseDaemonArgs(args);
|
|
577
610
|
}
|
|
578
|
-
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || 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") {
|
|
579
612
|
command = args.shift();
|
|
580
613
|
}
|
|
581
614
|
if (command === "learn") {
|
|
@@ -598,6 +631,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
598
631
|
index += 1;
|
|
599
632
|
continue;
|
|
600
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
|
+
}
|
|
601
643
|
if (arg.startsWith("--")) {
|
|
602
644
|
throw new Error(`unknown argument for learn: ${arg}`);
|
|
603
645
|
}
|
|
@@ -607,7 +649,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
607
649
|
}
|
|
608
650
|
return {
|
|
609
651
|
command,
|
|
610
|
-
activationRoot:
|
|
652
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
611
653
|
json,
|
|
612
654
|
help
|
|
613
655
|
};
|
|
@@ -663,9 +705,12 @@ export function parseOperatorCliArgs(argv) {
|
|
|
663
705
|
if (help) {
|
|
664
706
|
return { command, activationRoot: "", scanRoot: null, interval: 30, json, help };
|
|
665
707
|
}
|
|
708
|
+
if (activationRoot === null || activationRoot.trim().length === 0) {
|
|
709
|
+
throw new Error("watch requires --activation-root <path>");
|
|
710
|
+
}
|
|
666
711
|
return {
|
|
667
712
|
command,
|
|
668
|
-
activationRoot:
|
|
713
|
+
activationRoot: path.resolve(activationRoot),
|
|
669
714
|
scanRoot: watchScanRoot,
|
|
670
715
|
interval: watchInterval,
|
|
671
716
|
json,
|
|
@@ -693,6 +738,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
693
738
|
index += 1;
|
|
694
739
|
continue;
|
|
695
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
|
+
}
|
|
696
750
|
if (arg.startsWith("--")) {
|
|
697
751
|
throw new Error(`unknown argument for context: ${arg}`);
|
|
698
752
|
}
|
|
@@ -707,7 +761,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
707
761
|
return {
|
|
708
762
|
command,
|
|
709
763
|
message: messageParts.join(" "),
|
|
710
|
-
activationRoot:
|
|
764
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
711
765
|
json,
|
|
712
766
|
help
|
|
713
767
|
};
|
|
@@ -733,6 +787,15 @@ export function parseOperatorCliArgs(argv) {
|
|
|
733
787
|
index += 1;
|
|
734
788
|
continue;
|
|
735
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
|
+
}
|
|
736
799
|
if (arg === "--limit") {
|
|
737
800
|
const next = args[index + 1];
|
|
738
801
|
if (next === undefined) {
|
|
@@ -755,7 +818,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
755
818
|
}
|
|
756
819
|
return {
|
|
757
820
|
command,
|
|
758
|
-
activationRoot:
|
|
821
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
759
822
|
limit: historyLimit,
|
|
760
823
|
json,
|
|
761
824
|
help
|
|
@@ -781,6 +844,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
781
844
|
index += 1;
|
|
782
845
|
continue;
|
|
783
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
|
+
}
|
|
784
855
|
if (arg === "-o" || arg === "--output") {
|
|
785
856
|
const next = args[index + 1];
|
|
786
857
|
if (next === undefined)
|
|
@@ -798,7 +869,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
798
869
|
throw new Error("export requires -o <output.tar.gz>");
|
|
799
870
|
return {
|
|
800
871
|
command,
|
|
801
|
-
activationRoot:
|
|
872
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
802
873
|
outputPath: path.resolve(outputPath),
|
|
803
874
|
json,
|
|
804
875
|
help,
|
|
@@ -829,6 +900,14 @@ export function parseOperatorCliArgs(argv) {
|
|
|
829
900
|
index += 1;
|
|
830
901
|
continue;
|
|
831
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
|
+
}
|
|
832
911
|
if (arg.startsWith("--"))
|
|
833
912
|
throw new Error(`unknown argument for import: ${arg}`);
|
|
834
913
|
if (archivePath === null) {
|
|
@@ -845,7 +924,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
845
924
|
return {
|
|
846
925
|
command,
|
|
847
926
|
archivePath: path.resolve(archivePath),
|
|
848
|
-
activationRoot:
|
|
927
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
849
928
|
force,
|
|
850
929
|
json,
|
|
851
930
|
help,
|
|
@@ -875,13 +954,21 @@ export function parseOperatorCliArgs(argv) {
|
|
|
875
954
|
index += 1;
|
|
876
955
|
continue;
|
|
877
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
|
+
}
|
|
878
965
|
throw new Error(`unknown argument for reset: ${arg}`);
|
|
879
966
|
}
|
|
880
967
|
if (help)
|
|
881
968
|
return { command, activationRoot: "", yes: false, json, help };
|
|
882
969
|
return {
|
|
883
970
|
command,
|
|
884
|
-
activationRoot:
|
|
971
|
+
activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
|
|
885
972
|
yes,
|
|
886
973
|
json,
|
|
887
974
|
help
|
|
@@ -1065,7 +1152,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1065
1152
|
if (command !== "uninstall" && purgeData) {
|
|
1066
1153
|
throw new Error("--purge-data only applies to uninstall");
|
|
1067
1154
|
}
|
|
1068
|
-
if (command === "install"
|
|
1155
|
+
if (command === "install") {
|
|
1069
1156
|
if (help) {
|
|
1070
1157
|
return {
|
|
1071
1158
|
command,
|
|
@@ -1080,9 +1167,9 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1080
1167
|
help
|
|
1081
1168
|
};
|
|
1082
1169
|
}
|
|
1083
|
-
const resolvedOpenclawHome =
|
|
1084
|
-
const resolvedActivationRoot =
|
|
1085
|
-
const resolvedWorkspaceId =
|
|
1170
|
+
const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
|
|
1171
|
+
const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
|
|
1172
|
+
const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
|
|
1086
1173
|
return {
|
|
1087
1174
|
command,
|
|
1088
1175
|
openclawHome: resolvedOpenclawHome.openclawHome,
|
|
@@ -1219,6 +1306,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
1219
1306
|
updatedAt,
|
|
1220
1307
|
brainAttachmentPolicy
|
|
1221
1308
|
},
|
|
1309
|
+
openclawHome: normalizeOptionalCliString(openclawHome),
|
|
1222
1310
|
json,
|
|
1223
1311
|
help,
|
|
1224
1312
|
dryRun,
|
|
@@ -1282,6 +1370,36 @@ const LOCAL_WORKSPACE_EXTENSION_PACKAGES = [
|
|
|
1282
1370
|
"provenance",
|
|
1283
1371
|
"workspace-metadata"
|
|
1284
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
|
+
}
|
|
1285
1403
|
function resolveLocalWorkspaceRootForExtensionInstall() {
|
|
1286
1404
|
const candidates = [
|
|
1287
1405
|
path.resolve(__dirname, "..", "..", "..", ".."),
|
|
@@ -1458,6 +1576,85 @@ function buildHistoryEntry(record, slot, isActive) {
|
|
|
1458
1576
|
current: isActive
|
|
1459
1577
|
};
|
|
1460
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
|
+
}
|
|
1461
1658
|
function runHistoryCommand(parsed) {
|
|
1462
1659
|
const activationRoot = parsed.activationRoot;
|
|
1463
1660
|
const pointersPath = path.join(activationRoot, "activation-pointers.json");
|
|
@@ -1539,29 +1736,25 @@ function runHistoryCommand(parsed) {
|
|
|
1539
1736
|
}
|
|
1540
1737
|
return 0;
|
|
1541
1738
|
}
|
|
1542
|
-
function
|
|
1739
|
+
function runInstallCommand(parsed) {
|
|
1543
1740
|
const steps = [];
|
|
1544
1741
|
const commandLabel = parsed.command.toUpperCase();
|
|
1545
|
-
|
|
1546
|
-
let bootstrapped = false;
|
|
1547
|
-
steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatSetupOpenClawHomeSource(parsed.openclawHomeSource)})`);
|
|
1742
|
+
steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
|
|
1548
1743
|
// 1. Validate --openclaw-home exists and has openclaw.json
|
|
1549
1744
|
validateOpenClawHome(parsed.openclawHome);
|
|
1550
|
-
// 2.
|
|
1551
|
-
|
|
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) {
|
|
1552
1749
|
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1553
1750
|
steps.push(`Created activation root: ${parsed.activationRoot}`);
|
|
1554
1751
|
}
|
|
1555
1752
|
else {
|
|
1556
1753
|
steps.push(`Activation root exists: ${parsed.activationRoot}`);
|
|
1557
1754
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
if (
|
|
1561
|
-
steps.push("Brain already attached (activation-pointers.json exists), skipping bootstrap.");
|
|
1562
|
-
}
|
|
1563
|
-
else {
|
|
1564
|
-
bootstrapped = true;
|
|
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") {
|
|
1565
1758
|
const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
|
|
1566
1759
|
mkdirSync(packRoot, { recursive: true });
|
|
1567
1760
|
const brainAttachmentPolicy = parsed.shared ? "shared" : "dedicated";
|
|
@@ -1570,27 +1763,30 @@ function runSetupCommand(parsed) {
|
|
|
1570
1763
|
brainAttachmentPolicy,
|
|
1571
1764
|
activationRoot: parsed.activationRoot,
|
|
1572
1765
|
packRoot,
|
|
1573
|
-
packLabel: "
|
|
1766
|
+
packLabel: "install-cli",
|
|
1574
1767
|
workspace: {
|
|
1575
1768
|
workspaceId: parsed.workspaceId,
|
|
1576
|
-
snapshotId: `${parsed.workspaceId}@
|
|
1769
|
+
snapshotId: `${parsed.workspaceId}@install-${new Date().toISOString().slice(0, 10)}`,
|
|
1577
1770
|
capturedAt: new Date().toISOString(),
|
|
1578
1771
|
rootDir: parsed.openclawHome,
|
|
1579
|
-
revision: "cli-
|
|
1772
|
+
revision: "cli-install-v1"
|
|
1580
1773
|
},
|
|
1581
1774
|
interactionEvents: [],
|
|
1582
1775
|
feedbackEvents: []
|
|
1583
1776
|
});
|
|
1584
|
-
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}`);
|
|
1585
1781
|
}
|
|
1586
|
-
//
|
|
1782
|
+
// 5-8. Write extension files
|
|
1587
1783
|
const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
|
|
1588
1784
|
mkdirSync(extensionDir, { recursive: true });
|
|
1589
|
-
//
|
|
1785
|
+
// 5. Write index.ts
|
|
1590
1786
|
const indexTsPath = path.join(extensionDir, "index.ts");
|
|
1591
1787
|
writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
|
|
1592
1788
|
steps.push(`Wrote extension: ${indexTsPath}`);
|
|
1593
|
-
//
|
|
1789
|
+
// 5b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
|
|
1594
1790
|
const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
|
|
1595
1791
|
if (runtimeGuardPaths.ts !== null) {
|
|
1596
1792
|
const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
|
|
@@ -1600,17 +1796,31 @@ function runSetupCommand(parsed) {
|
|
|
1600
1796
|
const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
|
|
1601
1797
|
writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
|
|
1602
1798
|
steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
|
|
1603
|
-
//
|
|
1799
|
+
// 6. Write package.json
|
|
1604
1800
|
const packageJsonPath = path.join(extensionDir, "package.json");
|
|
1605
1801
|
writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
|
|
1606
1802
|
steps.push(`Wrote package.json: ${packageJsonPath}`);
|
|
1607
|
-
//
|
|
1803
|
+
// 7. npm install
|
|
1804
|
+
const releaseTarballInstall = resolveExtensionInstallReleaseTarballs();
|
|
1608
1805
|
try {
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
+
}
|
|
1611
1818
|
}
|
|
1612
1819
|
catch (err) {
|
|
1613
1820
|
const message = err instanceof Error ? err.message : String(err);
|
|
1821
|
+
if (releaseTarballInstall !== null) {
|
|
1822
|
+
throw new Error(`Extension dependency install from release artifacts failed: ${message}`);
|
|
1823
|
+
}
|
|
1614
1824
|
const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
|
|
1615
1825
|
if (linkedPackages !== null) {
|
|
1616
1826
|
steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
|
|
@@ -1619,19 +1829,19 @@ function runSetupCommand(parsed) {
|
|
|
1619
1829
|
steps.push(`npm install failed (non-fatal): ${message}`);
|
|
1620
1830
|
}
|
|
1621
1831
|
}
|
|
1622
|
-
//
|
|
1832
|
+
// 8. Write plugin manifest
|
|
1623
1833
|
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
1624
1834
|
writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
|
|
1625
1835
|
steps.push(`Wrote manifest: ${manifestPath}`);
|
|
1626
|
-
//
|
|
1836
|
+
// 9. Write BRAIN.md to workspace directories
|
|
1627
1837
|
const brainMdContent = [
|
|
1628
1838
|
"## OpenClawBrain",
|
|
1629
1839
|
`You have a learning brain attached at ${parsed.activationRoot}.`,
|
|
1630
1840
|
"- It learns automatically from your conversations",
|
|
1631
1841
|
'- Corrections matter — "no, actually X" teaches the brain X',
|
|
1632
1842
|
"- You don't manage it — background daemon handles learning",
|
|
1633
|
-
|
|
1634
|
-
|
|
1843
|
+
`- Check: \`openclawbrain status --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
|
|
1844
|
+
`- Rollback: \`openclawbrain rollback --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
|
|
1635
1845
|
'- See what brain knows: `openclawbrain context "your question"`',
|
|
1636
1846
|
""
|
|
1637
1847
|
].join("\n");
|
|
@@ -1705,16 +1915,24 @@ function runSetupCommand(parsed) {
|
|
|
1705
1915
|
`Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`
|
|
1706
1916
|
];
|
|
1707
1917
|
const lifecycleSummary = [
|
|
1708
|
-
`OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${
|
|
1709
|
-
`Activation root: ${shortenPath(parsed.activationRoot)} (${
|
|
1710
|
-
`Workspace ID: ${parsed.workspaceId} (${
|
|
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)})`,
|
|
1711
1921
|
`Profile hook: installed at ${shortenPath(extensionDir)}`,
|
|
1712
|
-
|
|
1922
|
+
activationPlan.resolution === "new_root"
|
|
1713
1923
|
? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
|
|
1714
|
-
:
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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`
|
|
1718
1936
|
];
|
|
1719
1937
|
// 9. Print summary
|
|
1720
1938
|
if (parsed.json) {
|
|
@@ -2341,16 +2559,9 @@ function formatTimestamp() {
|
|
|
2341
2559
|
function watchLog(message) {
|
|
2342
2560
|
console.log(`${formatTimestamp()} ${message}`);
|
|
2343
2561
|
}
|
|
2344
|
-
function
|
|
2345
|
-
|
|
2346
|
-
return explicitPath;
|
|
2347
|
-
}
|
|
2348
|
-
const asyncSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(activationRoot);
|
|
2349
|
-
return existsSync(asyncSnapshotPath) ? asyncSnapshotPath : null;
|
|
2562
|
+
function formatWatchError(error) {
|
|
2563
|
+
return error instanceof Error ? error.message : String(error);
|
|
2350
2564
|
}
|
|
2351
|
-
const WATCH_STATE_DIRNAME = "watch";
|
|
2352
|
-
const WATCH_SESSION_TAIL_CURSOR_BASENAME = "session-tail-cursor.json";
|
|
2353
|
-
const WATCH_TEACHER_SNAPSHOT_BASENAME = "teacher-snapshot.json";
|
|
2354
2565
|
function sanitizeWatchPathSegment(value) {
|
|
2355
2566
|
const sanitized = value
|
|
2356
2567
|
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
@@ -2358,15 +2569,6 @@ function sanitizeWatchPathSegment(value) {
|
|
|
2358
2569
|
.slice(0, 96);
|
|
2359
2570
|
return sanitized.length > 0 ? sanitized : "session";
|
|
2360
2571
|
}
|
|
2361
|
-
function resolveWatchStateRoot(activationRoot) {
|
|
2362
|
-
return path.resolve(activationRoot, WATCH_STATE_DIRNAME);
|
|
2363
|
-
}
|
|
2364
|
-
function resolveWatchSessionTailCursorPath(activationRoot) {
|
|
2365
|
-
return path.join(resolveWatchStateRoot(activationRoot), WATCH_SESSION_TAIL_CURSOR_BASENAME);
|
|
2366
|
-
}
|
|
2367
|
-
function resolveWatchTeacherSnapshotPath(activationRoot) {
|
|
2368
|
-
return path.join(resolveWatchStateRoot(activationRoot), WATCH_TEACHER_SNAPSHOT_BASENAME);
|
|
2369
|
-
}
|
|
2370
2572
|
function readOptionalJsonFile(filePath) {
|
|
2371
2573
|
if (!existsSync(filePath)) {
|
|
2372
2574
|
return null;
|
|
@@ -2400,28 +2602,8 @@ function persistWatchSessionTailCursor(cursorPath, cursor) {
|
|
|
2400
2602
|
cursor
|
|
2401
2603
|
});
|
|
2402
2604
|
}
|
|
2403
|
-
function
|
|
2404
|
-
|
|
2405
|
-
return {
|
|
2406
|
-
lastHandledMaterializationPackId: parsed !== null && typeof parsed.lastHandledMaterializationPackId === "string"
|
|
2407
|
-
? parsed.lastHandledMaterializationPackId
|
|
2408
|
-
: null
|
|
2409
|
-
};
|
|
2410
|
-
}
|
|
2411
|
-
function persistWatchTeacherSnapshot(snapshotPath, input) {
|
|
2412
|
-
writeJsonFile(snapshotPath, {
|
|
2413
|
-
contract: "openclaw_watch_teacher_snapshot.v1",
|
|
2414
|
-
runtimeOwner: "openclaw",
|
|
2415
|
-
updatedAt: new Date().toISOString(),
|
|
2416
|
-
scanRoot: input.scanRoot,
|
|
2417
|
-
replayedBundleCount: input.replayedBundleCount,
|
|
2418
|
-
replayedEventCount: input.replayedEventCount,
|
|
2419
|
-
exportedBundleCount: input.exportedBundleCount,
|
|
2420
|
-
exportedEventCount: input.exportedEventCount,
|
|
2421
|
-
localSessionTailNoopReason: input.localSessionTailNoopReason,
|
|
2422
|
-
lastHandledMaterializationPackId: input.lastHandledMaterializationPackId,
|
|
2423
|
-
snapshot: input.snapshot
|
|
2424
|
-
});
|
|
2605
|
+
function countWatchCursorBridgedEvents(cursor) {
|
|
2606
|
+
return cursor.reduce((sum, entry) => sum + entry.bridgedEventCount, 0);
|
|
2425
2607
|
}
|
|
2426
2608
|
function listWatchRuntimeEventExportBundleRoots(scanRoot) {
|
|
2427
2609
|
if (!existsSync(scanRoot)) {
|
|
@@ -2528,7 +2710,8 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2528
2710
|
return {
|
|
2529
2711
|
lastHandledMaterializationPackId,
|
|
2530
2712
|
logLine: null,
|
|
2531
|
-
materializedPackId: null
|
|
2713
|
+
materializedPackId: null,
|
|
2714
|
+
failure: null
|
|
2532
2715
|
};
|
|
2533
2716
|
}
|
|
2534
2717
|
const packId = typeof materialization?.candidate?.summary?.packId === "string"
|
|
@@ -2538,14 +2721,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2538
2721
|
return {
|
|
2539
2722
|
lastHandledMaterializationPackId,
|
|
2540
2723
|
logLine: null,
|
|
2541
|
-
materializedPackId: packId
|
|
2724
|
+
materializedPackId: packId,
|
|
2725
|
+
failure: null
|
|
2542
2726
|
};
|
|
2543
2727
|
}
|
|
2544
2728
|
const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
|
|
2545
2729
|
try {
|
|
2546
2730
|
const candidateRootDir = path.resolve(activationRoot, "packs", packId);
|
|
2547
2731
|
mkdirSync(candidateRootDir, { recursive: true });
|
|
2548
|
-
|
|
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
|
+
});
|
|
2549
2746
|
const now = new Date().toISOString();
|
|
2550
2747
|
stageCandidatePack(activationRoot, candidateRootDir, {
|
|
2551
2748
|
updatedAt: now,
|
|
@@ -2560,13 +2757,15 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2560
2757
|
return {
|
|
2561
2758
|
lastHandledMaterializationPackId: packId,
|
|
2562
2759
|
materializedPackId: packId,
|
|
2563
|
-
logLine: `Promoted ${shortPackId} → active
|
|
2760
|
+
logLine: `Promoted ${shortPackId} → active`,
|
|
2761
|
+
failure: null
|
|
2564
2762
|
};
|
|
2565
2763
|
}
|
|
2566
2764
|
return {
|
|
2567
2765
|
lastHandledMaterializationPackId: packId,
|
|
2568
2766
|
materializedPackId: packId,
|
|
2569
|
-
logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})
|
|
2767
|
+
logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
|
|
2768
|
+
failure: null
|
|
2570
2769
|
};
|
|
2571
2770
|
}
|
|
2572
2771
|
catch (error) {
|
|
@@ -2574,26 +2773,77 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
|
|
|
2574
2773
|
return {
|
|
2575
2774
|
lastHandledMaterializationPackId,
|
|
2576
2775
|
materializedPackId: packId,
|
|
2577
|
-
logLine: `Promotion failed for ${shortPackId}: ${message}
|
|
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: []
|
|
2578
2790
|
};
|
|
2579
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
|
+
};
|
|
2580
2818
|
}
|
|
2581
2819
|
export async function createWatchCommandRuntime(input) {
|
|
2582
2820
|
const activationRoot = path.resolve(input.activationRoot);
|
|
2821
|
+
const bootstrapObservedAt = new Date().toISOString();
|
|
2583
2822
|
const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
|
|
2584
2823
|
? path.resolve(input.scanRoot)
|
|
2585
2824
|
: path.resolve(activationRoot, "event-exports");
|
|
2586
2825
|
const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
|
|
2587
2826
|
const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
|
|
2827
|
+
const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
|
|
2588
2828
|
const log = input.log ?? watchLog;
|
|
2829
|
+
const startupWarnings = [];
|
|
2589
2830
|
mkdirSync(scanRoot, { recursive: true });
|
|
2590
2831
|
mkdirSync(resolveWatchStateRoot(activationRoot), { recursive: true });
|
|
2591
2832
|
log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
|
|
2592
2833
|
log(`Scan root: ${shortenPath(scanRoot)}`);
|
|
2593
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
|
+
}
|
|
2594
2844
|
const scanner = createRuntimeEventExportScanner({ scanRoot });
|
|
2595
2845
|
let lastServeTimeFallbackReason = null;
|
|
2596
|
-
const
|
|
2846
|
+
const baseTeacherLoopInput = {
|
|
2597
2847
|
packLabel: "watch-cli",
|
|
2598
2848
|
workspace: {
|
|
2599
2849
|
workspaceId: "watch-cli",
|
|
@@ -2603,6 +2853,7 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2603
2853
|
revision: "watch-cli-v2"
|
|
2604
2854
|
},
|
|
2605
2855
|
learnedRouting: true,
|
|
2856
|
+
...(teacherLabeler !== null ? { teacherLabeler } : {}),
|
|
2606
2857
|
resolveLearnedRoutingState: () => {
|
|
2607
2858
|
const resolved = resolveServeTimeLearningRuntimeInput(activationRoot);
|
|
2608
2859
|
if (resolved.fallbackReason !== null && resolved.fallbackReason !== lastServeTimeFallbackReason) {
|
|
@@ -2620,11 +2871,38 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2620
2871
|
persistBaseline(activationRoot, state);
|
|
2621
2872
|
}
|
|
2622
2873
|
catch (error) {
|
|
2623
|
-
|
|
2624
|
-
log(`Baseline persist failed: ${message}`);
|
|
2874
|
+
log(`Baseline persist failed: ${formatWatchError(error)}`);
|
|
2625
2875
|
}
|
|
2626
2876
|
}
|
|
2627
|
-
}
|
|
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
|
+
}
|
|
2628
2906
|
let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
|
|
2629
2907
|
let localSessionTail;
|
|
2630
2908
|
try {
|
|
@@ -2644,8 +2922,18 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2644
2922
|
});
|
|
2645
2923
|
persistWatchSessionTailCursor(sessionTailCursorPath, []);
|
|
2646
2924
|
}
|
|
2647
|
-
let
|
|
2648
|
-
|
|
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
|
+
}
|
|
2649
2937
|
if (replayState.replayedBundleCount > 0) {
|
|
2650
2938
|
log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
|
|
2651
2939
|
}
|
|
@@ -2656,14 +2944,25 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2656
2944
|
log(replayPromotion.logLine);
|
|
2657
2945
|
bootstrapSnapshot = teacherLoop.snapshot();
|
|
2658
2946
|
}
|
|
2947
|
+
const bootstrapCursor = localSessionTail.snapshot();
|
|
2659
2948
|
persistWatchTeacherSnapshot(teacherSnapshotPath, {
|
|
2949
|
+
lastRunAt: bootstrapObservedAt,
|
|
2660
2950
|
scanRoot,
|
|
2951
|
+
sessionTailCursorPath,
|
|
2952
|
+
sessionTailCursorUpdatedAt: bootstrapObservedAt,
|
|
2953
|
+
sessionTailSessionsTracked: bootstrapCursor.length,
|
|
2954
|
+
sessionTailBridgedEventCount: countWatchCursorBridgedEvents(bootstrapCursor),
|
|
2955
|
+
scannerCheckpointPath: scanner.checkpointPath,
|
|
2956
|
+
scannerCheckpoint: scanner.snapshot(),
|
|
2661
2957
|
replayedBundleCount: replayState.replayedBundleCount,
|
|
2662
2958
|
replayedEventCount: replayState.replayedEventCount,
|
|
2663
2959
|
exportedBundleCount: 0,
|
|
2664
2960
|
exportedEventCount: 0,
|
|
2961
|
+
startupWarnings,
|
|
2962
|
+
lastTeacherError: null,
|
|
2665
2963
|
localSessionTailNoopReason: null,
|
|
2666
2964
|
lastHandledMaterializationPackId,
|
|
2965
|
+
failure: replayPromotion.failure,
|
|
2667
2966
|
snapshot: bootstrapSnapshot
|
|
2668
2967
|
});
|
|
2669
2968
|
return {
|
|
@@ -2671,6 +2970,8 @@ export async function createWatchCommandRuntime(input) {
|
|
|
2671
2970
|
scanRoot,
|
|
2672
2971
|
sessionTailCursorPath,
|
|
2673
2972
|
teacherSnapshotPath,
|
|
2973
|
+
startupWarnings,
|
|
2974
|
+
lastTeacherError: null,
|
|
2674
2975
|
replayState,
|
|
2675
2976
|
lastHandledMaterializationPackId,
|
|
2676
2977
|
scanner,
|
|
@@ -2684,6 +2985,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
2684
2985
|
const localPoll = runtime.localSessionTail.pollOnce({
|
|
2685
2986
|
observedAt
|
|
2686
2987
|
});
|
|
2988
|
+
const scannerCheckpointBeforeScan = runtime.scanner.snapshot();
|
|
2687
2989
|
const exported = exportLocalSessionTailChangesToScanRoot({
|
|
2688
2990
|
scanRoot: runtime.scanRoot,
|
|
2689
2991
|
polledAt: localPoll.polledAt,
|
|
@@ -2703,31 +3005,71 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
2703
3005
|
const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
|
|
2704
3006
|
let snapshot = runtime.teacherLoop.snapshot();
|
|
2705
3007
|
let materializedPackId = null;
|
|
3008
|
+
let failure = null;
|
|
2706
3009
|
if (totalSelected === 0) {
|
|
2707
3010
|
log("Scanning... no changes");
|
|
2708
3011
|
}
|
|
2709
3012
|
else {
|
|
2710
3013
|
log(`Scanning... ${totalSelected} export bundle${totalSelected === 1 ? "" : "s"} selected, ${totalEvents} event${totalEvents === 1 ? "" : "s"}`);
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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
|
+
}
|
|
2718
3049
|
snapshot = runtime.teacherLoop.snapshot();
|
|
2719
3050
|
}
|
|
2720
3051
|
}
|
|
2721
3052
|
persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
|
|
3053
|
+
lastRunAt: observedAt,
|
|
2722
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(),
|
|
2723
3061
|
replayedBundleCount: runtime.replayState.replayedBundleCount,
|
|
2724
3062
|
replayedEventCount: runtime.replayState.replayedEventCount,
|
|
2725
3063
|
exportedBundleCount: exported.exportedBundleCount,
|
|
2726
3064
|
exportedEventCount: exported.exportedEventCount,
|
|
3065
|
+
startupWarnings: runtime.startupWarnings,
|
|
3066
|
+
lastTeacherError: runtime.lastTeacherError,
|
|
2727
3067
|
localSessionTailNoopReason: localPoll.noopReason,
|
|
2728
3068
|
lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
|
|
3069
|
+
failure,
|
|
2729
3070
|
snapshot
|
|
2730
3071
|
});
|
|
3072
|
+
const persistedScannerCheckpoint = runtime.scanner.snapshot();
|
|
2731
3073
|
if (options.json) {
|
|
2732
3074
|
console.log(JSON.stringify({
|
|
2733
3075
|
timestamp: observedAt,
|
|
@@ -2739,6 +3081,10 @@ export async function runWatchCommandPass(runtime, options = {}) {
|
|
|
2739
3081
|
events: totalEvents,
|
|
2740
3082
|
live: scanResult.live.length,
|
|
2741
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,
|
|
2742
3088
|
materialized: materializedPackId,
|
|
2743
3089
|
diagnostics: snapshot.diagnostics ?? null,
|
|
2744
3090
|
localSessionTailNoopReason: localPoll.noopReason
|
|
@@ -2842,12 +3188,13 @@ function resetActivationRoot(activationRoot) {
|
|
|
2842
3188
|
function runResetCommand(parsed) {
|
|
2843
3189
|
if (parsed.help) {
|
|
2844
3190
|
console.log([
|
|
2845
|
-
"Usage: openclawbrain reset [--activation-root <path>] [--yes] [--json]",
|
|
3191
|
+
"Usage: openclawbrain reset [--activation-root <path>|--openclaw-home <path>] [--yes] [--json]",
|
|
2846
3192
|
"",
|
|
2847
3193
|
"Wipes all learned state and returns the brain to seed state.",
|
|
2848
3194
|
"",
|
|
2849
3195
|
"Options:",
|
|
2850
3196
|
" --activation-root <path> Activation root (auto-detected if omitted)",
|
|
3197
|
+
" --openclaw-home <path> Pin auto-detection to one installed OpenClaw profile",
|
|
2851
3198
|
" --yes, -y Skip confirmation prompt",
|
|
2852
3199
|
" --json Emit machine-readable JSON output",
|
|
2853
3200
|
" --help Show this help"
|
|
@@ -2898,7 +3245,7 @@ function runResetCommand(parsed) {
|
|
|
2898
3245
|
}
|
|
2899
3246
|
console.log(" Activation pointers reset to seed state.");
|
|
2900
3247
|
console.log(`\nBrain at ${shortenPath(activationRoot)} is now in seed state.`);
|
|
2901
|
-
console.log(
|
|
3248
|
+
console.log(`Run \`openclawbrain status --activation-root ${quoteShellArg(activationRoot)}\` to verify.`);
|
|
2902
3249
|
}
|
|
2903
3250
|
return 0;
|
|
2904
3251
|
}
|
|
@@ -2973,8 +3320,8 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
2973
3320
|
});
|
|
2974
3321
|
return 0;
|
|
2975
3322
|
}
|
|
2976
|
-
if (parsed.command === "install"
|
|
2977
|
-
return
|
|
3323
|
+
if (parsed.command === "install") {
|
|
3324
|
+
return runInstallCommand(parsed);
|
|
2978
3325
|
}
|
|
2979
3326
|
if (parsed.command === "detach") {
|
|
2980
3327
|
return runDetachCommand(parsed);
|
|
@@ -3044,7 +3391,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
3044
3391
|
}
|
|
3045
3392
|
// At this point only status/rollback commands remain
|
|
3046
3393
|
const statusOrRollback = parsed;
|
|
3047
|
-
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.command);
|
|
3394
|
+
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
|
|
3048
3395
|
if (statusOrRollback.command === "rollback") {
|
|
3049
3396
|
const result = rollbackRuntimeAttach({
|
|
3050
3397
|
activationRoot,
|