@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
@@ -20,6 +20,8 @@
20
20
  *
21
21
  * Test coverage: tests/unit/action/policy-bound-dispatch.test.ts
22
22
  */
23
+ import { serializeSourceRefs } from "../../../shared/serialization.js";
24
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
23
25
  // ───────────────────────────────────────────────────────────────
24
26
  // Helpers
25
27
  // ───────────────────────────────────────────────────────────────
@@ -60,7 +62,8 @@ export function dispatchAllowedAction(proposal, decision, options) {
60
62
  actionKind: target,
61
63
  draftType,
62
64
  policyProof: { decisionId: decision.id, decision: decision.decision },
63
- sourceRefs: JSON.stringify(decision.proofRefs),
65
+ sourceRefs: serializeSourceRefs(proposal.sourceRefs),
66
+ proofRefs: serializeSourceRefs(decision.proofRefs),
64
67
  },
65
68
  };
66
69
  }
@@ -75,7 +78,8 @@ export function dispatchAllowedAction(proposal, decision, options) {
75
78
  capabilityId: proposal.targetCapabilityId ?? "run_connector",
76
79
  idempotencyKey: proposal.idempotencyKey,
77
80
  policyProof: { decisionId: decision.id, decision: decision.decision },
78
- sourceRefs: JSON.stringify(proposal.sourceRefs),
81
+ sourceRefs: serializeSourceRefs(proposal.sourceRefs),
82
+ proofRefs: serializeSourceRefs(decision.proofRefs),
79
83
  },
80
84
  };
81
85
  }
@@ -91,7 +95,8 @@ export function dispatchAllowedAction(proposal, decision, options) {
91
95
  actionKind: proposal.actionKind,
92
96
  draftType,
93
97
  policyProof: { decisionId: decision.id, decision: decision.decision },
94
- sourceRefs: JSON.stringify(proposal.sourceRefs),
98
+ sourceRefs: serializeSourceRefs(proposal.sourceRefs),
99
+ proofRefs: serializeSourceRefs(decision.proofRefs),
95
100
  },
96
101
  };
97
102
  }
@@ -101,10 +106,10 @@ export function dispatchAllowedAction(proposal, decision, options) {
101
106
  return {
102
107
  type: "degraded",
103
108
  degraded: {
104
- status: "degraded",
109
+ status: classifyDegradedStatus("closure_failed"),
105
110
  reason: "closure_failed",
106
111
  ownerStage: "execution",
107
- sourceRefs: decision.proofRefs,
112
+ sourceRefs: proposal.sourceRefs,
108
113
  operatorNextAction: "Unexpected policy decision shape",
109
114
  retryable: false,
110
115
  },
@@ -20,6 +20,7 @@
20
20
  * Test coverage: tests/unit/control-plane/accepted-projection-loader.test.ts
21
21
  */
22
22
  import { readMemoryProjectionsByStatus, } from "../../../storage/v8-state-stores.js";
23
+ import { parseSourceRefs } from "../../../shared/serialization.js";
23
24
  // ───────────────────────────────────────────────────────────────
24
25
  // Helpers
25
26
  // ───────────────────────────────────────────────────────────────
@@ -33,17 +34,6 @@ function parsePayloadJson(json) {
33
34
  return {};
34
35
  }
35
36
  }
36
- function parseSourceRefs(json) {
37
- if (!json)
38
- return [];
39
- try {
40
- const parsed = JSON.parse(json);
41
- return Array.isArray(parsed) ? parsed : [];
42
- }
43
- catch {
44
- return [];
45
- }
46
- }
47
37
  // ───────────────────────────────────────────────────────────────
48
38
  // Public API
49
39
  // ───────────────────────────────────────────────────────────────
@@ -0,0 +1,82 @@
1
+ /**
2
+ * CycleFinalizer — v8 exactly-one closure invariant (T-AC.R.2, T-AC.R.3)
3
+ *
4
+ * Core logic: provide a single boundary that records exactly one
5
+ * ActionClosureRecord or no-action closure per heartbeat cycle.
6
+ * Enforces idempotency key = cycleId, write order (closure row first),
7
+ * and restart reconcile for orphaned closure/event rows.
8
+ *
9
+ * Design authority:
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.md §6.1a`
11
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.4`
12
+ * - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §3.4`
13
+ *
14
+ * Dependencies:
15
+ * - `src/core/second-nature/action/action-closure-recorder.js`
16
+ * - `src/storage/v8-state-stores.js`
17
+ *
18
+ * Boundary:
19
+ * - One cycle → one closure row.
20
+ * - Duplicate terminal closure for same cycleId → `unsafe` idempotency conflict.
21
+ * - On partial failure, returns degraded diagnostic; caller records stage event.
22
+ * - Reconcile detects orphaned closure (event missing) or orphaned event (closure missing).
23
+ *
24
+ * Test coverage: tests/unit/control-plane/cycle-finalizer.test.ts
25
+ */
26
+ import type { StateDatabase } from "../../../storage/db/index.js";
27
+ import type { DegradedOperationResult, V8ReasonCode, SourceRef } from "../../../shared/types/v8-contracts.js";
28
+ export interface CycleFinalizerResult {
29
+ closureRef?: SourceRef;
30
+ noActionReason?: V8ReasonCode;
31
+ degraded?: DegradedOperationResult;
32
+ }
33
+ export type ClosureKind = {
34
+ kind: "no_action";
35
+ reason: V8ReasonCode;
36
+ } | {
37
+ kind: "policy";
38
+ closureStatus: "completed" | "denied" | "deferred" | "downgraded";
39
+ reason: V8ReasonCode;
40
+ proposalId: string;
41
+ decisionId: string;
42
+ platformId?: string;
43
+ capabilityId?: string;
44
+ downgradedActionKind?: string;
45
+ } | {
46
+ kind: "execution";
47
+ closureStatus: "completed" | "failed";
48
+ reason: V8ReasonCode;
49
+ proposalId: string;
50
+ decisionId: string;
51
+ platformId?: string;
52
+ capabilityId?: string;
53
+ executionResultRef?: string;
54
+ outputSummary?: string;
55
+ };
56
+ export declare function finalizeCycle(db: StateDatabase, cycleId: string, closure: ClosureKind, options?: {
57
+ now?: string;
58
+ }): Promise<CycleFinalizerResult>;
59
+ export interface ReconcileResult {
60
+ /** Closure row exists but no closure stage event — event should be replayed. */
61
+ orphanedClosure?: {
62
+ closureId: string;
63
+ cycleId: string;
64
+ };
65
+ /** Stage event exists but no closure row — closure is missing, do not fabricate. */
66
+ orphanedEvent?: {
67
+ cycleId: string;
68
+ stage: string;
69
+ };
70
+ /** No orphan detected — cycle is consistent. */
71
+ consistent: boolean;
72
+ degraded?: DegradedOperationResult;
73
+ }
74
+ /**
75
+ * Reconcile a cycle's closure and stage event rows.
76
+ * Called at cycle start or by `loop_status` to detect partial-failure leftovers.
77
+ *
78
+ * Per design §6.1a:
79
+ * - closure row written, event missing → replay event with traceRefs
80
+ * - event written, closure row missing → report closure_unavailable / unsafe
81
+ */
82
+ export declare function reconcileCycleClosure(db: StateDatabase, cycleId: string): Promise<ReconcileResult>;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * CycleFinalizer — v8 exactly-one closure invariant (T-AC.R.2, T-AC.R.3)
3
+ *
4
+ * Core logic: provide a single boundary that records exactly one
5
+ * ActionClosureRecord or no-action closure per heartbeat cycle.
6
+ * Enforces idempotency key = cycleId, write order (closure row first),
7
+ * and restart reconcile for orphaned closure/event rows.
8
+ *
9
+ * Design authority:
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.md §6.1a`
11
+ * - `.anws/v8/04_SYSTEM_DESIGN/action-closure-policy-system.detail.md §3.4`
12
+ * - `.anws/v8/04_SYSTEM_DESIGN/control-plane-system.md §3.4`
13
+ *
14
+ * Dependencies:
15
+ * - `src/core/second-nature/action/action-closure-recorder.js`
16
+ * - `src/storage/v8-state-stores.js`
17
+ *
18
+ * Boundary:
19
+ * - One cycle → one closure row.
20
+ * - Duplicate terminal closure for same cycleId → `unsafe` idempotency conflict.
21
+ * - On partial failure, returns degraded diagnostic; caller records stage event.
22
+ * - Reconcile detects orphaned closure (event missing) or orphaned event (closure missing).
23
+ *
24
+ * Test coverage: tests/unit/control-plane/cycle-finalizer.test.ts
25
+ */
26
+ import { recordNoActionClosure, recordPolicyOutcomeClosure, recordExecutionClosure, } from "../action/action-closure-recorder.js";
27
+ import { readActionClosuresByCycle, readLoopStageEventsByCycle, } from "../../../storage/v8-state-stores.js";
28
+ import { classifyDegradedStatus } from "../../../shared/degraded-status-classifier.js";
29
+ // Terminal closure statuses — a cycle with any of these is considered finalized.
30
+ const TERMINAL_CLOSURE_STATUSES = new Set([
31
+ "completed",
32
+ "no_action",
33
+ "denied",
34
+ "deferred",
35
+ "downgraded",
36
+ "failed",
37
+ ]);
38
+ /**
39
+ * Check whether a cycle already has a terminal closure row.
40
+ * This is the idempotency gate for `finalizeCycle`.
41
+ */
42
+ async function checkExistingTerminalClosure(db, cycleId) {
43
+ const existing = await readActionClosuresByCycle(db, cycleId);
44
+ if (existing.degraded)
45
+ return { hasTerminal: false };
46
+ const terminal = existing.rows.find((r) => TERMINAL_CLOSURE_STATUSES.has(r.status));
47
+ if (terminal)
48
+ return { hasTerminal: true, existingClosureId: terminal.id };
49
+ return { hasTerminal: false };
50
+ }
51
+ export async function finalizeCycle(db, cycleId, closure, options) {
52
+ const now = options?.now ?? new Date().toISOString();
53
+ // Idempotency gate: check if this cycle already has a terminal closure.
54
+ // recordNoActionClosure has its own idempotency check, but policy/execution do not.
55
+ // We check uniformly here to enforce the exactly-one invariant.
56
+ if (closure.kind !== "no_action") {
57
+ const existing = await checkExistingTerminalClosure(db, cycleId);
58
+ if (existing.hasTerminal) {
59
+ return {
60
+ degraded: {
61
+ status: "unsafe",
62
+ reason: "closure_idempotency_conflict",
63
+ ownerStage: "closure",
64
+ sourceRefs: [],
65
+ operatorNextAction: `Cycle ${cycleId} already has terminal closure ${existing.existingClosureId}; duplicate finalize blocked`,
66
+ retryable: false,
67
+ },
68
+ };
69
+ }
70
+ }
71
+ switch (closure.kind) {
72
+ case "no_action": {
73
+ const result = await recordNoActionClosure(db, cycleId, closure.reason, { now });
74
+ if ("closureId" in result) {
75
+ return {
76
+ closureRef: {
77
+ uri: `sn://closure/${result.closureId}`,
78
+ family: "action_closure",
79
+ id: result.closureId,
80
+ redactionClass: "none",
81
+ resolveStatus: "resolvable",
82
+ },
83
+ noActionReason: closure.reason,
84
+ };
85
+ }
86
+ return {
87
+ noActionReason: closure.reason,
88
+ degraded: result,
89
+ };
90
+ }
91
+ case "policy": {
92
+ const result = await recordPolicyOutcomeClosure(db, cycleId, closure.closureStatus, closure.reason, {
93
+ proposalId: closure.proposalId,
94
+ decisionId: closure.decisionId,
95
+ platformId: closure.platformId,
96
+ capabilityId: closure.capabilityId,
97
+ downgradedActionKind: closure.downgradedActionKind,
98
+ }, { now });
99
+ if ("closureId" in result) {
100
+ return {
101
+ closureRef: {
102
+ uri: `sn://closure/${result.closureId}`,
103
+ family: "action_closure",
104
+ id: result.closureId,
105
+ redactionClass: "none",
106
+ resolveStatus: "resolvable",
107
+ },
108
+ };
109
+ }
110
+ return { degraded: result };
111
+ }
112
+ case "execution": {
113
+ const result = await recordExecutionClosure(db, cycleId, closure.closureStatus, closure.reason, {
114
+ proposalId: closure.proposalId,
115
+ decisionId: closure.decisionId,
116
+ platformId: closure.platformId,
117
+ capabilityId: closure.capabilityId,
118
+ executionResultRef: closure.executionResultRef,
119
+ outputSummary: closure.outputSummary,
120
+ }, { now });
121
+ if ("closureId" in result) {
122
+ return {
123
+ closureRef: {
124
+ uri: `sn://closure/${result.closureId}`,
125
+ family: "action_closure",
126
+ id: result.closureId,
127
+ redactionClass: "none",
128
+ resolveStatus: "resolvable",
129
+ },
130
+ };
131
+ }
132
+ return { degraded: result };
133
+ }
134
+ default: {
135
+ const exhaustive = closure;
136
+ return {
137
+ degraded: {
138
+ status: classifyDegradedStatus("closure_failed"),
139
+ reason: "closure_failed",
140
+ ownerStage: "closure",
141
+ sourceRefs: [],
142
+ operatorNextAction: `Unknown closure kind: ${JSON.stringify(exhaustive)}`,
143
+ retryable: false,
144
+ },
145
+ };
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Reconcile a cycle's closure and stage event rows.
151
+ * Called at cycle start or by `loop_status` to detect partial-failure leftovers.
152
+ *
153
+ * Per design §6.1a:
154
+ * - closure row written, event missing → replay event with traceRefs
155
+ * - event written, closure row missing → report closure_unavailable / unsafe
156
+ */
157
+ export async function reconcileCycleClosure(db, cycleId) {
158
+ const closures = await readActionClosuresByCycle(db, cycleId);
159
+ if (closures.degraded) {
160
+ return {
161
+ consistent: false,
162
+ degraded: closures.degraded,
163
+ };
164
+ }
165
+ const events = await readLoopStageEventsByCycle(db, cycleId);
166
+ if (events.degraded) {
167
+ return {
168
+ consistent: false,
169
+ degraded: events.degraded,
170
+ };
171
+ }
172
+ const terminalClosure = closures.rows.find((r) => TERMINAL_CLOSURE_STATUSES.has(r.status));
173
+ const closureEvent = events.rows.find((r) => r.stage === "closure" || r.stage === "action_closure");
174
+ if (terminalClosure && !closureEvent) {
175
+ return {
176
+ orphanedClosure: { closureId: terminalClosure.id, cycleId },
177
+ consistent: false,
178
+ };
179
+ }
180
+ if (!terminalClosure && closureEvent) {
181
+ return {
182
+ orphanedEvent: { cycleId, stage: closureEvent.stage },
183
+ consistent: false,
184
+ };
185
+ }
186
+ return { consistent: true };
187
+ }
@@ -35,6 +35,7 @@ export interface HeartbeatOrchestrationResult {
35
35
  closureRef?: SourceRef;
36
36
  noActionReason?: V8ReasonCode;
37
37
  degraded?: DegradedOperationResult;
38
+ rhythmDegraded?: DegradedOperationResult;
38
39
  rhythmState?: DailyRhythmState;
39
40
  }
40
41
  export declare function runHeartbeatCycle(db: StateDatabase, request: HeartbeatOrchestrationRequest): Promise<HeartbeatOrchestrationResult | DegradedOperationResult>;
@@ -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, {
@@ -191,7 +192,7 @@ export async function runHeartbeatCycle(db, request) {
191
192
  reason: degradedReason,
192
193
  sourceRefs: degradedClosureRef ? [degradedClosureRef, cycleRef] : [cycleRef],
193
194
  });
194
- const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
195
+ const { rhythmState, rhythmDegraded } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
195
196
  return {
196
197
  cycleId,
197
198
  cycleSequence,
@@ -199,14 +200,15 @@ 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",
207
208
  retryable: true,
208
209
  }
209
- : undefined,
210
+ : rhythmDegraded,
211
+ rhythmDegraded,
210
212
  rhythmState,
211
213
  };
212
214
  }
@@ -245,12 +247,14 @@ export async function runHeartbeatCycle(db, request) {
245
247
  reason: "evidence_batch_empty",
246
248
  sourceRefs: emptyClosureRef ? [emptyClosureRef, cycleRef] : [cycleRef],
247
249
  });
248
- const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
250
+ const { rhythmState, rhythmDegraded } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
249
251
  return {
250
252
  cycleId,
251
253
  cycleSequence,
252
254
  closureRef: emptyClosureRef,
253
255
  noActionReason: "evidence_batch_empty",
256
+ degraded: rhythmDegraded,
257
+ rhythmDegraded,
254
258
  rhythmState,
255
259
  };
256
260
  }
@@ -324,7 +328,7 @@ export async function runHeartbeatCycle(db, request) {
324
328
  else {
325
329
  // Build proposal for the actionable verdict
326
330
  const proposalResult = await buildActionProposal(db, actionableVerdict.id, { now });
327
- if ("status" in proposalResult && proposalResult.status === "degraded") {
331
+ if ("operatorNextAction" in proposalResult) {
328
332
  // Proposal build failed — still need a closure
329
333
  closureDegraded = proposalResult;
330
334
  const closureResult = await recordNoActionClosure(db, cycleId, closureDegraded.reason, { now });
@@ -365,7 +369,7 @@ export async function runHeartbeatCycle(db, request) {
365
369
  }
366
370
  else if (proposalResult.status === "remember_for_review") {
367
371
  const remember = proposalResult;
368
- const closureResult = await recordRememberClosure(db, cycleId, remember.memoryReviewCandidate, { now });
372
+ const closureResult = await recordRememberClosure(db, cycleId, remember.memoryReviewCandidate, { now, platformId: "heartbeat" });
369
373
  if ("closureId" in closureResult) {
370
374
  closureRef = {
371
375
  uri: `sn://closure/${closureResult.closureId}`,
@@ -395,7 +399,8 @@ export async function runHeartbeatCycle(db, request) {
395
399
  status: "completed",
396
400
  occurredAt: new Date().toISOString(),
397
401
  reason: decision.decisionReason,
398
- sourceRefs: decision.proofRefs,
402
+ sourceRefs: proposal.sourceRefs,
403
+ proofRefs: decision.proofRefs,
399
404
  });
400
405
  // Record execution stage started
401
406
  await recordLoopStageEvent(db, {
@@ -405,7 +410,8 @@ export async function runHeartbeatCycle(db, request) {
405
410
  stage: "execution",
406
411
  status: "started",
407
412
  occurredAt: new Date().toISOString(),
408
- sourceRefs: decision.proofRefs,
413
+ sourceRefs: proposal.sourceRefs,
414
+ proofRefs: decision.proofRefs,
409
415
  });
410
416
  // Dispatch — no real external write in T-CP.R.2
411
417
  const dispatchResult = dispatchAllowedAction(proposal, decision, { guidanceAvailable: false });
@@ -509,7 +515,8 @@ export async function runHeartbeatCycle(db, request) {
509
515
  status: closureDegraded ? "failed" : "completed",
510
516
  occurredAt: new Date().toISOString(),
511
517
  reason: closureDegraded?.reason,
512
- sourceRefs: decision.proofRefs,
518
+ sourceRefs: proposal.sourceRefs,
519
+ proofRefs: decision.proofRefs,
513
520
  });
514
521
  }
515
522
  }
@@ -540,13 +547,14 @@ export async function runHeartbeatCycle(db, request) {
540
547
  noActionReason = "proposal_no_action";
541
548
  }
542
549
  // T-CP.R.3: Advance daily rhythm after closure/no-action
543
- const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
550
+ const { rhythmState, rhythmDegraded } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
544
551
  return {
545
552
  cycleId,
546
553
  cycleSequence,
547
554
  closureRef,
548
555
  noActionReason,
549
- degraded: closureDegraded,
556
+ degraded: closureDegraded ?? rhythmDegraded,
557
+ rhythmDegraded,
550
558
  rhythmState,
551
559
  };
552
560
  }
@@ -31,5 +31,7 @@ export interface RealRuntimeSpineResult {
31
31
  noActionReason?: V8ReasonCode;
32
32
  degraded?: DegradedOperationResult;
33
33
  rhythmState?: DailyRhythmState;
34
+ rhythmDegraded?: DegradedOperationResult;
35
+ impulseContextArtifactId?: string;
34
36
  }
35
37
  export declare function runRealRuntimeHeartbeatCycle(options: RealRuntimeSpineOptions): Promise<RealRuntimeSpineResult | DegradedOperationResult>;
@@ -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;
@@ -38,5 +38,6 @@ export async function runRealRuntimeHeartbeatCycle(options) {
38
38
  noActionReason: orchestrationResult.noActionReason,
39
39
  degraded: orchestrationResult.degraded,
40
40
  rhythmState: orchestrationResult.rhythmState,
41
+ rhythmDegraded: orchestrationResult.rhythmDegraded,
41
42
  };
42
43
  }
@@ -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
  },
@@ -12,6 +12,7 @@ import { mapLifeEvidence } from "../../../connectors/base/map-life-evidence.js";
12
12
  import { appendLifeEvidence } from "../../../storage/life-evidence/append-life-evidence.js";
13
13
  import { normalizeConnectorEvidence } from "../../../connectors/evidence-normalizer.js";
14
14
  import { recordConnectorAttemptAudit } from "../../../observability/services/audit-closure-recorders.js";
15
+ import { makeCanonicalSourceRef } from "../../../shared/source-ref-compat.js";
15
16
  /**
16
17
  * Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
17
18
  * Exported for unit tests (CR-M1 wiring).
@@ -302,11 +303,11 @@ export async function ingestRhythmSignal(signal, deps) {
302
303
  effectClass: "connector_action",
303
304
  capabilityIntent: idleResult.candidate.capabilityId,
304
305
  sourceRefs: [
305
- {
306
+ makeCanonicalSourceRef({
306
307
  id: "idle_curiosity",
307
- kind: "workspace_artifact",
308
+ family: "audit",
308
309
  uri: `idle://${idleResult.candidate.platformId}`,
309
- },
310
+ }),
310
311
  ],
311
312
  idempotencyKey: `idle:${idleResult.candidate.platformId}:${idleResult.candidate.capabilityId}`,
312
313
  goalInfluenceRefs: [],
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * HeartbeatRuntimeSnapshot assembly for candidate planner + hard guards (T2.1.3, T2.2.1).
3
3
  */
4
- import type { ContinuitySnapshot, ControlPlaneSourceRef } from "../types.js";
4
+ import type { ContinuitySnapshot } from "../types.js";
5
+ import type { SourceRef } from "../../../shared/types/v8-contracts.js";
5
6
  import type { RhythmPolicy } from "../rhythm/rhythm-policy.js";
6
7
  import { type PlannerRhythmWindowSlice } from "../rhythm/planner-rhythm-window.js";
7
8
  import type { SnapshotInputs } from "./snapshot-builder.js";
8
9
  import type { AffordanceMap } from "../../../shared/types/v7-entities.js";
9
10
  export interface PlannerLifeEvidenceSlice {
10
- evidenceRefs: ControlPlaneSourceRef[];
11
+ evidenceRefs: SourceRef[];
11
12
  platformEventCount: number;
12
13
  workEventCount: number;
13
14
  emptyReason?: "no_sources" | "state_unavailable" | "redacted_only";
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * Per design doc §4.2: SnapshotBuilder prepares inputs for the Rhythm Engine.
8
8
  */
9
- import type { ContinuitySnapshot, ControlPlaneSourceRef, TopLevelMode } from "../types.js";
9
+ import type { ContinuitySnapshot, TopLevelMode } from "../types.js";
10
+ import type { SourceRef } from "../../../shared/types/v8-contracts.js";
10
11
  import type { RhythmPolicy } from "../rhythm/rhythm-policy.js";
11
12
  import type { DeliveryCapabilitySnapshot } from "../outreach/delivery-target.js";
12
13
  import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
@@ -30,7 +31,7 @@ export interface SnapshotInputs {
30
31
  awaitingUserInput?: boolean;
31
32
  riskSuppressed?: boolean;
32
33
  /** Evidence refs for source-backed planner/guards (T2.1.3 / T2.2.1). */
33
- lifeEvidenceRefs?: ControlPlaneSourceRef[];
34
+ lifeEvidenceRefs?: SourceRef[];
34
35
  platformEventCount?: number;
35
36
  workEventCount?: number;
36
37
  lifeEvidenceEmptyReason?: "no_sources" | "state_unavailable" | "redacted_only";
@@ -1,10 +1,11 @@
1
+ import { makeCanonicalSourceRef } from "../../../shared/source-ref-compat.js";
1
2
  import { isLifeEvidenceSliceEmpty } from "../heartbeat/runtime-snapshot.js";
2
3
  import { buildHeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
3
4
  import { resolvePlatformForIntent, } from "./platform-capability-router.js";
4
5
  import { isGoalRelatedToCandidate } from "./goal-priority.js";
5
6
  const MAX_CANDIDATE_INTENTS = 6;
6
7
  const OBLIGATION_SOURCE = [
7
- { id: "obligation-anchor", kind: "workspace_artifact", uri: "workspace://obligations/pending" },
8
+ makeCanonicalSourceRef({ id: "obligation-anchor", family: "audit", uri: "workspace://obligations/pending" }),
8
9
  ];
9
10
  function evidenceRefsForConnector(runtime) {
10
11
  if (!isLifeEvidenceSliceEmpty(runtime.lifeEvidence) && runtime.lifeEvidence.evidenceRefs.length > 0) {
@@ -253,8 +254,9 @@ export function planCandidateIntents(runtime, options) {
253
254
  if (intent.sourceRefs.length === 0 && related.length > 0) {
254
255
  intent.sourceRefs = related.slice(0, 4).map((g) => ({
255
256
  id: g.goalId,
256
- kind: "workspace_artifact",
257
+ family: "audit",
257
258
  uri: `goal://${g.goalId}`,
259
+ redactionClass: "none",
258
260
  }));
259
261
  }
260
262
  }
@@ -3,9 +3,8 @@ const DEFAULT_NARRATIVE_ID = "default";
3
3
  function mapControlPlaneRefToSourceRef(ref) {
4
4
  return {
5
5
  sourceId: ref.id,
6
- kind: ref.kind,
6
+ kind: ref.family,
7
7
  url: ref.uri,
8
- snippet: ref.excerptHash,
9
8
  };
10
9
  }
11
10
  /**