@haaaiawd/second-nature 0.2.12 → 0.2.13

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.
Files changed (59) hide show
  1. package/index.js +96 -6
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/runtime/cli/commands/index.js +85 -11
  5. package/runtime/cli/host-capability/host-discovery-port.d.ts +85 -0
  6. package/runtime/cli/host-capability/host-discovery-port.js +137 -0
  7. package/runtime/cli/ops/heartbeat-surface.d.ts +3 -3
  8. package/runtime/cli/ops/heartbeat-surface.js +6 -5
  9. package/runtime/cli/ops/ops-router.d.ts +6 -2
  10. package/runtime/cli/ops/ops-router.js +1273 -1145
  11. package/runtime/connectors/base/normalized-evidence-content.d.ts +4 -0
  12. package/runtime/connectors/base/normalized-evidence-content.js +21 -2
  13. package/runtime/connectors/evidence-normalizer.js +32 -1
  14. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +2 -0
  15. package/runtime/core/second-nature/action/action-closure-recorder.js +49 -34
  16. package/runtime/core/second-nature/action/action-proposal-builder.js +3 -2
  17. package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +2 -0
  18. package/runtime/core/second-nature/action/policy-bound-dispatch.js +7 -3
  19. package/runtime/core/second-nature/control-plane/cycle-finalizer.d.ts +82 -0
  20. package/runtime/core/second-nature/control-plane/cycle-finalizer.js +187 -0
  21. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +13 -9
  22. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -1
  23. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +2 -1
  24. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +4 -2
  25. package/runtime/core/second-nature/perception/judgment-engine.js +8 -4
  26. package/runtime/core/second-nature/perception/perception-builder.js +14 -2
  27. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +30 -3
  28. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +5 -1
  29. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +68 -29
  30. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +2 -1
  31. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +2 -1
  32. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +1 -0
  33. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +33 -0
  34. package/runtime/observability/causal-loop-health.d.ts +2 -1
  35. package/runtime/observability/causal-loop-health.js +7 -0
  36. package/runtime/observability/loop-stage-event-sink.js +6 -1
  37. package/runtime/observability/loop-status.d.ts +2 -0
  38. package/runtime/observability/loop-status.js +14 -1
  39. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +3 -0
  40. package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
  41. package/runtime/shared/degraded-status-classifier.d.ts +16 -0
  42. package/runtime/shared/degraded-status-classifier.js +68 -0
  43. package/runtime/shared/evidence-level-classifier.d.ts +61 -0
  44. package/runtime/shared/evidence-level-classifier.js +116 -0
  45. package/runtime/shared/provenance-tier.d.ts +37 -0
  46. package/runtime/shared/provenance-tier.js +97 -0
  47. package/runtime/shared/setup-ack.d.ts +54 -0
  48. package/runtime/shared/setup-ack.js +108 -0
  49. package/runtime/shared/source-ref-compat.js +5 -2
  50. package/runtime/shared/types/v8-contracts.d.ts +13 -2
  51. package/runtime/storage/db/index.js +71 -28
  52. package/runtime/storage/db/migrations/v8-005-single-status-schema.js +2 -2
  53. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.d.ts +9 -0
  54. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.js +15 -0
  55. package/runtime/storage/db/schema/v8-entities.d.ts +76 -0
  56. package/runtime/storage/db/schema/v8-entities.js +4 -0
  57. package/runtime/storage/services/write-validation-gate.js +1 -1
  58. package/runtime/storage/v8-state-stores.d.ts +7 -2
  59. package/runtime/storage/v8-state-stores.js +37 -19
@@ -31,6 +31,7 @@ import { evaluateActionPolicy } from "../action/autonomy-policy-evaluator.js";
31
31
  import { dispatchAllowedAction } from "../action/policy-bound-dispatch.js";
32
32
  import { recordNoActionClosure, recordRememberClosure, recordPolicyOutcomeClosure, recordExecutionClosure, } from "../action/action-closure-recorder.js";
33
33
  import { checkDailyRhythm } from "../quiet-dream/daily-rhythm-scheduler.js";
34
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
34
35
  // ───────────────────────────────────────────────────────────────
35
36
  // Helpers
36
37
  // ───────────────────────────────────────────────────────────────
@@ -84,7 +85,7 @@ async function advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef,
84
85
  catch (rhythmErr) {
85
86
  const errMsg = rhythmErr instanceof Error ? rhythmErr.message : String(rhythmErr);
86
87
  const degraded = {
87
- status: "degraded",
88
+ status: classifyDegradedStatus("state_unreadable"),
88
89
  reason: "state_unreadable",
89
90
  ownerStage: "quiet",
90
91
  sourceRefs: [cycleRef],
@@ -130,7 +131,7 @@ export async function runHeartbeatCycle(db, request) {
130
131
  });
131
132
  if ("reason" in traceResult) {
132
133
  return {
133
- status: "degraded",
134
+ status: classifyDegradedStatus("state_unreadable"),
134
135
  reason: "state_unreadable",
135
136
  ownerStage: "ingestion",
136
137
  sourceRefs: [cycleRef],
@@ -150,7 +151,7 @@ export async function runHeartbeatCycle(db, request) {
150
151
  });
151
152
  // ── Perception stage ──
152
153
  const perceptionResult = await buildPerceptionCards(db, { cycleId, now });
153
- const perceptionDegraded = "status" in perceptionResult && perceptionResult.status === "degraded"
154
+ const perceptionDegraded = "ownerStage" in perceptionResult
154
155
  ? perceptionResult
155
156
  : null;
156
157
  await recordLoopStageEvent(db, {
@@ -199,8 +200,8 @@ export async function runHeartbeatCycle(db, request) {
199
200
  noActionReason: degradedReason,
200
201
  degraded: perceptionDegraded
201
202
  ? {
202
- status: "degraded",
203
- reason: perceptionResult.reason ?? "state_unreadable",
203
+ status: classifyDegradedStatus(degradedReason),
204
+ reason: degradedReason,
204
205
  ownerStage: "perception",
205
206
  sourceRefs: [cycleRef],
206
207
  operatorNextAction: "Retry heartbeat after perception recovery",
@@ -327,7 +328,7 @@ export async function runHeartbeatCycle(db, request) {
327
328
  else {
328
329
  // Build proposal for the actionable verdict
329
330
  const proposalResult = await buildActionProposal(db, actionableVerdict.id, { now });
330
- if ("status" in proposalResult && proposalResult.status === "degraded") {
331
+ if ("operatorNextAction" in proposalResult) {
331
332
  // Proposal build failed — still need a closure
332
333
  closureDegraded = proposalResult;
333
334
  const closureResult = await recordNoActionClosure(db, cycleId, closureDegraded.reason, { now });
@@ -398,7 +399,8 @@ export async function runHeartbeatCycle(db, request) {
398
399
  status: "completed",
399
400
  occurredAt: new Date().toISOString(),
400
401
  reason: decision.decisionReason,
401
- sourceRefs: decision.proofRefs,
402
+ sourceRefs: proposal.sourceRefs,
403
+ proofRefs: decision.proofRefs,
402
404
  });
403
405
  // Record execution stage started
404
406
  await recordLoopStageEvent(db, {
@@ -408,7 +410,8 @@ export async function runHeartbeatCycle(db, request) {
408
410
  stage: "execution",
409
411
  status: "started",
410
412
  occurredAt: new Date().toISOString(),
411
- sourceRefs: decision.proofRefs,
413
+ sourceRefs: proposal.sourceRefs,
414
+ proofRefs: decision.proofRefs,
412
415
  });
413
416
  // Dispatch — no real external write in T-CP.R.2
414
417
  const dispatchResult = dispatchAllowedAction(proposal, decision, { guidanceAvailable: false });
@@ -512,7 +515,8 @@ export async function runHeartbeatCycle(db, request) {
512
515
  status: closureDegraded ? "failed" : "completed",
513
516
  occurredAt: new Date().toISOString(),
514
517
  reason: closureDegraded?.reason,
515
- sourceRefs: decision.proofRefs,
518
+ sourceRefs: proposal.sourceRefs,
519
+ proofRefs: decision.proofRefs,
516
520
  });
517
521
  }
518
522
  }
@@ -27,7 +27,7 @@ export async function runRealRuntimeHeartbeatCycle(options) {
27
27
  };
28
28
  const result = await runHeartbeatCycle(options.state, request);
29
29
  // Pass through degraded results directly
30
- if ("status" in result && result.status === "degraded") {
30
+ if ("status" in result) {
31
31
  return result;
32
32
  }
33
33
  const orchestrationResult = result;
@@ -29,10 +29,11 @@ export interface GuidanceOutput {
29
29
  mode: "draft" | "notify";
30
30
  textRef: SourceRef;
31
31
  sourceRefs: SourceRef[];
32
+ proofRefs: SourceRef[];
32
33
  deliveryClaim: "not_delivered";
33
34
  decisionId: string;
34
35
  actionKind: PlatformNeutralActionKind;
35
- ownerVisible: boolean;
36
+ ownerVisible: true;
36
37
  }
37
38
  export type GuidanceValidationResult = {
38
39
  ok: true;
@@ -68,7 +68,8 @@ function buildGuidanceOutput(proposal, decision) {
68
68
  id: `guidance_${decision.id}_${Date.now()}`,
69
69
  mode,
70
70
  textRef,
71
- sourceRefs: [...proposal.sourceRefs, ...decision.proofRefs],
71
+ sourceRefs: proposal.sourceRefs,
72
+ proofRefs: decision.proofRefs,
72
73
  deliveryClaim: "not_delivered",
73
74
  decisionId: decision.id,
74
75
  actionKind,
@@ -87,7 +88,8 @@ export function consumeGuidanceProposal(proposal, decision) {
87
88
  status: "blocked",
88
89
  reason: "policy_denied_high_risk",
89
90
  ownerStage: "execution",
90
- sourceRefs: decision.proofRefs,
91
+ sourceRefs: proposal.sourceRefs,
92
+ proofRefs: decision.proofRefs,
91
93
  operatorNextAction: "Review policy decision before requesting guidance",
92
94
  retryable: false,
93
95
  },
@@ -24,6 +24,7 @@
24
24
  import { readPerceptionCardById, writeJudgmentVerdict, } from "../../../storage/v8-state-stores.js";
25
25
  import { parseSourceRefs } from "../../../shared/serialization.js";
26
26
  import { ACTION_KIND_REGISTRY } from "../../../shared/types/v8-contracts.js";
27
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
27
28
  // ───────────────────────────────────────────────────────────────
28
29
  // Config
29
30
  // ───────────────────────────────────────────────────────────────
@@ -107,7 +108,7 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
107
108
  const card = readResult.row;
108
109
  if (!card) {
109
110
  return {
110
- status: "degraded",
111
+ status: classifyDegradedStatus("state_unreadable"),
111
112
  reason: "state_unreadable",
112
113
  ownerStage: "judgment",
113
114
  sourceRefs: [],
@@ -194,9 +195,12 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
194
195
  });
195
196
  if ("reason" in writeResult) {
196
197
  return {
197
- status: "degraded",
198
- verdicts: [],
198
+ status: classifyDegradedStatus(writeResult.reason),
199
199
  reason: writeResult.reason,
200
+ ownerStage: "judgment",
201
+ sourceRefs: [],
202
+ operatorNextAction: `Failed to persist JudgmentVerdict: ${writeResult.reason}`,
203
+ retryable: true,
200
204
  };
201
205
  }
202
206
  return {
@@ -218,7 +222,7 @@ export async function runAgentJudgments(db, perceptionCardIds, options) {
218
222
  failed.push({
219
223
  perceptionCardId,
220
224
  degraded: {
221
- status: "degraded",
225
+ status: classifyDegradedStatus(result.reason ?? "judgment_low_confidence"),
222
226
  reason: result.reason ?? "judgment_low_confidence",
223
227
  ownerStage: "judgment",
224
228
  sourceRefs: [],
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { readEvidenceItemsByStatus, writePerceptionCard, updateEvidenceItemLifecycleStatus, } from "../../../storage/v8-state-stores.js";
25
25
  import { parseSourceRefs } from "../../../shared/serialization.js";
26
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
26
27
  // ───────────────────────────────────────────────────────────────
27
28
  // Config
28
29
  // ───────────────────────────────────────────────────────────────
@@ -110,6 +111,14 @@ function inferRelevanceClass(score) {
110
111
  }
111
112
  function inferSummary(evidence) {
112
113
  const payload = parsePayload(evidence.payloadJson);
114
+ if (payload?.contentStatus === "content_missing") {
115
+ return {
116
+ summary: payload.contentMissingReason
117
+ ? `Content missing from ${evidence.platformId}: ${payload.contentMissingReason}`
118
+ : `Ref-only observation from ${evidence.platformId}: no readable content`,
119
+ contentMissing: true,
120
+ };
121
+ }
113
122
  if (payload?.summary && String(payload.summary).trim().length > 0) {
114
123
  return { summary: String(payload.summary), contentMissing: false };
115
124
  }
@@ -246,9 +255,12 @@ export async function buildPerceptionCards(db, options) {
246
255
  });
247
256
  if ("reason" in writeResult) {
248
257
  return {
249
- status: "degraded",
250
- cards,
258
+ status: classifyDegradedStatus(writeResult.reason),
251
259
  reason: writeResult.reason,
260
+ ownerStage: "perception",
261
+ sourceRefs: card.evidenceRefs,
262
+ operatorNextAction: `Failed to persist PerceptionCard: ${writeResult.reason}`,
263
+ retryable: true,
252
264
  };
253
265
  }
254
266
  await updateEvidenceItemLifecycleStatus(db, evidence.id, "perceived");
@@ -26,6 +26,7 @@ import { writeDailyRhythmState, readDailyRhythmStateByDay, readActionClosuresByD
26
26
  import { buildQuietDailyReview } from "./quiet-daily-review-builder.js";
27
27
  import { scheduleDreamAfterQuiet } from "./dream-scheduler.js";
28
28
  import { runDreamConsolidation } from "./dream-consolidation-runner.js";
29
+ import { acceptMemoryProjection } from "./memory-projection-lifecycle.js";
29
30
  // ───────────────────────────────────────────────────────────────
30
31
  // Config
31
32
  // ───────────────────────────────────────────────────────────────
@@ -106,7 +107,7 @@ async function executeStaleScheduledDreams(db, state, now) {
106
107
  continue;
107
108
  if ((run.status === "scheduled" || run.status === "started") && isStaleScheduled(run, now)) {
108
109
  const consolidateResult = await runDreamConsolidation(db, runId, { now });
109
- if ("status" in consolidateResult && consolidateResult.status !== "degraded") {
110
+ if ("status" in consolidateResult && !("ownerStage" in consolidateResult)) {
110
111
  const dreamResult = consolidateResult;
111
112
  const finalStatus = dreamResult.status;
112
113
  const finalReason = dreamResult.reason ?? undefined;
@@ -122,6 +123,20 @@ async function executeStaleScheduledDreams(db, state, now) {
122
123
  if ("reason" in updateResult) {
123
124
  return updateResult;
124
125
  }
126
+ // T-DQ.R.10: Accept valid candidates as long-term memory projections.
127
+ // This step was moved out of the runner to separate candidate generation
128
+ // from acceptance, per design §4.2.
129
+ if (dreamResult.status === "completed") {
130
+ for (const candidate of dreamResult.candidates.filter((c) => c.validationStatus === "valid")) {
131
+ const acceptResult = await acceptMemoryProjection(db, candidate.id, `topic_${state.day}`, candidate.candidateText, candidate.sourceRefs, { now });
132
+ if ("projectionId" in acceptResult) {
133
+ candidate.acceptedProjectionId = acceptResult.projectionId;
134
+ }
135
+ else {
136
+ return acceptResult;
137
+ }
138
+ }
139
+ }
125
140
  lastResult = { completed: true, reason: finalReason ?? "dream_scheduled_stalled" };
126
141
  }
127
142
  else {
@@ -207,7 +222,7 @@ export async function checkDailyRhythm(db, options) {
207
222
  else if (state.dreamStatus === "scheduled") {
208
223
  // Stale scheduled run: try to execute consolidation now
209
224
  const staleResult = await executeStaleScheduledDreams(db, state, now);
210
- if ("status" in staleResult && staleResult.status === "degraded") {
225
+ if ("status" in staleResult) {
211
226
  return staleResult;
212
227
  }
213
228
  const { completed, reason } = staleResult;
@@ -262,7 +277,7 @@ export async function checkDailyRhythm(db, options) {
262
277
  // Immediately execute the freshly scheduled dream so it does not sit
263
278
  // pending forever (T-DQ.R.7).
264
279
  const consolidateResult = await runDreamConsolidation(db, dreamResult.id, { now });
265
- if ("status" in consolidateResult && consolidateResult.status !== "degraded") {
280
+ if ("status" in consolidateResult && !("ownerStage" in consolidateResult)) {
266
281
  const dreamOutcome = consolidateResult;
267
282
  const updateResult = await updateDreamConsolidationRunStatus(db, dreamResult.id, dreamOutcome.status, {
268
283
  reason: dreamOutcome.reason ?? null,
@@ -278,6 +293,18 @@ export async function checkDailyRhythm(db, options) {
278
293
  state.dreamReason = dreamOutcome.reason ?? (dreamOutcome.status === "completed" ? "dream_completed" : "dream_failed");
279
294
  if (dreamOutcome.status === "completed") {
280
295
  state.dreamCompletedAt = now;
296
+ // T-DQ.R.10: Accept valid candidates as long-term memory projections.
297
+ // This step was moved out of the runner to separate candidate generation
298
+ // from acceptance, per design §4.2.
299
+ for (const candidate of dreamOutcome.candidates.filter((c) => c.validationStatus === "valid")) {
300
+ const acceptResult = await acceptMemoryProjection(db, candidate.id, `topic_${day}`, candidate.candidateText, candidate.sourceRefs, { now });
301
+ if ("projectionId" in acceptResult) {
302
+ candidate.acceptedProjectionId = acceptResult.projectionId;
303
+ }
304
+ else {
305
+ return acceptResult;
306
+ }
307
+ }
281
308
  }
282
309
  }
283
310
  else {
@@ -10,13 +10,17 @@
10
10
  * - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4.2`
11
11
  *
12
12
  * Dependencies:
13
- * - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById, writeLongTermMemoryProjection)
13
+ * - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById)
14
14
  * - `src/shared/types/v8-contracts.js` (SourceRef, DegradedOperationResult, V8ReasonCode)
15
15
  *
16
16
  * Boundary:
17
17
  * - Rules-only candidate generation; no model assist in this version.
18
18
  * - Does not accept/reject projections; only creates candidates.
19
19
  * - Redaction gate blocks sensitive private content, preserves public technical.
20
+ * - T-DQ.R.10: Does NOT call acceptMemoryProjection. Candidate acceptance is a
21
+ * separate step owned by the caller (dream-scheduler or explicit accept API).
22
+ * The runner only generates and validates candidates; it returns them for
23
+ * the caller to accept via `acceptMemoryProjection(candidateId)`.
20
24
  *
21
25
  * Test coverage: tests/unit/dream/dream-consolidation-runner.test.ts
22
26
  */
@@ -10,18 +10,22 @@
10
10
  * - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4.2`
11
11
  *
12
12
  * Dependencies:
13
- * - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById, writeLongTermMemoryProjection)
13
+ * - `src/storage/v8-state-stores.js` (readDreamConsolidationRunById, readQuietDailyReviewById)
14
14
  * - `src/shared/types/v8-contracts.js` (SourceRef, DegradedOperationResult, V8ReasonCode)
15
15
  *
16
16
  * Boundary:
17
17
  * - Rules-only candidate generation; no model assist in this version.
18
18
  * - Does not accept/reject projections; only creates candidates.
19
19
  * - Redaction gate blocks sensitive private content, preserves public technical.
20
+ * - T-DQ.R.10: Does NOT call acceptMemoryProjection. Candidate acceptance is a
21
+ * separate step owned by the caller (dream-scheduler or explicit accept API).
22
+ * The runner only generates and validates candidates; it returns them for
23
+ * the caller to accept via `acceptMemoryProjection(candidateId)`.
20
24
  *
21
25
  * Test coverage: tests/unit/dream/dream-consolidation-runner.test.ts
22
26
  */
23
27
  import { readDreamConsolidationRunById, readQuietDailyReviewById, } from "../../../storage/v8-state-stores.js";
24
- import { acceptMemoryProjection } from "./memory-projection-lifecycle.js";
28
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
25
29
  // ───────────────────────────────────────────────────────────────
26
30
  // Helpers
27
31
  // ───────────────────────────────────────────────────────────────
@@ -47,9 +51,13 @@ function buildSourceRefsFromReview(review) {
47
51
  ];
48
52
  }
49
53
  function redactSensitive(input) {
50
- // Simple redaction: block credential-shaped patterns
54
+ // Credential-shaped patterns first highest sensitivity.
51
55
  if (/\b(?:Bearer|token|secret|password|key)\s*[:=]\s*[a-zA-Z0-9+/=]{8,}\b/i.test(input)) {
52
- return { text: "[redacted: credential shape detected]", blocked: true };
56
+ return { text: "[redacted: credential shape detected]", blocked: true, reason: "dream_blocked_credential" };
57
+ }
58
+ // Private context markers (names, addresses, phone numbers) — lower threshold than credential.
59
+ if (/\b(?:ssn|social.security|phone|address|email)\s*[:=]\s*[^\s]+/i.test(input)) {
60
+ return { text: "[redacted: private context]", blocked: true, reason: "dream_blocked_private_redacted" };
53
61
  }
54
62
  return { text: input, blocked: false };
55
63
  }
@@ -57,7 +65,7 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
57
65
  const candidates = [];
58
66
  const summary = String(reviewPayload.reviewSummary ?? "");
59
67
  if (summary.length > 0) {
60
- const { text, blocked } = redactSensitive(summary);
68
+ const { text, blocked, reason } = redactSensitive(summary);
61
69
  if (blocked) {
62
70
  candidates.push({
63
71
  id: `cand_${runId}_summary`,
@@ -67,7 +75,7 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
67
75
  sourceRefs: buildSourceRefsFromReview({ id: reviewId, day: "" }),
68
76
  confidence: 0.3,
69
77
  validationStatus: "blocked",
70
- validationReason: "redaction_blocked",
78
+ validationReason: reason,
71
79
  });
72
80
  }
73
81
  else {
@@ -84,7 +92,7 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
84
92
  }
85
93
  const importanceSignals = reviewPayload.importanceSignals;
86
94
  if (importanceSignals && importanceSignals.length > 0) {
87
- const { text, blocked } = redactSensitive(importanceSignals.join("; "));
95
+ const { text, blocked, reason } = redactSensitive(importanceSignals.join("; "));
88
96
  if (!blocked) {
89
97
  candidates.push({
90
98
  id: `cand_${runId}_signals`,
@@ -96,9 +104,33 @@ function generateCandidatesFromReview(runId, reviewId, reviewPayload) {
96
104
  validationStatus: "valid",
97
105
  });
98
106
  }
107
+ else {
108
+ candidates.push({
109
+ id: `cand_${runId}_signals`,
110
+ runId,
111
+ reviewId,
112
+ candidateText: text,
113
+ sourceRefs: buildSourceRefsFromReview({ id: reviewId, day: "" }),
114
+ confidence: 0.3,
115
+ validationStatus: "blocked",
116
+ validationReason: reason,
117
+ });
118
+ }
99
119
  }
100
120
  return candidates;
101
121
  }
122
+ function validateCandidate(candidate) {
123
+ if (!candidate.candidateText || candidate.candidateText.trim().length === 0) {
124
+ return "dream_blocked_validation_failed";
125
+ }
126
+ if (!candidate.sourceRefs || candidate.sourceRefs.length === 0) {
127
+ return "dream_blocked_validation_failed";
128
+ }
129
+ if (candidate.confidence < 0.1) {
130
+ return "dream_blocked_validation_failed";
131
+ }
132
+ return undefined;
133
+ }
102
134
  // ───────────────────────────────────────────────────────────────
103
135
  // Public API
104
136
  // ───────────────────────────────────────────────────────────────
@@ -111,7 +143,7 @@ export async function runDreamConsolidation(db, runId, options) {
111
143
  const run = runRead.row;
112
144
  if (!run) {
113
145
  return {
114
- status: "degraded",
146
+ status: classifyDegradedStatus("state_unreadable"),
115
147
  reason: "state_unreadable",
116
148
  ownerStage: "dream",
117
149
  sourceRefs: [],
@@ -126,7 +158,7 @@ export async function runDreamConsolidation(db, runId, options) {
126
158
  const review = reviewRead.row;
127
159
  if (!review) {
128
160
  return {
129
- status: "degraded",
161
+ status: classifyDegradedStatus("state_unreadable"),
130
162
  reason: "state_unreadable",
131
163
  ownerStage: "dream",
132
164
  sourceRefs: [],
@@ -135,34 +167,41 @@ export async function runDreamConsolidation(db, runId, options) {
135
167
  };
136
168
  }
137
169
  const reviewPayload = parsePayloadJson(review.payloadJson);
170
+ const contentStatus = String(reviewPayload.contentStatus ?? "");
171
+ // Block placeholder or empty Quiet reviews before candidate generation.
172
+ if (contentStatus === "placeholder_rejected" || contentStatus === "content_missing" || contentStatus === "empty") {
173
+ return {
174
+ runId,
175
+ status: "blocked",
176
+ candidates: [],
177
+ reason: "dream_blocked_no_content",
178
+ };
179
+ }
138
180
  const candidates = generateCandidatesFromReview(runId, run.quietReviewId, reviewPayload);
139
- // If all candidates blocked run blocked
181
+ // Run candidate validation; invalid candidates block the run with a precise reason.
182
+ for (const candidate of candidates) {
183
+ if (candidate.validationStatus === "valid") {
184
+ const validationReason = validateCandidate(candidate);
185
+ if (validationReason) {
186
+ candidate.validationStatus = "blocked";
187
+ candidate.validationReason = validationReason;
188
+ }
189
+ }
190
+ }
191
+ // If all candidates blocked → run blocked with the first precise reason.
140
192
  if (candidates.length > 0 && candidates.every((c) => c.validationStatus === "blocked")) {
193
+ const firstReason = candidates[0]?.validationReason;
141
194
  return {
142
195
  runId,
143
196
  status: "blocked",
144
197
  candidates,
145
- reason: "dream_blocked_redaction",
198
+ reason: firstReason ?? "dream_blocked_private_redacted",
146
199
  };
147
200
  }
148
- // Accept valid candidates as active long-term memory projections.
149
- // This completes the Dream→memory lifecycle so accepted projections can be
150
- // loaded by EmbodiedContext in subsequent heartbeats (T-DQ.R.3 followup).
151
- const validCandidates = candidates.filter((c) => c.validationStatus === "valid");
152
- for (const candidate of validCandidates) {
153
- const acceptResult = await acceptMemoryProjection(db, candidate.id, `topic_${review.day}`, candidate.candidateText, candidate.sourceRefs, { now });
154
- if ("projectionId" in acceptResult) {
155
- candidate.acceptedProjectionId = acceptResult.projectionId;
156
- }
157
- else {
158
- return {
159
- runId,
160
- status: "failed",
161
- candidates,
162
- reason: acceptResult.reason,
163
- };
164
- }
165
- }
201
+ // T-DQ.R.10: Runner only generates and validates candidates.
202
+ // Acceptance is a separate step owned by the caller via acceptMemoryProjection.
203
+ // Valid candidates are returned with validationStatus="valid" for the caller
204
+ // to accept; the runner does NOT call acceptMemoryProjection here.
166
205
  return {
167
206
  runId,
168
207
  status: "completed",
@@ -21,6 +21,7 @@
21
21
  * Test coverage: tests/unit/dream/dream-scheduler-lifecycle.test.ts
22
22
  */
23
23
  import { readQuietDailyReviewById, writeDreamConsolidationRun, } from "../../../storage/v8-state-stores.js";
24
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
24
25
  // ───────────────────────────────────────────────────────────────
25
26
  // Public API
26
27
  // ───────────────────────────────────────────────────────────────
@@ -33,7 +34,7 @@ export async function scheduleDreamAfterQuiet(db, quietReviewId, options) {
33
34
  const review = readResult.row;
34
35
  if (!review) {
35
36
  return {
36
- status: "degraded",
37
+ status: classifyDegradedStatus("state_unreadable"),
37
38
  reason: "state_unreadable",
38
39
  ownerStage: "dream",
39
40
  sourceRefs: [],
@@ -21,6 +21,7 @@
21
21
  * Test coverage: tests/unit/dream/memory-projection-lifecycle.test.ts
22
22
  */
23
23
  import { readMemoryProjectionsByTopic, writeLongTermMemoryProjection, updateLongTermMemoryProjectionStatus, } from "../../../storage/v8-state-stores.js";
24
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
24
25
  // ───────────────────────────────────────────────────────────────
25
26
  // Public API
26
27
  // ───────────────────────────────────────────────────────────────
@@ -28,7 +29,7 @@ export async function acceptMemoryProjection(db, candidateId, topicKey, memoryTe
28
29
  const now = options?.now ?? new Date().toISOString();
29
30
  if (sourceRefs.length === 0) {
30
31
  return {
31
- status: "degraded",
32
+ status: classifyDegradedStatus("source_refs_unresolved"),
32
33
  reason: "source_refs_unresolved",
33
34
  ownerStage: "projection",
34
35
  sourceRefs: [],
@@ -62,4 +62,5 @@ export type BuildQuietDailyReviewOutput = {
62
62
  status: "empty";
63
63
  reason: V8ReasonCode;
64
64
  } | DegradedOperationResult;
65
+ export type QuietReviewContentStatus = "content_present" | "empty" | "placeholder_rejected" | "content_missing";
65
66
  export declare function buildQuietDailyReview(db: StateDatabase, options?: BuildQuietDailyReviewOptions): Promise<BuildQuietDailyReviewOutput>;
@@ -107,6 +107,25 @@ function groupByStatus(entries) {
107
107
  }
108
108
  return groups;
109
109
  }
110
+ // Placeholder/template detector: true when the review has no content-bearing
111
+ // evidence or perception signals and no memory-review candidates. Closure-only
112
+ // system text is not meaningful memory input.
113
+ function isPlaceholderReview(notableSignals, memoryCandidates, evidenceRows, perceptionRows) {
114
+ if (memoryCandidates.length > 0)
115
+ return false;
116
+ if (notableSignals.length > 0)
117
+ return false;
118
+ const hasContentEvidence = evidenceRows.some((ev) => {
119
+ const payload = parsePayloadJson(ev.payloadJson);
120
+ return payload.contentStatus !== "content_missing";
121
+ });
122
+ if (hasContentEvidence)
123
+ return false;
124
+ const hasContentPerception = perceptionRows.some((p) => !!p.summary);
125
+ if (hasContentPerception)
126
+ return false;
127
+ return true;
128
+ }
110
129
  // ───────────────────────────────────────────────────────────────
111
130
  // Public API
112
131
  // ───────────────────────────────────────────────────────────────
@@ -177,11 +196,18 @@ export async function buildQuietDailyReview(db, options) {
177
196
  }
178
197
  for (const perception of perceptionRows) {
179
198
  if (perception.summary) {
199
+ const perceptionPayload = parsePayloadJson(perception.payloadJson);
200
+ if (perceptionPayload.contentMissing) {
201
+ continue;
202
+ }
180
203
  notableSignals.push(`Perception: ${perception.summary}`);
181
204
  }
182
205
  }
183
206
  for (const evidence of evidenceRows) {
184
207
  const payload = parsePayloadJson(evidence.payloadJson);
208
+ if (payload.contentStatus === "content_missing") {
209
+ continue;
210
+ }
185
211
  if (payload.summary) {
186
212
  notableSignals.push(`${evidence.platformId}: ${String(payload.summary)}`);
187
213
  }
@@ -245,6 +271,12 @@ export async function buildQuietDailyReview(db, options) {
245
271
  const reviewSummary = firstTopic
246
272
  ? `Day ${day}: ${closures.length} closures around ${firstTopic}${notableSignals.length > 0 ? ` with ${notableSignals.length} notable signals` : ""}.`
247
273
  : `Day ${day}: ${closures.length} closures (${completedCount} completed, ${deniedCount} deferred/denied, ${failedCount} failed)`;
274
+ const isPlaceholder = isPlaceholderReview(notableSignals, memoryCandidates, evidenceRows, perceptionRows);
275
+ const contentStatus = isPlaceholder
276
+ ? "placeholder_rejected"
277
+ : (notableSignals.length > 0 || memoryCandidates.length > 0)
278
+ ? "content_present"
279
+ : "content_missing";
248
280
  const importanceSignals = [];
249
281
  if (memoryCandidates.length > 0) {
250
282
  importanceSignals.push(`${memoryCandidates.length} memory-review candidates`);
@@ -268,6 +300,7 @@ export async function buildQuietDailyReview(db, options) {
268
300
  lifecycleStatus: "pending",
269
301
  payloadJson: JSON.stringify({
270
302
  reviewSummary,
303
+ contentStatus,
271
304
  importanceSignals,
272
305
  memoryCandidates,
273
306
  sections,
@@ -21,7 +21,7 @@
21
21
  * Test coverage: tests/unit/observability/causal-loop-health.test.ts
22
22
  */
23
23
  import type { StateDatabase } from "../storage/db/index.js";
24
- import type { LoopStage, DegradedOperationResult } from "../shared/types/v8-contracts.js";
24
+ import type { LoopStage, DegradedOperationResult, EvidenceLevel } from "../shared/types/v8-contracts.js";
25
25
  export interface StageHealth {
26
26
  stage: LoopStage;
27
27
  lastEventAt?: string;
@@ -36,6 +36,7 @@ export interface CausalLoopHealthSnapshot {
36
36
  lastHeartbeatAt?: string;
37
37
  stages: StageHealth[];
38
38
  reason?: string;
39
+ evidenceLevel: EvidenceLevel;
39
40
  }
40
41
  export interface AssembleLoopStatusOptions {
41
42
  stallThresholdCycles?: number;
@@ -21,6 +21,7 @@
21
21
  * Test coverage: tests/unit/observability/causal-loop-health.test.ts
22
22
  */
23
23
  import { readHeartbeatCycleTraces, readLoopStageEventsByStage, } from "../storage/v8-state-stores.js";
24
+ import { classifyEvidenceLevel } from "../shared/evidence-level-classifier.js";
24
25
  // ───────────────────────────────────────────────────────────────
25
26
  // Config
26
27
  // ───────────────────────────────────────────────────────────────
@@ -77,6 +78,7 @@ export async function assembleLoopStatus(db, options) {
77
78
  lastCycleSequence: 0,
78
79
  stages: [],
79
80
  reason: "no heartbeat cycles recorded",
81
+ evidenceLevel: classifyEvidenceLevel({ hasCarrierEnvelope: true }),
80
82
  };
81
83
  }
82
84
  const lastCycle = cycles[0];
@@ -114,5 +116,10 @@ export async function assembleLoopStatus(db, options) {
114
116
  lastHeartbeatAt: lastCycle.heartbeatStartedAt,
115
117
  stages,
116
118
  reason: stalledAt ? `stage ${stalledAt} stalled for >=${threshold} cycles` : undefined,
119
+ evidenceLevel: classifyEvidenceLevel({
120
+ hasCarrierEnvelope: true,
121
+ hasContractSmoke: true,
122
+ hasCycleExecution: stalledAt === undefined,
123
+ }),
117
124
  };
118
125
  }