@openclawbrain/cli 0.4.12 → 0.4.14

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.
@@ -3670,7 +3670,11 @@ export function buildGraphLocalActionSet(nodeBlockId, neighborBlockIds, graph, v
3670
3670
  // TrajectoryStepV1, TrajectoryV1 defined below in PG5 section
3671
3671
  export const STOP_ACTION_ID = "__STOP__";
3672
3672
  const STOP_ACTION = STOP_ACTION_ID;
3673
+ const STOP_ACTION_UPDATE_SEPARATOR = "::";
3673
3674
  const DEFAULT_STOP_BIAS = 0.0;
3675
+ export function buildStopActionUpdateBlockId(nodeBlockId) {
3676
+ return `${nodeBlockId}${STOP_ACTION_UPDATE_SEPARATOR}${STOP_ACTION_ID}`;
3677
+ }
3674
3678
  const DEFAULT_TAU = 1.0;
3675
3679
  const DEFAULT_BASELINE_ALPHA = 0.05;
3676
3680
  const MAX_TRAJECTORY_LENGTH = 20;
@@ -3741,15 +3745,12 @@ export function computeTrajectoryPolicyGradient(trajectory, adjacency, graph, ve
3741
3745
  grad = (1 / tau) * (-prob);
3742
3746
  }
3743
3747
  }
3744
- tailGradient.set(j, (tailGradient.get(j) ?? 0) + grad);
3748
+ const gradientKey = j === STOP_ACTION_ID ? buildStopActionUpdateBlockId(step.nodeBlockId) : j;
3749
+ tailGradient.set(gradientKey, (tailGradient.get(gradientKey) ?? 0) + grad);
3745
3750
  }
3746
3751
  }
3747
3752
  // Weight by advantage and pgScale, accumulate into updates
3748
3753
  for (const [blockId, grad] of tailGradient) {
3749
- // Skip STOP action — it has no learnable weight
3750
- if (blockId === STOP_ACTION_ID) {
3751
- continue;
3752
- }
3753
3754
  const scaledDelta = advantage * grad * pgScale;
3754
3755
  const current = updates.get(blockId);
3755
3756
  if (current === undefined) {
@@ -4053,15 +4054,36 @@ function normalizeOutcomeMatchText(value) {
4053
4054
  ? value.replace(/\s+/g, " ").trim().toLowerCase()
4054
4055
  : null;
4055
4056
  }
4057
+ function normalizeObservationOptionalString(value) {
4058
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
4059
+ }
4060
+ function buildObservationSelectionDigestKey(selectionDigest, activePackGraphChecksum) {
4061
+ const normalizedSelectionDigest = normalizeObservationOptionalString(selectionDigest);
4062
+ const normalizedGraphChecksum = normalizeObservationOptionalString(activePackGraphChecksum);
4063
+ if (normalizedSelectionDigest === null || normalizedGraphChecksum === null) {
4064
+ return null;
4065
+ }
4066
+ return `${normalizedGraphChecksum}|${normalizedSelectionDigest}`;
4067
+ }
4056
4068
  function parseObservationRouteMetadata(value) {
4057
4069
  if (typeof value !== "string" || value.trim().length === 0) {
4058
4070
  return {
4071
+ serveDecisionRecordId: null,
4072
+ selectionDigest: null,
4073
+ turnCompileEventId: null,
4074
+ activePackId: null,
4075
+ activePackGraphChecksum: null,
4059
4076
  selectedNodeIds: [],
4060
4077
  selectedPathNodeIds: []
4061
4078
  };
4062
4079
  }
4063
4080
  try {
4064
4081
  const parsed = JSON.parse(value);
4082
+ const serveDecisionRecordId = normalizeObservationOptionalString(parsed?.serveDecisionRecordId);
4083
+ const selectionDigest = normalizeObservationOptionalString(parsed?.selectionDigest);
4084
+ const turnCompileEventId = normalizeObservationOptionalString(parsed?.turnCompileEventId);
4085
+ const activePackId = normalizeObservationOptionalString(parsed?.activePackId);
4086
+ const activePackGraphChecksum = normalizeObservationOptionalString(parsed?.activePackGraphChecksum);
4065
4087
  const selectedNodeIds = Array.isArray(parsed?.selectedNodeIds)
4066
4088
  ? parsed.selectedNodeIds.filter((entry) => typeof entry === "string")
4067
4089
  : [];
@@ -4069,12 +4091,22 @@ function parseObservationRouteMetadata(value) {
4069
4091
  ? parsed.selectedPathNodeIds.filter((entry) => typeof entry === "string")
4070
4092
  : [];
4071
4093
  return {
4094
+ serveDecisionRecordId,
4095
+ selectionDigest,
4096
+ turnCompileEventId,
4097
+ activePackId,
4098
+ activePackGraphChecksum,
4072
4099
  selectedNodeIds,
4073
4100
  selectedPathNodeIds
4074
4101
  };
4075
4102
  }
4076
4103
  catch {
4077
4104
  return {
4105
+ serveDecisionRecordId: null,
4106
+ selectionDigest: null,
4107
+ turnCompileEventId: null,
4108
+ activePackId: null,
4109
+ activePackGraphChecksum: null,
4078
4110
  selectedNodeIds: [],
4079
4111
  selectedPathNodeIds: []
4080
4112
  };
@@ -4098,8 +4130,24 @@ function loadTeacherObservationOutcomesFromActivation(activationRoot) {
4098
4130
  if (table === undefined) {
4099
4131
  return [];
4100
4132
  }
4133
+ const columns = new Set(db.prepare("PRAGMA table_info(brain_observations)").all().map((column) => String(column.name ?? "")));
4134
+ const selectableColumns = [
4135
+ "id",
4136
+ "query_text",
4137
+ "route_metadata_json",
4138
+ "final_score",
4139
+ "confidence",
4140
+ "created_at",
4141
+ "updated_at",
4142
+ "evaluated_at",
4143
+ columns.has("serve_decision_record_id") ? "serve_decision_record_id" : "NULL AS serve_decision_record_id",
4144
+ columns.has("selection_digest") ? "selection_digest" : "NULL AS selection_digest",
4145
+ columns.has("turn_compile_event_id") ? "turn_compile_event_id" : "NULL AS turn_compile_event_id",
4146
+ columns.has("active_pack_id") ? "active_pack_id" : "NULL AS active_pack_id",
4147
+ columns.has("active_pack_graph_checksum") ? "active_pack_graph_checksum" : "NULL AS active_pack_graph_checksum"
4148
+ ];
4101
4149
  const rows = db.prepare(`
4102
- SELECT id, query_text, route_metadata_json, final_score, confidence, created_at, updated_at, evaluated_at
4150
+ SELECT ${selectableColumns.join(", ")}
4103
4151
  FROM brain_observations
4104
4152
  WHERE final_score IS NOT NULL
4105
4153
  ORDER BY created_at DESC
@@ -4115,6 +4163,11 @@ function loadTeacherObservationOutcomesFromActivation(activationRoot) {
4115
4163
  createdAt: Number.isFinite(Number(row.created_at)) ? Number(row.created_at) : null,
4116
4164
  updatedAt: Number.isFinite(Number(row.updated_at)) ? Number(row.updated_at) : null,
4117
4165
  evaluatedAt: Number.isFinite(Number(row.evaluated_at)) ? Number(row.evaluated_at) : null,
4166
+ serveDecisionRecordId: normalizeObservationOptionalString(row.serve_decision_record_id) ?? routeMetadata.serveDecisionRecordId,
4167
+ selectionDigest: normalizeObservationOptionalString(row.selection_digest) ?? routeMetadata.selectionDigest,
4168
+ turnCompileEventId: normalizeObservationOptionalString(row.turn_compile_event_id) ?? routeMetadata.turnCompileEventId,
4169
+ activePackId: normalizeObservationOptionalString(row.active_pack_id) ?? routeMetadata.activePackId,
4170
+ activePackGraphChecksum: normalizeObservationOptionalString(row.active_pack_graph_checksum) ?? routeMetadata.activePackGraphChecksum,
4118
4171
  selectedNodeIds: routeMetadata.selectedNodeIds,
4119
4172
  selectedPathNodeIds: routeMetadata.selectedPathNodeIds
4120
4173
  };
@@ -4196,45 +4249,172 @@ function compareDecisionObservationMatch(left, right) {
4196
4249
  }
4197
4250
  return 0;
4198
4251
  }
4252
+ function emptyTeacherObservationBindingStats() {
4253
+ return {
4254
+ totalObservationCount: 0,
4255
+ nonZeroObservationCount: 0,
4256
+ skippedZeroRewardCount: 0,
4257
+ matched: {
4258
+ exactDecisionId: 0,
4259
+ exactSelectionDigest: 0,
4260
+ turnCompileEventId: 0,
4261
+ legacyHeuristic: 0
4262
+ },
4263
+ unmatched: {
4264
+ exactDecisionId: 0,
4265
+ exactSelectionDigest: 0,
4266
+ turnCompileEventId: 0,
4267
+ legacyHeuristic: 0
4268
+ },
4269
+ ambiguous: {
4270
+ exactDecisionId: 0,
4271
+ exactSelectionDigest: 0,
4272
+ turnCompileEventId: 0,
4273
+ legacyHeuristic: 0
4274
+ }
4275
+ };
4276
+ }
4277
+ function summarizeTeacherObservationBindingStats(stats) {
4278
+ return [
4279
+ `matched(exact_decision_id=${stats.matched.exactDecisionId},exact_selection_digest=${stats.matched.exactSelectionDigest},turn_compile_event_id=${stats.matched.turnCompileEventId},legacy_heuristic=${stats.matched.legacyHeuristic})`,
4280
+ `unmatched(exact_decision_id=${stats.unmatched.exactDecisionId},exact_selection_digest=${stats.unmatched.exactSelectionDigest},turn_compile_event_id=${stats.unmatched.turnCompileEventId},legacy_heuristic=${stats.unmatched.legacyHeuristic})`,
4281
+ `ambiguous(exact_decision_id=${stats.ambiguous.exactDecisionId},exact_selection_digest=${stats.ambiguous.exactSelectionDigest},turn_compile_event_id=${stats.ambiguous.turnCompileEventId},legacy_heuristic=${stats.ambiguous.legacyHeuristic})`,
4282
+ `non_zero=${stats.nonZeroObservationCount}`,
4283
+ `skipped_zero=${stats.skippedZeroRewardCount}`
4284
+ ].join(" ");
4285
+ }
4286
+ function buildTeacherObservationDecisionIndexes(decisions) {
4287
+ const byRecordId = new Map();
4288
+ const bySelectionDigest = new Map();
4289
+ const ambiguousSelectionDigests = new Set();
4290
+ const byTurnCompileEventId = new Map();
4291
+ const ambiguousTurnCompileEventIds = new Set();
4292
+ for (const decision of decisions) {
4293
+ byRecordId.set(decision.recordId, decision);
4294
+ const selectionDigestKey = buildObservationSelectionDigestKey(decision.selectionDigest, decision.activePackGraphChecksum);
4295
+ if (selectionDigestKey !== null) {
4296
+ if (bySelectionDigest.has(selectionDigestKey)) {
4297
+ bySelectionDigest.delete(selectionDigestKey);
4298
+ ambiguousSelectionDigests.add(selectionDigestKey);
4299
+ }
4300
+ else if (!ambiguousSelectionDigests.has(selectionDigestKey)) {
4301
+ bySelectionDigest.set(selectionDigestKey, decision);
4302
+ }
4303
+ }
4304
+ const turnCompileEventId = normalizeObservationOptionalString(decision.turnCompileEventId);
4305
+ if (turnCompileEventId !== null) {
4306
+ if (byTurnCompileEventId.has(turnCompileEventId)) {
4307
+ byTurnCompileEventId.delete(turnCompileEventId);
4308
+ ambiguousTurnCompileEventIds.add(turnCompileEventId);
4309
+ }
4310
+ else if (!ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
4311
+ byTurnCompileEventId.set(turnCompileEventId, decision);
4312
+ }
4313
+ }
4314
+ }
4315
+ return {
4316
+ byRecordId,
4317
+ bySelectionDigest,
4318
+ ambiguousSelectionDigests,
4319
+ byTurnCompileEventId,
4320
+ ambiguousTurnCompileEventIds
4321
+ };
4322
+ }
4323
+ function resolveLegacyTeacherObservationMatch(decisions, observation) {
4324
+ const matches = decisions
4325
+ .map((decision) => {
4326
+ const key = decisionObservationMatchKey(decision, observation);
4327
+ return key === null
4328
+ ? null
4329
+ : {
4330
+ decision,
4331
+ key
4332
+ };
4333
+ })
4334
+ .filter((entry) => entry !== null)
4335
+ .sort((left, right) => compareDecisionObservationMatch(left.key, right.key));
4336
+ const best = matches[0] ?? null;
4337
+ const runnerUp = matches[1] ?? null;
4338
+ if (best === null) {
4339
+ return { kind: "unmatched", mode: "legacyHeuristic" };
4340
+ }
4341
+ if (runnerUp !== null && compareDecisionObservationMatch(best.key, runnerUp.key) === 0) {
4342
+ return { kind: "ambiguous", mode: "legacyHeuristic" };
4343
+ }
4344
+ return {
4345
+ kind: "matched",
4346
+ mode: "legacyHeuristic",
4347
+ decision: best.decision
4348
+ };
4349
+ }
4350
+ function resolveTeacherObservationDecisionMatch(decisions, observation, indexes) {
4351
+ const serveDecisionRecordId = normalizeObservationOptionalString(observation.serveDecisionRecordId);
4352
+ if (serveDecisionRecordId !== null) {
4353
+ const decision = indexes.byRecordId.get(serveDecisionRecordId) ?? null;
4354
+ return decision === null
4355
+ ? { kind: "unmatched", mode: "exactDecisionId" }
4356
+ : { kind: "matched", mode: "exactDecisionId", decision };
4357
+ }
4358
+ const selectionDigestKey = buildObservationSelectionDigestKey(observation.selectionDigest, observation.activePackGraphChecksum);
4359
+ if (selectionDigestKey !== null) {
4360
+ if (indexes.ambiguousSelectionDigests.has(selectionDigestKey)) {
4361
+ return { kind: "ambiguous", mode: "exactSelectionDigest" };
4362
+ }
4363
+ const decision = indexes.bySelectionDigest.get(selectionDigestKey) ?? null;
4364
+ return decision === null
4365
+ ? { kind: "unmatched", mode: "exactSelectionDigest" }
4366
+ : { kind: "matched", mode: "exactSelectionDigest", decision };
4367
+ }
4368
+ if (normalizeObservationOptionalString(observation.selectionDigest) !== null
4369
+ || normalizeObservationOptionalString(observation.activePackGraphChecksum) !== null) {
4370
+ return { kind: "unmatched", mode: "exactSelectionDigest" };
4371
+ }
4372
+ const turnCompileEventId = normalizeObservationOptionalString(observation.turnCompileEventId);
4373
+ if (turnCompileEventId !== null) {
4374
+ if (indexes.ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
4375
+ return { kind: "ambiguous", mode: "turnCompileEventId" };
4376
+ }
4377
+ const decision = indexes.byTurnCompileEventId.get(turnCompileEventId) ?? null;
4378
+ return decision === null
4379
+ ? { kind: "unmatched", mode: "turnCompileEventId" }
4380
+ : { kind: "matched", mode: "turnCompileEventId", decision };
4381
+ }
4382
+ return resolveLegacyTeacherObservationMatch(decisions, observation);
4383
+ }
4199
4384
  function joinDecisionsWithTeacherObservationOutcomes(decisions, observationOutcomes) {
4200
4385
  const outcomes = new Map();
4386
+ const stats = emptyTeacherObservationBindingStats();
4201
4387
  for (const decision of decisions) {
4202
4388
  outcomes.set(decision.recordId, 0);
4203
4389
  }
4204
4390
  if (!Array.isArray(observationOutcomes) || observationOutcomes.length === 0) {
4205
- return outcomes;
4391
+ return { outcomes, stats };
4206
4392
  }
4393
+ const indexes = buildTeacherObservationDecisionIndexes(decisions);
4394
+ stats.totalObservationCount = observationOutcomes.length;
4207
4395
  for (const observation of observationOutcomes) {
4208
- const matches = decisions
4209
- .map((decision) => {
4210
- const key = decisionObservationMatchKey(decision, observation);
4211
- return key === null
4212
- ? null
4213
- : {
4214
- decision,
4215
- key
4216
- };
4217
- })
4218
- .filter((entry) => entry !== null)
4219
- .sort((left, right) => compareDecisionObservationMatch(left.key, right.key));
4220
- const best = matches[0] ?? null;
4221
- const runnerUp = matches[1] ?? null;
4222
- if (best === null) {
4396
+ const reward = clampTeacherObservationOutcome(observation.finalScore);
4397
+ if (reward === 0) {
4398
+ stats.skippedZeroRewardCount += 1;
4223
4399
  continue;
4224
4400
  }
4225
- if (runnerUp !== null && compareDecisionObservationMatch(best.key, runnerUp.key) === 0) {
4401
+ stats.nonZeroObservationCount += 1;
4402
+ const match = resolveTeacherObservationDecisionMatch(decisions, observation, indexes);
4403
+ if (match.kind === "unmatched") {
4404
+ stats.unmatched[match.mode] += 1;
4226
4405
  continue;
4227
4406
  }
4228
- const reward = clampTeacherObservationOutcome(observation.finalScore);
4229
- if (reward === 0) {
4407
+ if (match.kind === "ambiguous") {
4408
+ stats.ambiguous[match.mode] += 1;
4230
4409
  continue;
4231
4410
  }
4232
- const current = outcomes.get(best.decision.recordId) ?? 0;
4411
+ stats.matched[match.mode] += 1;
4412
+ const current = outcomes.get(match.decision.recordId) ?? 0;
4233
4413
  if (current === 0 || Math.abs(reward) > Math.abs(current)) {
4234
- outcomes.set(best.decision.recordId, reward);
4414
+ outcomes.set(match.decision.recordId, reward);
4235
4415
  }
4236
4416
  }
4237
- return outcomes;
4417
+ return { outcomes, stats };
4238
4418
  }
4239
4419
 
4240
4420
  export function joinDecisionsWithFeedback(decisions, eventExport, maxDelayMs = 300_000) {
@@ -4587,13 +4767,12 @@ function computeTrajectoryPolicyGradientV2(trajectory, adjacency, graph, vectors
4587
4767
  const grad = neighborId === actionKey
4588
4768
  ? (1 / tau) * (1 - prob)
4589
4769
  : (1 / tau) * (-prob);
4590
- tailGradient.set(neighborId, (tailGradient.get(neighborId) ?? 0) + grad);
4770
+ const gradientKey = neighborId === STOP_ACTION ? buildStopActionUpdateBlockId(step.nodeBlockId) : neighborId;
4771
+ tailGradient.set(gradientKey, (tailGradient.get(gradientKey) ?? 0) + grad);
4591
4772
  }
4592
4773
  }
4593
4774
  // Weight by advantage and pgScale
4594
4775
  for (const [blockId, grad] of tailGradient) {
4595
- if (blockId === STOP_ACTION)
4596
- continue; // don't update virtual STOP
4597
4776
  const delta = roundPolicyGradientValue(advantage * grad * pgScale);
4598
4777
  if (delta === 0)
4599
4778
  continue;
@@ -4627,8 +4806,8 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4627
4806
  const remappedServeTimeDecisions = serveTimeDecisions.map((decision) => remapServeTimeDecisionToGraph(decision, resolveBlockId));
4628
4807
  // 2. Join serve-time decisions with explicit feedback first, then let teacher-v2 observations override when available.
4629
4808
  const outcomeMap = joinDecisionsWithFeedback(remappedServeTimeDecisions, eventExport);
4630
- const teacherObservationOutcomeMap = joinDecisionsWithTeacherObservationOutcomes(remappedServeTimeDecisions, teacherObservationOutcomes);
4631
- for (const [decisionId, reward] of teacherObservationOutcomeMap.entries()) {
4809
+ const teacherObservationBindings = joinDecisionsWithTeacherObservationOutcomes(remappedServeTimeDecisions, teacherObservationOutcomes);
4810
+ for (const [decisionId, reward] of teacherObservationBindings.outcomes.entries()) {
4632
4811
  if (reward !== 0) {
4633
4812
  outcomeMap.set(decisionId, reward);
4634
4813
  }
@@ -4695,7 +4874,8 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4695
4874
  if (fallback.policyUpdates.length > 0) {
4696
4875
  return {
4697
4876
  artifact: fallback,
4698
- updatedBaseline: currentBaseline
4877
+ updatedBaseline: currentBaseline,
4878
+ observationBindingStats: teacherObservationBindings.stats
4699
4879
  };
4700
4880
  }
4701
4881
  }
@@ -4717,7 +4897,7 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4717
4897
  : remappedServeTimeDecisions.length === 0
4718
4898
  ? "no serve-time decisions supplied for V2 learned routing refresh"
4719
4899
  : supervisedTrajectoryCount === 0
4720
- ? "no outcomes found for serve-time decisions"
4900
+ ? `no outcomes found for serve-time decisions (${summarizeTeacherObservationBindingStats(teacherObservationBindings.stats)})`
4721
4901
  : "trajectory updates produced no learned routing delta";
4722
4902
  const artifact = {
4723
4903
  routerIdentity: `${packId}:route_fn`,
@@ -4765,7 +4945,11 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4765
4945
  traces,
4766
4946
  policyUpdates
4767
4947
  };
4768
- return { artifact, updatedBaseline: currentBaseline };
4948
+ return {
4949
+ artifact,
4950
+ updatedBaseline: currentBaseline,
4951
+ observationBindingStats: teacherObservationBindings.stats
4952
+ };
4769
4953
  }
4770
4954
  function resolveEventExport(input) {
4771
4955
  if (input.eventExports === undefined) {
@@ -4922,6 +5106,7 @@ export function buildCandidatePack(input) {
4922
5106
  const vectors = createVectorsPayload(graph);
4923
5107
  let router = null;
4924
5108
  let updatedBaseline = null;
5109
+ let observationBindingStats = null;
4925
5110
  const teacherObservationOutcomes = input.activationRoot === undefined
4926
5111
  ? []
4927
5112
  : loadTeacherObservationOutcomesFromActivation(input.activationRoot);
@@ -4930,6 +5115,7 @@ export function buildCandidatePack(input) {
4930
5115
  const v2Result = createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, input.serveTimeDecisions, input.baselineState ?? initBaseline(), input.sparseFeedback, input.principalBacklog, teacherObservationOutcomes);
4931
5116
  router = v2Result.artifact;
4932
5117
  updatedBaseline = v2Result.updatedBaseline;
5118
+ observationBindingStats = v2Result.observationBindingStats;
4933
5119
  }
4934
5120
  else {
4935
5121
  router = createRouterArtifact(packId, builtAt, graph, vectors, eventExport, input.sparseFeedback, input.principalBacklog);
@@ -5045,7 +5231,8 @@ export function buildCandidatePack(input) {
5045
5231
  pgVersionUsed: input.learnedRouting ? (useV2 ? "v2" : "v1") : null,
5046
5232
  decisionLogCount,
5047
5233
  fallbackReason,
5048
- updatedBaseline
5234
+ updatedBaseline,
5235
+ observationBindingStats
5049
5236
  },
5050
5237
  summary: {
5051
5238
  packId,
@@ -19,6 +19,22 @@ function normalizeOptionalCliString(value) {
19
19
  return trimmed.length > 0 ? trimmed : null;
20
20
  }
21
21
 
22
+ function normalizeReportedProofPath(filePath) {
23
+ const normalizedPath = normalizeOptionalCliString(filePath);
24
+ if (normalizedPath === null) {
25
+ return null;
26
+ }
27
+ if (normalizedPath === "~") {
28
+ return homedir();
29
+ }
30
+ if (normalizedPath.startsWith("~/")) {
31
+ return path.join(homedir(), normalizedPath.slice(2));
32
+ }
33
+ return path.isAbsolute(normalizedPath)
34
+ ? normalizedPath
35
+ : path.resolve(normalizedPath);
36
+ }
37
+
22
38
  function canonicalizeExistingProofPath(filePath) {
23
39
  const resolvedPath = path.resolve(filePath);
24
40
  try {
@@ -191,6 +207,12 @@ function readJsonSnapshot(filePath) {
191
207
  }
192
208
  }
193
209
 
210
+ function describeStepWarning(step) {
211
+ return step.captureState === "partial"
212
+ ? `${step.stepId} ended as ${step.resultClass} with partial capture`
213
+ : `${step.stepId} ended as ${step.resultClass}`;
214
+ }
215
+
194
216
  function extractStartupBreadcrumbs(logText, bundleStartedAtIso) {
195
217
  if (!logText) {
196
218
  return { all: [], afterBundleStart: [] };
@@ -230,6 +252,7 @@ function extractStatusSignals(statusText) {
230
252
  serveActivePack: /serve\s+state=serving_active_pack/.test(statusText),
231
253
  routeFnAvailable: /routeFn\s+available=yes/.test(statusText),
232
254
  proofPath: statusText.match(/proofPath=([^\s]+)/)?.[1] ?? null,
255
+ proofError: statusText.match(/proofError=([^\s]+)/)?.[1] ?? null,
233
256
  };
234
257
  }
235
258
 
@@ -238,69 +261,106 @@ function hasPackagedHookSource(pluginInspectText) {
238
261
  }
239
262
 
240
263
  function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, openclawHome }) {
241
- const failedStep = steps.find((step) => step.resultClass !== "success" && step.skipped !== true);
242
- if (failedStep) {
243
- return {
244
- verdict: "command_failed",
245
- severity: "blocking",
246
- why: `${failedStep.stepId} exited as ${failedStep.resultClass}`,
247
- };
248
- }
264
+ const failedSteps = steps.filter((step) => step.resultClass !== "success" && step.skipped !== true);
265
+ const failedDetailedStatusStep = failedSteps.find((step) => step.stepId === "05-detailed-status");
249
266
  const gatewayHealthy = /Runtime:\s+running/m.test(gatewayStatus) && /RPC probe:\s+ok/m.test(gatewayStatus);
250
267
  const pluginLoaded = /Status:\s+loaded/m.test(pluginInspect);
251
268
  const packagedHookPath = hasPackagedHookSource(pluginInspect);
252
269
  const breadcrumbLoaded = breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded");
253
270
  const runtimeProofMatched = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
254
271
  && runtimeLoadProofSnapshot.value.profiles.some((profile) => canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome));
255
- const missingProofs = [];
256
- if (!gatewayHealthy)
257
- missingProofs.push("gateway_health");
258
- if (!pluginLoaded)
259
- missingProofs.push("plugin_loaded");
260
- if (!packagedHookPath)
261
- missingProofs.push("packaged_hook_path");
272
+ const runtimeTruthGaps = [];
262
273
  if (!statusSignals.statusOk)
263
- missingProofs.push("status_ok");
274
+ runtimeTruthGaps.push("status_ok");
264
275
  if (!statusSignals.loadProofReady)
265
- missingProofs.push("load_proof");
276
+ runtimeTruthGaps.push("load_proof");
266
277
  if (!statusSignals.runtimeProven)
267
- missingProofs.push("runtime_proven");
278
+ runtimeTruthGaps.push("runtime_proven");
268
279
  if (!statusSignals.serveActivePack)
269
- missingProofs.push("serve_active_pack");
280
+ runtimeTruthGaps.push("serve_active_pack");
270
281
  if (!statusSignals.routeFnAvailable)
271
- missingProofs.push("route_fn");
272
- if (!breadcrumbLoaded)
273
- missingProofs.push("startup_breadcrumb");
274
- if (!runtimeProofMatched)
275
- missingProofs.push("runtime_load_proof_record");
276
- if (missingProofs.length === 0) {
282
+ runtimeTruthGaps.push("route_fn");
283
+ const warningCodes = [];
284
+ const warnings = [];
285
+ if (!gatewayHealthy) {
286
+ warningCodes.push("gateway_health");
287
+ warnings.push("gateway status did not confirm runtime running and RPC probe ok");
288
+ }
289
+ if (!pluginLoaded) {
290
+ warningCodes.push("plugin_loaded");
291
+ warnings.push("plugin inspect did not report Status: loaded");
292
+ }
293
+ if (!packagedHookPath) {
294
+ warningCodes.push("packaged_hook_path");
295
+ warnings.push("plugin inspect did not confirm the packaged hook source");
296
+ }
297
+ if (!breadcrumbLoaded) {
298
+ warningCodes.push("startup_breadcrumb");
299
+ warnings.push("startup log did not contain a post-bundle [openclawbrain] BRAIN LOADED breadcrumb");
300
+ }
301
+ if (!runtimeProofMatched) {
302
+ warningCodes.push("runtime_load_proof_record");
303
+ warnings.push(runtimeLoadProofSnapshot.error !== null
304
+ ? `runtime-load-proof snapshot was unreadable: ${runtimeLoadProofSnapshot.error}`
305
+ : runtimeLoadProofSnapshot.exists
306
+ ? "runtime-load-proof snapshot did not include the current openclaw home"
307
+ : "runtime-load-proof snapshot was missing");
308
+ }
309
+ if (statusSignals.proofError !== null && statusSignals.proofError !== "none") {
310
+ warningCodes.push(`proof_error:${statusSignals.proofError}`);
311
+ warnings.push(`detailed status reported proofError=${statusSignals.proofError}`);
312
+ }
313
+ for (const step of failedSteps) {
314
+ warningCodes.push(`step:${step.stepId}:${step.resultClass}:${step.captureState}`);
315
+ warnings.push(describeStepWarning(step));
316
+ }
317
+ const uniqueWarningCodes = [...new Set(warningCodes)];
318
+ const uniqueWarnings = [...new Set(warnings)];
319
+ if (runtimeTruthGaps.length === 0 && uniqueWarningCodes.length === 0) {
277
320
  return {
278
321
  verdict: "success_and_proven",
279
322
  severity: "none",
280
323
  why: "install, restart, gateway health, plugin load, startup breadcrumb, runtime-load-proof record, and detailed status all aligned",
324
+ missingProofs: [],
325
+ warnings: [],
326
+ };
327
+ }
328
+ if (runtimeTruthGaps.length === 0) {
329
+ return {
330
+ verdict: "success_but_proof_incomplete",
331
+ severity: "degraded",
332
+ why: `status/runtime evidence stayed healthy, but proof warnings remained: ${uniqueWarningCodes.join(", ")}`,
333
+ missingProofs: uniqueWarningCodes,
334
+ warnings: uniqueWarnings,
335
+ };
336
+ }
337
+ const hasUsableStatusTruth = statusSignals.statusOk
338
+ || statusSignals.loadProofReady
339
+ || statusSignals.runtimeProven
340
+ || statusSignals.serveActivePack
341
+ || statusSignals.routeFnAvailable;
342
+ if (failedDetailedStatusStep && !hasUsableStatusTruth) {
343
+ return {
344
+ verdict: "command_failed",
345
+ severity: "blocking",
346
+ why: `${failedDetailedStatusStep.stepId} ended as ${failedDetailedStatusStep.resultClass} before runtime truth could be established`,
347
+ missingProofs: runtimeTruthGaps,
348
+ warnings: uniqueWarnings,
281
349
  };
282
350
  }
283
- const blocking = missingProofs.some((item) => [
284
- "gateway_health",
285
- "plugin_loaded",
286
- "packaged_hook_path",
287
- "status_ok",
288
- "load_proof",
289
- "runtime_proven",
290
- "serve_active_pack",
291
- "route_fn",
292
- ].includes(item));
293
351
  return {
294
- verdict: blocking ? "degraded_or_failed_proof" : "success_but_proof_incomplete",
295
- severity: blocking ? "blocking" : "degraded",
296
- why: `missing or conflicting proofs: ${missingProofs.join(", ")}`,
297
- missingProofs,
352
+ verdict: "degraded_or_failed_proof",
353
+ severity: "blocking",
354
+ why: `missing or conflicting runtime truths: ${runtimeTruthGaps.join(", ")}`,
355
+ missingProofs: [...new Set([...runtimeTruthGaps, ...uniqueWarningCodes])],
356
+ warnings: uniqueWarnings,
298
357
  };
299
358
  }
300
359
 
301
360
  function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot }) {
302
361
  const passed = [];
303
362
  const missing = [];
363
+ const warnings = Array.isArray(verdict.warnings) ? verdict.warnings : [];
304
364
  if (steps.find((step) => step.stepId === "01-install")?.resultClass === "success") {
305
365
  passed.push("install command succeeded");
306
366
  }
@@ -350,6 +410,9 @@ function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspec
350
410
  "## Missing / incomplete",
351
411
  ...(missing.length === 0 ? ["- none"] : missing.map((item) => `- ${item}`)),
352
412
  "",
413
+ "## Warnings",
414
+ ...(warnings.length === 0 ? ["- none"] : warnings.map((item) => `- ${item}`)),
415
+ "",
353
416
  "## Step ledger",
354
417
  ...steps.map((step) => `- ${step.stepId}: ${step.skipped ? "skipped" : `${step.resultClass} (${step.captureState})`} - ${step.summary}`),
355
418
  ];
@@ -570,11 +633,12 @@ export function captureOperatorProofBundle(options) {
570
633
  const statusCapture = addStep("05-detailed-status", "detailed status", cliInvocation.command, [...cliInvocation.args, "status", "--openclaw-home", options.openclawHome, "--detailed"]);
571
634
  const gatewayLogPath = extractGatewayLogPath(gatewayStatusCapture.stdout);
572
635
  const activationRoot = extractActivationRoot(statusCapture.stdout, options.activationRoot ?? null);
573
- const runtimeLoadProofPath = path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
636
+ const statusSignals = extractStatusSignals(statusCapture.stdout);
637
+ const runtimeLoadProofPath = normalizeReportedProofPath(statusSignals.proofPath)
638
+ ?? path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
574
639
  const runtimeLoadProofSnapshot = readJsonSnapshot(runtimeLoadProofPath);
575
640
  const gatewayLogText = readTextIfExists(gatewayLogPath);
576
641
  const breadcrumbs = extractStartupBreadcrumbs(gatewayLogText, bundleStartedAt);
577
- const statusSignals = extractStatusSignals(statusCapture.stdout);
578
642
  writeText(path.join(bundleDir, "extracted-startup-breadcrumbs.log"), breadcrumbs.all.length === 0
579
643
  ? "<no matching breadcrumbs found>\n"
580
644
  : `${breadcrumbs.all.map((entry) => entry.line).join("\n")}\n`);