@haaaiawd/second-nature 0.2.9 → 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 (115) hide show
  1. package/index.js +102 -6
  2. package/openclaw.plugin.json +2 -5
  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/host-capability/probe-host-capability.js +1 -1
  8. package/runtime/cli/host-capability/types.d.ts +2 -7
  9. package/runtime/cli/host-capability/types.js +0 -5
  10. package/runtime/cli/ops/heartbeat-surface.d.ts +5 -3
  11. package/runtime/cli/ops/heartbeat-surface.js +38 -8
  12. package/runtime/cli/ops/ops-router.d.ts +6 -2
  13. package/runtime/cli/ops/ops-router.js +1275 -1147
  14. package/runtime/cli/ops/workspace-heartbeat-runner.js +2 -5
  15. package/runtime/connectors/base/normalized-evidence-content.d.ts +4 -0
  16. package/runtime/connectors/base/normalized-evidence-content.js +21 -2
  17. package/runtime/connectors/evidence-normalizer.js +32 -1
  18. package/runtime/connectors/manifest/manifest-schema.d.ts +2 -2
  19. package/runtime/connectors/registry/dynamic-connector-registry.js +16 -1
  20. package/runtime/connectors/services/connector-executor-adapter.js +54 -35
  21. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  22. package/runtime/core/second-nature/action/action-closure-recorder.js +51 -38
  23. package/runtime/core/second-nature/action/action-proposal-builder.js +8 -34
  24. package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +2 -0
  25. package/runtime/core/second-nature/action/policy-bound-dispatch.js +10 -5
  26. package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +1 -11
  27. package/runtime/core/second-nature/control-plane/cycle-finalizer.d.ts +82 -0
  28. package/runtime/core/second-nature/control-plane/cycle-finalizer.js +187 -0
  29. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +1 -0
  30. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +23 -15
  31. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
  32. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +2 -1
  33. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +2 -1
  34. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +4 -2
  35. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +4 -3
  36. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +3 -2
  37. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +3 -2
  38. package/runtime/core/second-nature/orchestrator/intent-planner.js +4 -2
  39. package/runtime/core/second-nature/orchestrator/narrative-update.js +1 -2
  40. package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +2 -2
  41. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +1 -1
  42. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +2 -3
  43. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +2 -2
  44. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +6 -1
  45. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +3 -14
  46. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +6 -5
  47. package/runtime/core/second-nature/perception/judgment-engine.js +10 -16
  48. package/runtime/core/second-nature/perception/perception-builder.js +15 -11
  49. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +13 -15
  50. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +40 -16
  51. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +5 -1
  52. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +68 -29
  53. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +2 -3
  54. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +2 -13
  55. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +1 -0
  56. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +34 -11
  57. package/runtime/core/second-nature/types.d.ts +2 -9
  58. package/runtime/dream/dream-engine.js +11 -4
  59. package/runtime/guidance/outreach-draft-schema.d.ts +12 -12
  60. package/runtime/guidance/persona-selection.js +5 -0
  61. package/runtime/guidance/template-registry.js +6 -1
  62. package/runtime/guidance/types.d.ts +2 -2
  63. package/runtime/observability/causal-loop-health.d.ts +2 -1
  64. package/runtime/observability/causal-loop-health.js +7 -0
  65. package/runtime/observability/living-loop-health-gate.js +2 -8
  66. package/runtime/observability/loop-stage-event-sink.js +6 -2
  67. package/runtime/observability/loop-status.d.ts +2 -0
  68. package/runtime/observability/loop-status.js +14 -1
  69. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +3 -0
  70. package/runtime/observability/services/heartbeat-digest-assembler.js +9 -0
  71. package/runtime/observability/services/lived-experience-audit.d.ts +7 -7
  72. package/runtime/shared/degraded-status-classifier.d.ts +16 -0
  73. package/runtime/shared/degraded-status-classifier.js +68 -0
  74. package/runtime/shared/evidence-level-classifier.d.ts +61 -0
  75. package/runtime/shared/evidence-level-classifier.js +116 -0
  76. package/runtime/shared/provenance-tier.d.ts +37 -0
  77. package/runtime/shared/provenance-tier.js +97 -0
  78. package/runtime/shared/serialization.d.ts +17 -0
  79. package/runtime/shared/serialization.js +27 -0
  80. package/runtime/shared/setup-ack.d.ts +54 -0
  81. package/runtime/shared/setup-ack.js +108 -0
  82. package/runtime/shared/source-ref-compat.d.ts +26 -0
  83. package/runtime/shared/source-ref-compat.js +64 -0
  84. package/runtime/shared/types/goal.d.ts +4 -4
  85. package/runtime/shared/types/goal.js +1 -1
  86. package/runtime/shared/types/index.d.ts +1 -0
  87. package/runtime/shared/types/index.js +1 -3
  88. package/runtime/shared/types/source-ref.d.ts +2 -2
  89. package/runtime/shared/types/v7-entities.d.ts +5 -5
  90. package/runtime/shared/types/v7-entities.js +1 -1
  91. package/runtime/shared/types/v8-contracts.d.ts +13 -2
  92. package/runtime/storage/db/index.js +60 -12
  93. package/runtime/storage/db/migrations/index.js +4 -0
  94. package/runtime/storage/db/migrations/v8-004-schema-closure.d.ts +19 -0
  95. package/runtime/storage/db/migrations/v8-004-schema-closure.js +74 -0
  96. package/runtime/storage/db/migrations/v8-005-single-status-schema.d.ts +11 -0
  97. package/runtime/storage/db/migrations/v8-005-single-status-schema.js +16 -0
  98. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.d.ts +9 -0
  99. package/runtime/storage/db/migrations/v8-006-loop-stage-event-proof-trace-columns.js +15 -0
  100. package/runtime/storage/db/schema/v8-entities.d.ts +65 -84
  101. package/runtime/storage/db/schema/v8-entities.js +8 -7
  102. package/runtime/storage/delivery/types.d.ts +2 -2
  103. package/runtime/storage/fallback/load-operator-fallback.d.ts +2 -2
  104. package/runtime/storage/fallback/operator-fallback-types.d.ts +2 -2
  105. package/runtime/storage/fallback/operator-fallback-view.d.ts +2 -2
  106. package/runtime/storage/index.d.ts +1 -1
  107. package/runtime/storage/life-evidence/types.d.ts +5 -5
  108. package/runtime/storage/quiet/quiet-artifact-types.d.ts +4 -4
  109. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +2 -2
  110. package/runtime/storage/services/write-validation-gate.d.ts +1 -1
  111. package/runtime/storage/services/write-validation-gate.js +15 -3
  112. package/runtime/storage/snapshots/types.d.ts +8 -8
  113. package/runtime/storage/user-interest/types.d.ts +3 -3
  114. package/runtime/storage/v8-state-stores.d.ts +15 -3
  115. package/runtime/storage/v8-state-stores.js +60 -39
@@ -12,7 +12,7 @@
12
12
  * - Optional registry: when absent, resolution is best-effort from goals/evidence.
13
13
  */
14
14
  import type { IntentKind } from "../types.js";
15
- import type { ControlPlaneSourceRef } from "../types.js";
15
+ import type { SourceRef } from "../../../shared/types/v8-contracts.js";
16
16
  import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
17
17
  /** Minimal goal shape accepted by the router to avoid coupling to AgentGoal. M-03 decoupling. */
18
18
  interface GoalRouterContext {
@@ -24,7 +24,7 @@ export interface PlatformResolutionContext {
24
24
  /** Accepted goals that may name a platform or capability. */
25
25
  acceptedGoals?: GoalRouterContext[];
26
26
  /** Evidence refs that may embed platform identity. */
27
- evidenceRefs?: ControlPlaneSourceRef[];
27
+ evidenceRefs?: SourceRef[];
28
28
  }
29
29
  /**
30
30
  * Resolve an explicit platformId for a candidate intent kind.
@@ -48,7 +48,7 @@ function extractPlatformIdsFromGoals(goals, kind, platformIds) {
48
48
  function extractPlatformIdsFromEvidence(refs, platformIds) {
49
49
  const results = new Set();
50
50
  for (const ref of refs) {
51
- if (ref.kind === "connector_result" && ref.id) {
51
+ if (ref.family === "connector_result" && ref.id) {
52
52
  for (const pid of platformIds) {
53
53
  if (ref.id.includes(pid)) {
54
54
  results.add(pid);
@@ -3,6 +3,7 @@
3
3
  * Aligns with control-plane-system.detail §3.9 buildOutreachDraftRequest.
4
4
  */
5
5
  import * as crypto from "node:crypto";
6
+ import { legacyKindFromSourceRef } from "../../../shared/source-ref-compat.js";
6
7
  function inferRhythmWindowKind(windowId) {
7
8
  const id = windowId.toLowerCase();
8
9
  if (id.includes("work"))
@@ -20,10 +21,8 @@ function inferRhythmWindowKind(windowId) {
20
21
  function toGuidanceRefs(refs) {
21
22
  return refs.map((r) => ({
22
23
  id: r.id,
23
- kind: r.kind,
24
+ kind: legacyKindFromSourceRef(r),
24
25
  uri: r.uri,
25
- excerptHash: r.excerptHash,
26
- observedAt: r.observedAt,
27
26
  }));
28
27
  }
29
28
  function mapDeliveryVerdict(verdict) {
@@ -7,7 +7,7 @@ import type { CandidateIntent } from "../types.js";
7
7
  import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
8
8
  import type { HeartbeatCycleResult } from "../heartbeat/signal.js";
9
9
  import type { StateDatabase } from "../../../storage/db/index.js";
10
- import type { SourceRef } from "../../../storage/life-evidence/types.js";
10
+ import type { LifeEvidenceSourceRef } from "../../../storage/life-evidence/types.js";
11
11
  import { type JudgeOutreachInput } from "./judge-outreach.js";
12
12
  import { type DeliveryTargetResolution } from "./delivery-target.js";
13
13
  export interface OpenClawDeliverySendResult {
@@ -16,7 +16,7 @@ export interface OpenClawDeliverySendResult {
16
16
  errorClass?: string;
17
17
  messageId?: string;
18
18
  /** Host-reported delivery proof when messageId is absent (T4.3.1). */
19
- hostProofRef?: SourceRef;
19
+ hostProofRef?: LifeEvidenceSourceRef;
20
20
  }
21
21
  export interface OpenClawDeliveryPort {
22
22
  sendDeliveryRequest(input: {
@@ -1,3 +1,4 @@
1
+ import { legacyKindFromSourceRef } from "../../../shared/source-ref-compat.js";
1
2
  import { writeDeliveryAttempt } from "../../../storage/delivery/write-delivery-attempt.js";
2
3
  import { writeOperatorFallback } from "../../../storage/fallback/write-operator-fallback.js";
3
4
  import { judgeOutreach } from "./judge-outreach.js";
@@ -6,7 +7,11 @@ import { buildOutreachDraftRequest } from "./build-outreach-draft-request.js";
6
7
  import { createNarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
7
8
  import { createRelationshipMemoryStore } from "../../../storage/relationship/relationship-memory-store.js";
8
9
  function toSourceRefs(refs) {
9
- return refs.map((r) => ({ ...r }));
10
+ return refs.map((r) => ({
11
+ id: r.id,
12
+ kind: legacyKindFromSourceRef(r),
13
+ uri: r.uri,
14
+ }));
10
15
  }
11
16
  function hasDeliveryProof(attempt) {
12
17
  return Boolean(attempt.messageId?.trim()) || Boolean(attempt.hostProofRef);
@@ -1,12 +1,7 @@
1
+ import { toCanonicalSourceRef } from "../../../shared/source-ref-compat.js";
1
2
  import { isLifeEvidenceSliceEmpty } from "../heartbeat/runtime-snapshot.js";
2
3
  function toControlPlaneRefs(refs) {
3
- return refs.map((r) => ({
4
- id: r.id,
5
- kind: r.kind,
6
- uri: r.uri,
7
- excerptHash: r.excerptHash,
8
- observedAt: r.observedAt,
9
- }));
4
+ return refs.map((r) => toCanonicalSourceRef(r));
10
5
  }
11
6
  export function userInterestSnapshotToJudge(snapshot) {
12
7
  if (!snapshot) {
@@ -18,13 +13,7 @@ export function userInterestSnapshotToJudge(snapshot) {
18
13
  signals: snapshot.signals.map((s) => ({
19
14
  topic: s.topic,
20
15
  confidence: s.confidence,
21
- sourceRefs: s.sourceRefs.map((r) => ({
22
- id: r.id,
23
- kind: r.kind,
24
- uri: r.uri,
25
- excerptHash: r.excerptHash,
26
- observedAt: r.observedAt,
27
- })),
16
+ sourceRefs: s.sourceRefs.map((r) => toCanonicalSourceRef(r)),
28
17
  })),
29
18
  sourceRefs: toControlPlaneRefs(snapshot.sourceRefs),
30
19
  };
@@ -1,4 +1,5 @@
1
- import type { CandidateIntent, ControlPlaneSourceRef } from "../types.js";
1
+ import type { CandidateIntent } from "../types.js";
2
+ import type { SourceRef } from "../../../shared/types/v8-contracts.js";
2
3
  import { type DeliveryCapabilitySnapshot, type DeliveryTargetResolution } from "./delivery-target.js";
3
4
  export type OutreachJudgmentVerdict = "allow" | "deny" | "defer";
4
5
  export type CooldownState = "clear" | "cooling_down" | "duplicate";
@@ -8,9 +9,9 @@ export interface JudgeOutreachUserInterest {
8
9
  signals: Array<{
9
10
  topic: string;
10
11
  confidence: number;
11
- sourceRefs: ControlPlaneSourceRef[];
12
+ sourceRefs: SourceRef[];
12
13
  }>;
13
- sourceRefs: ControlPlaneSourceRef[];
14
+ sourceRefs: SourceRef[];
14
15
  }
15
16
  export interface JudgeOutreachLifeEvidence {
16
17
  empty: boolean;
@@ -31,8 +32,8 @@ export interface OutreachJudgment {
31
32
  valueScore: number;
32
33
  userRelevance: number;
33
34
  actionability: number;
34
- interestRefs: ControlPlaneSourceRef[];
35
- sourceRefs: ControlPlaneSourceRef[];
35
+ interestRefs: SourceRef[];
36
+ sourceRefs: SourceRef[];
36
37
  cooldownState: CooldownState;
37
38
  deliveryVerdict: DeliveryTargetResolution["verdict"];
38
39
  reasons: string[];
@@ -22,7 +22,9 @@
22
22
  * Test coverage: tests/unit/judgment/judgment-engine.test.ts
23
23
  */
24
24
  import { readPerceptionCardById, writeJudgmentVerdict, } from "../../../storage/v8-state-stores.js";
25
+ import { parseSourceRefs } from "../../../shared/serialization.js";
25
26
  import { ACTION_KIND_REGISTRY } from "../../../shared/types/v8-contracts.js";
27
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
26
28
  // ───────────────────────────────────────────────────────────────
27
29
  // Config
28
30
  // ───────────────────────────────────────────────────────────────
@@ -42,17 +44,6 @@ function inferRiskPosture(sensitivityClass, riskFlags) {
42
44
  }
43
45
  return "low";
44
46
  }
45
- function parseCardSourceRefs(json) {
46
- if (!json)
47
- return [];
48
- try {
49
- const parsed = JSON.parse(json);
50
- return Array.isArray(parsed) ? parsed : [];
51
- }
52
- catch {
53
- return [];
54
- }
55
- }
56
47
  function selectVerdict(relevance, confidence, riskPosture, hasSourceRefs, possibleIntents) {
57
48
  // Missing source refs → ignore/watch only
58
49
  if (!hasSourceRefs) {
@@ -117,7 +108,7 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
117
108
  const card = readResult.row;
118
109
  if (!card) {
119
110
  return {
120
- status: "degraded",
111
+ status: classifyDegradedStatus("state_unreadable"),
121
112
  reason: "state_unreadable",
122
113
  ownerStage: "judgment",
123
114
  sourceRefs: [],
@@ -125,7 +116,7 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
125
116
  retryable: false,
126
117
  };
127
118
  }
128
- const sourceRefs = parseCardSourceRefs(card.sourceRefsJson);
119
+ const sourceRefs = parseSourceRefs(card.sourceRefsJson);
129
120
  const hasSourceRefs = sourceRefs.length > 0;
130
121
  // Parse sensitivity class from payload (stored there by perception-builder)
131
122
  let sensitivityClass = "public_general";
@@ -204,9 +195,12 @@ export async function runAgentJudgment(db, perceptionCardId, options) {
204
195
  });
205
196
  if ("reason" in writeResult) {
206
197
  return {
207
- status: "degraded",
208
- verdicts: [],
198
+ status: classifyDegradedStatus(writeResult.reason),
209
199
  reason: writeResult.reason,
200
+ ownerStage: "judgment",
201
+ sourceRefs: [],
202
+ operatorNextAction: `Failed to persist JudgmentVerdict: ${writeResult.reason}`,
203
+ retryable: true,
210
204
  };
211
205
  }
212
206
  return {
@@ -228,7 +222,7 @@ export async function runAgentJudgments(db, perceptionCardIds, options) {
228
222
  failed.push({
229
223
  perceptionCardId,
230
224
  degraded: {
231
- status: "degraded",
225
+ status: classifyDegradedStatus(result.reason ?? "judgment_low_confidence"),
232
226
  reason: result.reason ?? "judgment_low_confidence",
233
227
  ownerStage: "judgment",
234
228
  sourceRefs: [],
@@ -22,6 +22,8 @@
22
22
  * Test coverage: tests/unit/perception/perception-builder.test.ts
23
23
  */
24
24
  import { readEvidenceItemsByStatus, writePerceptionCard, updateEvidenceItemLifecycleStatus, } from "../../../storage/v8-state-stores.js";
25
+ import { parseSourceRefs } from "../../../shared/serialization.js";
26
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
25
27
  // ───────────────────────────────────────────────────────────────
26
28
  // Config
27
29
  // ───────────────────────────────────────────────────────────────
@@ -29,15 +31,6 @@ const PERCEPTION_MAX_EVIDENCE_PER_CYCLE = 50;
29
31
  // ───────────────────────────────────────────────────────────────
30
32
  // Helpers
31
33
  // ───────────────────────────────────────────────────────────────
32
- function parseSourceRefs(json) {
33
- try {
34
- const parsed = JSON.parse(json);
35
- return Array.isArray(parsed) ? parsed : [];
36
- }
37
- catch {
38
- return [];
39
- }
40
- }
41
34
  function parsePayload(json) {
42
35
  if (!json)
43
36
  return undefined;
@@ -118,6 +111,14 @@ function inferRelevanceClass(score) {
118
111
  }
119
112
  function inferSummary(evidence) {
120
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
+ }
121
122
  if (payload?.summary && String(payload.summary).trim().length > 0) {
122
123
  return { summary: String(payload.summary), contentMissing: false };
123
124
  }
@@ -254,9 +255,12 @@ export async function buildPerceptionCards(db, options) {
254
255
  });
255
256
  if ("reason" in writeResult) {
256
257
  return {
257
- status: "degraded",
258
- cards,
258
+ status: classifyDegradedStatus(writeResult.reason),
259
259
  reason: writeResult.reason,
260
+ ownerStage: "perception",
261
+ sourceRefs: card.evidenceRefs,
262
+ operatorNextAction: `Failed to persist PerceptionCard: ${writeResult.reason}`,
263
+ retryable: true,
260
264
  };
261
265
  }
262
266
  await updateEvidenceItemLifecycleStatus(db, evidence.id, "perceived");
@@ -3,13 +3,21 @@ import { writeQuietArtifact } from "../../../storage/quiet/quiet-artifact-writer
3
3
  import { persistQuietArtifactToWorkspace } from "../../../storage/quiet/persist-quiet-artifact.js";
4
4
  import { buildEvidencePack, buildQuietNarrativeGuidance, selectInterestBasis } from "../../../guidance/evidence-guidance.js";
5
5
  import { recordQuietArtifactAudit } from "../../../observability/services/audit-closure-recorders.js";
6
+ import { legacyKindFromSourceRef } from "../../../shared/source-ref-compat.js";
6
7
  function toGuidanceRef(r) {
7
8
  return {
8
9
  id: r.id,
9
- kind: r.kind,
10
+ kind: legacyKindFromSourceRef(r),
10
11
  uri: r.uri,
11
- excerptHash: r.excerptHash,
12
- observedAt: r.observedAt,
12
+ };
13
+ }
14
+ function toLifeEvidenceRef(ref) {
15
+ return {
16
+ id: ref.id,
17
+ kind: ref.kind,
18
+ uri: ref.uri,
19
+ excerptHash: ref.excerptHash,
20
+ observedAt: ref.observedAt,
13
21
  };
14
22
  }
15
23
  /**
@@ -116,24 +124,14 @@ export async function runSourceBackedQuiet(params) {
116
124
  confidence: userInterestSnapshot?.confidence ?? 0,
117
125
  signalCount: userInterestSnapshot?.signals.length ?? 0,
118
126
  });
119
- const groundedSourceRefs = ep.pack.groundedRefs.map((g) => ({
120
- id: g.id,
121
- kind: g.kind,
122
- uri: g.uri,
123
- excerptHash: g.excerptHash,
124
- observedAt: g.observedAt,
125
- }));
127
+ const groundedSourceRefs = ep.pack.groundedRefs.map(toLifeEvidenceRef);
126
128
  const claims = ep.pack.groundedRefs.map((g, i) => ({
127
129
  id: `fact:${g.id}`,
128
130
  text: `Evidence-backed note ${i + 1}`,
129
131
  claimType: "fact",
130
132
  sourceRefs: [
131
133
  {
132
- id: g.id,
133
- kind: g.kind,
134
- uri: g.uri,
135
- excerptHash: g.excerptHash,
136
- observedAt: g.observedAt,
134
+ ...toLifeEvidenceRef(g),
137
135
  },
138
136
  ],
139
137
  }));
@@ -22,10 +22,11 @@
22
22
  * so `dreamStatus` reaches completed/blocked.
23
23
  * - Does not bypass Dream runner; only records due/completed/blocked.
24
24
  */
25
- import { writeDailyRhythmState, readDailyRhythmStateByDay, readActionClosuresByDay, readDreamConsolidationRunById, readDreamConsolidationRunsByQuietId, updateDreamConsolidationRunStatus, } from "../../../storage/v8-state-stores.js";
25
+ import { writeDailyRhythmState, readDailyRhythmStateByDay, readActionClosuresByDay, readDreamConsolidationRunById, readDreamConsolidationRunsByQuietId, readLatestDreamConsolidationRunByStatus, updateDreamConsolidationRunStatus, } from "../../../storage/v8-state-stores.js";
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,13 +107,12 @@ 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;
113
114
  const updateResult = await updateDreamConsolidationRunStatus(db, runId, finalStatus, {
114
115
  reason: finalReason ?? null,
115
- lifecycleStatus: finalStatus === "completed" ? "completed" : "archived",
116
116
  payloadJson: JSON.stringify({
117
117
  ...parsePayloadJson(run.payloadJson),
118
118
  consolidatedAt: now,
@@ -123,6 +123,20 @@ async function executeStaleScheduledDreams(db, state, now) {
123
123
  if ("reason" in updateResult) {
124
124
  return updateResult;
125
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
+ }
126
140
  lastResult = { completed: true, reason: finalReason ?? "dream_scheduled_stalled" };
127
141
  }
128
142
  else {
@@ -208,7 +222,7 @@ export async function checkDailyRhythm(db, options) {
208
222
  else if (state.dreamStatus === "scheduled") {
209
223
  // Stale scheduled run: try to execute consolidation now
210
224
  const staleResult = await executeStaleScheduledDreams(db, state, now);
211
- if ("status" in staleResult && staleResult.status === "degraded") {
225
+ if ("status" in staleResult) {
212
226
  return staleResult;
213
227
  }
214
228
  const { completed, reason } = staleResult;
@@ -226,18 +240,17 @@ export async function checkDailyRhythm(db, options) {
226
240
  // Already handled; do not re-schedule
227
241
  }
228
242
  else {
243
+ // Global 7-day interval check: look across all quiet reviews, not just today's.
229
244
  const quietId = `quiet_${day}`;
230
- const latestRun = await loadLatestDreamRunForQuiet(db, quietId);
231
- if (latestRun.degraded) {
232
- return latestRun.degraded;
245
+ const globalLatest = await readLatestDreamConsolidationRunByStatus(db, ["completed", "blocked"]);
246
+ if (globalLatest.degraded) {
247
+ return globalLatest.degraded;
233
248
  }
234
- // If a completed/blocked run already exists within the 7-day interval, honor it.
235
- if (latestRun.row &&
236
- (latestRun.row.status === "completed" || latestRun.row.status === "blocked") &&
237
- isWithinDays(latestRun.row.createdAt, now, DREAM_DEFAULT_INTERVAL_DAYS)) {
238
- state.dreamStatus = latestRun.row.status;
239
- state.dreamReason = latestRun.row.reason ?? "dream_completed";
240
- if (latestRun.row.status === "completed") {
249
+ if (globalLatest.row &&
250
+ isWithinDays(globalLatest.row.createdAt, now, DREAM_DEFAULT_INTERVAL_DAYS)) {
251
+ state.dreamStatus = globalLatest.row.status;
252
+ state.dreamReason = "dream_interval_active";
253
+ if (globalLatest.row.status === "completed") {
241
254
  state.dreamCompletedAt = now;
242
255
  }
243
256
  }
@@ -264,11 +277,10 @@ export async function checkDailyRhythm(db, options) {
264
277
  // Immediately execute the freshly scheduled dream so it does not sit
265
278
  // pending forever (T-DQ.R.7).
266
279
  const consolidateResult = await runDreamConsolidation(db, dreamResult.id, { now });
267
- if ("status" in consolidateResult && consolidateResult.status !== "degraded") {
280
+ if ("status" in consolidateResult && !("ownerStage" in consolidateResult)) {
268
281
  const dreamOutcome = consolidateResult;
269
282
  const updateResult = await updateDreamConsolidationRunStatus(db, dreamResult.id, dreamOutcome.status, {
270
283
  reason: dreamOutcome.reason ?? null,
271
- lifecycleStatus: dreamOutcome.status === "completed" ? "completed" : "archived",
272
284
  payloadJson: JSON.stringify({
273
285
  consolidatedAt: now,
274
286
  candidateCount: dreamOutcome.candidates.length,
@@ -281,6 +293,18 @@ export async function checkDailyRhythm(db, options) {
281
293
  state.dreamReason = dreamOutcome.reason ?? (dreamOutcome.status === "completed" ? "dream_completed" : "dream_failed");
282
294
  if (dreamOutcome.status === "completed") {
283
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
+ }
284
308
  }
285
309
  }
286
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",