@openclawbrain/openclaw 0.2.1 → 0.2.3
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/src/cli.d.ts +61 -3
- package/dist/src/cli.js +1424 -167
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.js +55 -0
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/index.d.ts +64 -3
- package/dist/src/index.js +399 -35
- package/dist/src/index.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/resolve-activation-root.js +44 -21
- 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/extension/index.ts +69 -34
- package/extension/runtime-guard.ts +338 -0
- package/package.json +13 -14
package/dist/src/cli.js
CHANGED
|
@@ -1,21 +1,168 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync } from "node:fs";
|
|
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, 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, loadRuntimeEventExportBundle, rollbackRuntimeAttach, resolveAsyncTeacherLiveLoopSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
|
|
14
15
|
import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
|
|
15
|
-
import {
|
|
16
|
+
import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
16
17
|
function quoteShellArg(value) {
|
|
17
18
|
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
18
19
|
}
|
|
20
|
+
function normalizeOptionalCliString(value) {
|
|
21
|
+
if (typeof value !== "string") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
26
|
+
}
|
|
27
|
+
function getCliHomeDir() {
|
|
28
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
29
|
+
}
|
|
30
|
+
function discoverSetupCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
|
|
31
|
+
const resolvedHomeDir = path.resolve(homeDir);
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = readdirSync(resolvedHomeDir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return entries
|
|
40
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(".openclaw-"))
|
|
41
|
+
.map((entry) => path.join(resolvedHomeDir, entry.name))
|
|
42
|
+
.filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
|
|
43
|
+
.sort((left, right) => left.localeCompare(right));
|
|
44
|
+
}
|
|
45
|
+
function formatSetupOpenClawHomeSource(source) {
|
|
46
|
+
switch (source) {
|
|
47
|
+
case "explicit":
|
|
48
|
+
return "--openclaw-home";
|
|
49
|
+
case "env":
|
|
50
|
+
return "OPENCLAW_HOME";
|
|
51
|
+
case "discovered_single_profile":
|
|
52
|
+
return "single discovered live profile";
|
|
53
|
+
default:
|
|
54
|
+
return source;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function resolveSetupOpenClawHome(explicitOpenclawHome) {
|
|
58
|
+
const normalizedExplicitHome = normalizeOptionalCliString(explicitOpenclawHome);
|
|
59
|
+
if (normalizedExplicitHome !== null) {
|
|
60
|
+
return {
|
|
61
|
+
openclawHome: path.resolve(normalizedExplicitHome),
|
|
62
|
+
openclawHomeSource: "explicit"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const envOpenClawHome = normalizeOptionalCliString(process.env.OPENCLAW_HOME);
|
|
66
|
+
if (envOpenClawHome !== null) {
|
|
67
|
+
return {
|
|
68
|
+
openclawHome: path.resolve(envOpenClawHome),
|
|
69
|
+
openclawHomeSource: "env"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const discoveredHomes = discoverSetupCandidateOpenClawHomes();
|
|
73
|
+
if (discoveredHomes.length === 1) {
|
|
74
|
+
return {
|
|
75
|
+
openclawHome: path.resolve(discoveredHomes[0]),
|
|
76
|
+
openclawHomeSource: "discovered_single_profile"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (discoveredHomes.length > 1) {
|
|
80
|
+
const installPrefix = detectConsumerSafeOperatorCliPrefix();
|
|
81
|
+
const targetChoices = discoveredHomes
|
|
82
|
+
.map((candidate) => {
|
|
83
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
84
|
+
return ` - ${resolvedCandidate}\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
|
|
85
|
+
})
|
|
86
|
+
.join("\n");
|
|
87
|
+
throw new Error([
|
|
88
|
+
"Refusing ambiguous live OpenClaw targets for install/setup.",
|
|
89
|
+
targetChoices,
|
|
90
|
+
"Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one profile."
|
|
91
|
+
].join("\n"));
|
|
92
|
+
}
|
|
93
|
+
throw new Error("No OpenClaw profile home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
|
|
94
|
+
}
|
|
95
|
+
function resolveSetupActivationRoot(openclawHome, explicitActivationRoot) {
|
|
96
|
+
const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
|
|
97
|
+
if (normalizedExplicitActivationRoot !== null) {
|
|
98
|
+
return {
|
|
99
|
+
activationRoot: path.resolve(normalizedExplicitActivationRoot),
|
|
100
|
+
source: "explicit"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
activationRoot: path.resolve(path.dirname(openclawHome), ".openclawbrain", "activation"),
|
|
105
|
+
source: "default_from_openclaw_home"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
|
|
109
|
+
const normalizedExplicitWorkspaceId = normalizeOptionalCliString(explicitWorkspaceId);
|
|
110
|
+
if (normalizedExplicitWorkspaceId !== null) {
|
|
111
|
+
return {
|
|
112
|
+
workspaceId: normalizedExplicitWorkspaceId,
|
|
113
|
+
source: "explicit"
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const openclawConfigPath = path.join(openclawHome, "openclaw.json");
|
|
118
|
+
const openclawConfig = JSON.parse(readFileSync(openclawConfigPath, "utf8"));
|
|
119
|
+
if (typeof openclawConfig.profile === "string" && openclawConfig.profile.trim().length > 0) {
|
|
120
|
+
return {
|
|
121
|
+
workspaceId: openclawConfig.profile.trim(),
|
|
122
|
+
source: "openclaw_json_profile"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Fall back to the profile-home name when setup is pointed at an incomplete or not-yet-readable profile.
|
|
128
|
+
}
|
|
129
|
+
const dirName = path.basename(openclawHome);
|
|
130
|
+
if (dirName === ".openclaw") {
|
|
131
|
+
return {
|
|
132
|
+
workspaceId: "default",
|
|
133
|
+
source: "openclaw_home_dir"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const derivedWorkspaceId = dirName.startsWith(".openclaw-") ? dirName.slice(".openclaw-".length) : dirName;
|
|
137
|
+
if (derivedWorkspaceId.trim().length > 0) {
|
|
138
|
+
return {
|
|
139
|
+
workspaceId: derivedWorkspaceId,
|
|
140
|
+
source: "openclaw_home_dir"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
workspaceId: "workspace",
|
|
145
|
+
source: "fallback"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function formatSetupActivationRootSource(source) {
|
|
149
|
+
if (source === "explicit") {
|
|
150
|
+
return "explicit --activation-root";
|
|
151
|
+
}
|
|
152
|
+
return "default beside --openclaw-home";
|
|
153
|
+
}
|
|
154
|
+
function formatSetupWorkspaceIdSource(source) {
|
|
155
|
+
switch (source) {
|
|
156
|
+
case "explicit":
|
|
157
|
+
return "explicit --workspace-id";
|
|
158
|
+
case "openclaw_json_profile":
|
|
159
|
+
return "from openclaw.json profile";
|
|
160
|
+
case "openclaw_home_dir":
|
|
161
|
+
return "from OpenClaw home dir";
|
|
162
|
+
default:
|
|
163
|
+
return "fallback default";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
19
166
|
function detectConsumerSafeOperatorCliPrefix() {
|
|
20
167
|
const npmExecPath = (process.env.npm_execpath ?? "").toLowerCase();
|
|
21
168
|
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
@@ -132,7 +279,10 @@ function buildDoctorDeletedMessage(args) {
|
|
|
132
279
|
function operatorCliHelp() {
|
|
133
280
|
return [
|
|
134
281
|
"Usage:",
|
|
135
|
-
" openclawbrain
|
|
282
|
+
" openclawbrain install [--openclaw-home <path>] [options]",
|
|
283
|
+
" openclawbrain setup [--openclaw-home <path>] [options] # compatibility alias",
|
|
284
|
+
" openclawbrain detach --openclaw-home <path> [options]",
|
|
285
|
+
" openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
|
|
136
286
|
" openclawbrain attach --activation-root <path> [options]",
|
|
137
287
|
" openclawbrain <status|rollback> --activation-root <path> [options]",
|
|
138
288
|
" openclawbrain context \"message\" [--activation-root <path>]",
|
|
@@ -146,11 +296,14 @@ function operatorCliHelp() {
|
|
|
146
296
|
" openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
|
|
147
297
|
"",
|
|
148
298
|
"Options:",
|
|
149
|
-
" --openclaw-home <path> OpenClaw profile home dir for setup (e.g. ~/.openclaw-Tern).",
|
|
150
|
-
" --shared Set brain-attachment-policy to shared instead of dedicated (setup only).",
|
|
151
|
-
" --activation-root <path> Activation root to
|
|
299
|
+
" --openclaw-home <path> OpenClaw profile home dir for install/setup/detach/uninstall (e.g. ~/.openclaw-Tern). Auto-selects for install/setup when OPENCLAW_HOME is set or exactly one live profile home exists.",
|
|
300
|
+
" --shared Set brain-attachment-policy to shared instead of dedicated (install/setup only).",
|
|
301
|
+
" --activation-root <path> Activation root for attach/detach/uninstall; install/setup defaults to sibling .openclawbrain/activation next to the selected OpenClaw home.",
|
|
302
|
+
" --keep-data Preserve activation data on uninstall; detach always behaves this way.",
|
|
303
|
+
" --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
|
|
304
|
+
" --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
|
|
152
305
|
" --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
|
|
153
|
-
" --workspace-id <id> Workspace identifier for attach provenance
|
|
306
|
+
" --workspace-id <id> Workspace identifier for attach/install/setup provenance; install/setup defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
|
|
154
307
|
" --event-export <path> Event-export bundle root or normalized export JSON payload.",
|
|
155
308
|
" --teacher-snapshot <path> Async teacher snapshot JSON from teacherLoop.snapshot()/flush(); keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
|
|
156
309
|
" --updated-at <iso> Observation time to use for freshness checks.",
|
|
@@ -171,6 +324,9 @@ function operatorCliHelp() {
|
|
|
171
324
|
" --help Show this help.",
|
|
172
325
|
"",
|
|
173
326
|
"Common flow:",
|
|
327
|
+
" 0. install openclawbrain install — attach the brain with sane defaults; pass --openclaw-home for explicit targeting on many-profile hosts",
|
|
328
|
+
" 0. detach openclawbrain detach --openclaw-home <path> — remove the profile hook only; activation data stays in place",
|
|
329
|
+
" 0. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data — remove the profile hook and choose the data outcome explicitly",
|
|
174
330
|
" 0. context openclawbrain context \"hello\" — preview the brain context that would be injected for a message",
|
|
175
331
|
" 0. attach openclawbrain attach --activation-root <path>",
|
|
176
332
|
" 1. status answer \"How's the brain?\" for the current profile on that activation root",
|
|
@@ -180,10 +336,13 @@ function operatorCliHelp() {
|
|
|
180
336
|
" 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
|
|
181
337
|
" 6. scan --live scan one live event export into teacher/learner state without claiming a daemon is running",
|
|
182
338
|
" status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
|
|
339
|
+
" watch/daemon persist that snapshot at <activation-root>/async-teacher-live-loop.snapshot.json; --teacher-snapshot overrides the default path",
|
|
183
340
|
"",
|
|
184
341
|
"Exit codes:",
|
|
185
342
|
" status: 0 on successful inspection, 1 on input/read failure.",
|
|
186
343
|
" rollback: 0 when ready/applied, 1 when blocked or on input/read failure.",
|
|
344
|
+
" detach: 0 on successful unhook, 1 on input/read failure.",
|
|
345
|
+
" uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
|
|
187
346
|
" scan: 0 on successful replay/scan, 1 on input/read failure."
|
|
188
347
|
].join("\n");
|
|
189
348
|
}
|
|
@@ -225,6 +384,12 @@ function formatLearningBuckets(report) {
|
|
|
225
384
|
function formatLearningWarnings(report) {
|
|
226
385
|
return report.learning.warningStates.length === 0 ? "none" : report.learning.warningStates.join("|");
|
|
227
386
|
}
|
|
387
|
+
function formatLabelFlowSummary(labelFlow) {
|
|
388
|
+
return `source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`;
|
|
389
|
+
}
|
|
390
|
+
function formatLearningPathSummary(learningPath) {
|
|
391
|
+
return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
|
|
392
|
+
}
|
|
228
393
|
function formatCurrentProfileStatusSummary(status, report) {
|
|
229
394
|
const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
|
|
230
395
|
return [
|
|
@@ -240,8 +405,11 @@ function formatCurrentProfileStatusSummary(status, report) {
|
|
|
240
405
|
`decision ${status.brainStatus.structuralDecision.detail}`,
|
|
241
406
|
`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
407
|
`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"}`,
|
|
408
|
+
`labels ${formatLabelFlowSummary(report.labelFlow)}`,
|
|
243
409
|
`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
|
+
`path ${formatLearningPathSummary(report.learningPath)}`,
|
|
244
411
|
`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"} started=${report.teacherLoop.startedAt ?? "none"} heartbeat=${report.teacherLoop.lastHeartbeatAt ?? "none"} scan=${report.teacherLoop.lastScanAt ?? report.teacherLoop.lastProcessedAt ?? "none"} queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"} running=${yesNo(report.teacherLoop.running)} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
|
|
245
413
|
`rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
246
414
|
`proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
|
|
247
415
|
`logs root=${status.brain.logRoot ?? "none"}`,
|
|
@@ -258,6 +426,40 @@ function shortenPath(fullPath) {
|
|
|
258
426
|
}
|
|
259
427
|
return fullPath;
|
|
260
428
|
}
|
|
429
|
+
function buildInstallStatusCommand(activationRoot) {
|
|
430
|
+
return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
|
|
431
|
+
}
|
|
432
|
+
function buildInstallCommand(openclawHome) {
|
|
433
|
+
return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
|
|
434
|
+
}
|
|
435
|
+
function buildInstallReloadGuidance() {
|
|
436
|
+
return "If this OpenClaw profile is currently running, restart it before expecting the new brain hook to load. If it is stopped, the next launch will pick it up.";
|
|
437
|
+
}
|
|
438
|
+
function buildCleanupRestartGuidance(restart) {
|
|
439
|
+
if (restart === "never") {
|
|
440
|
+
return "No restart requested. If this OpenClaw profile is currently running, it may keep the previous hook state until the next restart.";
|
|
441
|
+
}
|
|
442
|
+
if (restart === "external") {
|
|
443
|
+
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.";
|
|
444
|
+
}
|
|
445
|
+
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
|
+
}
|
|
447
|
+
function buildStatusNextStep(status, report) {
|
|
448
|
+
if (status.brainStatus.status === "fail") {
|
|
449
|
+
return "Run `openclawbrain status --detailed` before changing lifecycle state so the serve-path failure is explicit.";
|
|
450
|
+
}
|
|
451
|
+
if (status.brainStatus.awaitingFirstExport) {
|
|
452
|
+
return "Let the attached OpenClaw profile emit a real export, then rerun `openclawbrain status`.";
|
|
453
|
+
}
|
|
454
|
+
if (report.learning.warningStates.includes("principal_live_backlog") ||
|
|
455
|
+
report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
|
|
456
|
+
return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
|
|
457
|
+
}
|
|
458
|
+
if (report.rollback.allowed) {
|
|
459
|
+
return "Use `openclawbrain rollback --dry-run` before restoring the previous pack.";
|
|
460
|
+
}
|
|
461
|
+
return "Use `openclawbrain status --detailed` when you need the full lifecycle, serve-path, and backlog proof.";
|
|
462
|
+
}
|
|
261
463
|
function formatHumanFriendlyStatus(status, report) {
|
|
262
464
|
// Brain status line
|
|
263
465
|
const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
|
|
@@ -270,11 +472,21 @@ function formatHumanFriendlyStatus(status, report) {
|
|
|
270
472
|
const activationPath = shortenPath(status.host.activationRoot);
|
|
271
473
|
// Policy
|
|
272
474
|
const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
|
|
475
|
+
const principalFrontier = formatPrincipalCheckpointFrontier(report);
|
|
476
|
+
const pendingLive = String(report.learning.pendingLive ?? "none");
|
|
477
|
+
const pendingBackfill = String(report.learning.pendingBackfill ?? "none");
|
|
478
|
+
const nextLane = report.learning.nextPriorityLane ?? "none";
|
|
479
|
+
const nextBucket = report.learning.nextPriorityBucket ?? "none";
|
|
273
480
|
const lines = [
|
|
274
481
|
`Brain: ${brainIcon}`,
|
|
275
482
|
`Pack: ${packShort} (${state})`,
|
|
276
483
|
`Activation: ${activationPath}`,
|
|
277
|
-
`Policy: ${policy}
|
|
484
|
+
`Policy: ${policy}`,
|
|
485
|
+
`Lifecycle: attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
|
|
486
|
+
`Rollback: state=${report.rollback.state} ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
487
|
+
`Backlog: principal=${principalFrontier} live=${pendingLive} backfill=${pendingBackfill} next=${nextLane}/${nextBucket}`,
|
|
488
|
+
`Labels: ${formatLabelFlowSummary(report.labelFlow)}`,
|
|
489
|
+
`Learning: ${formatLearningPathSummary(report.learningPath)}`
|
|
278
490
|
];
|
|
279
491
|
// Add learning/serve warnings if relevant
|
|
280
492
|
if (report.learning.warningStates.length > 0) {
|
|
@@ -283,6 +495,7 @@ function formatHumanFriendlyStatus(status, report) {
|
|
|
283
495
|
if (status.brainStatus.awaitingFirstExport) {
|
|
284
496
|
lines.push(`Note: Awaiting first event export`);
|
|
285
497
|
}
|
|
498
|
+
lines.push(`Next: ${buildStatusNextStep(status, report)}`);
|
|
286
499
|
return lines.join("\n");
|
|
287
500
|
}
|
|
288
501
|
function requireActivationRoot(input, _command) {
|
|
@@ -321,6 +534,8 @@ function formatScanLiveSummary(result, snapshotOutPath) {
|
|
|
321
534
|
"SCAN live ok",
|
|
322
535
|
`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
536
|
`teacher artifacts=${result.snapshot.teacher.artifactCount} freshness=${result.snapshot.teacher.latestFreshness} humanLabels=${result.supervision.humanLabelCount} noop=${result.snapshot.diagnostics.lastNoOpReason}`,
|
|
537
|
+
`labels source=${result.labelFlow.source} human=${result.labelFlow.humanLabelCount ?? "none"} self=${result.labelFlow.selfLabelCount ?? "none"} implicitPositive=${result.labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${result.labelFlow.asyncTeacherArtifactCount ?? "none"}`,
|
|
538
|
+
`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
539
|
`learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
|
|
325
540
|
`observed ${result.observedAt}`,
|
|
326
541
|
`snapshot ${snapshotOutPath ?? "none"}`
|
|
@@ -344,6 +559,10 @@ export function parseOperatorCliArgs(argv) {
|
|
|
344
559
|
let snapshotOutPath = null;
|
|
345
560
|
let openclawHome = null;
|
|
346
561
|
let shared = false;
|
|
562
|
+
let keepData = false;
|
|
563
|
+
let purgeData = false;
|
|
564
|
+
let restart = "safe";
|
|
565
|
+
let restartExplicitlySet = false;
|
|
347
566
|
let json = false;
|
|
348
567
|
let help = false;
|
|
349
568
|
let dryRun = false;
|
|
@@ -356,7 +575,7 @@ export function parseOperatorCliArgs(argv) {
|
|
|
356
575
|
args.shift();
|
|
357
576
|
return parseDaemonArgs(args);
|
|
358
577
|
}
|
|
359
|
-
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "setup" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
|
|
578
|
+
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "setup" || 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
579
|
command = args.shift();
|
|
361
580
|
}
|
|
362
581
|
if (command === "learn") {
|
|
@@ -686,6 +905,27 @@ export function parseOperatorCliArgs(argv) {
|
|
|
686
905
|
shared = true;
|
|
687
906
|
continue;
|
|
688
907
|
}
|
|
908
|
+
if (arg === "--keep-data") {
|
|
909
|
+
keepData = true;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (arg === "--purge-data") {
|
|
913
|
+
purgeData = true;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (arg === "--restart") {
|
|
917
|
+
const next = args[index + 1];
|
|
918
|
+
if (next === undefined) {
|
|
919
|
+
throw new Error("--restart requires a value");
|
|
920
|
+
}
|
|
921
|
+
if (next !== "never" && next !== "safe" && next !== "external") {
|
|
922
|
+
throw new Error(`invalid --restart value: ${next}`);
|
|
923
|
+
}
|
|
924
|
+
restart = next;
|
|
925
|
+
restartExplicitlySet = true;
|
|
926
|
+
index += 1;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
689
929
|
if (arg === "--detailed") {
|
|
690
930
|
detailed = true;
|
|
691
931
|
continue;
|
|
@@ -816,25 +1056,99 @@ export function parseOperatorCliArgs(argv) {
|
|
|
816
1056
|
}
|
|
817
1057
|
throw new Error(`unknown argument: ${arg}`);
|
|
818
1058
|
}
|
|
819
|
-
if (command
|
|
1059
|
+
if (command !== "detach" && command !== "uninstall" && restartExplicitlySet) {
|
|
1060
|
+
throw new Error("--restart only applies to detach/uninstall");
|
|
1061
|
+
}
|
|
1062
|
+
if (command !== "uninstall" && keepData) {
|
|
1063
|
+
throw new Error("--keep-data only applies to uninstall; use detach to preserve activation data");
|
|
1064
|
+
}
|
|
1065
|
+
if (command !== "uninstall" && purgeData) {
|
|
1066
|
+
throw new Error("--purge-data only applies to uninstall");
|
|
1067
|
+
}
|
|
1068
|
+
if (command === "install" || command === "setup") {
|
|
820
1069
|
if (help) {
|
|
821
|
-
return {
|
|
1070
|
+
return {
|
|
1071
|
+
command,
|
|
1072
|
+
openclawHome: "",
|
|
1073
|
+
openclawHomeSource: "explicit",
|
|
1074
|
+
activationRoot: "",
|
|
1075
|
+
activationRootSource: "explicit",
|
|
1076
|
+
shared: false,
|
|
1077
|
+
workspaceId: "",
|
|
1078
|
+
workspaceIdSource: "explicit",
|
|
1079
|
+
json,
|
|
1080
|
+
help
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
const resolvedOpenclawHome = resolveSetupOpenClawHome(openclawHome);
|
|
1084
|
+
const resolvedActivationRoot = resolveSetupActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
|
|
1085
|
+
const resolvedWorkspaceId = resolveSetupWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
|
|
1086
|
+
return {
|
|
1087
|
+
command,
|
|
1088
|
+
openclawHome: resolvedOpenclawHome.openclawHome,
|
|
1089
|
+
openclawHomeSource: resolvedOpenclawHome.openclawHomeSource,
|
|
1090
|
+
activationRoot: resolvedActivationRoot.activationRoot,
|
|
1091
|
+
activationRootSource: resolvedActivationRoot.source,
|
|
1092
|
+
shared,
|
|
1093
|
+
workspaceId: resolvedWorkspaceId.workspaceId,
|
|
1094
|
+
workspaceIdSource: resolvedWorkspaceId.source,
|
|
1095
|
+
json,
|
|
1096
|
+
help
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
if (command === "detach") {
|
|
1100
|
+
if (help) {
|
|
1101
|
+
return { command, openclawHome: "", activationRoot: null, restart: "safe", json, help };
|
|
822
1102
|
}
|
|
823
1103
|
if (openclawHome === null || openclawHome.trim().length === 0) {
|
|
824
|
-
throw new Error("--openclaw-home is required for
|
|
1104
|
+
throw new Error("--openclaw-home is required for detach");
|
|
1105
|
+
}
|
|
1106
|
+
if (purgeData) {
|
|
1107
|
+
throw new Error("detach always preserves activation data; use uninstall --purge-data to remove it");
|
|
825
1108
|
}
|
|
826
1109
|
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1110
|
+
const resolvedActivationRoot = resolveActivationRoot({
|
|
1111
|
+
explicit: activationRoot,
|
|
1112
|
+
openclawHome: resolvedOpenclawHome,
|
|
1113
|
+
quiet: true
|
|
1114
|
+
});
|
|
832
1115
|
return {
|
|
833
1116
|
command,
|
|
834
1117
|
openclawHome: resolvedOpenclawHome,
|
|
835
|
-
activationRoot: resolvedActivationRoot,
|
|
836
|
-
|
|
837
|
-
|
|
1118
|
+
activationRoot: resolvedActivationRoot.trim().length === 0 ? null : path.resolve(resolvedActivationRoot),
|
|
1119
|
+
restart,
|
|
1120
|
+
json,
|
|
1121
|
+
help
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
if (command === "uninstall") {
|
|
1125
|
+
if (help) {
|
|
1126
|
+
return { command, openclawHome: "", activationRoot: null, dataMode: "keep", restart: "safe", json, help };
|
|
1127
|
+
}
|
|
1128
|
+
if (openclawHome === null || openclawHome.trim().length === 0) {
|
|
1129
|
+
throw new Error("--openclaw-home is required for uninstall");
|
|
1130
|
+
}
|
|
1131
|
+
if (!keepData && !purgeData) {
|
|
1132
|
+
throw new Error("uninstall requires exactly one of --keep-data or --purge-data");
|
|
1133
|
+
}
|
|
1134
|
+
if (keepData && purgeData) {
|
|
1135
|
+
throw new Error("--keep-data and --purge-data are mutually exclusive");
|
|
1136
|
+
}
|
|
1137
|
+
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
1138
|
+
const resolvedActivationRoot = resolveActivationRoot({
|
|
1139
|
+
explicit: activationRoot,
|
|
1140
|
+
openclawHome: resolvedOpenclawHome,
|
|
1141
|
+
quiet: true
|
|
1142
|
+
});
|
|
1143
|
+
if (purgeData && resolvedActivationRoot.trim().length === 0) {
|
|
1144
|
+
throw new Error("--purge-data requires a resolvable activation root from the installed profile hook or --activation-root <path>");
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
command,
|
|
1148
|
+
openclawHome: resolvedOpenclawHome,
|
|
1149
|
+
activationRoot: resolvedActivationRoot.trim().length === 0 ? null : path.resolve(resolvedActivationRoot),
|
|
1150
|
+
dataMode: purgeData ? "purge" : "keep",
|
|
1151
|
+
restart,
|
|
838
1152
|
json,
|
|
839
1153
|
help
|
|
840
1154
|
};
|
|
@@ -939,28 +1253,132 @@ function resolveExtensionTemplatePath() {
|
|
|
939
1253
|
throw new Error("Pre-built extension template not found. Searched:\n" +
|
|
940
1254
|
candidates.map((c) => ` - ${c}`).join("\n"));
|
|
941
1255
|
}
|
|
1256
|
+
function resolveExtensionRuntimeGuardPath() {
|
|
1257
|
+
const tsCandidates = [
|
|
1258
|
+
path.resolve(__dirname, "..", "extension", "runtime-guard.ts"),
|
|
1259
|
+
path.resolve(__dirname, "..", "..", "extension", "runtime-guard.ts"),
|
|
1260
|
+
];
|
|
1261
|
+
const jsCandidates = [
|
|
1262
|
+
path.resolve(__dirname, "extension", "runtime-guard.js"),
|
|
1263
|
+
path.resolve(__dirname, "..", "extension", "runtime-guard.js"),
|
|
1264
|
+
];
|
|
1265
|
+
const tsPath = tsCandidates.find((c) => existsSync(c)) ?? null;
|
|
1266
|
+
const jsPath = jsCandidates.find((c) => existsSync(c));
|
|
1267
|
+
if (!jsPath) {
|
|
1268
|
+
throw new Error("Pre-built extension runtime-guard.js not found. Searched:\n" +
|
|
1269
|
+
jsCandidates.map((c) => ` - ${c}`).join("\n"));
|
|
1270
|
+
}
|
|
1271
|
+
return { ts: tsPath, js: jsPath };
|
|
1272
|
+
}
|
|
1273
|
+
const LOCAL_WORKSPACE_EXTENSION_PACKAGES = [
|
|
1274
|
+
"activation",
|
|
1275
|
+
"compiler",
|
|
1276
|
+
"contracts",
|
|
1277
|
+
"event-export",
|
|
1278
|
+
"events",
|
|
1279
|
+
"learner",
|
|
1280
|
+
"openclaw",
|
|
1281
|
+
"pack-format",
|
|
1282
|
+
"provenance",
|
|
1283
|
+
"workspace-metadata"
|
|
1284
|
+
];
|
|
1285
|
+
function resolveLocalWorkspaceRootForExtensionInstall() {
|
|
1286
|
+
const candidates = [
|
|
1287
|
+
path.resolve(__dirname, "..", "..", "..", ".."),
|
|
1288
|
+
path.resolve(__dirname, "..", "..", "..")
|
|
1289
|
+
];
|
|
1290
|
+
for (const candidate of candidates) {
|
|
1291
|
+
const packageRoot = path.join(candidate, "packages", "openclaw");
|
|
1292
|
+
const distEntry = path.join(packageRoot, "dist", "src", "index.js");
|
|
1293
|
+
if (existsSync(packageRoot) && existsSync(distEntry)) {
|
|
1294
|
+
return candidate;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
function installExtensionFromLocalWorkspaceBuild(extensionDir) {
|
|
1300
|
+
const workspaceRoot = resolveLocalWorkspaceRootForExtensionInstall();
|
|
1301
|
+
if (workspaceRoot === null) {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
const nodeModulesRoot = path.join(extensionDir, "node_modules", "@openclawbrain");
|
|
1305
|
+
mkdirSync(nodeModulesRoot, { recursive: true });
|
|
1306
|
+
for (const packageName of LOCAL_WORKSPACE_EXTENSION_PACKAGES) {
|
|
1307
|
+
const packageDir = path.join(workspaceRoot, "packages", packageName);
|
|
1308
|
+
const packageDistEntry = path.join(packageDir, "dist", "src", "index.js");
|
|
1309
|
+
if (!existsSync(packageDir) || !existsSync(packageDistEntry)) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
for (const packageName of LOCAL_WORKSPACE_EXTENSION_PACKAGES) {
|
|
1314
|
+
const packageDir = path.join(workspaceRoot, "packages", packageName);
|
|
1315
|
+
const linkPath = path.join(nodeModulesRoot, packageName);
|
|
1316
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
1317
|
+
symlinkSync(packageDir, linkPath, "dir");
|
|
1318
|
+
}
|
|
1319
|
+
return [...LOCAL_WORKSPACE_EXTENSION_PACKAGES];
|
|
1320
|
+
}
|
|
1321
|
+
let cachedOpenClawPackageMetadata = null;
|
|
1322
|
+
function resolveOpenClawPackageManifestPath() {
|
|
1323
|
+
const candidates = [
|
|
1324
|
+
path.resolve(__dirname, "..", "package.json"),
|
|
1325
|
+
path.resolve(__dirname, "..", "..", "package.json"),
|
|
1326
|
+
];
|
|
1327
|
+
for (const candidate of candidates) {
|
|
1328
|
+
if (existsSync(candidate)) {
|
|
1329
|
+
return candidate;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
throw new Error("OpenClawBrain package manifest not found. Searched:\n" +
|
|
1333
|
+
candidates.map((candidate) => ` - ${candidate}`).join("\n"));
|
|
1334
|
+
}
|
|
1335
|
+
function readOpenClawPackageMetadata() {
|
|
1336
|
+
if (cachedOpenClawPackageMetadata !== null) {
|
|
1337
|
+
return cachedOpenClawPackageMetadata;
|
|
1338
|
+
}
|
|
1339
|
+
const manifestPath = resolveOpenClawPackageManifestPath();
|
|
1340
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1341
|
+
const name = typeof manifest.name === "string" && manifest.name.trim().length > 0
|
|
1342
|
+
? manifest.name.trim()
|
|
1343
|
+
: "@openclawbrain/openclaw";
|
|
1344
|
+
const version = typeof manifest.version === "string" && manifest.version.trim().length > 0
|
|
1345
|
+
? manifest.version.trim()
|
|
1346
|
+
: "0.0.0";
|
|
1347
|
+
cachedOpenClawPackageMetadata = { name, version };
|
|
1348
|
+
return cachedOpenClawPackageMetadata;
|
|
1349
|
+
}
|
|
942
1350
|
function buildExtensionIndexTs(activationRoot) {
|
|
943
1351
|
const templatePath = resolveExtensionTemplatePath();
|
|
944
1352
|
const template = readFileSync(templatePath, "utf8");
|
|
945
1353
|
return template.replace(/const ACTIVATION_ROOT = "__ACTIVATION_ROOT__";/, `const ACTIVATION_ROOT = ${JSON.stringify(activationRoot)};`);
|
|
946
1354
|
}
|
|
947
1355
|
function buildExtensionPackageJson() {
|
|
1356
|
+
const packageMetadata = readOpenClawPackageMetadata();
|
|
948
1357
|
return JSON.stringify({
|
|
949
1358
|
name: "openclawbrain-extension",
|
|
950
|
-
version:
|
|
1359
|
+
version: packageMetadata.version,
|
|
951
1360
|
private: true,
|
|
952
1361
|
type: "module",
|
|
1362
|
+
openclaw: {
|
|
1363
|
+
extensions: ["index.ts"]
|
|
1364
|
+
},
|
|
953
1365
|
dependencies: {
|
|
954
|
-
|
|
1366
|
+
[packageMetadata.name]: packageMetadata.version
|
|
955
1367
|
}
|
|
956
1368
|
}, null, 2) + "\n";
|
|
957
1369
|
}
|
|
958
1370
|
function buildExtensionPluginManifest() {
|
|
1371
|
+
const packageMetadata = readOpenClawPackageMetadata();
|
|
959
1372
|
return JSON.stringify({
|
|
960
1373
|
id: "openclawbrain",
|
|
961
1374
|
name: "OpenClawBrain",
|
|
962
1375
|
description: "Learned memory and context from OpenClawBrain",
|
|
963
|
-
version:
|
|
1376
|
+
version: packageMetadata.version,
|
|
1377
|
+
configSchema: {
|
|
1378
|
+
type: "object",
|
|
1379
|
+
additionalProperties: false,
|
|
1380
|
+
properties: {}
|
|
1381
|
+
}
|
|
964
1382
|
}, null, 2) + "\n";
|
|
965
1383
|
}
|
|
966
1384
|
function formatContextForHuman(result) {
|
|
@@ -1045,10 +1463,10 @@ function runHistoryCommand(parsed) {
|
|
|
1045
1463
|
const pointersPath = path.join(activationRoot, "activation-pointers.json");
|
|
1046
1464
|
if (!existsSync(pointersPath)) {
|
|
1047
1465
|
if (parsed.json) {
|
|
1048
|
-
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain
|
|
1466
|
+
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain install" }, null, 2));
|
|
1049
1467
|
}
|
|
1050
1468
|
else {
|
|
1051
|
-
console.log("No history yet. Run: openclawbrain
|
|
1469
|
+
console.log("No history yet. Run: openclawbrain install");
|
|
1052
1470
|
}
|
|
1053
1471
|
return 0;
|
|
1054
1472
|
}
|
|
@@ -1081,10 +1499,10 @@ function runHistoryCommand(parsed) {
|
|
|
1081
1499
|
}
|
|
1082
1500
|
if (entries.length === 0) {
|
|
1083
1501
|
if (parsed.json) {
|
|
1084
|
-
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain
|
|
1502
|
+
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain install" }, null, 2));
|
|
1085
1503
|
}
|
|
1086
1504
|
else {
|
|
1087
|
-
console.log("No history yet. Run: openclawbrain
|
|
1505
|
+
console.log("No history yet. Run: openclawbrain install");
|
|
1088
1506
|
}
|
|
1089
1507
|
return 0;
|
|
1090
1508
|
}
|
|
@@ -1123,16 +1541,14 @@ function runHistoryCommand(parsed) {
|
|
|
1123
1541
|
}
|
|
1124
1542
|
function runSetupCommand(parsed) {
|
|
1125
1543
|
const steps = [];
|
|
1544
|
+
const commandLabel = parsed.command.toUpperCase();
|
|
1545
|
+
const activationRootCreated = !existsSync(parsed.activationRoot);
|
|
1546
|
+
let bootstrapped = false;
|
|
1547
|
+
steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatSetupOpenClawHomeSource(parsed.openclawHomeSource)})`);
|
|
1126
1548
|
// 1. Validate --openclaw-home exists and has openclaw.json
|
|
1127
|
-
|
|
1128
|
-
throw new Error(`--openclaw-home directory does not exist: ${parsed.openclawHome}`);
|
|
1129
|
-
}
|
|
1130
|
-
const openclawJsonPath = path.join(parsed.openclawHome, "openclaw.json");
|
|
1131
|
-
if (!existsSync(openclawJsonPath)) {
|
|
1132
|
-
throw new Error(`openclaw.json not found in ${parsed.openclawHome}`);
|
|
1133
|
-
}
|
|
1549
|
+
validateOpenClawHome(parsed.openclawHome);
|
|
1134
1550
|
// 2. Create activation root if needed
|
|
1135
|
-
if (
|
|
1551
|
+
if (activationRootCreated) {
|
|
1136
1552
|
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1137
1553
|
steps.push(`Created activation root: ${parsed.activationRoot}`);
|
|
1138
1554
|
}
|
|
@@ -1145,6 +1561,7 @@ function runSetupCommand(parsed) {
|
|
|
1145
1561
|
steps.push("Brain already attached (activation-pointers.json exists), skipping bootstrap.");
|
|
1146
1562
|
}
|
|
1147
1563
|
else {
|
|
1564
|
+
bootstrapped = true;
|
|
1148
1565
|
const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
|
|
1149
1566
|
mkdirSync(packRoot, { recursive: true });
|
|
1150
1567
|
const brainAttachmentPolicy = parsed.shared ? "shared" : "dedicated";
|
|
@@ -1173,6 +1590,16 @@ function runSetupCommand(parsed) {
|
|
|
1173
1590
|
const indexTsPath = path.join(extensionDir, "index.ts");
|
|
1174
1591
|
writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
|
|
1175
1592
|
steps.push(`Wrote extension: ${indexTsPath}`);
|
|
1593
|
+
// 4b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
|
|
1594
|
+
const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
|
|
1595
|
+
if (runtimeGuardPaths.ts !== null) {
|
|
1596
|
+
const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
|
|
1597
|
+
writeFileSync(runtimeGuardTsPath, readFileSync(runtimeGuardPaths.ts, "utf8"), "utf8");
|
|
1598
|
+
steps.push(`Wrote extension runtime-guard source: ${runtimeGuardTsPath}`);
|
|
1599
|
+
}
|
|
1600
|
+
const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
|
|
1601
|
+
writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
|
|
1602
|
+
steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
|
|
1176
1603
|
// 5. Write package.json
|
|
1177
1604
|
const packageJsonPath = path.join(extensionDir, "package.json");
|
|
1178
1605
|
writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
|
|
@@ -1184,7 +1611,13 @@ function runSetupCommand(parsed) {
|
|
|
1184
1611
|
}
|
|
1185
1612
|
catch (err) {
|
|
1186
1613
|
const message = err instanceof Error ? err.message : String(err);
|
|
1187
|
-
|
|
1614
|
+
const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
|
|
1615
|
+
if (linkedPackages !== null) {
|
|
1616
|
+
steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
steps.push(`npm install failed (non-fatal): ${message}`);
|
|
1620
|
+
}
|
|
1188
1621
|
}
|
|
1189
1622
|
// 7. Write plugin manifest
|
|
1190
1623
|
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
@@ -1266,46 +1699,341 @@ function runSetupCommand(parsed) {
|
|
|
1266
1699
|
const message = err instanceof Error ? err.message : String(err);
|
|
1267
1700
|
steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
|
|
1268
1701
|
}
|
|
1702
|
+
const restartGuidance = buildInstallReloadGuidance();
|
|
1703
|
+
const nextSteps = [
|
|
1704
|
+
restartGuidance,
|
|
1705
|
+
`Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`
|
|
1706
|
+
];
|
|
1707
|
+
const lifecycleSummary = [
|
|
1708
|
+
`OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${formatSetupOpenClawHomeSource(parsed.openclawHomeSource)})`,
|
|
1709
|
+
`Activation root: ${shortenPath(parsed.activationRoot)} (${formatSetupActivationRootSource(parsed.activationRootSource)})`,
|
|
1710
|
+
`Workspace ID: ${parsed.workspaceId} (${formatSetupWorkspaceIdSource(parsed.workspaceIdSource)})`,
|
|
1711
|
+
`Profile hook: installed at ${shortenPath(extensionDir)}`,
|
|
1712
|
+
activationRootCreated
|
|
1713
|
+
? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
|
|
1714
|
+
: `Activation data: reused at ${shortenPath(parsed.activationRoot)}`,
|
|
1715
|
+
bootstrapped
|
|
1716
|
+
? "Brain attach: bootstrapped a seed/current-profile attach"
|
|
1717
|
+
: "Brain attach: existing activation state kept in place"
|
|
1718
|
+
];
|
|
1269
1719
|
// 9. Print summary
|
|
1270
1720
|
if (parsed.json) {
|
|
1271
1721
|
console.log(JSON.stringify({
|
|
1272
|
-
command:
|
|
1722
|
+
command: parsed.command,
|
|
1273
1723
|
openclawHome: parsed.openclawHome,
|
|
1724
|
+
openclawHomeSource: parsed.openclawHomeSource,
|
|
1274
1725
|
activationRoot: parsed.activationRoot,
|
|
1726
|
+
resolvedInputs: {
|
|
1727
|
+
activationRoot: {
|
|
1728
|
+
value: parsed.activationRoot,
|
|
1729
|
+
source: parsed.activationRootSource
|
|
1730
|
+
},
|
|
1731
|
+
workspaceId: {
|
|
1732
|
+
value: parsed.workspaceId,
|
|
1733
|
+
source: parsed.workspaceIdSource
|
|
1734
|
+
}
|
|
1735
|
+
},
|
|
1275
1736
|
workspaceId: parsed.workspaceId,
|
|
1276
1737
|
shared: parsed.shared,
|
|
1277
1738
|
extensionDir,
|
|
1739
|
+
lifecycleSummary,
|
|
1740
|
+
restartGuidance,
|
|
1741
|
+
nextSteps,
|
|
1742
|
+
steps
|
|
1743
|
+
}, null, 2));
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
console.log(`${commandLabel} complete\n`);
|
|
1747
|
+
for (const step of steps) {
|
|
1748
|
+
console.log(` ✓ ${step}`);
|
|
1749
|
+
}
|
|
1750
|
+
console.log("");
|
|
1751
|
+
console.log("Lifecycle:");
|
|
1752
|
+
for (const line of lifecycleSummary) {
|
|
1753
|
+
console.log(` ${line}`);
|
|
1754
|
+
}
|
|
1755
|
+
console.log(`Next: ${restartGuidance}`);
|
|
1756
|
+
console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
|
|
1757
|
+
}
|
|
1758
|
+
return 0;
|
|
1759
|
+
}
|
|
1760
|
+
function validateOpenClawHome(openclawHome) {
|
|
1761
|
+
if (!existsSync(openclawHome)) {
|
|
1762
|
+
throw new Error(`--openclaw-home directory does not exist: ${openclawHome}`);
|
|
1763
|
+
}
|
|
1764
|
+
const openclawJsonPath = path.join(openclawHome, "openclaw.json");
|
|
1765
|
+
if (!existsSync(openclawJsonPath)) {
|
|
1766
|
+
throw new Error(`openclaw.json not found in ${openclawHome}`);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function resolveCleanupActivationRoot(openclawHome, explicitActivationRoot) {
|
|
1770
|
+
if (explicitActivationRoot !== null) {
|
|
1771
|
+
return path.resolve(explicitActivationRoot);
|
|
1772
|
+
}
|
|
1773
|
+
const resolved = resolveActivationRoot({
|
|
1774
|
+
openclawHome,
|
|
1775
|
+
quiet: true
|
|
1776
|
+
});
|
|
1777
|
+
return resolved.trim().length === 0 ? null : path.resolve(resolved);
|
|
1778
|
+
}
|
|
1779
|
+
function removeProfileHookup(openclawHome, steps) {
|
|
1780
|
+
const extensionDir = path.join(openclawHome, "extensions", "openclawbrain");
|
|
1781
|
+
if (!existsSync(extensionDir)) {
|
|
1782
|
+
steps.push(`Profile hookup already absent: ${extensionDir}`);
|
|
1783
|
+
return extensionDir;
|
|
1784
|
+
}
|
|
1785
|
+
rmSync(extensionDir, { recursive: true, force: true });
|
|
1786
|
+
steps.push(`Removed profile hookup: ${extensionDir}`);
|
|
1787
|
+
return extensionDir;
|
|
1788
|
+
}
|
|
1789
|
+
function summarizeKeptActivationData(activationRoot) {
|
|
1790
|
+
if (activationRoot === null) {
|
|
1791
|
+
return {
|
|
1792
|
+
activationRoot: null,
|
|
1793
|
+
activationDataState: "unresolved",
|
|
1794
|
+
activationDataDetail: "Activation data preserved, but the activation root could not be resolved from the profile hook."
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
activationRoot,
|
|
1799
|
+
activationDataState: "kept",
|
|
1800
|
+
activationDataDetail: `Activation data preserved at ${activationRoot}`
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function buildRestartGuidance(restart) {
|
|
1804
|
+
return buildCleanupRestartGuidance(restart);
|
|
1805
|
+
}
|
|
1806
|
+
function runDetachCommand(parsed) {
|
|
1807
|
+
const steps = [];
|
|
1808
|
+
validateOpenClawHome(parsed.openclawHome);
|
|
1809
|
+
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
1810
|
+
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
1811
|
+
const activationData = summarizeKeptActivationData(activationRoot);
|
|
1812
|
+
const restartGuidance = buildRestartGuidance(parsed.restart);
|
|
1813
|
+
const nextSteps = [
|
|
1814
|
+
restartGuidance,
|
|
1815
|
+
activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
|
|
1816
|
+
`Reattach later: ${buildInstallCommand(parsed.openclawHome)}`
|
|
1817
|
+
].filter((step) => step !== null);
|
|
1818
|
+
steps.push(activationData.activationDataDetail);
|
|
1819
|
+
steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
|
|
1820
|
+
if (parsed.json) {
|
|
1821
|
+
console.log(JSON.stringify({
|
|
1822
|
+
command: "detach",
|
|
1823
|
+
openclawHome: parsed.openclawHome,
|
|
1824
|
+
extensionDir,
|
|
1825
|
+
activationRoot,
|
|
1826
|
+
dataAction: "kept",
|
|
1827
|
+
activationDataState: activationData.activationDataState,
|
|
1828
|
+
restartMode: parsed.restart,
|
|
1829
|
+
restartGuidance,
|
|
1830
|
+
nextSteps,
|
|
1278
1831
|
steps
|
|
1279
1832
|
}, null, 2));
|
|
1280
1833
|
}
|
|
1281
1834
|
else {
|
|
1282
|
-
console.log("
|
|
1835
|
+
console.log("DETACH complete\n");
|
|
1283
1836
|
for (const step of steps) {
|
|
1284
1837
|
console.log(` ✓ ${step}`);
|
|
1285
1838
|
}
|
|
1286
1839
|
console.log("");
|
|
1287
|
-
console.log(`
|
|
1288
|
-
|
|
1840
|
+
console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
|
|
1841
|
+
if (activationRoot !== null) {
|
|
1842
|
+
console.log(`Brain data: ${shortenPath(activationRoot)} remains available for inspection or reattach.`);
|
|
1843
|
+
}
|
|
1844
|
+
else {
|
|
1845
|
+
console.log("Brain data: preserved, but the activation root could not be resolved from the removed hook.");
|
|
1846
|
+
}
|
|
1847
|
+
console.log(`Next: ${restartGuidance}`);
|
|
1848
|
+
if (activationRoot !== null) {
|
|
1849
|
+
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
1850
|
+
}
|
|
1851
|
+
console.log(`Reattach: ${buildInstallCommand(parsed.openclawHome)}`);
|
|
1289
1852
|
}
|
|
1290
1853
|
return 0;
|
|
1291
1854
|
}
|
|
1855
|
+
function runUninstallCommand(parsed) {
|
|
1856
|
+
const steps = [];
|
|
1857
|
+
validateOpenClawHome(parsed.openclawHome);
|
|
1858
|
+
const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
|
|
1859
|
+
const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
|
|
1860
|
+
let activationData;
|
|
1861
|
+
if (parsed.dataMode === "purge") {
|
|
1862
|
+
if (activationRoot === null) {
|
|
1863
|
+
throw new Error("--purge-data requires a resolvable activation root from the installed profile hook or --activation-root <path>");
|
|
1864
|
+
}
|
|
1865
|
+
if (existsSync(activationRoot)) {
|
|
1866
|
+
rmSync(activationRoot, { recursive: true, force: true });
|
|
1867
|
+
activationData = {
|
|
1868
|
+
activationRoot,
|
|
1869
|
+
activationDataState: "removed",
|
|
1870
|
+
activationDataDetail: `Activation data removed at ${activationRoot}`
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
else {
|
|
1874
|
+
activationData = {
|
|
1875
|
+
activationRoot,
|
|
1876
|
+
activationDataState: "already_absent",
|
|
1877
|
+
activationDataDetail: `Activation data already absent at ${activationRoot}`
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
else {
|
|
1882
|
+
activationData = summarizeKeptActivationData(activationRoot);
|
|
1883
|
+
}
|
|
1884
|
+
const restartGuidance = buildRestartGuidance(parsed.restart);
|
|
1885
|
+
const nextSteps = [
|
|
1886
|
+
restartGuidance,
|
|
1887
|
+
parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
|
|
1888
|
+
`Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
|
|
1889
|
+
].filter((step) => step !== null);
|
|
1890
|
+
steps.push(activationData.activationDataDetail);
|
|
1891
|
+
steps.push(parsed.dataMode === "purge"
|
|
1892
|
+
? "Uninstall removed the OpenClaw profile hook and activation data."
|
|
1893
|
+
: "Uninstall removed the OpenClaw profile hook and kept activation data explicitly.");
|
|
1894
|
+
if (parsed.json) {
|
|
1895
|
+
console.log(JSON.stringify({
|
|
1896
|
+
command: "uninstall",
|
|
1897
|
+
openclawHome: parsed.openclawHome,
|
|
1898
|
+
extensionDir,
|
|
1899
|
+
activationRoot,
|
|
1900
|
+
dataAction: parsed.dataMode,
|
|
1901
|
+
activationDataState: activationData.activationDataState,
|
|
1902
|
+
restartMode: parsed.restart,
|
|
1903
|
+
restartGuidance,
|
|
1904
|
+
nextSteps,
|
|
1905
|
+
steps
|
|
1906
|
+
}, null, 2));
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
console.log("UNINSTALL complete\n");
|
|
1910
|
+
for (const step of steps) {
|
|
1911
|
+
console.log(` ✓ ${step}`);
|
|
1912
|
+
}
|
|
1913
|
+
console.log("");
|
|
1914
|
+
console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
|
|
1915
|
+
console.log(`Data mode: ${parsed.dataMode === "purge" ? "purged" : "kept"}`);
|
|
1916
|
+
if (activationRoot !== null) {
|
|
1917
|
+
console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
|
|
1918
|
+
}
|
|
1919
|
+
console.log(`Next: ${restartGuidance}`);
|
|
1920
|
+
if (parsed.dataMode === "keep" && activationRoot !== null) {
|
|
1921
|
+
console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
|
|
1922
|
+
}
|
|
1923
|
+
console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
|
|
1924
|
+
}
|
|
1925
|
+
return 0;
|
|
1926
|
+
}
|
|
1927
|
+
function resolveServeTimeLearningRuntimeInput(activationRoot) {
|
|
1928
|
+
let serveTimeDecisions = [];
|
|
1929
|
+
let fallbackReason = null;
|
|
1930
|
+
try {
|
|
1931
|
+
serveTimeDecisions = readLearningSpineLogEntries(activationRoot, "serveTimeRouteDecisions");
|
|
1932
|
+
}
|
|
1933
|
+
catch {
|
|
1934
|
+
fallbackReason = "serve_time_decision_log_read_failed";
|
|
1935
|
+
}
|
|
1936
|
+
const decisionLogCount = serveTimeDecisions.length;
|
|
1937
|
+
const pgVersion = decisionLogCount > 0 ? "v2" : "v1";
|
|
1938
|
+
return {
|
|
1939
|
+
pgVersion,
|
|
1940
|
+
serveTimeDecisions,
|
|
1941
|
+
decisionLogCount,
|
|
1942
|
+
baselineState: pgVersion === "v2" ? loadOrInitBaseline(activationRoot) : undefined,
|
|
1943
|
+
fallbackReason
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1292
1946
|
function runLearnCommand(parsed) {
|
|
1947
|
+
const learnStatePath = path.join(parsed.activationRoot, "learn-cli-state.json");
|
|
1948
|
+
const teacherSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(parsed.activationRoot);
|
|
1949
|
+
function isLearnRuntimeStateLike(value) {
|
|
1950
|
+
if (typeof value !== "object" || value === null) {
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
const candidate = value;
|
|
1954
|
+
return (candidate.runtimeOwner === "openclaw" &&
|
|
1955
|
+
typeof candidate.cursor === "object" &&
|
|
1956
|
+
candidate.cursor !== null &&
|
|
1957
|
+
typeof candidate.pending === "object" &&
|
|
1958
|
+
candidate.pending !== null &&
|
|
1959
|
+
Array.isArray(candidate.pending.live) &&
|
|
1960
|
+
Array.isArray(candidate.pending.backfill) &&
|
|
1961
|
+
typeof candidate.materializationCount === "number" &&
|
|
1962
|
+
typeof candidate.sparseFeedback === "object" &&
|
|
1963
|
+
candidate.sparseFeedback !== null);
|
|
1964
|
+
}
|
|
1965
|
+
function loadPersistedLearnCliState() {
|
|
1966
|
+
if (!existsSync(learnStatePath)) {
|
|
1967
|
+
return {
|
|
1968
|
+
state: createAlwaysOnLearningRuntimeState(),
|
|
1969
|
+
loaded: false,
|
|
1970
|
+
resetReason: null
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
try {
|
|
1974
|
+
const persisted = readJsonFile(learnStatePath);
|
|
1975
|
+
if (persisted.contract !== "openclaw.learn_cli_state.v1" || !isLearnRuntimeStateLike(persisted.state)) {
|
|
1976
|
+
throw new Error("persisted learn state shape is invalid");
|
|
1977
|
+
}
|
|
1978
|
+
return {
|
|
1979
|
+
state: persisted.state,
|
|
1980
|
+
loaded: true,
|
|
1981
|
+
resetReason: null
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
catch (error) {
|
|
1985
|
+
return {
|
|
1986
|
+
state: createAlwaysOnLearningRuntimeState(),
|
|
1987
|
+
loaded: false,
|
|
1988
|
+
resetReason: error instanceof Error ? error.message : "persisted learn state could not be parsed"
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function persistLearnCliState(state, updatedAt) {
|
|
1993
|
+
const payload = {
|
|
1994
|
+
contract: "openclaw.learn_cli_state.v1",
|
|
1995
|
+
updatedAt,
|
|
1996
|
+
state
|
|
1997
|
+
};
|
|
1998
|
+
mkdirSync(path.dirname(learnStatePath), { recursive: true });
|
|
1999
|
+
writeFileSync(learnStatePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
2000
|
+
}
|
|
1293
2001
|
const activationRoot = parsed.activationRoot;
|
|
1294
|
-
|
|
1295
|
-
const stores =
|
|
2002
|
+
const persistedState = loadPersistedLearnCliState();
|
|
2003
|
+
const stores = discoverOpenClawSessionStores();
|
|
1296
2004
|
if (stores.length === 0) {
|
|
2005
|
+
const labelFlow = {
|
|
2006
|
+
source: "missing",
|
|
2007
|
+
humanLabelCount: 0,
|
|
2008
|
+
selfLabelCount: 0,
|
|
2009
|
+
asyncTeacherArtifactCount: 0,
|
|
2010
|
+
implicitPositiveCount: 0,
|
|
2011
|
+
detail: "no local session stores were found"
|
|
2012
|
+
};
|
|
2013
|
+
const learningPath = summarizeLearningPathFromMaterialization(null);
|
|
1297
2014
|
if (parsed.json) {
|
|
1298
|
-
console.log(JSON.stringify({
|
|
2015
|
+
console.log(JSON.stringify({
|
|
2016
|
+
command: "learn",
|
|
2017
|
+
activationRoot,
|
|
2018
|
+
scannedSessions: 0,
|
|
2019
|
+
newEvents: 0,
|
|
2020
|
+
loadedState: persistedState.loaded,
|
|
2021
|
+
statePath: learnStatePath,
|
|
2022
|
+
stateResetReason: persistedState.resetReason,
|
|
2023
|
+
materialized: null,
|
|
2024
|
+
promoted: false,
|
|
2025
|
+
graph: null,
|
|
2026
|
+
labelFlow,
|
|
2027
|
+
learningPath,
|
|
2028
|
+
message: "No local session stores found."
|
|
2029
|
+
}));
|
|
1299
2030
|
}
|
|
1300
2031
|
else {
|
|
1301
2032
|
console.log("No new session data. Brain is up to date.");
|
|
1302
2033
|
}
|
|
1303
2034
|
return 0;
|
|
1304
2035
|
}
|
|
1305
|
-
// 2. Build passive learning export from ALL discovered sessions in one monotonic sequence space
|
|
1306
2036
|
let totalSessions = 0;
|
|
1307
|
-
let totalInteractionEvents = 0;
|
|
1308
|
-
let totalFeedbackEvents = 0;
|
|
1309
2037
|
const allInteractionEvents = [];
|
|
1310
2038
|
const allFeedbackEvents = [];
|
|
1311
2039
|
let nextSequence = 1;
|
|
@@ -1326,7 +2054,20 @@ function runLearnCommand(parsed) {
|
|
|
1326
2054
|
return left.store.indexPath.localeCompare(right.store.indexPath);
|
|
1327
2055
|
}
|
|
1328
2056
|
return left.sessionKey.localeCompare(right.sessionKey);
|
|
1329
|
-
})
|
|
2057
|
+
})
|
|
2058
|
+
.filter((() => {
|
|
2059
|
+
const seenSessionIds = new Set();
|
|
2060
|
+
return (session) => {
|
|
2061
|
+
const sessionId = session.entry.sessionId;
|
|
2062
|
+
if (sessionId !== undefined && seenSessionIds.has(sessionId)) {
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
if (sessionId !== undefined) {
|
|
2066
|
+
seenSessionIds.add(sessionId);
|
|
2067
|
+
}
|
|
2068
|
+
return true;
|
|
2069
|
+
};
|
|
2070
|
+
})());
|
|
1330
2071
|
for (const session of discoveredSessions) {
|
|
1331
2072
|
const sessionFile = session.entry.sessionFile;
|
|
1332
2073
|
const records = typeof sessionFile !== "string" || sessionFile.trim().length === 0
|
|
@@ -1348,24 +2089,75 @@ function runLearnCommand(parsed) {
|
|
|
1348
2089
|
});
|
|
1349
2090
|
nextSequence = sessionExport.nextSequence;
|
|
1350
2091
|
totalSessions += 1;
|
|
1351
|
-
totalInteractionEvents += sessionExport.interactionEvents.length;
|
|
1352
|
-
totalFeedbackEvents += sessionExport.feedbackEvents.length;
|
|
1353
2092
|
allInteractionEvents.push(...sessionExport.interactionEvents);
|
|
1354
2093
|
allFeedbackEvents.push(...sessionExport.feedbackEvents);
|
|
1355
2094
|
}
|
|
1356
|
-
const
|
|
2095
|
+
const seenInteractionIds = new Set();
|
|
2096
|
+
const dedupedInteractionEvents = [];
|
|
2097
|
+
for (const event of allInteractionEvents) {
|
|
2098
|
+
if (!seenInteractionIds.has(event.eventId)) {
|
|
2099
|
+
seenInteractionIds.add(event.eventId);
|
|
2100
|
+
dedupedInteractionEvents.push(event);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const seenFeedbackIds = new Set();
|
|
2104
|
+
const dedupedFeedbackEvents = [];
|
|
2105
|
+
for (const event of allFeedbackEvents) {
|
|
2106
|
+
if (!seenFeedbackIds.has(event.eventId)) {
|
|
2107
|
+
seenFeedbackIds.add(event.eventId);
|
|
2108
|
+
dedupedFeedbackEvents.push(event);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
const totalEvents = dedupedInteractionEvents.length + dedupedFeedbackEvents.length;
|
|
2112
|
+
const now = new Date().toISOString();
|
|
2113
|
+
const normalizedEventExport = totalEvents === 0
|
|
2114
|
+
? null
|
|
2115
|
+
: buildNormalizedEventExport({
|
|
2116
|
+
interactionEvents: dedupedInteractionEvents,
|
|
2117
|
+
feedbackEvents: dedupedFeedbackEvents
|
|
2118
|
+
});
|
|
2119
|
+
const teacherArtifacts = normalizedEventExport === null
|
|
2120
|
+
? []
|
|
2121
|
+
: buildTeacherSupervisionArtifactsFromNormalizedEventExport({
|
|
2122
|
+
normalizedEventExport,
|
|
2123
|
+
observedAt: now
|
|
2124
|
+
});
|
|
2125
|
+
const labelFlow = normalizedEventExport === null
|
|
2126
|
+
? {
|
|
2127
|
+
source: "missing",
|
|
2128
|
+
humanLabelCount: 0,
|
|
2129
|
+
selfLabelCount: 0,
|
|
2130
|
+
asyncTeacherArtifactCount: 0,
|
|
2131
|
+
implicitPositiveCount: 0,
|
|
2132
|
+
detail: "no normalized learning export was built"
|
|
2133
|
+
}
|
|
2134
|
+
: summarizeNormalizedEventExportLabelFlow(normalizedEventExport, teacherArtifacts.length);
|
|
1357
2135
|
if (totalEvents === 0) {
|
|
1358
2136
|
if (parsed.json) {
|
|
1359
|
-
console.log(JSON.stringify({
|
|
2137
|
+
console.log(JSON.stringify({
|
|
2138
|
+
command: "learn",
|
|
2139
|
+
activationRoot,
|
|
2140
|
+
scannedSessions: totalSessions,
|
|
2141
|
+
newEvents: 0,
|
|
2142
|
+
loadedState: persistedState.loaded,
|
|
2143
|
+
statePath: learnStatePath,
|
|
2144
|
+
stateResetReason: persistedState.resetReason,
|
|
2145
|
+
materialized: null,
|
|
2146
|
+
promoted: false,
|
|
2147
|
+
graph: null,
|
|
2148
|
+
labelFlow,
|
|
2149
|
+
learningPath: summarizeLearningPathFromMaterialization(null),
|
|
2150
|
+
message: "No new session data. Brain is up to date."
|
|
2151
|
+
}));
|
|
1360
2152
|
}
|
|
1361
2153
|
else {
|
|
1362
2154
|
console.log("No new session data. Brain is up to date.");
|
|
1363
2155
|
}
|
|
1364
2156
|
return 0;
|
|
1365
2157
|
}
|
|
1366
|
-
|
|
1367
|
-
const
|
|
1368
|
-
const learnerResult =
|
|
2158
|
+
const learningExport = normalizedEventExport;
|
|
2159
|
+
const serveTimeLearning = resolveServeTimeLearningRuntimeInput(activationRoot);
|
|
2160
|
+
const learnerResult = drainAlwaysOnLearningRuntime({
|
|
1369
2161
|
packLabel: "learn-cli",
|
|
1370
2162
|
workspace: {
|
|
1371
2163
|
workspaceId: "learn-cli",
|
|
@@ -1374,17 +2166,58 @@ function runLearnCommand(parsed) {
|
|
|
1374
2166
|
rootDir: activationRoot,
|
|
1375
2167
|
revision: "learn-cli-v1"
|
|
1376
2168
|
},
|
|
1377
|
-
interactionEvents:
|
|
1378
|
-
feedbackEvents:
|
|
2169
|
+
interactionEvents: dedupedInteractionEvents,
|
|
2170
|
+
feedbackEvents: dedupedFeedbackEvents,
|
|
2171
|
+
teacherSupervisionArtifacts: teacherArtifacts,
|
|
1379
2172
|
learnedRouting: true,
|
|
1380
|
-
state:
|
|
1381
|
-
builtAt: now
|
|
2173
|
+
state: persistedState.state,
|
|
2174
|
+
builtAt: now,
|
|
2175
|
+
maxCycles: 16,
|
|
2176
|
+
pgVersion: serveTimeLearning.pgVersion,
|
|
2177
|
+
...(serveTimeLearning.decisionLogCount > 0 ? { serveTimeDecisions: serveTimeLearning.serveTimeDecisions } : {}),
|
|
2178
|
+
...(serveTimeLearning.baselineState !== undefined ? { baselineState: serveTimeLearning.baselineState } : {})
|
|
1382
2179
|
});
|
|
1383
|
-
|
|
1384
|
-
|
|
2180
|
+
const lastMaterialization = learnerResult.materializations.at(-1) ?? null;
|
|
2181
|
+
const plan = describeAlwaysOnLearningRuntimeState(learnerResult.state, lastMaterialization);
|
|
2182
|
+
const learningPath = summarizeLearningPathFromMaterialization(lastMaterialization);
|
|
2183
|
+
const supervisionCount = lastMaterialization?.candidate.summary.learnedRouter.supervisionCount ?? 0;
|
|
2184
|
+
const routerUpdateCount = lastMaterialization?.candidate.summary.learnedRouter.updateCount ?? 0;
|
|
2185
|
+
const routerNoOpReason = lastMaterialization?.candidate.summary.learnedRouter.noOpReason ?? null;
|
|
2186
|
+
const graphEvolution = lastMaterialization?.candidate.payloads.graph.evolution;
|
|
2187
|
+
const graphSummary = graphEvolution === undefined
|
|
2188
|
+
? null
|
|
2189
|
+
: {
|
|
2190
|
+
structuralOps: graphEvolution.structuralOps,
|
|
2191
|
+
connectDiagnostics: graphEvolution.connectDiagnostics ?? null
|
|
2192
|
+
};
|
|
2193
|
+
const connectSummary = graphSummary?.connectDiagnostics === null || graphSummary?.connectDiagnostics === undefined
|
|
2194
|
+
? ""
|
|
2195
|
+
: ` connect candidates=${graphSummary.connectDiagnostics.candidatePairCount} applied=${graphSummary.connectDiagnostics.appliedPairCount} edges=${graphSummary.connectDiagnostics.createdEdgeCount}.`;
|
|
2196
|
+
const routingBuild = lastMaterialization?.candidate.routingBuild ?? {
|
|
2197
|
+
learnedRoutingPath: serveTimeLearning.pgVersion === "v2" ? "policy_gradient_v2" : "policy_gradient_v1",
|
|
2198
|
+
pgVersionRequested: serveTimeLearning.pgVersion,
|
|
2199
|
+
pgVersionUsed: serveTimeLearning.pgVersion,
|
|
2200
|
+
decisionLogCount: serveTimeLearning.decisionLogCount,
|
|
2201
|
+
fallbackReason: serveTimeLearning.pgVersion === "v1" ? serveTimeLearning.fallbackReason ?? "no_serve_time_decisions" : null,
|
|
2202
|
+
updatedBaseline: null
|
|
2203
|
+
};
|
|
2204
|
+
const learnPathReport = {
|
|
2205
|
+
...routingBuild,
|
|
2206
|
+
fallbackReason: routingBuild.fallbackReason ??
|
|
2207
|
+
(routingBuild.pgVersionUsed === "v1" ? serveTimeLearning.fallbackReason ?? "no_serve_time_decisions" : null)
|
|
2208
|
+
};
|
|
2209
|
+
let promoted = false;
|
|
2210
|
+
let materializedPackId = null;
|
|
2211
|
+
let baselinePersisted = false;
|
|
2212
|
+
const latestTeacherFreshness = teacherArtifacts.length === 0
|
|
2213
|
+
? "none"
|
|
2214
|
+
: teacherArtifacts.some((artifact) => artifact.freshness.status === "fresh")
|
|
2215
|
+
? "fresh"
|
|
2216
|
+
: "stale";
|
|
2217
|
+
if (lastMaterialization !== null) {
|
|
1385
2218
|
const candidatePackRoot = path.join(activationRoot, "packs", `learn-cli-${Date.now()}`);
|
|
1386
2219
|
mkdirSync(candidatePackRoot, { recursive: true });
|
|
1387
|
-
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidatePackRoot,
|
|
2220
|
+
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidatePackRoot, lastMaterialization);
|
|
1388
2221
|
stageCandidatePack(activationRoot, candidatePackRoot, {
|
|
1389
2222
|
updatedAt: now,
|
|
1390
2223
|
reason: "learn_cli_stage"
|
|
@@ -1393,37 +2226,111 @@ function runLearnCommand(parsed) {
|
|
|
1393
2226
|
updatedAt: now,
|
|
1394
2227
|
reason: "learn_cli_promote"
|
|
1395
2228
|
});
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
2229
|
+
if (learnPathReport.pgVersionUsed === "v2" && learnPathReport.updatedBaseline !== null) {
|
|
2230
|
+
persistBaseline(activationRoot, learnPathReport.updatedBaseline);
|
|
2231
|
+
baselinePersisted = true;
|
|
2232
|
+
}
|
|
2233
|
+
materializedPackId = candidateDescriptor.manifest.packId;
|
|
2234
|
+
promoted = true;
|
|
2235
|
+
}
|
|
2236
|
+
persistLearnCliState(learnerResult.state, now);
|
|
2237
|
+
writeJsonFile(teacherSnapshotPath, {
|
|
2238
|
+
runtimeOwner: "openclaw",
|
|
2239
|
+
queue: {
|
|
2240
|
+
capacity: 1,
|
|
2241
|
+
depth: 0,
|
|
2242
|
+
running: false
|
|
2243
|
+
},
|
|
2244
|
+
teacher: {
|
|
2245
|
+
artifactCount: teacherArtifacts.length,
|
|
2246
|
+
artifacts: teacherArtifacts,
|
|
2247
|
+
latestFreshness: latestTeacherFreshness
|
|
2248
|
+
},
|
|
2249
|
+
learner: {
|
|
2250
|
+
state: learnerResult.state,
|
|
2251
|
+
lastMaterialization
|
|
2252
|
+
},
|
|
2253
|
+
diagnostics: {
|
|
2254
|
+
acceptedExportCount: 1,
|
|
2255
|
+
processedExportCount: 1,
|
|
2256
|
+
duplicateExportCount: 0,
|
|
2257
|
+
droppedExportCount: 0,
|
|
2258
|
+
emittedArtifactCount: teacherArtifacts.length,
|
|
2259
|
+
dedupedArtifactCount: 0,
|
|
2260
|
+
lastProcessedAt: now,
|
|
2261
|
+
latestFreshness: latestTeacherFreshness,
|
|
2262
|
+
lastNoOpReason: teacherArtifacts.length === 0 ? "no_teacher_artifacts" : "none",
|
|
2263
|
+
notes: [
|
|
2264
|
+
`learn-cli export=${learningExport.provenance.exportDigest} range=${learningExport.range.start}-${learningExport.range.end}/${learningExport.range.count}`,
|
|
2265
|
+
`teacher artifacts=${teacherArtifacts.length} freshness=${latestTeacherFreshness}`,
|
|
2266
|
+
`last materialized pack=${materializedPackId ?? "none"}`
|
|
2267
|
+
]
|
|
2268
|
+
},
|
|
2269
|
+
state: {
|
|
2270
|
+
interactionEvents: learningExport.interactionEvents,
|
|
2271
|
+
feedbackEvents: learningExport.feedbackEvents,
|
|
2272
|
+
seenExportDigests: [learningExport.provenance.exportDigest]
|
|
2273
|
+
},
|
|
2274
|
+
runtime: {
|
|
2275
|
+
startedAt: now,
|
|
2276
|
+
lastHeartbeatAt: now,
|
|
2277
|
+
lastScanAt: now,
|
|
2278
|
+
scanRoot: null,
|
|
2279
|
+
lastAppliedMaterializationJobId: lastMaterialization?.jobId ?? null
|
|
1410
2280
|
}
|
|
2281
|
+
});
|
|
2282
|
+
const summaryMessage = materializedPackId === null
|
|
2283
|
+
? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no candidate materialized, no promotion.`
|
|
2284
|
+
: `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary}`;
|
|
2285
|
+
if (parsed.json) {
|
|
2286
|
+
console.log(JSON.stringify({
|
|
2287
|
+
command: "learn",
|
|
2288
|
+
activationRoot,
|
|
2289
|
+
scannedSessions: totalSessions,
|
|
2290
|
+
newEvents: totalEvents,
|
|
2291
|
+
loadedState: persistedState.loaded,
|
|
2292
|
+
statePath: learnStatePath,
|
|
2293
|
+
stateResetReason: persistedState.resetReason,
|
|
2294
|
+
drain: {
|
|
2295
|
+
cyclesRun: learnerResult.cycles.length,
|
|
2296
|
+
stopReason: learnerResult.stopReason,
|
|
2297
|
+
drained: learnerResult.drained,
|
|
2298
|
+
materializationCount: learnerResult.materializations.length
|
|
2299
|
+
},
|
|
2300
|
+
learner: {
|
|
2301
|
+
teacherBudget: learnerResult.state.sparseFeedback.teacherBudget,
|
|
2302
|
+
eligibleFeedbackCount: learnerResult.state.sparseFeedback.eligibleFeedbackCount,
|
|
2303
|
+
budgetedOutFeedbackCount: learnerResult.state.sparseFeedback.budgetedOutFeedbackCount,
|
|
2304
|
+
supervisionCount,
|
|
2305
|
+
routerUpdateCount,
|
|
2306
|
+
routerNoOpReason,
|
|
2307
|
+
pending: plan.pending,
|
|
2308
|
+
learnedRange: plan.learnedRange
|
|
2309
|
+
},
|
|
2310
|
+
materialized: materializedPackId,
|
|
2311
|
+
promoted,
|
|
2312
|
+
graph: graphSummary,
|
|
2313
|
+
labelFlow,
|
|
2314
|
+
learningPath,
|
|
2315
|
+
learnedRoutingPath: learnPathReport.learnedRoutingPath,
|
|
2316
|
+
pgVersionRequested: learnPathReport.pgVersionRequested,
|
|
2317
|
+
pgVersionUsed: learnPathReport.pgVersionUsed,
|
|
2318
|
+
decisionLogCount: learnPathReport.decisionLogCount,
|
|
2319
|
+
fallbackReason: learnPathReport.fallbackReason,
|
|
2320
|
+
baselinePersisted,
|
|
2321
|
+
message: summaryMessage
|
|
2322
|
+
}, null, 2));
|
|
1411
2323
|
}
|
|
1412
2324
|
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
|
-
}
|
|
2325
|
+
const text = materializedPackId === null
|
|
2326
|
+
? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no promotion. cycles=${learnerResult.cycles.length} stop=${learnerResult.stopReason} supervision=${supervisionCount}.`
|
|
2327
|
+
: `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary} cycles=${learnerResult.cycles.length} supervision=${supervisionCount}.`;
|
|
2328
|
+
console.log(text);
|
|
2329
|
+
console.log(`labels: source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`);
|
|
2330
|
+
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"}`);
|
|
2331
|
+
console.log(`learned routing: path=${learnPathReport.learnedRoutingPath} pg=${learnPathReport.pgVersionUsed ?? "n/a"} decisions=${learnPathReport.decisionLogCount}` +
|
|
2332
|
+
`${learnPathReport.fallbackReason === null ? "" : ` fallback=${learnPathReport.fallbackReason}`}` +
|
|
2333
|
+
`${learnPathReport.pgVersionUsed === "v2" ? ` baseline=${baselinePersisted ? "persisted" : "unchanged"}` : ""}`);
|
|
1427
2334
|
}
|
|
1428
2335
|
return 0;
|
|
1429
2336
|
}
|
|
@@ -1434,15 +2341,258 @@ function formatTimestamp() {
|
|
|
1434
2341
|
function watchLog(message) {
|
|
1435
2342
|
console.log(`${formatTimestamp()} ${message}`);
|
|
1436
2343
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
2344
|
+
function resolveOperatorTeacherSnapshotPath(activationRoot, explicitPath) {
|
|
2345
|
+
if (explicitPath !== null && explicitPath !== undefined) {
|
|
2346
|
+
return explicitPath;
|
|
2347
|
+
}
|
|
2348
|
+
const asyncSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(activationRoot);
|
|
2349
|
+
return existsSync(asyncSnapshotPath) ? asyncSnapshotPath : null;
|
|
2350
|
+
}
|
|
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
|
+
function sanitizeWatchPathSegment(value) {
|
|
2355
|
+
const sanitized = value
|
|
2356
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
2357
|
+
.replace(/^-+|-+$/g, "")
|
|
2358
|
+
.slice(0, 96);
|
|
2359
|
+
return sanitized.length > 0 ? sanitized : "session";
|
|
2360
|
+
}
|
|
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
|
+
function readOptionalJsonFile(filePath) {
|
|
2371
|
+
if (!existsSync(filePath)) {
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
try {
|
|
2375
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
2376
|
+
}
|
|
2377
|
+
catch {
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
function writeJsonFile(filePath, value) {
|
|
2382
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
2383
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
2384
|
+
}
|
|
2385
|
+
function loadWatchSessionTailCursor(cursorPath) {
|
|
2386
|
+
const parsed = readOptionalJsonFile(cursorPath);
|
|
2387
|
+
if (Array.isArray(parsed)) {
|
|
2388
|
+
return parsed;
|
|
2389
|
+
}
|
|
2390
|
+
if (parsed !== null && Array.isArray(parsed.cursor)) {
|
|
2391
|
+
return parsed.cursor;
|
|
2392
|
+
}
|
|
2393
|
+
return [];
|
|
2394
|
+
}
|
|
2395
|
+
function persistWatchSessionTailCursor(cursorPath, cursor) {
|
|
2396
|
+
writeJsonFile(cursorPath, {
|
|
2397
|
+
contract: "openclaw_watch_session_tail_cursor.v1",
|
|
2398
|
+
runtimeOwner: "openclaw",
|
|
2399
|
+
updatedAt: new Date().toISOString(),
|
|
2400
|
+
cursor
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
function loadWatchTeacherSnapshotState(snapshotPath) {
|
|
2404
|
+
const parsed = readOptionalJsonFile(snapshotPath);
|
|
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
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
function listWatchRuntimeEventExportBundleRoots(scanRoot) {
|
|
2427
|
+
if (!existsSync(scanRoot)) {
|
|
2428
|
+
return [];
|
|
2429
|
+
}
|
|
2430
|
+
return readdirSync(scanRoot, { withFileTypes: true })
|
|
2431
|
+
.filter((entry) => entry.isDirectory())
|
|
2432
|
+
.map((entry) => path.join(scanRoot, entry.name))
|
|
2433
|
+
.sort((left, right) => left.localeCompare(right));
|
|
2434
|
+
}
|
|
2435
|
+
async function replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot) {
|
|
2436
|
+
const seenExportDigests = new Set();
|
|
2437
|
+
const bundles = listWatchRuntimeEventExportBundleRoots(scanRoot)
|
|
2438
|
+
.map((rootDir) => {
|
|
2439
|
+
try {
|
|
2440
|
+
return loadRuntimeEventExportBundle(rootDir);
|
|
2441
|
+
}
|
|
2442
|
+
catch {
|
|
2443
|
+
return null;
|
|
2444
|
+
}
|
|
2445
|
+
})
|
|
2446
|
+
.filter((bundle) => bundle !== null)
|
|
2447
|
+
.sort((left, right) => {
|
|
2448
|
+
const exportedAtCompare = left.manifest.exportedAt.localeCompare(right.manifest.exportedAt);
|
|
2449
|
+
if (exportedAtCompare !== 0) {
|
|
2450
|
+
return exportedAtCompare;
|
|
2451
|
+
}
|
|
2452
|
+
if (left.normalizedEventExport.range.start !== right.normalizedEventExport.range.start) {
|
|
2453
|
+
return left.normalizedEventExport.range.start - right.normalizedEventExport.range.start;
|
|
2454
|
+
}
|
|
2455
|
+
if (left.normalizedEventExport.range.end !== right.normalizedEventExport.range.end) {
|
|
2456
|
+
return left.normalizedEventExport.range.end - right.normalizedEventExport.range.end;
|
|
2457
|
+
}
|
|
2458
|
+
return left.normalizedEventExport.provenance.exportDigest.localeCompare(right.normalizedEventExport.provenance.exportDigest);
|
|
2459
|
+
});
|
|
2460
|
+
let replayedBundleCount = 0;
|
|
2461
|
+
let replayedEventCount = 0;
|
|
2462
|
+
for (const bundle of bundles) {
|
|
2463
|
+
const exportDigest = bundle.normalizedEventExport.provenance.exportDigest;
|
|
2464
|
+
if (seenExportDigests.has(exportDigest)) {
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
seenExportDigests.add(exportDigest);
|
|
2468
|
+
let enqueue = teacherLoop.enqueueNormalizedEventExport(bundle.normalizedEventExport, {
|
|
2469
|
+
observedAt: bundle.manifest.exportedAt
|
|
2470
|
+
});
|
|
2471
|
+
if (!enqueue.accepted && enqueue.reason === "queue_full") {
|
|
2472
|
+
await teacherLoop.flush();
|
|
2473
|
+
enqueue = teacherLoop.enqueueNormalizedEventExport(bundle.normalizedEventExport, {
|
|
2474
|
+
observedAt: bundle.manifest.exportedAt
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
if (!enqueue.accepted) {
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
replayedBundleCount += 1;
|
|
2481
|
+
replayedEventCount += bundle.normalizedEventExport.range.count;
|
|
2482
|
+
}
|
|
2483
|
+
if (replayedBundleCount > 0) {
|
|
2484
|
+
await teacherLoop.flush();
|
|
2485
|
+
}
|
|
2486
|
+
return {
|
|
2487
|
+
replayedBundleCount,
|
|
2488
|
+
replayedEventCount
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
function exportLocalSessionTailChangesToScanRoot(input) {
|
|
2492
|
+
let exportedBundleCount = 0;
|
|
2493
|
+
let exportedEventCount = 0;
|
|
2494
|
+
const warnings = [];
|
|
2495
|
+
for (const change of input.changes) {
|
|
2496
|
+
if (change.scannedEventExport === null) {
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
const built = buildNormalizedEventExportFromScannedEvents(change.scannedEventExport);
|
|
2500
|
+
if (!built.ok) {
|
|
2501
|
+
warnings.push(`${change.sessionKey}: ${built.error}`);
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
const exportDigest = built.normalizedEventExport.provenance.exportDigest.replace(/^sha256-/u, "");
|
|
2505
|
+
const exportName = `session-tail-${sanitizeWatchPathSegment(change.sessionKey)}-${built.normalizedEventExport.range.start}-${built.normalizedEventExport.range.end}-${exportDigest.slice(0, 12)}`;
|
|
2506
|
+
const result = writeScannedEventExportBundle({
|
|
2507
|
+
rootDir: path.join(input.scanRoot, exportName),
|
|
2508
|
+
exportName,
|
|
2509
|
+
exportedAt: input.polledAt,
|
|
2510
|
+
scannedEventExport: change.scannedEventExport
|
|
2511
|
+
});
|
|
2512
|
+
if (!result.ok) {
|
|
2513
|
+
warnings.push(`${change.sessionKey}: ${result.error}`);
|
|
2514
|
+
continue;
|
|
2515
|
+
}
|
|
2516
|
+
exportedBundleCount += 1;
|
|
2517
|
+
exportedEventCount += result.normalizedEventExport.range.count;
|
|
2518
|
+
}
|
|
2519
|
+
return {
|
|
2520
|
+
exportedBundleCount,
|
|
2521
|
+
exportedEventCount,
|
|
2522
|
+
warnings
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId) {
|
|
2526
|
+
const materialization = snapshot?.learner?.lastMaterialization ?? null;
|
|
2527
|
+
if (materialization === null) {
|
|
2528
|
+
return {
|
|
2529
|
+
lastHandledMaterializationPackId,
|
|
2530
|
+
logLine: null,
|
|
2531
|
+
materializedPackId: null
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
const packId = typeof materialization?.candidate?.summary?.packId === "string"
|
|
2535
|
+
? materialization.candidate.summary.packId
|
|
2536
|
+
: null;
|
|
2537
|
+
if (packId === null || packId === lastHandledMaterializationPackId) {
|
|
2538
|
+
return {
|
|
2539
|
+
lastHandledMaterializationPackId,
|
|
2540
|
+
logLine: null,
|
|
2541
|
+
materializedPackId: packId
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
|
|
2545
|
+
try {
|
|
2546
|
+
const candidateRootDir = path.resolve(activationRoot, "packs", packId);
|
|
2547
|
+
mkdirSync(candidateRootDir, { recursive: true });
|
|
2548
|
+
materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
|
|
2549
|
+
const now = new Date().toISOString();
|
|
2550
|
+
stageCandidatePack(activationRoot, candidateRootDir, {
|
|
2551
|
+
updatedAt: now,
|
|
2552
|
+
reason: `watch_stage:${materialization.reason}:${materialization.lane}`
|
|
2553
|
+
});
|
|
2554
|
+
const inspection = inspectActivationState(activationRoot, now);
|
|
2555
|
+
if (inspection.promotion.allowed) {
|
|
2556
|
+
promoteCandidatePack(activationRoot, {
|
|
2557
|
+
updatedAt: now,
|
|
2558
|
+
reason: `watch_promote:${materialization.reason}:${materialization.lane}`
|
|
2559
|
+
});
|
|
2560
|
+
return {
|
|
2561
|
+
lastHandledMaterializationPackId: packId,
|
|
2562
|
+
materializedPackId: packId,
|
|
2563
|
+
logLine: `Promoted ${shortPackId} → active`
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
lastHandledMaterializationPackId: packId,
|
|
2568
|
+
materializedPackId: packId,
|
|
2569
|
+
logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
catch (error) {
|
|
2573
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2574
|
+
return {
|
|
2575
|
+
lastHandledMaterializationPackId,
|
|
2576
|
+
materializedPackId: packId,
|
|
2577
|
+
logLine: `Promotion failed for ${shortPackId}: ${message}`
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
export async function createWatchCommandRuntime(input) {
|
|
2582
|
+
const activationRoot = path.resolve(input.activationRoot);
|
|
2583
|
+
const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
|
|
2584
|
+
? path.resolve(input.scanRoot)
|
|
1441
2585
|
: path.resolve(activationRoot, "event-exports");
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
2586
|
+
const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
|
|
2587
|
+
const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
|
|
2588
|
+
const log = input.log ?? watchLog;
|
|
2589
|
+
mkdirSync(scanRoot, { recursive: true });
|
|
2590
|
+
mkdirSync(resolveWatchStateRoot(activationRoot), { recursive: true });
|
|
2591
|
+
log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
|
|
2592
|
+
log(`Scan root: ${shortenPath(scanRoot)}`);
|
|
2593
|
+
log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
|
|
1445
2594
|
const scanner = createRuntimeEventExportScanner({ scanRoot });
|
|
2595
|
+
let lastServeTimeFallbackReason = null;
|
|
1446
2596
|
const teacherLoop = createAsyncTeacherLiveLoop({
|
|
1447
2597
|
packLabel: "watch-cli",
|
|
1448
2598
|
workspace: {
|
|
@@ -1450,10 +2600,166 @@ async function runWatchCommand(parsed) {
|
|
|
1450
2600
|
snapshotId: `watch-cli@${new Date().toISOString().slice(0, 10)}`,
|
|
1451
2601
|
capturedAt: new Date().toISOString(),
|
|
1452
2602
|
rootDir: activationRoot,
|
|
1453
|
-
revision: "watch-cli-
|
|
2603
|
+
revision: "watch-cli-v2"
|
|
1454
2604
|
},
|
|
1455
|
-
learnedRouting: true
|
|
2605
|
+
learnedRouting: true,
|
|
2606
|
+
resolveLearnedRoutingState: () => {
|
|
2607
|
+
const resolved = resolveServeTimeLearningRuntimeInput(activationRoot);
|
|
2608
|
+
if (resolved.fallbackReason !== null && resolved.fallbackReason !== lastServeTimeFallbackReason) {
|
|
2609
|
+
log(`Serve-time routing fallback: ${resolved.fallbackReason}`);
|
|
2610
|
+
}
|
|
2611
|
+
lastServeTimeFallbackReason = resolved.fallbackReason;
|
|
2612
|
+
return {
|
|
2613
|
+
pgVersion: resolved.pgVersion,
|
|
2614
|
+
...(resolved.decisionLogCount > 0 ? { serveTimeDecisions: resolved.serveTimeDecisions } : {}),
|
|
2615
|
+
...(resolved.baselineState !== undefined ? { baselineState: resolved.baselineState } : {})
|
|
2616
|
+
};
|
|
2617
|
+
},
|
|
2618
|
+
persistUpdatedBaseline: (state) => {
|
|
2619
|
+
try {
|
|
2620
|
+
persistBaseline(activationRoot, state);
|
|
2621
|
+
}
|
|
2622
|
+
catch (error) {
|
|
2623
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2624
|
+
log(`Baseline persist failed: ${message}`);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
|
|
2629
|
+
let localSessionTail;
|
|
2630
|
+
try {
|
|
2631
|
+
localSessionTail = createOpenClawLocalSessionTail({
|
|
2632
|
+
...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
|
|
2633
|
+
cursor: restoredCursor,
|
|
2634
|
+
emitExistingOnFirstPoll: restoredCursor.length === 0
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
catch (error) {
|
|
2638
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2639
|
+
log(`Session tail cursor reset: ${message}`);
|
|
2640
|
+
restoredCursor = [];
|
|
2641
|
+
localSessionTail = createOpenClawLocalSessionTail({
|
|
2642
|
+
...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
|
|
2643
|
+
emitExistingOnFirstPoll: true
|
|
2644
|
+
});
|
|
2645
|
+
persistWatchSessionTailCursor(sessionTailCursorPath, []);
|
|
2646
|
+
}
|
|
2647
|
+
let lastHandledMaterializationPackId = loadWatchTeacherSnapshotState(teacherSnapshotPath).lastHandledMaterializationPackId;
|
|
2648
|
+
const replayState = await replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot);
|
|
2649
|
+
if (replayState.replayedBundleCount > 0) {
|
|
2650
|
+
log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
|
|
2651
|
+
}
|
|
2652
|
+
let bootstrapSnapshot = teacherLoop.snapshot();
|
|
2653
|
+
const replayPromotion = applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId);
|
|
2654
|
+
lastHandledMaterializationPackId = replayPromotion.lastHandledMaterializationPackId;
|
|
2655
|
+
if (replayPromotion.logLine !== null) {
|
|
2656
|
+
log(replayPromotion.logLine);
|
|
2657
|
+
bootstrapSnapshot = teacherLoop.snapshot();
|
|
2658
|
+
}
|
|
2659
|
+
persistWatchTeacherSnapshot(teacherSnapshotPath, {
|
|
2660
|
+
scanRoot,
|
|
2661
|
+
replayedBundleCount: replayState.replayedBundleCount,
|
|
2662
|
+
replayedEventCount: replayState.replayedEventCount,
|
|
2663
|
+
exportedBundleCount: 0,
|
|
2664
|
+
exportedEventCount: 0,
|
|
2665
|
+
localSessionTailNoopReason: null,
|
|
2666
|
+
lastHandledMaterializationPackId,
|
|
2667
|
+
snapshot: bootstrapSnapshot
|
|
1456
2668
|
});
|
|
2669
|
+
return {
|
|
2670
|
+
activationRoot,
|
|
2671
|
+
scanRoot,
|
|
2672
|
+
sessionTailCursorPath,
|
|
2673
|
+
teacherSnapshotPath,
|
|
2674
|
+
replayState,
|
|
2675
|
+
lastHandledMaterializationPackId,
|
|
2676
|
+
scanner,
|
|
2677
|
+
teacherLoop,
|
|
2678
|
+
localSessionTail
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
export async function runWatchCommandPass(runtime, options = {}) {
|
|
2682
|
+
const log = options.log ?? watchLog;
|
|
2683
|
+
const observedAt = options.observedAt ?? new Date().toISOString();
|
|
2684
|
+
const localPoll = runtime.localSessionTail.pollOnce({
|
|
2685
|
+
observedAt
|
|
2686
|
+
});
|
|
2687
|
+
const exported = exportLocalSessionTailChangesToScanRoot({
|
|
2688
|
+
scanRoot: runtime.scanRoot,
|
|
2689
|
+
polledAt: localPoll.polledAt,
|
|
2690
|
+
changes: localPoll.changes
|
|
2691
|
+
});
|
|
2692
|
+
persistWatchSessionTailCursor(runtime.sessionTailCursorPath, localPoll.cursor);
|
|
2693
|
+
for (const warning of [...localPoll.warnings, ...exported.warnings]) {
|
|
2694
|
+
log(`Session tail warning: ${warning}`);
|
|
2695
|
+
}
|
|
2696
|
+
if (exported.exportedBundleCount > 0) {
|
|
2697
|
+
log(`Session tail exported ${exported.exportedBundleCount} bundle${exported.exportedBundleCount === 1 ? "" : "s"} from ${localPoll.changes.length} changed session${localPoll.changes.length === 1 ? "" : "s"}`);
|
|
2698
|
+
}
|
|
2699
|
+
const scanResult = runtime.scanner.scanOnce({
|
|
2700
|
+
scannedAt: observedAt
|
|
2701
|
+
});
|
|
2702
|
+
const totalSelected = scanResult.selected.length;
|
|
2703
|
+
const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
|
|
2704
|
+
let snapshot = runtime.teacherLoop.snapshot();
|
|
2705
|
+
let materializedPackId = null;
|
|
2706
|
+
if (totalSelected === 0) {
|
|
2707
|
+
log("Scanning... no changes");
|
|
2708
|
+
}
|
|
2709
|
+
else {
|
|
2710
|
+
log(`Scanning... ${totalSelected} export bundle${totalSelected === 1 ? "" : "s"} selected, ${totalEvents} event${totalEvents === 1 ? "" : "s"}`);
|
|
2711
|
+
const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
|
|
2712
|
+
snapshot = ingestResult.snapshot;
|
|
2713
|
+
const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
|
|
2714
|
+
runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
|
|
2715
|
+
materializedPackId = promotion.materializedPackId;
|
|
2716
|
+
if (promotion.logLine !== null) {
|
|
2717
|
+
log(promotion.logLine);
|
|
2718
|
+
snapshot = runtime.teacherLoop.snapshot();
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
|
|
2722
|
+
scanRoot: runtime.scanRoot,
|
|
2723
|
+
replayedBundleCount: runtime.replayState.replayedBundleCount,
|
|
2724
|
+
replayedEventCount: runtime.replayState.replayedEventCount,
|
|
2725
|
+
exportedBundleCount: exported.exportedBundleCount,
|
|
2726
|
+
exportedEventCount: exported.exportedEventCount,
|
|
2727
|
+
localSessionTailNoopReason: localPoll.noopReason,
|
|
2728
|
+
lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
|
|
2729
|
+
snapshot
|
|
2730
|
+
});
|
|
2731
|
+
if (options.json) {
|
|
2732
|
+
console.log(JSON.stringify({
|
|
2733
|
+
timestamp: observedAt,
|
|
2734
|
+
replayedBundles: runtime.replayState.replayedBundleCount,
|
|
2735
|
+
replayedEvents: runtime.replayState.replayedEventCount,
|
|
2736
|
+
exportedBundles: exported.exportedBundleCount,
|
|
2737
|
+
exportedEvents: exported.exportedEventCount,
|
|
2738
|
+
selected: totalSelected,
|
|
2739
|
+
events: totalEvents,
|
|
2740
|
+
live: scanResult.live.length,
|
|
2741
|
+
backfill: scanResult.backfill.length,
|
|
2742
|
+
materialized: materializedPackId,
|
|
2743
|
+
diagnostics: snapshot.diagnostics ?? null,
|
|
2744
|
+
localSessionTailNoopReason: localPoll.noopReason
|
|
2745
|
+
}));
|
|
2746
|
+
}
|
|
2747
|
+
return {
|
|
2748
|
+
localPoll,
|
|
2749
|
+
exported,
|
|
2750
|
+
scanResult,
|
|
2751
|
+
snapshot,
|
|
2752
|
+
materializedPackId
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
async function runWatchCommand(parsed) {
|
|
2756
|
+
const intervalMs = parsed.interval * 1000;
|
|
2757
|
+
const runtime = await createWatchCommandRuntime({
|
|
2758
|
+
activationRoot: parsed.activationRoot,
|
|
2759
|
+
scanRoot: parsed.scanRoot,
|
|
2760
|
+
log: watchLog
|
|
2761
|
+
});
|
|
2762
|
+
watchLog(`Interval: ${parsed.interval}s`);
|
|
1457
2763
|
let stopping = false;
|
|
1458
2764
|
const onSignal = () => {
|
|
1459
2765
|
if (stopping) {
|
|
@@ -1466,69 +2772,15 @@ async function runWatchCommand(parsed) {
|
|
|
1466
2772
|
process.on("SIGTERM", onSignal);
|
|
1467
2773
|
while (!stopping) {
|
|
1468
2774
|
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
|
-
}
|
|
2775
|
+
await runWatchCommandPass(runtime, {
|
|
2776
|
+
json: parsed.json,
|
|
2777
|
+
log: watchLog
|
|
2778
|
+
});
|
|
1526
2779
|
}
|
|
1527
2780
|
catch (error) {
|
|
1528
2781
|
const message = error instanceof Error ? error.message : String(error);
|
|
1529
2782
|
watchLog(`Error: ${message}`);
|
|
1530
2783
|
}
|
|
1531
|
-
// Wait for the next interval, checking for stop signal periodically
|
|
1532
2784
|
const deadline = Date.now() + intervalMs;
|
|
1533
2785
|
while (!stopping && Date.now() < deadline) {
|
|
1534
2786
|
await new Promise((resolve) => {
|
|
@@ -1721,9 +2973,15 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
1721
2973
|
});
|
|
1722
2974
|
return 0;
|
|
1723
2975
|
}
|
|
1724
|
-
if (parsed.command === "setup") {
|
|
2976
|
+
if (parsed.command === "install" || parsed.command === "setup") {
|
|
1725
2977
|
return runSetupCommand(parsed);
|
|
1726
2978
|
}
|
|
2979
|
+
if (parsed.command === "detach") {
|
|
2980
|
+
return runDetachCommand(parsed);
|
|
2981
|
+
}
|
|
2982
|
+
if (parsed.command === "uninstall") {
|
|
2983
|
+
return runUninstallCommand(parsed);
|
|
2984
|
+
}
|
|
1727
2985
|
if (parsed.command === "attach") {
|
|
1728
2986
|
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1729
2987
|
mkdirSync(parsed.packRoot, { recursive: true });
|
|
@@ -1801,18 +3059,17 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
1801
3059
|
}
|
|
1802
3060
|
return result.allowed ? 0 : 1;
|
|
1803
3061
|
}
|
|
1804
|
-
const
|
|
3062
|
+
const operatorInput = {
|
|
1805
3063
|
...statusOrRollback.input,
|
|
1806
|
-
activationRoot
|
|
1807
|
-
|
|
3064
|
+
activationRoot,
|
|
3065
|
+
teacherSnapshotPath: resolveOperatorTeacherSnapshotPath(activationRoot, statusOrRollback.input.teacherSnapshotPath)
|
|
3066
|
+
};
|
|
3067
|
+
const status = describeCurrentProfileBrainStatus(operatorInput);
|
|
1808
3068
|
if (statusOrRollback.json) {
|
|
1809
3069
|
console.log(JSON.stringify(status, null, 2));
|
|
1810
3070
|
}
|
|
1811
3071
|
else {
|
|
1812
|
-
const report = buildOperatorSurfaceReport(
|
|
1813
|
-
...statusOrRollback.input,
|
|
1814
|
-
activationRoot
|
|
1815
|
-
});
|
|
3072
|
+
const report = buildOperatorSurfaceReport(operatorInput);
|
|
1816
3073
|
if (statusOrRollback.detailed) {
|
|
1817
3074
|
console.log(formatCurrentProfileStatusSummary(status, report));
|
|
1818
3075
|
}
|