@openclawbrain/cli 0.4.13 → 0.4.15

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.
@@ -3,9 +3,10 @@ import path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { buildRouteArtifactReference, CONTRACT_IDS, PACK_GRAPH_SCHEMAS, ROUTER_PG_PROFILE_V1, ROUTER_PG_PROFILE_V2, checksumJsonPayload, computeRouterCollectedLabelCounts, computeRouterFreshnessChecksum, computeRouterObjectiveChecksum, computeRouterQueryChecksum, computeRouterWeightsChecksum, sortNormalizedEvents, validateTeacherSupervisionArtifact } from "@openclawbrain/contracts";
5
5
  import { buildNormalizedEventDedupId, buildNormalizedEventExport, buildNormalizedEventExportBridge, createDefaultLearningSurface, createEventExportCursor, createExplicitEventRange, validateNormalizedEventExport, validateNormalizedEventExportBridge, validateNormalizedEventExportSlice } from "@openclawbrain/event-export";
6
- import { computePayloadChecksum, loadPack, PACK_LAYOUT, summarizeStructuralGraphEvolution, writePackFile } from "@openclawbrain/pack-format";
6
+ import { computePayloadChecksum, loadPack, loadPackFromActivation, PACK_LAYOUT, summarizeStructuralGraphEvolution, writePackFile } from "@openclawbrain/pack-format";
7
7
  import { buildArtifactProvenance } from "@openclawbrain/provenance";
8
8
  import { createWorkspaceMetadata } from "@openclawbrain/workspace-metadata";
9
+ import { createServeTimeDecisionMatcher } from "./teacher-decision-match.js";
9
10
  export const DEFAULT_ALWAYS_ON_LEARNING_LIVE_SLICES_PER_CYCLE = 1;
10
11
  export const DEFAULT_ALWAYS_ON_LEARNING_BACKFILL_SLICES_PER_CYCLE = 1;
11
12
  export const DEFAULT_TEACHER_SUPERVISION_STALE_AFTER_MS = 5 * 60 * 1_000;
@@ -26,6 +27,7 @@ export const ALWAYS_ON_STRUCTURAL_PLASTICITY_OP_CEILING = {
26
27
  export const ALWAYS_ON_STRUCTURAL_PLASTICITY_MIN_INTERACTIONS = 2;
27
28
  export const ALWAYS_ON_STRUCTURAL_PLASTICITY_MIN_FEEDBACK = 1;
28
29
  const CONNECT_PAIR_SCORE_THRESHOLD = 2;
30
+ const MAX_CARRY_FORWARD_SEED_BLOCKS = 12;
29
31
  export const DEFAULT_SPARSE_FEEDBACK_POLICY = {
30
32
  teacherBudget: 32,
31
33
  teacherDelayMs: 0,
@@ -4054,15 +4056,36 @@ function normalizeOutcomeMatchText(value) {
4054
4056
  ? value.replace(/\s+/g, " ").trim().toLowerCase()
4055
4057
  : null;
4056
4058
  }
4059
+ function normalizeObservationOptionalString(value) {
4060
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
4061
+ }
4062
+ function buildObservationSelectionDigestKey(selectionDigest, activePackGraphChecksum) {
4063
+ const normalizedSelectionDigest = normalizeObservationOptionalString(selectionDigest);
4064
+ const normalizedGraphChecksum = normalizeObservationOptionalString(activePackGraphChecksum);
4065
+ if (normalizedSelectionDigest === null || normalizedGraphChecksum === null) {
4066
+ return null;
4067
+ }
4068
+ return `${normalizedGraphChecksum}|${normalizedSelectionDigest}`;
4069
+ }
4057
4070
  function parseObservationRouteMetadata(value) {
4058
4071
  if (typeof value !== "string" || value.trim().length === 0) {
4059
4072
  return {
4073
+ serveDecisionRecordId: null,
4074
+ selectionDigest: null,
4075
+ turnCompileEventId: null,
4076
+ activePackId: null,
4077
+ activePackGraphChecksum: null,
4060
4078
  selectedNodeIds: [],
4061
4079
  selectedPathNodeIds: []
4062
4080
  };
4063
4081
  }
4064
4082
  try {
4065
4083
  const parsed = JSON.parse(value);
4084
+ const serveDecisionRecordId = normalizeObservationOptionalString(parsed?.serveDecisionRecordId);
4085
+ const selectionDigest = normalizeObservationOptionalString(parsed?.selectionDigest);
4086
+ const turnCompileEventId = normalizeObservationOptionalString(parsed?.turnCompileEventId);
4087
+ const activePackId = normalizeObservationOptionalString(parsed?.activePackId);
4088
+ const activePackGraphChecksum = normalizeObservationOptionalString(parsed?.activePackGraphChecksum);
4066
4089
  const selectedNodeIds = Array.isArray(parsed?.selectedNodeIds)
4067
4090
  ? parsed.selectedNodeIds.filter((entry) => typeof entry === "string")
4068
4091
  : [];
@@ -4070,12 +4093,22 @@ function parseObservationRouteMetadata(value) {
4070
4093
  ? parsed.selectedPathNodeIds.filter((entry) => typeof entry === "string")
4071
4094
  : [];
4072
4095
  return {
4096
+ serveDecisionRecordId,
4097
+ selectionDigest,
4098
+ turnCompileEventId,
4099
+ activePackId,
4100
+ activePackGraphChecksum,
4073
4101
  selectedNodeIds,
4074
4102
  selectedPathNodeIds
4075
4103
  };
4076
4104
  }
4077
4105
  catch {
4078
4106
  return {
4107
+ serveDecisionRecordId: null,
4108
+ selectionDigest: null,
4109
+ turnCompileEventId: null,
4110
+ activePackId: null,
4111
+ activePackGraphChecksum: null,
4079
4112
  selectedNodeIds: [],
4080
4113
  selectedPathNodeIds: []
4081
4114
  };
@@ -4099,8 +4132,24 @@ function loadTeacherObservationOutcomesFromActivation(activationRoot) {
4099
4132
  if (table === undefined) {
4100
4133
  return [];
4101
4134
  }
4135
+ const columns = new Set(db.prepare("PRAGMA table_info(brain_observations)").all().map((column) => String(column.name ?? "")));
4136
+ const selectableColumns = [
4137
+ "id",
4138
+ "query_text",
4139
+ "route_metadata_json",
4140
+ "final_score",
4141
+ "confidence",
4142
+ "created_at",
4143
+ "updated_at",
4144
+ "evaluated_at",
4145
+ columns.has("serve_decision_record_id") ? "serve_decision_record_id" : "NULL AS serve_decision_record_id",
4146
+ columns.has("selection_digest") ? "selection_digest" : "NULL AS selection_digest",
4147
+ columns.has("turn_compile_event_id") ? "turn_compile_event_id" : "NULL AS turn_compile_event_id",
4148
+ columns.has("active_pack_id") ? "active_pack_id" : "NULL AS active_pack_id",
4149
+ columns.has("active_pack_graph_checksum") ? "active_pack_graph_checksum" : "NULL AS active_pack_graph_checksum"
4150
+ ];
4102
4151
  const rows = db.prepare(`
4103
- SELECT id, query_text, route_metadata_json, final_score, confidence, created_at, updated_at, evaluated_at
4152
+ SELECT ${selectableColumns.join(", ")}
4104
4153
  FROM brain_observations
4105
4154
  WHERE final_score IS NOT NULL
4106
4155
  ORDER BY created_at DESC
@@ -4116,6 +4165,11 @@ function loadTeacherObservationOutcomesFromActivation(activationRoot) {
4116
4165
  createdAt: Number.isFinite(Number(row.created_at)) ? Number(row.created_at) : null,
4117
4166
  updatedAt: Number.isFinite(Number(row.updated_at)) ? Number(row.updated_at) : null,
4118
4167
  evaluatedAt: Number.isFinite(Number(row.evaluated_at)) ? Number(row.evaluated_at) : null,
4168
+ serveDecisionRecordId: normalizeObservationOptionalString(row.serve_decision_record_id) ?? routeMetadata.serveDecisionRecordId,
4169
+ selectionDigest: normalizeObservationOptionalString(row.selection_digest) ?? routeMetadata.selectionDigest,
4170
+ turnCompileEventId: normalizeObservationOptionalString(row.turn_compile_event_id) ?? routeMetadata.turnCompileEventId,
4171
+ activePackId: normalizeObservationOptionalString(row.active_pack_id) ?? routeMetadata.activePackId,
4172
+ activePackGraphChecksum: normalizeObservationOptionalString(row.active_pack_graph_checksum) ?? routeMetadata.activePackGraphChecksum,
4119
4173
  selectedNodeIds: routeMetadata.selectedNodeIds,
4120
4174
  selectedPathNodeIds: routeMetadata.selectedPathNodeIds
4121
4175
  };
@@ -4197,45 +4251,186 @@ function compareDecisionObservationMatch(left, right) {
4197
4251
  }
4198
4252
  return 0;
4199
4253
  }
4254
+ function emptyTeacherObservationBindingStats() {
4255
+ return {
4256
+ totalObservationCount: 0,
4257
+ nonZeroObservationCount: 0,
4258
+ skippedZeroRewardCount: 0,
4259
+ accounting: {
4260
+ exact: 0,
4261
+ heuristic: 0,
4262
+ unmatched: 0,
4263
+ ambiguous: 0
4264
+ },
4265
+ matched: {
4266
+ exactDecisionId: 0,
4267
+ exactSelectionDigest: 0,
4268
+ turnCompileEventId: 0,
4269
+ legacyHeuristic: 0
4270
+ },
4271
+ unmatched: {
4272
+ exactDecisionId: 0,
4273
+ exactSelectionDigest: 0,
4274
+ turnCompileEventId: 0,
4275
+ legacyHeuristic: 0
4276
+ },
4277
+ ambiguous: {
4278
+ exactDecisionId: 0,
4279
+ exactSelectionDigest: 0,
4280
+ turnCompileEventId: 0,
4281
+ legacyHeuristic: 0
4282
+ }
4283
+ };
4284
+ }
4285
+ function summarizeTeacherObservationBindingStats(stats) {
4286
+ return [
4287
+ `accounting(exact=${stats.accounting.exact},heuristic=${stats.accounting.heuristic},unmatched=${stats.accounting.unmatched},ambiguous=${stats.accounting.ambiguous})`,
4288
+ `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})`,
4289
+ `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})`,
4290
+ `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})`,
4291
+ `non_zero=${stats.nonZeroObservationCount}`,
4292
+ `skipped_zero=${stats.skippedZeroRewardCount}`
4293
+ ].join(" ");
4294
+ }
4295
+ function buildTeacherObservationDecisionIndexes(decisions) {
4296
+ const byRecordId = new Map();
4297
+ const bySelectionDigest = new Map();
4298
+ const ambiguousSelectionDigests = new Set();
4299
+ const byTurnCompileEventId = new Map();
4300
+ const ambiguousTurnCompileEventIds = new Set();
4301
+ for (const decision of decisions) {
4302
+ byRecordId.set(decision.recordId, decision);
4303
+ const selectionDigestKey = buildObservationSelectionDigestKey(decision.selectionDigest, decision.activePackGraphChecksum);
4304
+ if (selectionDigestKey !== null) {
4305
+ if (bySelectionDigest.has(selectionDigestKey)) {
4306
+ bySelectionDigest.delete(selectionDigestKey);
4307
+ ambiguousSelectionDigests.add(selectionDigestKey);
4308
+ }
4309
+ else if (!ambiguousSelectionDigests.has(selectionDigestKey)) {
4310
+ bySelectionDigest.set(selectionDigestKey, decision);
4311
+ }
4312
+ }
4313
+ const turnCompileEventId = normalizeObservationOptionalString(decision.turnCompileEventId);
4314
+ if (turnCompileEventId !== null) {
4315
+ if (byTurnCompileEventId.has(turnCompileEventId)) {
4316
+ byTurnCompileEventId.delete(turnCompileEventId);
4317
+ ambiguousTurnCompileEventIds.add(turnCompileEventId);
4318
+ }
4319
+ else if (!ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
4320
+ byTurnCompileEventId.set(turnCompileEventId, decision);
4321
+ }
4322
+ }
4323
+ }
4324
+ return {
4325
+ byRecordId,
4326
+ bySelectionDigest,
4327
+ ambiguousSelectionDigests,
4328
+ byTurnCompileEventId,
4329
+ ambiguousTurnCompileEventIds
4330
+ };
4331
+ }
4332
+ function resolveLegacyTeacherObservationMatch(decisions, observation) {
4333
+ const matches = decisions
4334
+ .map((decision) => {
4335
+ const key = decisionObservationMatchKey(decision, observation);
4336
+ return key === null
4337
+ ? null
4338
+ : {
4339
+ decision,
4340
+ key
4341
+ };
4342
+ })
4343
+ .filter((entry) => entry !== null)
4344
+ .sort((left, right) => compareDecisionObservationMatch(left.key, right.key));
4345
+ const best = matches[0] ?? null;
4346
+ const runnerUp = matches[1] ?? null;
4347
+ if (best === null) {
4348
+ return { kind: "unmatched", mode: "legacyHeuristic" };
4349
+ }
4350
+ if (runnerUp !== null && compareDecisionObservationMatch(best.key, runnerUp.key) === 0) {
4351
+ return { kind: "ambiguous", mode: "legacyHeuristic" };
4352
+ }
4353
+ return {
4354
+ kind: "matched",
4355
+ mode: "legacyHeuristic",
4356
+ decision: best.decision
4357
+ };
4358
+ }
4359
+ function resolveTeacherObservationDecisionMatch(decisions, observation, indexes) {
4360
+ const serveDecisionRecordId = normalizeObservationOptionalString(observation.serveDecisionRecordId);
4361
+ if (serveDecisionRecordId !== null) {
4362
+ const decision = indexes.byRecordId.get(serveDecisionRecordId) ?? null;
4363
+ return decision === null
4364
+ ? { kind: "unmatched", mode: "exactDecisionId" }
4365
+ : { kind: "matched", mode: "exactDecisionId", decision };
4366
+ }
4367
+ const selectionDigestKey = buildObservationSelectionDigestKey(observation.selectionDigest, observation.activePackGraphChecksum);
4368
+ if (selectionDigestKey !== null) {
4369
+ if (indexes.ambiguousSelectionDigests.has(selectionDigestKey)) {
4370
+ return { kind: "ambiguous", mode: "exactSelectionDigest" };
4371
+ }
4372
+ const decision = indexes.bySelectionDigest.get(selectionDigestKey) ?? null;
4373
+ return decision === null
4374
+ ? { kind: "unmatched", mode: "exactSelectionDigest" }
4375
+ : { kind: "matched", mode: "exactSelectionDigest", decision };
4376
+ }
4377
+ if (normalizeObservationOptionalString(observation.selectionDigest) !== null) {
4378
+ return { kind: "unmatched", mode: "exactSelectionDigest" };
4379
+ }
4380
+ const turnCompileEventId = normalizeObservationOptionalString(observation.turnCompileEventId);
4381
+ if (turnCompileEventId !== null) {
4382
+ if (indexes.ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
4383
+ return { kind: "ambiguous", mode: "turnCompileEventId" };
4384
+ }
4385
+ const decision = indexes.byTurnCompileEventId.get(turnCompileEventId) ?? null;
4386
+ return decision === null
4387
+ ? { kind: "unmatched", mode: "turnCompileEventId" }
4388
+ : { kind: "matched", mode: "turnCompileEventId", decision };
4389
+ }
4390
+ return resolveLegacyTeacherObservationMatch(decisions, observation);
4391
+ }
4200
4392
  function joinDecisionsWithTeacherObservationOutcomes(decisions, observationOutcomes) {
4201
4393
  const outcomes = new Map();
4394
+ const stats = emptyTeacherObservationBindingStats();
4202
4395
  for (const decision of decisions) {
4203
4396
  outcomes.set(decision.recordId, 0);
4204
4397
  }
4205
4398
  if (!Array.isArray(observationOutcomes) || observationOutcomes.length === 0) {
4206
- return outcomes;
4399
+ return { outcomes, stats };
4207
4400
  }
4401
+ const indexes = buildTeacherObservationDecisionIndexes(decisions);
4402
+ stats.totalObservationCount = observationOutcomes.length;
4208
4403
  for (const observation of observationOutcomes) {
4209
- const matches = decisions
4210
- .map((decision) => {
4211
- const key = decisionObservationMatchKey(decision, observation);
4212
- return key === null
4213
- ? null
4214
- : {
4215
- decision,
4216
- key
4217
- };
4218
- })
4219
- .filter((entry) => entry !== null)
4220
- .sort((left, right) => compareDecisionObservationMatch(left.key, right.key));
4221
- const best = matches[0] ?? null;
4222
- const runnerUp = matches[1] ?? null;
4223
- if (best === null) {
4404
+ const reward = clampTeacherObservationOutcome(observation.finalScore);
4405
+ if (reward === 0) {
4406
+ stats.skippedZeroRewardCount += 1;
4224
4407
  continue;
4225
4408
  }
4226
- if (runnerUp !== null && compareDecisionObservationMatch(best.key, runnerUp.key) === 0) {
4409
+ stats.nonZeroObservationCount += 1;
4410
+ const match = resolveTeacherObservationDecisionMatch(decisions, observation, indexes);
4411
+ if (match.kind === "unmatched") {
4412
+ stats.accounting.unmatched += 1;
4413
+ stats.unmatched[match.mode] += 1;
4227
4414
  continue;
4228
4415
  }
4229
- const reward = clampTeacherObservationOutcome(observation.finalScore);
4230
- if (reward === 0) {
4416
+ if (match.kind === "ambiguous") {
4417
+ stats.accounting.ambiguous += 1;
4418
+ stats.ambiguous[match.mode] += 1;
4231
4419
  continue;
4232
4420
  }
4233
- const current = outcomes.get(best.decision.recordId) ?? 0;
4421
+ if (match.mode === "legacyHeuristic") {
4422
+ stats.accounting.heuristic += 1;
4423
+ }
4424
+ else {
4425
+ stats.accounting.exact += 1;
4426
+ }
4427
+ stats.matched[match.mode] += 1;
4428
+ const current = outcomes.get(match.decision.recordId) ?? 0;
4234
4429
  if (current === 0 || Math.abs(reward) > Math.abs(current)) {
4235
- outcomes.set(best.decision.recordId, reward);
4430
+ outcomes.set(match.decision.recordId, reward);
4236
4431
  }
4237
4432
  }
4238
- return outcomes;
4433
+ return { outcomes, stats };
4239
4434
  }
4240
4435
 
4241
4436
  export function joinDecisionsWithFeedback(decisions, eventExport, maxDelayMs = 300_000) {
@@ -4247,149 +4442,7 @@ export function joinDecisionsWithFeedback(decisions, eventExport, maxDelayMs = 3
4247
4442
  return outcomes;
4248
4443
  }
4249
4444
  const normalizeOptionalString = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
4250
- const toTimestamp = (value) => {
4251
- const normalized = normalizeOptionalString(value);
4252
- if (normalized === undefined) {
4253
- return null;
4254
- }
4255
- const parsed = Date.parse(normalized);
4256
- return Number.isFinite(parsed) ? parsed : null;
4257
- };
4258
- const buildSessionChannelKey = (sessionId, channel) => {
4259
- const normalizedSessionId = normalizeOptionalString(sessionId);
4260
- const normalizedChannel = normalizeOptionalString(channel);
4261
- if (normalizedSessionId === undefined || normalizedChannel === undefined) {
4262
- return null;
4263
- }
4264
- return `${normalizedSessionId}|${normalizedChannel}`;
4265
- };
4266
- const buildCandidateKey = (sessionId, channel, createdAt) => {
4267
- const sessionChannelKey = buildSessionChannelKey(sessionId, channel);
4268
- const normalizedCreatedAt = normalizeOptionalString(createdAt);
4269
- if (sessionChannelKey === null || normalizedCreatedAt === undefined) {
4270
- return null;
4271
- }
4272
- return `${sessionChannelKey}|${normalizedCreatedAt}`;
4273
- };
4274
- const buildDecisionTimestamps = (decision) => {
4275
- const timestamps = [];
4276
- const turnCreatedAt = toTimestamp(decision.turnCreatedAt);
4277
- const recordedAt = toTimestamp(decision.recordedAt);
4278
- if (turnCreatedAt !== null) {
4279
- timestamps.push(turnCreatedAt);
4280
- }
4281
- if (recordedAt !== null && !timestamps.includes(recordedAt)) {
4282
- timestamps.push(recordedAt);
4283
- }
4284
- return timestamps;
4285
- };
4286
- const operationalPatterns = [
4287
- /^NO_REPLY$/i,
4288
- /^HEARTBEAT_OK$/i,
4289
- /^read heartbeat\.md if it exists/i,
4290
- /^a new session was started via \/new or \/reset\./i,
4291
- /\[cron:[a-f0-9-]+\s/i,
4292
- /\[system message\]\s*\[sessionid:/i,
4293
- ];
4294
- const isOperationalDecision = (decision) => {
4295
- const userMessage = normalizeOptionalString(decision.userMessage);
4296
- if (userMessage === undefined) {
4297
- return true;
4298
- }
4299
- return operationalPatterns.some((pattern) => pattern.test(userMessage));
4300
- };
4301
- const selectNearestDecision = (entries, interactionAt) => {
4302
- const candidates = entries
4303
- .map((entry) => {
4304
- const deltas = entry.timestamps.map((timestamp) => Math.abs(timestamp - interactionAt));
4305
- const bestDelta = deltas.length === 0 ? null : Math.min(...deltas);
4306
- return bestDelta === null || bestDelta > maxDelayMs
4307
- ? null
4308
- : {
4309
- decision: entry.decision,
4310
- deltaMs: bestDelta,
4311
- recordedAt: toTimestamp(entry.decision.recordedAt) ?? 0,
4312
- };
4313
- })
4314
- .filter((entry) => entry !== null)
4315
- .sort((left, right) => {
4316
- if (left.deltaMs !== right.deltaMs) {
4317
- return left.deltaMs - right.deltaMs;
4318
- }
4319
- return right.recordedAt - left.recordedAt;
4320
- });
4321
- const best = candidates[0] ?? null;
4322
- const runnerUp = candidates[1] ?? null;
4323
- if (best === null) {
4324
- return null;
4325
- }
4326
- if (runnerUp !== null && runnerUp.deltaMs === best.deltaMs && runnerUp.decision !== best.decision) {
4327
- return null;
4328
- }
4329
- return best.decision;
4330
- };
4331
- const exactDecisions = new Map();
4332
- const fallbackDecisions = new Map();
4333
- const decisionsBySessionChannel = new Map();
4334
- const globalFallbackDecisions = [];
4335
- for (const decision of [...decisions].sort((left, right) => Date.parse(right.recordedAt) - Date.parse(left.recordedAt))) {
4336
- const userMessage = normalizeOptionalString(decision.userMessage);
4337
- if (userMessage === undefined) {
4338
- continue;
4339
- }
4340
- const turnCompileEventId = normalizeOptionalString(decision.turnCompileEventId);
4341
- if (turnCompileEventId !== undefined && !exactDecisions.has(turnCompileEventId)) {
4342
- exactDecisions.set(turnCompileEventId, decision);
4343
- }
4344
- for (const candidateKey of [
4345
- buildCandidateKey(decision.sessionId, decision.channel, decision.turnCreatedAt),
4346
- buildCandidateKey(decision.sessionId, decision.channel, decision.recordedAt),
4347
- ]) {
4348
- if (candidateKey !== null && !fallbackDecisions.has(candidateKey)) {
4349
- fallbackDecisions.set(candidateKey, decision);
4350
- }
4351
- }
4352
- const sessionChannelKey = buildSessionChannelKey(decision.sessionId, decision.channel);
4353
- const indexedEntry = {
4354
- decision,
4355
- timestamps: buildDecisionTimestamps(decision),
4356
- operational: isOperationalDecision(decision),
4357
- };
4358
- if (sessionChannelKey !== null) {
4359
- const indexed = decisionsBySessionChannel.get(sessionChannelKey) ?? [];
4360
- indexed.push(indexedEntry);
4361
- decisionsBySessionChannel.set(sessionChannelKey, indexed);
4362
- }
4363
- if (!indexedEntry.operational) {
4364
- globalFallbackDecisions.push(indexedEntry);
4365
- }
4366
- }
4367
- const matchInteractionToDecision = (interaction) => {
4368
- const exact = exactDecisions.get(interaction.eventId);
4369
- if (exact !== undefined) {
4370
- return exact;
4371
- }
4372
- const exactFallbackKey = buildCandidateKey(interaction.sessionId, interaction.channel, interaction.createdAt);
4373
- if (exactFallbackKey !== null) {
4374
- const fallback = fallbackDecisions.get(exactFallbackKey);
4375
- if (fallback !== undefined) {
4376
- return fallback;
4377
- }
4378
- }
4379
- const interactionAt = toTimestamp(interaction.createdAt);
4380
- const sessionChannelKey = buildSessionChannelKey(interaction.sessionId, interaction.channel);
4381
- if (interactionAt === null) {
4382
- return null;
4383
- }
4384
- if (sessionChannelKey !== null) {
4385
- const sessionEntries = (decisionsBySessionChannel.get(sessionChannelKey) ?? []).filter((entry) => entry.operational !== true);
4386
- const sessionMatch = selectNearestDecision(sessionEntries, interactionAt);
4387
- if (sessionMatch !== null) {
4388
- return sessionMatch;
4389
- }
4390
- }
4391
- return selectNearestDecision(globalFallbackDecisions, interactionAt);
4392
- };
4445
+ const matchInteractionToDecision = createServeTimeDecisionMatcher(decisions, { maxTimeDeltaMs: maxDelayMs });
4393
4446
  const interactionById = new Map();
4394
4447
  for (const interaction of eventExport.interactionEvents ?? []) {
4395
4448
  const interactionId = normalizeOptionalString(interaction.eventId);
@@ -4397,7 +4450,7 @@ export function joinDecisionsWithFeedback(decisions, eventExport, maxDelayMs = 3
4397
4450
  interactionById.set(interactionId, interaction);
4398
4451
  }
4399
4452
  }
4400
- const matchedFeedbackIds = new Set();
4453
+ const consumedFeedbackIds = new Set();
4401
4454
  for (const event of eventExport.feedbackEvents ?? []) {
4402
4455
  const relatedInteractionId = normalizeOptionalString(event.relatedInteractionId);
4403
4456
  if (relatedInteractionId === undefined) {
@@ -4407,6 +4460,8 @@ export function joinDecisionsWithFeedback(decisions, eventExport, maxDelayMs = 3
4407
4460
  if (interaction === undefined) {
4408
4461
  continue;
4409
4462
  }
4463
+ const feedbackId = normalizeOptionalString(event.eventId) ?? `${relatedInteractionId}|${normalizeOptionalString(event.createdAt) ?? 'unknown'}`;
4464
+ consumedFeedbackIds.add(feedbackId);
4410
4465
  const matchedDecision = matchInteractionToDecision(interaction);
4411
4466
  if (matchedDecision === null) {
4412
4467
  continue;
@@ -4418,13 +4473,11 @@ export function joinDecisionsWithFeedback(decisions, eventExport, maxDelayMs = 3
4418
4473
  outcomes.set(matchedDecision.recordId, reward);
4419
4474
  }
4420
4475
  }
4421
- const feedbackId = normalizeOptionalString(event.eventId) ?? `${relatedInteractionId}|${normalizeOptionalString(event.createdAt) ?? 'unknown'}`;
4422
- matchedFeedbackIds.add(feedbackId);
4423
4476
  }
4424
4477
  const feedbackBySession = new Map();
4425
4478
  for (const event of eventExport.feedbackEvents ?? []) {
4426
4479
  const feedbackId = normalizeOptionalString(event.eventId) ?? `${normalizeOptionalString(event.relatedInteractionId) ?? '__none__'}|${normalizeOptionalString(event.createdAt) ?? 'unknown'}`;
4427
- if (matchedFeedbackIds.has(feedbackId)) {
4480
+ if (consumedFeedbackIds.has(feedbackId)) {
4428
4481
  continue;
4429
4482
  }
4430
4483
  const sessionId = event.sessionId ?? "__none__";
@@ -4627,8 +4680,8 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4627
4680
  const remappedServeTimeDecisions = serveTimeDecisions.map((decision) => remapServeTimeDecisionToGraph(decision, resolveBlockId));
4628
4681
  // 2. Join serve-time decisions with explicit feedback first, then let teacher-v2 observations override when available.
4629
4682
  const outcomeMap = joinDecisionsWithFeedback(remappedServeTimeDecisions, eventExport);
4630
- const teacherObservationOutcomeMap = joinDecisionsWithTeacherObservationOutcomes(remappedServeTimeDecisions, teacherObservationOutcomes);
4631
- for (const [decisionId, reward] of teacherObservationOutcomeMap.entries()) {
4683
+ const teacherObservationBindings = joinDecisionsWithTeacherObservationOutcomes(remappedServeTimeDecisions, teacherObservationOutcomes);
4684
+ for (const [decisionId, reward] of teacherObservationBindings.outcomes.entries()) {
4632
4685
  if (reward !== 0) {
4633
4686
  outcomeMap.set(decisionId, reward);
4634
4687
  }
@@ -4695,7 +4748,8 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4695
4748
  if (fallback.policyUpdates.length > 0) {
4696
4749
  return {
4697
4750
  artifact: fallback,
4698
- updatedBaseline: currentBaseline
4751
+ updatedBaseline: currentBaseline,
4752
+ observationBindingStats: teacherObservationBindings.stats
4699
4753
  };
4700
4754
  }
4701
4755
  }
@@ -4717,7 +4771,7 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4717
4771
  : remappedServeTimeDecisions.length === 0
4718
4772
  ? "no serve-time decisions supplied for V2 learned routing refresh"
4719
4773
  : supervisedTrajectoryCount === 0
4720
- ? "no outcomes found for serve-time decisions"
4774
+ ? `no outcomes found for serve-time decisions (${summarizeTeacherObservationBindingStats(teacherObservationBindings.stats)})`
4721
4775
  : "trajectory updates produced no learned routing delta";
4722
4776
  const artifact = {
4723
4777
  routerIdentity: `${packId}:route_fn`,
@@ -4765,7 +4819,11 @@ function createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, se
4765
4819
  traces,
4766
4820
  policyUpdates
4767
4821
  };
4768
- return { artifact, updatedBaseline: currentBaseline };
4822
+ return {
4823
+ artifact,
4824
+ updatedBaseline: currentBaseline,
4825
+ observationBindingStats: teacherObservationBindings.stats
4826
+ };
4769
4827
  }
4770
4828
  function resolveEventExport(input) {
4771
4829
  if (input.eventExports === undefined) {
@@ -4788,6 +4846,55 @@ function defaultLearningSurface(workspace, offlineArtifacts, workspaceInit) {
4788
4846
  ...offlineArtifacts.map((artifact) => `offline:${artifact}`)
4789
4847
  ]));
4790
4848
  }
4849
+ function isCarryForwardSeedBlock(block) {
4850
+ const role = block.learning?.role ?? "";
4851
+ if (role !== "interaction" && role !== "feedback" && role !== "teacher_supervision" && role !== "label_surface") {
4852
+ return false;
4853
+ }
4854
+ if (typeof block.text !== "string" || block.text.trim().length === 0) {
4855
+ return false;
4856
+ }
4857
+ return block.initSeed !== undefined ||
4858
+ block.semantic?.sourceKind === "recorded_session_seed" ||
4859
+ block.source.includes("/seed:");
4860
+ }
4861
+ function carryForwardSeedBlockScore(block) {
4862
+ return (block.initSeed?.score ?? 0) +
4863
+ (block.state?.strength ?? 0) +
4864
+ (block.learning?.humanLabels ?? 0) * 2 +
4865
+ (block.learning?.selfLabels ?? 0);
4866
+ }
4867
+ function loadCarryForwardSeedBlocksFromActivation(activationRoot, currentGraphBlockIds) {
4868
+ try {
4869
+ const activePack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
4870
+ if (activePack === null) {
4871
+ return [];
4872
+ }
4873
+ const candidates = activePack.graph.blocks
4874
+ .filter((block) => isCarryForwardSeedBlock(block) && !currentGraphBlockIds.has(block.id));
4875
+ const preferred = candidates.filter((block) => block.learning?.role !== "interaction");
4876
+ const selected = (preferred.length > 0 ? preferred : candidates)
4877
+ .sort((left, right) => carryForwardSeedBlockScore(right) - carryForwardSeedBlockScore(left) ||
4878
+ right.priority - left.priority ||
4879
+ left.id.localeCompare(right.id))
4880
+ .slice(0, MAX_CARRY_FORWARD_SEED_BLOCKS);
4881
+ if (selected.length === 0) {
4882
+ return [];
4883
+ }
4884
+ const selectedIds = new Set(selected.map((block) => block.id));
4885
+ return selected.map((block) => ({
4886
+ ...structuredClone(block),
4887
+ ...(block.edges === undefined
4888
+ ? {}
4889
+ : {
4890
+ edges: block.edges.filter((edge) => selectedIds.has(edge.targetBlockId))
4891
+ })
4892
+ }));
4893
+ }
4894
+ catch {
4895
+ return [];
4896
+ }
4897
+ }
4791
4898
  function cloneRuntimeGraphForPack(packId, runtimeGraph, builtAt) {
4792
4899
  const cloned = structuredClone(runtimeGraph);
4793
4900
  cloned.packId = packId;
@@ -4861,9 +4968,18 @@ function buildRuntimeGraphSnapshot(input) {
4861
4968
  rootDir: workspace.rootDir,
4862
4969
  observedAt: builtAt
4863
4970
  }));
4971
+ const carryForwardSeedBlocks = input.activationRoot === undefined
4972
+ ? []
4973
+ : loadCarryForwardSeedBlocksFromActivation(input.activationRoot, new Set(graph.blocks.map((block) => block.id)));
4974
+ const graphWithCarryForwardSeed = carryForwardSeedBlocks.length === 0
4975
+ ? graph
4976
+ : {
4977
+ ...graph,
4978
+ blocks: [...graph.blocks, ...carryForwardSeedBlocks]
4979
+ };
4864
4980
  return {
4865
- graph,
4866
- plasticity: summarizeRuntimeGraphPlasticity("live_loop", graph, builtAt, null, normalizedEventExport)
4981
+ graph: graphWithCarryForwardSeed,
4982
+ plasticity: summarizeRuntimeGraphPlasticity("live_loop", graphWithCarryForwardSeed, builtAt, null, normalizedEventExport)
4867
4983
  };
4868
4984
  }
4869
4985
  export function buildCandidatePack(input) {
@@ -4922,6 +5038,7 @@ export function buildCandidatePack(input) {
4922
5038
  const vectors = createVectorsPayload(graph);
4923
5039
  let router = null;
4924
5040
  let updatedBaseline = null;
5041
+ let observationBindingStats = null;
4925
5042
  const teacherObservationOutcomes = input.activationRoot === undefined
4926
5043
  ? []
4927
5044
  : loadTeacherObservationOutcomesFromActivation(input.activationRoot);
@@ -4930,6 +5047,7 @@ export function buildCandidatePack(input) {
4930
5047
  const v2Result = createRouterArtifactV2(packId, builtAt, graph, vectors, eventExport, input.serveTimeDecisions, input.baselineState ?? initBaseline(), input.sparseFeedback, input.principalBacklog, teacherObservationOutcomes);
4931
5048
  router = v2Result.artifact;
4932
5049
  updatedBaseline = v2Result.updatedBaseline;
5050
+ observationBindingStats = v2Result.observationBindingStats;
4933
5051
  }
4934
5052
  else {
4935
5053
  router = createRouterArtifact(packId, builtAt, graph, vectors, eventExport, input.sparseFeedback, input.principalBacklog);
@@ -5045,7 +5163,8 @@ export function buildCandidatePack(input) {
5045
5163
  pgVersionUsed: input.learnedRouting ? (useV2 ? "v2" : "v1") : null,
5046
5164
  decisionLogCount,
5047
5165
  fallbackReason,
5048
- updatedBaseline
5166
+ updatedBaseline,
5167
+ observationBindingStats
5049
5168
  },
5050
5169
  summary: {
5051
5170
  packId,