@haaaiawd/second-nature 0.2.4 → 0.2.5

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 (33) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/ops/ops-router.js +4 -0
  4. package/runtime/connectors/base/contract.d.ts +1 -0
  5. package/runtime/connectors/base/failure-taxonomy.js +45 -26
  6. package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
  7. package/runtime/connectors/services/connector-cooldown-port.js +123 -0
  8. package/runtime/connectors/services/connector-executor-adapter.js +10 -4
  9. package/runtime/connectors/services/credential-route-context.d.ts +3 -2
  10. package/runtime/connectors/services/credential-route-context.js +19 -3
  11. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  12. package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
  13. package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
  14. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
  15. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +76 -0
  16. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +2 -0
  17. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +1 -0
  18. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +1 -1
  19. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +10 -5
  20. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
  21. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +10 -28
  22. package/runtime/observability/living-loop-health-gate.d.ts +6 -2
  23. package/runtime/observability/living-loop-health-gate.js +52 -5
  24. package/runtime/observability/loop-status.d.ts +19 -0
  25. package/runtime/observability/loop-status.js +121 -7
  26. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +9 -0
  27. package/runtime/observability/services/heartbeat-digest-assembler.js +44 -9
  28. package/runtime/shared/types/v8-contracts.d.ts +1 -1
  29. package/runtime/storage/db/index.js +28 -8
  30. package/runtime/storage/db/schema/v8-entities.d.ts +288 -0
  31. package/runtime/storage/db/schema/v8-entities.js +23 -1
  32. package/runtime/storage/v8-state-stores.d.ts +10 -1
  33. package/runtime/storage/v8-state-stores.js +86 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.2.4",
4
+ "version": "0.2.5",
5
5
  "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
6
6
  "activation": {
7
7
  "onStartup": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -1067,6 +1067,8 @@ export function createOpsRouter(deps) {
1067
1067
  hasRealClosure: realRunResult.gate.hasRealClosure,
1068
1068
  hasQuietArtifact: realRunResult.gate.hasQuietArtifact,
1069
1069
  hasDreamArtifact: realRunResult.gate.hasDreamArtifact,
1070
+ hasFreshImpulseContext: realRunResult.gate.hasFreshImpulseContext,
1071
+ hasProjectionFeedback: realRunResult.gate.hasProjectionFeedback,
1070
1072
  missingStage: realRunResult.gate.missingStage,
1071
1073
  missingReason: realRunResult.gate.missingReason,
1072
1074
  };
@@ -1079,6 +1081,8 @@ export function createOpsRouter(deps) {
1079
1081
  hasRealClosure: false,
1080
1082
  hasQuietArtifact: false,
1081
1083
  hasDreamArtifact: false,
1084
+ hasFreshImpulseContext: false,
1085
+ hasProjectionFeedback: false,
1082
1086
  missingReason: "Real-run health check degraded: " + realRunResult.degraded.reason,
1083
1087
  };
1084
1088
  }
@@ -73,6 +73,7 @@ export interface CooldownLedgerPort {
73
73
  loadCooldownState(platformId: string, intent: CapabilityIntent): Promise<{
74
74
  blocked: boolean;
75
75
  retryAfterMs?: number;
76
+ reason?: string;
76
77
  }>;
77
78
  }
78
79
  export interface RouteContextPort extends CredentialContextPort, CooldownLedgerPort {
@@ -61,6 +61,23 @@ function readRetryAfterMs(input) {
61
61
  }
62
62
  return undefined;
63
63
  }
64
+ function readStatusCode(record) {
65
+ if (typeof record.status === "number")
66
+ return record.status;
67
+ if (typeof record.statusCode === "number")
68
+ return record.statusCode;
69
+ if (typeof record.status === "string") {
70
+ const parsed = Number.parseInt(record.status, 10);
71
+ if (Number.isFinite(parsed))
72
+ return parsed;
73
+ }
74
+ if (typeof record.statusCode === "string") {
75
+ const parsed = Number.parseInt(record.statusCode, 10);
76
+ if (Number.isFinite(parsed))
77
+ return parsed;
78
+ }
79
+ return undefined;
80
+ }
64
81
  export function classifyFailure(error) {
65
82
  if (error instanceof ConnectorPolicyError) {
66
83
  return {
@@ -74,6 +91,34 @@ export function classifyFailure(error) {
74
91
  }
75
92
  if (error && typeof error === "object") {
76
93
  const record = error;
94
+ const status = readStatusCode(record);
95
+ if (status !== undefined) {
96
+ if (status === 429) {
97
+ return {
98
+ class: "rate_limited",
99
+ retryable: RETRYABLE_BY_CLASS.rate_limited,
100
+ retryAfterMs: readRetryAfterMs(record),
101
+ };
102
+ }
103
+ if (status === 401 || status === 403) {
104
+ return {
105
+ class: "auth_failure",
106
+ retryable: RETRYABLE_BY_CLASS.auth_failure,
107
+ };
108
+ }
109
+ if (status === 400 || status === 404 || status === 422) {
110
+ return {
111
+ class: "permanent_input_error",
112
+ retryable: RETRYABLE_BY_CLASS.permanent_input_error,
113
+ };
114
+ }
115
+ if (status >= 500 && status <= 599) {
116
+ return {
117
+ class: "transport_failure",
118
+ retryable: RETRYABLE_BY_CLASS.transport_failure,
119
+ };
120
+ }
121
+ }
77
122
  const code = record.code;
78
123
  if (typeof code === "string") {
79
124
  if (code === "auth_failure")
@@ -152,32 +197,6 @@ export function classifyFailure(error) {
152
197
  retryable: RETRYABLE_BY_CLASS.unknown_platform_change,
153
198
  };
154
199
  }
155
- const status = record.status;
156
- if (status === 429) {
157
- return {
158
- class: "rate_limited",
159
- retryable: RETRYABLE_BY_CLASS.rate_limited,
160
- retryAfterMs: readRetryAfterMs(record),
161
- };
162
- }
163
- if (status === 401 || status === 403) {
164
- return {
165
- class: "auth_failure",
166
- retryable: RETRYABLE_BY_CLASS.auth_failure,
167
- };
168
- }
169
- if (status === 400 || status === 404 || status === 422) {
170
- return {
171
- class: "permanent_input_error",
172
- retryable: RETRYABLE_BY_CLASS.permanent_input_error,
173
- };
174
- }
175
- if (status === 500 || status === 502 || status === 503 || status === 504) {
176
- return {
177
- class: "transport_failure",
178
- retryable: RETRYABLE_BY_CLASS.transport_failure,
179
- };
180
- }
181
200
  }
182
201
  return {
183
202
  class: "unknown_platform_change",
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ConnectorCooldownPort — Durable cooldown ledger for repeated terminal failures.
3
+ *
4
+ * Core logic: Track terminal failures per platform/capability and block replay
5
+ * for a bounded window after repeated failures. Successful recovery is allowed
6
+ * to bypass stale cooldown.
7
+ *
8
+ * Design authority:
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §6`
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/body-tool-system.md §4`
11
+ *
12
+ * Dependencies:
13
+ * - `src/storage/v8-state-stores.js` (read/write connector cooldown state)
14
+ * - `src/connectors/base/failure-taxonomy.js` (FailureClass, retryable lookup)
15
+ *
16
+ * Boundary:
17
+ * - Does not execute connectors; only records/read cooldown state.
18
+ * - Does not permanently blacklist platforms; cooldown expires.
19
+ */
20
+ import type { StateDatabase } from "../../storage/db/index.js";
21
+ import type { CooldownPort } from "../base/policy-layer.js";
22
+ export declare function createConnectorCooldownPort(db: StateDatabase): CooldownPort;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * ConnectorCooldownPort — Durable cooldown ledger for repeated terminal failures.
3
+ *
4
+ * Core logic: Track terminal failures per platform/capability and block replay
5
+ * for a bounded window after repeated failures. Successful recovery is allowed
6
+ * to bypass stale cooldown.
7
+ *
8
+ * Design authority:
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md §6`
10
+ * - `.anws/v8/04_SYSTEM_DESIGN/body-tool-system.md §4`
11
+ *
12
+ * Dependencies:
13
+ * - `src/storage/v8-state-stores.js` (read/write connector cooldown state)
14
+ * - `src/connectors/base/failure-taxonomy.js` (FailureClass, retryable lookup)
15
+ *
16
+ * Boundary:
17
+ * - Does not execute connectors; only records/read cooldown state.
18
+ * - Does not permanently blacklist platforms; cooldown expires.
19
+ */
20
+ import { readConnectorCooldownState, writeConnectorCooldownState, } from "../../storage/v8-state-stores.js";
21
+ // ───────────────────────────────────────────────────────────────
22
+ // Config
23
+ // ───────────────────────────────────────────────────────────────
24
+ const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
25
+ const TERMINAL_FAILURE_THRESHOLD = 2;
26
+ const RETRYABLE_FAILURE_CLASSES = new Set([
27
+ "transport_failure",
28
+ "rate_limited",
29
+ "timeout",
30
+ "concurrency_conflict",
31
+ ]);
32
+ // ───────────────────────────────────────────────────────────────
33
+ // Helpers
34
+ // ───────────────────────────────────────────────────────────────
35
+ function makeCooldownId(platformId, capabilityId) {
36
+ return `cooldown_${platformId}_${capabilityId}`;
37
+ }
38
+ function addMs(iso, ms) {
39
+ return new Date(new Date(iso).getTime() + ms).toISOString();
40
+ }
41
+ function isAfter(a, b) {
42
+ return new Date(a).getTime() > new Date(b).getTime();
43
+ }
44
+ // ───────────────────────────────────────────────────────────────
45
+ // Public API
46
+ // ───────────────────────────────────────────────────────────────
47
+ export function createConnectorCooldownPort(db) {
48
+ return {
49
+ async isBlocked(platformId, intent) {
50
+ const read = await readConnectorCooldownState(db, platformId, intent);
51
+ if (read.degraded) {
52
+ // Fail-closed: if we cannot read cooldown state, prevent replay to avoid hammering
53
+ return {
54
+ blocked: true,
55
+ reason: "cooldown_state_unreadable",
56
+ };
57
+ }
58
+ if (!read.row) {
59
+ return { blocked: false };
60
+ }
61
+ const now = new Date().toISOString();
62
+ const blocked = isAfter(read.row.blockedUntil, now);
63
+ return {
64
+ blocked,
65
+ retryAfterMs: blocked
66
+ ? Math.max(0, new Date(read.row.blockedUntil).getTime() - new Date(now).getTime())
67
+ : undefined,
68
+ };
69
+ },
70
+ async markFailure(platformId, intent, failureClass, retryAfterMs) {
71
+ const id = makeCooldownId(platformId, intent);
72
+ const now = new Date().toISOString();
73
+ const existing = await readConnectorCooldownState(db, platformId, intent);
74
+ const isRetryable = RETRYABLE_FAILURE_CLASSES.has(failureClass);
75
+ let failureCount = 1;
76
+ let terminalCount = isRetryable ? 0 : 1;
77
+ let blockedUntil = now;
78
+ if (!existing.degraded && existing.row) {
79
+ failureCount = existing.row.failureCount + 1;
80
+ terminalCount = (existing.row.terminalCount ?? 0) + (isRetryable ? 0 : 1);
81
+ // Extend blocked window if already blocked
82
+ if (isAfter(existing.row.blockedUntil, now)) {
83
+ blockedUntil = existing.row.blockedUntil;
84
+ }
85
+ }
86
+ if (retryAfterMs && retryAfterMs > 0) {
87
+ // Rate-limit or explicit retry-after takes precedence
88
+ blockedUntil = addMs(now, retryAfterMs);
89
+ }
90
+ else if (!isRetryable && terminalCount >= TERMINAL_FAILURE_THRESHOLD) {
91
+ // Repeated terminal failures enter bounded cooldown
92
+ blockedUntil = addMs(now, DEFAULT_COOLDOWN_MS);
93
+ }
94
+ else if (isRetryable) {
95
+ // Retryable failures do not accumulate terminal cooldown
96
+ blockedUntil = now;
97
+ }
98
+ await writeConnectorCooldownState(db, {
99
+ id,
100
+ platformId,
101
+ capabilityId: intent,
102
+ failureClass,
103
+ retryAfterMs: retryAfterMs ?? null,
104
+ blockedUntil,
105
+ failureCount,
106
+ terminalCount,
107
+ sourceRefs: [
108
+ {
109
+ uri: `sn://cooldown/${platformId}/${intent}`,
110
+ family: "audit",
111
+ id,
112
+ redactionClass: "none",
113
+ resolveStatus: "resolvable",
114
+ },
115
+ ],
116
+ payloadJson: JSON.stringify({ markedAt: now, failureCount, terminalCount, isRetryable }),
117
+ createdAt: existing.row?.createdAt ?? now,
118
+ updatedAt: now,
119
+ redactionClass: "none",
120
+ });
121
+ },
122
+ };
123
+ }
@@ -19,6 +19,7 @@ import { parseConnectorManifestV6 } from "../manifest/manifest-parser.js";
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
21
  import { pathToFileURL } from "node:url";
22
+ import { createConnectorCooldownPort } from "./connector-cooldown-port.js";
22
23
  const DEFAULT_AGENT_WORLD_USERNAME = "nyx_ha";
23
24
  const DEFAULT_AGENT_WORLD_PROFILE_PATH_TEMPLATE = "/api/agents/profile/{username}";
24
25
  function readString(value) {
@@ -104,7 +105,7 @@ async function fetchAgentWorldJson(input) {
104
105
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
105
106
  });
106
107
  if (!resp.ok) {
107
- throw { code: "api_error", detail: `agent-world ${input.label}: ${resp.status}` };
108
+ throw { code: "api_error", status: resp.status, detail: `agent-world ${input.label}: ${resp.status}` };
108
109
  }
109
110
  return resp.json();
110
111
  }
@@ -147,7 +148,7 @@ async function fetchEvoMapJson(input) {
147
148
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
148
149
  });
149
150
  if (!resp.ok) {
150
- throw { code: "api_error", detail: `evomap ${input.label}: ${resp.status}` };
151
+ throw { code: "api_error", status: resp.status, detail: `evomap ${input.label}: ${resp.status}` };
151
152
  }
152
153
  return resp.json();
153
154
  }
@@ -256,6 +257,8 @@ function createDeclarativeHttpRunner(manifest, credential) {
256
257
  body: method !== "GET" && request.payload ? JSON.stringify(request.payload) : undefined,
257
258
  });
258
259
  if (!resp.ok) {
260
+ const status = resp.status;
261
+ const body = await resp.text().catch(() => "");
259
262
  return {
260
263
  platformId: request.platformId,
261
264
  channel: plan.channel,
@@ -263,7 +266,8 @@ function createDeclarativeHttpRunner(manifest, credential) {
263
266
  success: false,
264
267
  error: {
265
268
  code: "api_error",
266
- detail: `HTTP ${resp.status}: ${await resp.text().catch(() => "")}`,
269
+ status,
270
+ detail: `HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`,
267
271
  },
268
272
  };
269
273
  }
@@ -618,7 +622,8 @@ export function createConnectorExecutorAdapter(options) {
618
622
  registry.register({ ...agentWorldManifest });
619
623
  registry.register({ ...instreetManifest });
620
624
  registerWorkspaceManifests(registry, options.workspaceRoot);
621
- const routeContextPort = createCredentialRouteContextPort(vault);
625
+ const cooldownPort = createConnectorCooldownPort(options.stateDb);
626
+ const routeContextPort = createCredentialRouteContextPort(vault, options.stateDb);
622
627
  const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
623
628
  const telemetry = new ExecutionTelemetry(options.observabilityDb);
624
629
  const executionRunner = createAdaptiveExecutionRunner(vault, options.workspaceRoot);
@@ -626,6 +631,7 @@ export function createConnectorExecutorAdapter(options) {
626
631
  routePlanner,
627
632
  executionRunner,
628
633
  telemetry,
634
+ cooldownPort,
629
635
  effectCommitLedger: new InMemoryEffectCommitLedger(),
630
636
  retryPolicy: { maxRetries: 2, jitter: true },
631
637
  });
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Loads decrypted credentials from state DB and maps them to the
5
5
  * CredentialContext shape expected by ConnectorRoutePlanner.
6
- * Cooldown is stubbed (always unblocked) until a cooldown ledger is modeled.
6
+ * Cooldown state is loaded from connector_cooldown_state table.
7
7
  */
8
8
  import type { RouteContextPort } from "../base/contract.js";
9
9
  import type { CredentialVault } from "../../storage/services/credential-vault.js";
10
- export declare function createCredentialRouteContextPort(vault: CredentialVault): RouteContextPort;
10
+ import type { StateDatabase } from "../../storage/db/index.js";
11
+ export declare function createCredentialRouteContextPort(vault: CredentialVault, db: StateDatabase): RouteContextPort;
@@ -1,4 +1,5 @@
1
- export function createCredentialRouteContextPort(vault) {
1
+ import { readConnectorCooldownState } from "../../storage/v8-state-stores.js";
2
+ export function createCredentialRouteContextPort(vault, db) {
2
3
  return {
3
4
  async loadCredentialState(platformId) {
4
5
  const ctx = await vault.loadCredentialContext(platformId);
@@ -12,8 +13,23 @@ export function createCredentialRouteContextPort(vault) {
12
13
  }
13
14
  return ctx;
14
15
  },
15
- async loadCooldownState() {
16
- return { blocked: false };
16
+ async loadCooldownState(platformId, intent) {
17
+ const read = await readConnectorCooldownState(db, platformId, intent);
18
+ if (read.degraded) {
19
+ // Fail-closed on unreadable cooldown state
20
+ return { blocked: true, reason: "cooldown_state_unreadable" };
21
+ }
22
+ if (!read.row) {
23
+ return { blocked: false };
24
+ }
25
+ const now = new Date().toISOString();
26
+ const blocked = new Date(read.row.blockedUntil).getTime() > new Date(now).getTime();
27
+ return {
28
+ blocked,
29
+ retryAfterMs: blocked
30
+ ? Math.max(0, new Date(read.row.blockedUntil).getTime() - new Date(now).getTime())
31
+ : undefined,
32
+ };
17
33
  },
18
34
  };
19
35
  }
@@ -56,6 +56,8 @@ export declare function recordRememberClosure(db: StateDatabase, cycleId: string
56
56
  export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: string, closureStatus: ClosureStatus, reason: V8ReasonCode, params: {
57
57
  proposalId?: string;
58
58
  decisionId?: string;
59
+ platformId?: string;
60
+ capabilityId?: string;
59
61
  downgradedActionKind?: string;
60
62
  postProcessing?: string[];
61
63
  nextState?: string;
@@ -63,6 +65,8 @@ export declare function recordPolicyOutcomeClosure(db: StateDatabase, cycleId: s
63
65
  export declare function recordExecutionClosure(db: StateDatabase, cycleId: string, closureStatus: "completed" | "failed", reason: V8ReasonCode, params: {
64
66
  proposalId: string;
65
67
  decisionId: string;
68
+ platformId?: string;
69
+ capabilityId?: string;
66
70
  executionResultRef?: string;
67
71
  outputSummary?: string;
68
72
  nextState?: string;
@@ -46,6 +46,7 @@ export async function recordNoActionClosure(db, cycleId, noActionReason, options
46
46
  id: closureId,
47
47
  createdAt: now,
48
48
  cycleId,
49
+ platformId: "heartbeat",
49
50
  status: "no_action",
50
51
  reason: noActionReason,
51
52
  nextState: "await_next_cycle",
@@ -122,6 +123,8 @@ export async function recordPolicyOutcomeClosure(db, cycleId, closureStatus, rea
122
123
  id: closureId,
123
124
  createdAt: now,
124
125
  cycleId,
126
+ platformId: params.platformId ?? "heartbeat",
127
+ capabilityId: params.capabilityId,
125
128
  proposalId: params.proposalId,
126
129
  decisionId: params.decisionId,
127
130
  status: closureStatus,
@@ -161,6 +164,8 @@ export async function recordExecutionClosure(db, cycleId, closureStatus, reason,
161
164
  id: closureId,
162
165
  createdAt: now,
163
166
  cycleId,
167
+ platformId: params.platformId ?? "heartbeat",
168
+ capabilityId: params.capabilityId,
164
169
  proposalId: params.proposalId,
165
170
  decisionId: params.decisionId,
166
171
  status: closureStatus,
@@ -136,6 +136,7 @@ export async function buildActionProposal(db, judgmentVerdictId, options) {
136
136
  id: closureId,
137
137
  createdAt: now,
138
138
  cycleId,
139
+ platformId: "heartbeat",
139
140
  status: "completed",
140
141
  reason: "remember_for_review",
141
142
  nextState: "pending_daily_review",
@@ -22,6 +22,7 @@
22
22
  * Test coverage: tests/unit/control-plane/heartbeat-cycle-trace.test.ts
23
23
  */
24
24
  import type { StateDatabase } from "../../../storage/db/index.js";
25
+ import { type DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
25
26
  import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
26
27
  export interface HeartbeatOrchestrationRequest {
27
28
  workspaceRoot: string;
@@ -34,5 +35,6 @@ export interface HeartbeatOrchestrationResult {
34
35
  closureRef?: SourceRef;
35
36
  noActionReason?: V8ReasonCode;
36
37
  degraded?: DegradedOperationResult;
38
+ rhythmState?: DailyRhythmState;
37
39
  }
38
40
  export declare function runHeartbeatCycle(db: StateDatabase, request: HeartbeatOrchestrationRequest): Promise<HeartbeatOrchestrationResult | DegradedOperationResult>;
@@ -30,6 +30,7 @@ import { buildActionProposal, } from "../action/action-proposal-builder.js";
30
30
  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
+ import { checkDailyRhythm } from "../quiet-dream/daily-rhythm-scheduler.js";
33
34
  // ───────────────────────────────────────────────────────────────
34
35
  // Helpers
35
36
  // ───────────────────────────────────────────────────────────────
@@ -43,6 +44,66 @@ async function nextCycleSequence(db) {
43
44
  function buildCycleId(sequence, now) {
44
45
  return `cyc_${now.replace(/[:.]/g, "")}_${sequence}`;
45
46
  }
47
+ async function advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now) {
48
+ try {
49
+ const rhythmResult = await checkDailyRhythm(db, { now });
50
+ if ("status" in rhythmResult && rhythmResult.status === "checked") {
51
+ await recordLoopStageEvent(db, {
52
+ id: `evt_${cycleId}_daily_rhythm`,
53
+ cycleId,
54
+ cycleSequence,
55
+ stage: "quiet",
56
+ status: "completed",
57
+ occurredAt: new Date().toISOString(),
58
+ sourceRefs: [
59
+ cycleRef,
60
+ {
61
+ uri: `sn://rhythm/${rhythmResult.state.day}`,
62
+ family: "dream_run",
63
+ id: `rhythm_${rhythmResult.state.day}`,
64
+ redactionClass: "none",
65
+ resolveStatus: "resolvable",
66
+ },
67
+ ],
68
+ });
69
+ return { rhythmState: rhythmResult.state };
70
+ }
71
+ const degraded = rhythmResult;
72
+ await recordLoopStageEvent(db, {
73
+ id: `evt_${cycleId}_daily_rhythm`,
74
+ cycleId,
75
+ cycleSequence,
76
+ stage: "quiet",
77
+ status: "failed",
78
+ occurredAt: new Date().toISOString(),
79
+ reason: degraded.reason,
80
+ sourceRefs: [cycleRef],
81
+ });
82
+ return { rhythmDegraded: degraded };
83
+ }
84
+ catch (rhythmErr) {
85
+ const errMsg = rhythmErr instanceof Error ? rhythmErr.message : String(rhythmErr);
86
+ const degraded = {
87
+ status: "degraded",
88
+ reason: "state_unreadable",
89
+ ownerStage: "quiet",
90
+ sourceRefs: [cycleRef],
91
+ operatorNextAction: `Daily rhythm check failed: ${errMsg.slice(0, 120)}`,
92
+ retryable: true,
93
+ };
94
+ await recordLoopStageEvent(db, {
95
+ id: `evt_${cycleId}_daily_rhythm`,
96
+ cycleId,
97
+ cycleSequence,
98
+ stage: "quiet",
99
+ status: "failed",
100
+ occurredAt: new Date().toISOString(),
101
+ reason: degraded.reason,
102
+ sourceRefs: [cycleRef],
103
+ });
104
+ return { rhythmDegraded: degraded };
105
+ }
106
+ }
46
107
  // ───────────────────────────────────────────────────────────────
47
108
  // Public API
48
109
  // ───────────────────────────────────────────────────────────────
@@ -130,6 +191,7 @@ export async function runHeartbeatCycle(db, request) {
130
191
  reason: degradedReason,
131
192
  sourceRefs: degradedClosureRef ? [degradedClosureRef, cycleRef] : [cycleRef],
132
193
  });
194
+ const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
133
195
  return {
134
196
  cycleId,
135
197
  cycleSequence,
@@ -145,6 +207,7 @@ export async function runHeartbeatCycle(db, request) {
145
207
  retryable: true,
146
208
  }
147
209
  : undefined,
210
+ rhythmState,
148
211
  };
149
212
  }
150
213
  const cards = perceptionResult.cards;
@@ -182,11 +245,13 @@ export async function runHeartbeatCycle(db, request) {
182
245
  reason: "evidence_batch_empty",
183
246
  sourceRefs: emptyClosureRef ? [emptyClosureRef, cycleRef] : [cycleRef],
184
247
  });
248
+ const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
185
249
  return {
186
250
  cycleId,
187
251
  cycleSequence,
188
252
  closureRef: emptyClosureRef,
189
253
  noActionReason: "evidence_batch_empty",
254
+ rhythmState,
190
255
  };
191
256
  }
192
257
  // ── Context assembly: load accepted projections (T-DQ.R.3) ──
@@ -350,6 +415,8 @@ export async function runHeartbeatCycle(db, request) {
350
415
  const closureResult = await recordPolicyOutcomeClosure(db, cycleId, closureStatus, decision.decisionReason, {
351
416
  proposalId: proposal.id,
352
417
  decisionId: decision.id,
418
+ platformId: proposal.targetPlatformId,
419
+ capabilityId: proposal.targetCapabilityId,
353
420
  nextState: "await_next_cycle",
354
421
  }, { now });
355
422
  if ("closureId" in closureResult) {
@@ -369,6 +436,8 @@ export async function runHeartbeatCycle(db, request) {
369
436
  const closureResult = await recordPolicyOutcomeClosure(db, cycleId, "downgraded", "guidance_unavailable", {
370
437
  proposalId: proposal.id,
371
438
  decisionId: decision.id,
439
+ platformId: proposal.targetPlatformId,
440
+ capabilityId: proposal.targetCapabilityId,
372
441
  downgradedActionKind: dispatchResult.downgradedActionKind,
373
442
  nextState: "await_guidance_recovery",
374
443
  }, { now });
@@ -390,6 +459,8 @@ export async function runHeartbeatCycle(db, request) {
390
459
  const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
391
460
  proposalId: proposal.id,
392
461
  decisionId: decision.id,
462
+ platformId: proposal.targetPlatformId,
463
+ capabilityId: proposal.targetCapabilityId,
393
464
  outputSummary: "Guidance draft dispatched (simulated)",
394
465
  nextState: "await_next_cycle",
395
466
  }, { now });
@@ -411,6 +482,8 @@ export async function runHeartbeatCycle(db, request) {
411
482
  const closureResult = await recordExecutionClosure(db, cycleId, "completed", "policy_allowed", {
412
483
  proposalId: proposal.id,
413
484
  decisionId: decision.id,
485
+ platformId: proposal.targetPlatformId,
486
+ capabilityId: proposal.targetCapabilityId,
414
487
  outputSummary: "Connector dispatch prepared (simulated — T-CP.R.2)",
415
488
  nextState: "await_real_execution",
416
489
  }, { now });
@@ -466,11 +539,14 @@ export async function runHeartbeatCycle(db, request) {
466
539
  }
467
540
  noActionReason = "proposal_no_action";
468
541
  }
542
+ // T-CP.R.3: Advance daily rhythm after closure/no-action
543
+ const { rhythmState } = await advanceAndRecordDailyRhythm(db, cycleId, cycleSequence, cycleRef, now);
469
544
  return {
470
545
  cycleId,
471
546
  cycleSequence,
472
547
  closureRef,
473
548
  noActionReason,
474
549
  degraded: closureDegraded,
550
+ rhythmState,
475
551
  };
476
552
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import type { StateDatabase } from "../../../storage/db/index.js";
19
19
  import type { SourceRef, DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
20
+ import type { DailyRhythmState } from "../quiet-dream/daily-rhythm-scheduler.js";
20
21
  export interface RealRuntimeSpineOptions {
21
22
  workspaceRoot: string;
22
23
  state: StateDatabase;
@@ -29,5 +30,6 @@ export interface RealRuntimeSpineResult {
29
30
  closureRef?: SourceRef;
30
31
  noActionReason?: V8ReasonCode;
31
32
  degraded?: DegradedOperationResult;
33
+ rhythmState?: DailyRhythmState;
32
34
  }
33
35
  export declare function runRealRuntimeHeartbeatCycle(options: RealRuntimeSpineOptions): Promise<RealRuntimeSpineResult | DegradedOperationResult>;
@@ -37,5 +37,6 @@ export async function runRealRuntimeHeartbeatCycle(options) {
37
37
  closureRef: orchestrationResult.closureRef,
38
38
  noActionReason: orchestrationResult.noActionReason,
39
39
  degraded: orchestrationResult.degraded,
40
+ rhythmState: orchestrationResult.rhythmState,
40
41
  };
41
42
  }
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import type { StateDatabase } from "../../../storage/db/index.js";
23
23
  import type { DegradedOperationResult, V8ReasonCode } from "../../../shared/types/v8-contracts.js";
24
- export type RhythmStatus = "due" | "completed" | "skipped" | "blocked" | "not_due";
24
+ export type RhythmStatus = "due" | "completed" | "scheduled" | "skipped" | "blocked" | "not_due";
25
25
  export interface DailyRhythmState {
26
26
  day: string;
27
27
  quietStatus: RhythmStatus;