@openclawbrain/cli 0.4.10 → 0.4.12

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.
@@ -8,7 +8,7 @@
8
8
  * Commands:
9
9
  * daemon start — generate and load a launchd plist
10
10
  * daemon stop — unload the plist
11
- * daemon status — show running/stopped + PID + last log lines
11
+ * daemon status — show running/stopped + PID + launch command + last log lines
12
12
  * daemon logs — tail the daemon log file
13
13
  */
14
14
  import { execSync } from "node:child_process";
@@ -22,6 +22,8 @@ const LOG_ROOT_DIRNAME = "daemon";
22
22
  const DEFAULT_SCAN_ROOT_DIRNAME = "event-exports";
23
23
  const BASELINE_STATE_BASENAME = "baseline-state.json";
24
24
  const SCANNER_CHECKPOINT_BASENAME = ".openclawbrain-scanner-checkpoint.json";
25
+ const CLI_PACKAGE_NAME = "@openclawbrain/cli";
26
+ const CLI_BIN_NAME = "openclawbrain";
25
27
  const DEFAULT_DAEMON_COMMAND_RUNNER = (command) => execSync(command, {
26
28
  encoding: "utf8",
27
29
  stdio: "pipe",
@@ -62,15 +64,81 @@ export function buildDaemonServiceIdentity(activationRoot) {
62
64
  export function setDaemonCommandRunnerForTesting(runner) {
63
65
  daemonCommandRunner = runner ?? DEFAULT_DAEMON_COMMAND_RUNNER;
64
66
  }
65
- function getOpenclawbrainBinPath() {
67
+ function readPackageMetadata(packageRoot) {
68
+ if (packageRoot === null) {
69
+ return null;
70
+ }
71
+ const packageJsonPath = path.join(packageRoot, "package.json");
72
+ if (!existsSync(packageJsonPath)) {
73
+ return null;
74
+ }
66
75
  try {
67
- const resolved = daemonCommandRunner("which openclawbrain").trim();
68
- return resolved.length > 0 ? resolved : null;
76
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
77
+ const name = typeof packageJson.name === "string" && packageJson.name.trim().length > 0
78
+ ? packageJson.name.trim()
79
+ : null;
80
+ const version = typeof packageJson.version === "string" && packageJson.version.trim().length > 0
81
+ ? packageJson.version.trim()
82
+ : null;
83
+ if (name === null) {
84
+ return null;
85
+ }
86
+ return { name, version };
69
87
  }
70
88
  catch {
71
89
  return null;
72
90
  }
73
91
  }
92
+ function isCliScriptPath(filePath) {
93
+ const basename = path.basename(filePath);
94
+ return basename === "cli.js" || basename === "cli.cjs" || basename === "cli.mjs";
95
+ }
96
+ function isNodeExecutablePath(filePath) {
97
+ return /^node(?:\.exe)?$/i.test(path.basename(filePath));
98
+ }
99
+ function isNpxCachePath(filePath) {
100
+ const resolvedPath = safeRealpath(path.resolve(filePath));
101
+ return resolvedPath.split(path.sep).includes("_npx");
102
+ }
103
+ function formatCommandArgument(value) {
104
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : JSON.stringify(value);
105
+ }
106
+ function formatCommand(programArguments) {
107
+ return programArguments.map((argument) => formatCommandArgument(argument)).join(" ");
108
+ }
109
+ function escapePlistString(value) {
110
+ return value
111
+ .replace(/&/g, "&")
112
+ .replace(/</g, "&lt;")
113
+ .replace(/>/g, "&gt;")
114
+ .replace(/"/g, "&quot;")
115
+ .replace(/'/g, "&apos;");
116
+ }
117
+ function unescapePlistString(value) {
118
+ return value
119
+ .replace(/&apos;/g, "'")
120
+ .replace(/&quot;/g, "\"")
121
+ .replace(/&gt;/g, ">")
122
+ .replace(/&lt;/g, "<")
123
+ .replace(/&amp;/g, "&");
124
+ }
125
+ function getCommandPaths(commandName) {
126
+ try {
127
+ return daemonCommandRunner(`which -a ${commandName}`)
128
+ .split("\n")
129
+ .map((entry) => entry.trim())
130
+ .filter((entry) => entry.length > 0);
131
+ }
132
+ catch {
133
+ try {
134
+ const resolved = daemonCommandRunner(`command -v ${commandName}`).trim();
135
+ return resolved.length > 0 ? [resolved] : [];
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ }
74
142
  function safeRealpath(filePath) {
75
143
  try {
76
144
  return realpathSync(filePath);
@@ -110,62 +178,129 @@ function resolveCliScriptCandidate(candidatePath) {
110
178
  return null;
111
179
  }
112
180
  const resolvedCandidate = safeRealpath(absoluteCandidate);
113
- const basename = path.basename(resolvedCandidate);
114
- if (basename !== "cli.js" && basename !== "cli.cjs" && basename !== "cli.mjs") {
181
+ if (!isCliScriptPath(resolvedCandidate)) {
115
182
  return null;
116
183
  }
117
184
  return resolvedCandidate;
118
185
  }
119
- function getOpenclawbrainCliScriptPath() {
186
+ function resolveCliPackageRoot(startDir) {
187
+ const packageRoot = resolvePackageRoot(startDir);
188
+ const packageMetadata = readPackageMetadata(packageRoot);
189
+ if (packageMetadata?.name === CLI_PACKAGE_NAME) {
190
+ return packageRoot;
191
+ }
192
+ if (packageRoot !== null) {
193
+ const siblingCliRoot = path.join(path.dirname(packageRoot), "cli");
194
+ const siblingMetadata = readPackageMetadata(siblingCliRoot);
195
+ if (siblingMetadata?.name === CLI_PACKAGE_NAME) {
196
+ return siblingCliRoot;
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ function resolveDaemonPackageManagerLaunchSpec(moduleDir) {
202
+ const cliPackageRoot = resolveCliPackageRoot(moduleDir);
203
+ const cliPackageMetadata = readPackageMetadata(cliPackageRoot);
204
+ if (cliPackageMetadata === null) {
205
+ return null;
206
+ }
207
+ const npmPath = getCommandPaths("npm").find((candidate) => !isNpxCachePath(candidate)) ?? null;
208
+ if (npmPath === null) {
209
+ return null;
210
+ }
211
+ const packageSpec = cliPackageMetadata.version === null
212
+ ? CLI_PACKAGE_NAME
213
+ : `${CLI_PACKAGE_NAME}@${cliPackageMetadata.version}`;
214
+ return {
215
+ programArguments: [npmPath, "exec", "--yes", `--package=${packageSpec}`, "--", CLI_BIN_NAME],
216
+ runtimePath: npmPath,
217
+ runtimePackageSpec: packageSpec,
218
+ };
219
+ }
220
+ function getOpenclawbrainCliScriptPathCandidates() {
120
221
  const moduleFilePath = fileURLToPath(import.meta.url);
121
222
  const moduleDir = path.dirname(moduleFilePath);
122
223
  const packageRoot = resolvePackageRoot(moduleDir);
123
- const candidates = [
224
+ return [
124
225
  process.argv[1],
125
226
  path.join(moduleDir, "cli.js"),
126
227
  packageRoot === null ? null : path.join(packageRoot, "dist", "src", "cli.js")
127
228
  ];
128
- for (const candidate of candidates) {
129
- const resolved = resolveCliScriptCandidate(candidate);
130
- if (resolved !== null) {
131
- return resolved;
132
- }
229
+ }
230
+ function buildDaemonLaunchProgramArguments(serviceIdentity, programArguments) {
231
+ return [...programArguments, "watch", "--activation-root", serviceIdentity.requestedActivationRoot];
232
+ }
233
+ function describeDaemonProgramArguments(programArguments) {
234
+ if (programArguments === null || programArguments.length === 0) {
235
+ return {
236
+ configuredProgramArguments: null,
237
+ configuredCommand: null,
238
+ configuredRuntimePath: null,
239
+ configuredRuntimePackageSpec: null,
240
+ configuredRuntimeLooksEphemeral: null
241
+ };
133
242
  }
134
- return null;
243
+ const runtimePath = programArguments.length >= 2 && isNodeExecutablePath(programArguments[0]) && isCliScriptPath(programArguments[1])
244
+ ? programArguments[1]
245
+ : programArguments[0];
246
+ const runtimePackageSpec = programArguments.find((argument) => argument.startsWith("--package="))?.slice("--package=".length) ?? null;
247
+ return {
248
+ configuredProgramArguments: programArguments,
249
+ configuredCommand: formatCommand(programArguments),
250
+ configuredRuntimePath: runtimePath,
251
+ configuredRuntimePackageSpec: runtimePackageSpec,
252
+ configuredRuntimeLooksEphemeral: runtimePath === null ? null : isNpxCachePath(runtimePath)
253
+ };
135
254
  }
136
255
  function resolveDaemonProgramArguments() {
137
- const cliScriptPath = getOpenclawbrainCliScriptPath();
138
- if (cliScriptPath !== null) {
139
- return [process.execPath, cliScriptPath];
256
+ for (const candidate of getOpenclawbrainCliScriptPathCandidates()) {
257
+ const cliScriptPath = resolveCliScriptCandidate(candidate);
258
+ if (cliScriptPath !== null && !isNpxCachePath(cliScriptPath)) {
259
+ return {
260
+ programArguments: [process.execPath, cliScriptPath],
261
+ runtimePath: cliScriptPath,
262
+ runtimePackageSpec: null,
263
+ };
264
+ }
140
265
  }
141
- const binPath = getOpenclawbrainBinPath();
142
- if (binPath !== null) {
143
- return [binPath];
266
+ const durableBinPath = getCommandPaths(CLI_BIN_NAME).find((candidate) => !isNpxCachePath(candidate)) ?? null;
267
+ if (durableBinPath !== null) {
268
+ return {
269
+ programArguments: [durableBinPath],
270
+ runtimePath: durableBinPath,
271
+ runtimePackageSpec: null,
272
+ };
273
+ }
274
+ const moduleFilePath = fileURLToPath(import.meta.url);
275
+ const moduleDir = path.dirname(moduleFilePath);
276
+ const packageManagerLaunchSpec = resolveDaemonPackageManagerLaunchSpec(moduleDir);
277
+ if (packageManagerLaunchSpec !== null) {
278
+ return packageManagerLaunchSpec;
144
279
  }
145
280
  return null;
146
281
  }
147
282
  function buildPlistXml(serviceIdentity, programArguments) {
148
283
  const logPath = serviceIdentity.logPath;
149
284
  const homeDir = getHomeDir();
150
- const daemonProgramArguments = [...programArguments, "watch", "--activation-root", serviceIdentity.requestedActivationRoot]
151
- .map((argument) => ` <string>${argument}</string>`)
285
+ const daemonProgramArguments = buildDaemonLaunchProgramArguments(serviceIdentity, programArguments)
286
+ .map((argument) => ` <string>${escapePlistString(argument)}</string>`)
152
287
  .join("\n");
153
288
  return `<?xml version="1.0" encoding="UTF-8"?>
154
289
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
155
290
  <plist version="1.0">
156
291
  <dict>
157
292
  <key>Label</key>
158
- <string>${serviceIdentity.label}</string>
293
+ <string>${escapePlistString(serviceIdentity.label)}</string>
159
294
  <key>ProgramArguments</key>
160
295
  <array>
161
296
  ${daemonProgramArguments}
162
297
  </array>
163
298
  <key>WorkingDirectory</key>
164
- <string>${serviceIdentity.requestedActivationRoot}</string>
299
+ <string>${escapePlistString(serviceIdentity.requestedActivationRoot)}</string>
165
300
  <key>StandardOutPath</key>
166
- <string>${logPath}</string>
301
+ <string>${escapePlistString(logPath)}</string>
167
302
  <key>StandardErrorPath</key>
168
- <string>${logPath}</string>
303
+ <string>${escapePlistString(logPath)}</string>
169
304
  <key>KeepAlive</key>
170
305
  <true/>
171
306
  <key>RunAtLoad</key>
@@ -173,9 +308,9 @@ ${daemonProgramArguments}
173
308
  <key>EnvironmentVariables</key>
174
309
  <dict>
175
310
  <key>HOME</key>
176
- <string>${homeDir}</string>
311
+ <string>${escapePlistString(homeDir)}</string>
177
312
  <key>PATH</key>
178
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
313
+ <string>${escapePlistString("/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin")}</string>
179
314
  </dict>
180
315
  </dict>
181
316
  </plist>
@@ -235,6 +370,7 @@ function getLaunchctlInfo(label) {
235
370
  function inspectManagedLearnerServiceInternal(activationRoot) {
236
371
  const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
237
372
  const configuredActivationRoot = readDaemonActivationRoot(serviceIdentity.plistPath);
373
+ const configuredProgramArguments = readDaemonProgramArguments(serviceIdentity.plistPath);
238
374
  const info = getLaunchctlInfo(serviceIdentity.label);
239
375
  return {
240
376
  requestedActivationRoot: serviceIdentity.requestedActivationRoot,
@@ -246,6 +382,7 @@ function inspectManagedLearnerServiceInternal(activationRoot) {
246
382
  running: info.running,
247
383
  pid: info.pid,
248
384
  configuredActivationRoot,
385
+ ...describeDaemonProgramArguments(configuredProgramArguments),
249
386
  matchesRequestedActivationRoot: configuredActivationRoot === null
250
387
  ? null
251
388
  : canonicalizeActivationRoot(configuredActivationRoot) === serviceIdentity.canonicalActivationRoot,
@@ -262,11 +399,11 @@ function startManagedLearnerService(activationRoot) {
262
399
  inspection: inspectionBeforeStart
263
400
  };
264
401
  }
265
- const programArguments = resolveDaemonProgramArguments();
266
- if (programArguments === null) {
402
+ const launchSpec = resolveDaemonProgramArguments();
403
+ if (launchSpec === null) {
267
404
  return {
268
405
  ok: false,
269
- message: "Failed to resolve an OpenClawBrain CLI launch command.",
406
+ message: "Failed to resolve an OpenClawBrain CLI launch command without pinning an npx cache path.",
270
407
  inspection: inspectionBeforeStart
271
408
  };
272
409
  }
@@ -275,7 +412,7 @@ function startManagedLearnerService(activationRoot) {
275
412
  mkdirSync(launchAgentsDir, { recursive: true });
276
413
  }
277
414
  ensureLogDir(serviceIdentity.logPath);
278
- const plistContent = buildPlistXml(serviceIdentity, programArguments);
415
+ const plistContent = buildPlistXml(serviceIdentity, launchSpec.programArguments);
279
416
  writeFileSync(inspectionBeforeStart.plistPath, plistContent, "utf8");
280
417
  const result = launchctlLoad(inspectionBeforeStart.plistPath);
281
418
  if (!result.ok && !inspectionBeforeStart.installed) {
@@ -347,7 +484,7 @@ export function ensureManagedLearnerServiceForActivationRoot(activationRoot) {
347
484
  }
348
485
  const reason = !inspection.launchctlAvailable
349
486
  ? "launchctl_unavailable"
350
- : startResult.message === "Failed to resolve an OpenClawBrain CLI launch command."
487
+ : startResult.message === "Failed to resolve an OpenClawBrain CLI launch command without pinning an npx cache path."
351
488
  ? "launch_command_unavailable"
352
489
  : "launch_failed";
353
490
  return {
@@ -419,19 +556,39 @@ function readOptionalJsonFile(filePath) {
419
556
  return null;
420
557
  }
421
558
  }
422
- function readDaemonActivationRoot(plistPath) {
559
+ function readDaemonProgramArguments(plistPath) {
423
560
  if (!existsSync(plistPath)) {
424
561
  return null;
425
562
  }
426
563
  try {
427
564
  const plist = readFileSync(plistPath, "utf8");
428
- const match = plist.match(/<string>--activation-root<\/string>\s*<string>([^<]+)<\/string>/);
429
- return match?.[1] ?? null;
565
+ const sectionMatch = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
566
+ if (sectionMatch === null) {
567
+ return null;
568
+ }
569
+ const programArguments = [];
570
+ const stringPattern = /<string>([\s\S]*?)<\/string>/g;
571
+ let match = stringPattern.exec(sectionMatch[1]);
572
+ while (match !== null) {
573
+ programArguments.push(unescapePlistString(match[1]));
574
+ match = stringPattern.exec(sectionMatch[1]);
575
+ }
576
+ return programArguments.length > 0 ? programArguments : null;
430
577
  }
431
578
  catch {
432
579
  return null;
433
580
  }
434
581
  }
582
+ function readDaemonActivationRoot(plistPath) {
583
+ const programArguments = readDaemonProgramArguments(plistPath);
584
+ if (programArguments !== null) {
585
+ const activationRootIndex = programArguments.indexOf("--activation-root");
586
+ if (activationRootIndex !== -1) {
587
+ return programArguments[activationRootIndex + 1] ?? null;
588
+ }
589
+ }
590
+ return null;
591
+ }
435
592
  function getWatchStatePaths(activationRoot) {
436
593
  if (activationRoot === null) {
437
594
  return {
@@ -611,9 +768,9 @@ export function daemonStart(activationRoot, json) {
611
768
  const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
612
769
  const plistPath = serviceIdentity.plistPath;
613
770
  const logPath = serviceIdentity.logPath;
614
- const programArguments = resolveDaemonProgramArguments();
615
- if (programArguments === null) {
616
- const message = "Failed to resolve an OpenClawBrain CLI launch command. Install/build the local package or make `openclawbrain` available on PATH.";
771
+ const launchSpec = resolveDaemonProgramArguments();
772
+ if (launchSpec === null) {
773
+ const message = "Failed to resolve an OpenClawBrain CLI launch command without pinning an npx cache path. Install/build @openclawbrain/cli or use a durable repo/runtime checkout.";
617
774
  if (json) {
618
775
  console.log(JSON.stringify({
619
776
  command: "daemon start",
@@ -630,6 +787,8 @@ export function daemonStart(activationRoot, json) {
630
787
  }
631
788
  return 1;
632
789
  }
790
+ const daemonProgramArguments = buildDaemonLaunchProgramArguments(serviceIdentity, launchSpec.programArguments);
791
+ const daemonLaunchDescription = describeDaemonProgramArguments(daemonProgramArguments);
633
792
  // Ensure LaunchAgents dir exists
634
793
  const launchAgentsDir = path.dirname(plistPath);
635
794
  if (!existsSync(launchAgentsDir)) {
@@ -637,7 +796,7 @@ export function daemonStart(activationRoot, json) {
637
796
  }
638
797
  ensureLogDir(logPath);
639
798
  // Write the plist
640
- const plistContent = buildPlistXml(serviceIdentity, programArguments);
799
+ const plistContent = buildPlistXml(serviceIdentity, launchSpec.programArguments);
641
800
  writeFileSync(plistPath, plistContent, "utf8");
642
801
  // Load it
643
802
  const result = launchctlLoad(plistPath);
@@ -649,6 +808,7 @@ export function daemonStart(activationRoot, json) {
649
808
  logPath,
650
809
  activationRoot: serviceIdentity.requestedActivationRoot,
651
810
  serviceLabel: serviceIdentity.label,
811
+ ...daemonLaunchDescription,
652
812
  message: result.message,
653
813
  }, null, 2));
654
814
  }
@@ -659,6 +819,15 @@ export function daemonStart(activationRoot, json) {
659
819
  console.log(` Plist: ${plistPath}`);
660
820
  console.log(` Log: ${logPath}`);
661
821
  console.log(` Root: ${serviceIdentity.requestedActivationRoot}`);
822
+ if (daemonLaunchDescription.configuredRuntimePath !== null) {
823
+ const runtimePackageSuffix = daemonLaunchDescription.configuredRuntimePackageSpec === null
824
+ ? ""
825
+ : ` (${daemonLaunchDescription.configuredRuntimePackageSpec})`;
826
+ console.log(` Runtime: ${daemonLaunchDescription.configuredRuntimePath}${runtimePackageSuffix}`);
827
+ }
828
+ if (daemonLaunchDescription.configuredCommand !== null) {
829
+ console.log(` Command: ${daemonLaunchDescription.configuredCommand}`);
830
+ }
662
831
  }
663
832
  else {
664
833
  console.error(`✗ ${result.message}`);
@@ -725,12 +894,14 @@ export function daemonStatus(activationRoot, json) {
725
894
  const info = getLaunchctlInfo(serviceIdentity.label);
726
895
  const lastLogLines = readLastLines(logPath, 5);
727
896
  const configuredActivationRoot = readDaemonActivationRoot(plistPath);
897
+ const configuredProgramArguments = readDaemonProgramArguments(plistPath);
728
898
  const requestedActivationRoot = serviceIdentity.requestedActivationRoot;
729
899
  const watchStatePaths = getWatchStatePaths(requestedActivationRoot);
730
900
  const watchState = readWatchStateSummary(requestedActivationRoot);
731
901
  const matchesRequestedActivationRoot = configuredActivationRoot === null
732
902
  ? null
733
903
  : canonicalizeActivationRoot(configuredActivationRoot) === serviceIdentity.canonicalActivationRoot;
904
+ const daemonLaunchDescription = describeDaemonProgramArguments(configuredProgramArguments);
734
905
  if (json) {
735
906
  console.log(JSON.stringify({
736
907
  command: "daemon status",
@@ -742,6 +913,7 @@ export function daemonStatus(activationRoot, json) {
742
913
  logPath,
743
914
  activationRoot: requestedActivationRoot,
744
915
  configuredActivationRoot,
916
+ ...daemonLaunchDescription,
745
917
  matchesRequestedActivationRoot,
746
918
  ...watchStatePaths,
747
919
  watchState,
@@ -766,6 +938,22 @@ export function daemonStatus(activationRoot, json) {
766
938
  if (matchesRequestedActivationRoot === false) {
767
939
  console.log(" Requested root does not match the installed daemon plist.");
768
940
  }
941
+ if (daemonLaunchDescription.configuredRuntimePath !== null) {
942
+ const runtimePackageSuffix = daemonLaunchDescription.configuredRuntimePackageSpec === null
943
+ ? ""
944
+ : ` (${daemonLaunchDescription.configuredRuntimePackageSpec})`;
945
+ const runtimeWarning = daemonLaunchDescription.configuredRuntimeLooksEphemeral ? " [ephemeral]" : "";
946
+ console.log(` Runtime: ${daemonLaunchDescription.configuredRuntimePath}${runtimePackageSuffix}${runtimeWarning}`);
947
+ }
948
+ if (configuredProgramArguments !== null && configuredProgramArguments.length > 0) {
949
+ console.log(` Program: ${configuredProgramArguments[0]}`);
950
+ if (configuredProgramArguments.length > 1) {
951
+ console.log(` Args: ${configuredProgramArguments.slice(1).map((argument) => formatCommandArgument(argument)).join(" ")}`);
952
+ }
953
+ }
954
+ if (daemonLaunchDescription.configuredCommand !== null) {
955
+ console.log(` Command: ${daemonLaunchDescription.configuredCommand}`);
956
+ }
769
957
  console.log(` Log: ${logPath}`);
770
958
  if (watchState.scanRoot !== null) {
771
959
  console.log(` Scan root: ${watchState.scanRoot}`);
@@ -904,7 +1092,7 @@ export function daemonHelp() {
904
1092
  "Subcommands:",
905
1093
  " start Generate a macOS launchd plist and start the daemon (runs openclawbrain watch).",
906
1094
  " stop Stop the daemon and remove the launchd plist.",
907
- " status Show whether the daemon is running, its PID, and recent log lines.",
1095
+ " status Show whether the daemon is running, its PID, configured launch command, and recent log lines.",
908
1096
  " logs Show the last 50 lines of the per-activation-root daemon log under ~/.openclawbrain/daemon/.",
909
1097
  "",
910
1098
  "Options:",
package/dist/src/index.js CHANGED
@@ -887,7 +887,8 @@ export class AsyncTeacherLiveLoop {
887
887
  ...(this.input.cadence !== undefined ? { cadence: this.input.cadence } : {}),
888
888
  ...(learnedRoutingState.pgVersion !== undefined ? { pgVersion: learnedRoutingState.pgVersion } : {}),
889
889
  ...(learnedRoutingState.serveTimeDecisions !== undefined ? { serveTimeDecisions: learnedRoutingState.serveTimeDecisions } : {}),
890
- ...(learnedRoutingState.baselineState !== undefined ? { baselineState: learnedRoutingState.baselineState } : {})
890
+ ...(learnedRoutingState.baselineState !== undefined ? { baselineState: learnedRoutingState.baselineState } : {}),
891
+ ...(this.input.activationRoot !== undefined ? { activationRoot: this.input.activationRoot } : {})
891
892
  });
892
893
  this.learnerState = structuredClone(learnerResult.state);
893
894
  this.lastMaterialization = cloneAlwaysOnLearningMaterializationJobOrNull(learnerResult.materialization);
@@ -996,6 +997,7 @@ export function scanLiveEventExport(input) {
996
997
  learnedRouting: input.learnedRouting ?? true,
997
998
  state: createAlwaysOnLearningRuntimeState(),
998
999
  builtAt: normalizeIsoTimestamp(input.builtAt, "builtAt", observedAt),
1000
+ ...(input.activationRoot !== undefined ? { activationRoot: input.activationRoot } : {}),
999
1001
  ...(input.liveSliceSize !== undefined ? { liveSliceSize: input.liveSliceSize } : {}),
1000
1002
  ...(input.backfillSliceSize !== undefined ? { backfillSliceSize: input.backfillSliceSize } : {})
1001
1003
  });
@@ -4037,7 +4039,8 @@ export function runContinuousProductLoopTurn(input) {
4037
4039
  ...(input.sparseFeedback !== undefined ? { sparseFeedback: input.sparseFeedback } : {}),
4038
4040
  ...(input.liveSliceSize !== undefined ? { liveSliceSize: input.liveSliceSize } : {}),
4039
4041
  ...(input.backfillSliceSize !== undefined ? { backfillSliceSize: input.backfillSliceSize } : {}),
4040
- ...(input.cadence !== undefined ? { cadence: input.cadence } : {})
4042
+ ...(input.cadence !== undefined ? { cadence: input.cadence } : {}),
4043
+ activationRoot
4041
4044
  });
4042
4045
  currentState.learner = structuredClone(learnerResult.state);
4043
4046
  currentState.runtimePlasticity = learnerResult.state.runtimePlasticity === null ? null : structuredClone(learnerResult.state.runtimePlasticity);
@@ -29,6 +29,7 @@ export interface CandidatePackBuildInput {
29
29
  serveTimeDecisions?: LearningSpineServeRouteDecisionLogEntryV1[];
30
30
  /** Baseline state for V2 variance reduction. */
31
31
  baselineState?: BaselineStateV1;
32
+ activationRoot?: string;
32
33
  }
33
34
  export interface CandidatePackFromNormalizedEventExportInput {
34
35
  packLabel: string;
@@ -45,6 +46,7 @@ export interface CandidatePackFromNormalizedEventExportInput {
45
46
  pgVersion?: "v1" | "v2";
46
47
  serveTimeDecisions?: LearningSpineServeRouteDecisionLogEntryV1[];
47
48
  baselineState?: BaselineStateV1;
49
+ activationRoot?: string;
48
50
  }
49
51
  export interface BuildTeacherSupervisionArtifactsInput {
50
52
  normalizedEventExport: NormalizedEventExportV1;
@@ -66,6 +68,7 @@ interface CandidatePackBridgeInputBase {
66
68
  pgVersion?: "v1" | "v2";
67
69
  serveTimeDecisions?: LearningSpineServeRouteDecisionLogEntryV1[];
68
70
  baselineState?: BaselineStateV1;
71
+ activationRoot?: string;
69
72
  }
70
73
  export interface CandidatePackFromNormalizedEventExportSliceInput extends CandidatePackBridgeInputBase {
71
74
  normalizedEventExportSlice: NormalizedEventExportSliceV1;
@@ -325,6 +328,7 @@ export interface AdvanceAlwaysOnLearningRuntimeInput {
325
328
  pgVersion?: "v1" | "v2";
326
329
  serveTimeDecisions?: LearningSpineServeRouteDecisionLogEntryV1[];
327
330
  baselineState?: BaselineStateV1;
331
+ activationRoot?: string;
328
332
  }
329
333
  export interface AlwaysOnLearningMaterializationJobV1 {
330
334
  jobId: string;