@haaaiawd/second-nature 0.2.2 → 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 (53) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/ops/heartbeat-surface.d.ts +20 -0
  4. package/runtime/cli/ops/heartbeat-surface.js +72 -1
  5. package/runtime/cli/ops/ops-router.js +119 -31
  6. package/runtime/connectors/base/contract.d.ts +11 -0
  7. package/runtime/connectors/base/failure-taxonomy.js +45 -26
  8. package/runtime/connectors/base/policy-bound-write-dispatch.d.ts +29 -0
  9. package/runtime/connectors/base/policy-bound-write-dispatch.js +127 -0
  10. package/runtime/connectors/services/connector-cooldown-port.d.ts +22 -0
  11. package/runtime/connectors/services/connector-cooldown-port.js +123 -0
  12. package/runtime/connectors/services/connector-executor-adapter.js +10 -4
  13. package/runtime/connectors/services/credential-route-context.d.ts +3 -2
  14. package/runtime/connectors/services/credential-route-context.js +19 -3
  15. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +4 -0
  16. package/runtime/core/second-nature/action/action-closure-recorder.js +5 -0
  17. package/runtime/core/second-nature/action/action-proposal-builder.js +1 -0
  18. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +2 -0
  19. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +412 -25
  20. package/runtime/core/second-nature/control-plane/real-runtime-spine.d.ts +35 -0
  21. package/runtime/core/second-nature/control-plane/real-runtime-spine.js +42 -0
  22. package/runtime/core/second-nature/guidance/impulse-context-reader.d.ts +44 -0
  23. package/runtime/core/second-nature/guidance/impulse-context-reader.js +84 -0
  24. package/runtime/core/second-nature/guidance/impulse-context-writer.d.ts +39 -0
  25. package/runtime/core/second-nature/guidance/impulse-context-writer.js +70 -0
  26. package/runtime/core/second-nature/perception/judgment-engine.d.ts +2 -0
  27. package/runtime/core/second-nature/perception/judgment-engine.js +11 -1
  28. package/runtime/core/second-nature/perception/perception-builder.d.ts +6 -2
  29. package/runtime/core/second-nature/perception/perception-builder.js +18 -7
  30. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +43 -0
  31. package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +162 -0
  32. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +2 -2
  33. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +27 -44
  34. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +3 -0
  35. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +4 -0
  36. package/runtime/observability/living-loop-health-gate.d.ts +49 -0
  37. package/runtime/observability/living-loop-health-gate.js +141 -0
  38. package/runtime/observability/loop-status.d.ts +30 -0
  39. package/runtime/observability/loop-status.js +167 -7
  40. package/runtime/observability/services/heartbeat-digest-assembler.d.ts +21 -0
  41. package/runtime/observability/services/heartbeat-digest-assembler.js +44 -0
  42. package/runtime/shared/types/v8-contracts.d.ts +2 -2
  43. package/runtime/storage/db/index.js +60 -6
  44. package/runtime/storage/db/migrations/index.js +4 -0
  45. package/runtime/storage/db/migrations/v8-001-living-perception-loop.js +119 -119
  46. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.d.ts +12 -0
  47. package/runtime/storage/db/migrations/v8-002-perception-contract-alignment.js +14 -0
  48. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.d.ts +10 -0
  49. package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +12 -0
  50. package/runtime/storage/db/schema/v8-entities.d.ts +874 -0
  51. package/runtime/storage/db/schema/v8-entities.js +62 -1
  52. package/runtime/storage/v8-state-stores.d.ts +41 -2
  53. package/runtime/storage/v8-state-stores.js +206 -2
@@ -12,7 +12,7 @@
12
12
  * Dependencies: drizzle-orm/sqlite-core
13
13
  * Boundary: Schema definitions only; no runtime logic.
14
14
  */
15
- import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
15
+ import { sqliteTable, text, integer, real, index } from "drizzle-orm/sqlite-core";
16
16
  // ───────────────────────────────────────────────────────────────
17
17
  // 1. EvidenceItem
18
18
  // ───────────────────────────────────────────────────────────────
@@ -39,6 +39,7 @@ export const perceptionCard = sqliteTable("perception_card", {
39
39
  entitiesJson: text("entities_json"),
40
40
  novelty: text("novelty"),
41
41
  relevance: real("relevance"),
42
+ relevanceClass: text("relevance_class"),
42
43
  summary: text("summary"),
43
44
  riskFlagsJson: text("risk_flags_json"),
44
45
  confidence: real("confidence"),
@@ -72,6 +73,8 @@ export const actionClosureRecord = sqliteTable("action_closure_record", {
72
73
  id: text("id").primaryKey(),
73
74
  createdAt: text("created_at").notNull(),
74
75
  cycleId: text("cycle_id").notNull(),
76
+ platformId: text("platform_id"),
77
+ capabilityId: text("capability_id"),
75
78
  proposalId: text("proposal_id"),
76
79
  decisionId: text("decision_id"),
77
80
  status: text("status").notNull(),
@@ -92,6 +95,7 @@ export const quietDailyReview = sqliteTable("quiet_daily_review", {
92
95
  closureCount: integer("closure_count").notNull().default(0),
93
96
  memoryCandidateCount: integer("memory_candidate_count").notNull().default(0),
94
97
  sourceRefsJson: text("source_refs_json").notNull(),
98
+ closureRefsJson: text("closure_refs_json"),
95
99
  redactionClass: text("redaction_class").notNull().default("none"),
96
100
  payloadJson: text("payload_json"),
97
101
  lifecycleStatus: text("lifecycle_status").notNull().default("pending"),
@@ -158,3 +162,60 @@ export const loopStageEvent = sqliteTable("loop_stage_event", {
158
162
  payloadJson: text("payload_json"),
159
163
  lifecycleStatus: text("lifecycle_status").notNull().default("started"),
160
164
  });
165
+ // ───────────────────────────────────────────────────────────────
166
+ // 10. ImpulseContextArtifact
167
+ // ───────────────────────────────────────────────────────────────
168
+ export const impulseContextArtifact = sqliteTable("impulse_context_artifact", {
169
+ id: text("id").primaryKey(),
170
+ createdAt: text("created_at").notNull(),
171
+ updatedAt: text("updated_at").notNull(),
172
+ sceneType: text("scene_type").notNull(),
173
+ capabilityIntent: text("capability_intent"),
174
+ platformId: text("platform_id"),
175
+ capabilityClass: text("capability_class"),
176
+ impulseSource: text("impulse_source").notNull(),
177
+ impulseText: text("impulse_text"),
178
+ atmosphereText: text("atmosphere_text"),
179
+ expressionBoundaryConstraintsJson: text("expression_boundary_constraints_json"),
180
+ expressionBoundaryStyle: text("expression_boundary_style"),
181
+ freshnessVersion: integer("freshness_version").notNull().default(1),
182
+ sourceRefsJson: text("source_refs_json").notNull(),
183
+ redactionClass: text("redaction_class").notNull().default("none"),
184
+ payloadJson: text("payload_json"),
185
+ lifecycleStatus: text("lifecycle_status").notNull().default("active"),
186
+ });
187
+ // 11. DailyRhythmState
188
+ // ───────────────────────────────────────────────────────────────
189
+ export const dailyRhythmState = sqliteTable("daily_rhythm_state", {
190
+ id: text("id").primaryKey(),
191
+ day: text("day").notNull(),
192
+ quietStatus: text("quiet_status").notNull().default("not_due"),
193
+ dreamStatus: text("dream_status").notNull().default("not_due"),
194
+ quietReason: text("quiet_reason"),
195
+ dreamReason: text("dream_reason"),
196
+ quietCompletedAt: text("quiet_completed_at"),
197
+ dreamCompletedAt: text("dream_completed_at"),
198
+ sourceRefsJson: text("source_refs_json").notNull(),
199
+ payloadJson: text("payload_json"),
200
+ updatedAt: text("updated_at").notNull(),
201
+ });
202
+ // ───────────────────────────────────────────────────────────────
203
+ // 12. ConnectorCooldownState
204
+ // ───────────────────────────────────────────────────────────────
205
+ export const connectorCooldownState = sqliteTable("connector_cooldown_state", {
206
+ id: text("id").primaryKey(),
207
+ platformId: text("platform_id").notNull(),
208
+ capabilityId: text("capability_id").notNull(),
209
+ failureClass: text("failure_class").notNull(),
210
+ retryAfterMs: integer("retry_after_ms"),
211
+ blockedUntil: text("blocked_until").notNull(),
212
+ failureCount: integer("failure_count").notNull().default(1),
213
+ terminalCount: integer("terminal_count").notNull().default(0),
214
+ sourceRefsJson: text("source_refs_json").notNull(),
215
+ redactionClass: text("redaction_class").notNull().default("none"),
216
+ payloadJson: text("payload_json"),
217
+ createdAt: text("created_at").notNull(),
218
+ updatedAt: text("updated_at").notNull(),
219
+ }, (table) => ({
220
+ platformCapabilityIdx: index("connector_cooldown_state_platform_capability_idx").on(table.platformId, table.capabilityId),
221
+ }));
@@ -22,7 +22,7 @@
22
22
  * Test coverage: tests/unit/storage/v8-state-stores.test.ts
23
23
  */
24
24
  import type { StateDatabase } from "./db/index.js";
25
- import { type EvidenceItemRecord, type NewEvidenceItemRecord, type PerceptionCardRecord, type NewPerceptionCardRecord, type JudgmentVerdictRecord, type NewJudgmentVerdictRecord, type ActionClosureRecordSelect, type ActionClosureRecordInsert, type QuietDailyReviewRecord, type NewQuietDailyReviewRecord, type DreamConsolidationRunRecord, type NewDreamConsolidationRunRecord, type LongTermMemoryProjectionRecord, type NewLongTermMemoryProjectionRecord, type HeartbeatCycleTraceRecord, type NewHeartbeatCycleTraceRecord, type LoopStageEventRecord, type NewLoopStageEventRecord } from "./db/schema/v8-entities.js";
25
+ import { type EvidenceItemRecord, type NewEvidenceItemRecord, type PerceptionCardRecord, type NewPerceptionCardRecord, type JudgmentVerdictRecord, type NewJudgmentVerdictRecord, type ActionClosureRecordSelect, type ActionClosureRecordInsert, type QuietDailyReviewRecord, type NewQuietDailyReviewRecord, type DreamConsolidationRunRecord, type NewDreamConsolidationRunRecord, type LongTermMemoryProjectionRecord, type NewLongTermMemoryProjectionRecord, type HeartbeatCycleTraceRecord, type NewHeartbeatCycleTraceRecord, type LoopStageEventRecord, type NewLoopStageEventRecord, type ImpulseContextArtifactRecord, type NewImpulseContextArtifactRecord, type DailyRhythmStateRecord, type NewDailyRhythmStateRecord, type ConnectorCooldownStateRecord, type NewConnectorCooldownStateRecord } from "./db/schema/v8-entities.js";
26
26
  import type { SourceRef, DegradedOperationResult } from "../shared/types/v8-contracts.js";
27
27
  export interface WriteValidationError {
28
28
  ok: false;
@@ -81,8 +81,9 @@ export declare function readActionClosuresByDay(db: StateDatabase, day: string):
81
81
  rows: ActionClosureRecordSelect[];
82
82
  degraded?: DegradedOperationResult;
83
83
  }>;
84
- export declare function writeQuietDailyReview(db: StateDatabase, row: Omit<NewQuietDailyReviewRecord, "sourceRefsJson"> & {
84
+ export declare function writeQuietDailyReview(db: StateDatabase, row: Omit<NewQuietDailyReviewRecord, "sourceRefsJson" | "closureRefsJson"> & {
85
85
  sourceRefs: SourceRef[];
86
+ closureRefs?: SourceRef[];
86
87
  }): Promise<{
87
88
  id: string;
88
89
  } | DegradedOperationResult>;
@@ -112,6 +113,13 @@ export declare function writeLongTermMemoryProjection(db: StateDatabase, row: Om
112
113
  }): Promise<{
113
114
  id: string;
114
115
  } | DegradedOperationResult>;
116
+ /**
117
+ * Update an existing projection's status — required for supersession lifecycle.
118
+ * Uses UPDATE instead of INSERT to avoid primary-key conflict.
119
+ */
120
+ export declare function updateLongTermMemoryProjectionStatus(db: StateDatabase, id: string, status: LongTermMemoryProjectionRecord["status"], payloadJson?: string): Promise<{
121
+ id: string;
122
+ } | DegradedOperationResult>;
115
123
  export declare function readMemoryProjectionsByStatus(db: StateDatabase, status: LongTermMemoryProjectionRecord["status"]): Promise<{
116
124
  rows: LongTermMemoryProjectionRecord[];
117
125
  degraded?: DegradedOperationResult;
@@ -120,6 +128,10 @@ export declare function readMemoryProjectionsByTopic(db: StateDatabase, topicKey
120
128
  rows: LongTermMemoryProjectionRecord[];
121
129
  degraded?: DegradedOperationResult;
122
130
  }>;
131
+ export declare function readLongTermMemoryProjectionById(db: StateDatabase, id: string): Promise<{
132
+ row?: LongTermMemoryProjectionRecord;
133
+ degraded?: DegradedOperationResult;
134
+ }>;
123
135
  export declare function writeHeartbeatCycleTrace(db: StateDatabase, row: Omit<NewHeartbeatCycleTraceRecord, "sourceRefsJson"> & {
124
136
  sourceRefs?: SourceRef[];
125
137
  }): Promise<{
@@ -142,6 +154,33 @@ export declare function readLoopStageEventsByStage(db: StateDatabase, stage: Loo
142
154
  rows: LoopStageEventRecord[];
143
155
  degraded?: DegradedOperationResult;
144
156
  }>;
157
+ export declare function writeImpulseContextArtifact(db: StateDatabase, row: Omit<NewImpulseContextArtifactRecord, "sourceRefsJson"> & {
158
+ sourceRefs: SourceRef[];
159
+ }): Promise<{
160
+ id: string;
161
+ } | DegradedOperationResult>;
162
+ export declare function readImpulseContextArtifact(db: StateDatabase, sceneType: string, capabilityIntent?: string, platformId?: string): Promise<{
163
+ row?: ImpulseContextArtifactRecord;
164
+ degraded?: DegradedOperationResult;
165
+ }>;
166
+ export declare function writeDailyRhythmState(db: StateDatabase, row: Omit<NewDailyRhythmStateRecord, "sourceRefsJson"> & {
167
+ sourceRefs: SourceRef[];
168
+ }): Promise<{
169
+ id: string;
170
+ } | DegradedOperationResult>;
171
+ export declare function readDailyRhythmStateByDay(db: StateDatabase, day: string): Promise<{
172
+ row?: DailyRhythmStateRecord;
173
+ degraded?: DegradedOperationResult;
174
+ }>;
175
+ export declare function readConnectorCooldownState(db: StateDatabase, platformId: string, capabilityId: string): Promise<{
176
+ row?: ConnectorCooldownStateRecord;
177
+ degraded?: DegradedOperationResult;
178
+ }>;
179
+ export declare function writeConnectorCooldownState(db: StateDatabase, row: Omit<NewConnectorCooldownStateRecord, "sourceRefsJson"> & {
180
+ sourceRefs: SourceRef[];
181
+ }): Promise<{
182
+ id: string;
183
+ } | DegradedOperationResult>;
145
184
  export declare function extractSourceRefs(row: {
146
185
  sourceRefsJson: string | null;
147
186
  }): SourceRef[];
@@ -21,8 +21,8 @@
21
21
  *
22
22
  * Test coverage: tests/unit/storage/v8-state-stores.test.ts
23
23
  */
24
- import { eq, desc, like } from "drizzle-orm";
25
- import { evidenceItem, perceptionCard, judgmentVerdict, actionClosureRecord, quietDailyReview, dreamConsolidationRun, longTermMemoryProjection, heartbeatCycleTrace, loopStageEvent, } from "./db/schema/v8-entities.js";
24
+ import { eq, and, desc, like, isNull } from "drizzle-orm";
25
+ import { evidenceItem, perceptionCard, judgmentVerdict, actionClosureRecord, quietDailyReview, dreamConsolidationRun, longTermMemoryProjection, heartbeatCycleTrace, loopStageEvent, impulseContextArtifact, dailyRhythmState, connectorCooldownState, } from "./db/schema/v8-entities.js";
26
26
  // ───────────────────────────────────────────────────────────────
27
27
  // Shared helpers
28
28
  // ───────────────────────────────────────────────────────────────
@@ -99,10 +99,62 @@ export async function readEvidenceItemsByStatus(db, lifecycleStatus) {
99
99
  // ───────────────────────────────────────────────────────────────
100
100
  // PerceptionCard store
101
101
  // ───────────────────────────────────────────────────────────────
102
+ const CANONICAL_NOVELTY_CLASSES = ["new", "changed", "duplicate", "stale"];
103
+ const CANONICAL_RELEVANCE_CLASSES = ["low", "medium", "high"];
104
+ function validatePerceptionCardCanonical(row) {
105
+ // Validate noveltyClass
106
+ if (row.novelty && !CANONICAL_NOVELTY_CLASSES.includes(row.novelty)) {
107
+ return {
108
+ ok: false,
109
+ degraded: {
110
+ status: "degraded",
111
+ reason: "perception_contract_drift",
112
+ ownerStage: "perception",
113
+ sourceRefs: row.sourceRefs,
114
+ operatorNextAction: `novelty "${row.novelty}" is not canonical. Expected one of: ${CANONICAL_NOVELTY_CLASSES.join(", ")}`,
115
+ retryable: false,
116
+ },
117
+ };
118
+ }
119
+ // Validate relevanceScore range
120
+ if (row.relevance !== undefined && row.relevance !== null) {
121
+ if (row.relevance < 0 || row.relevance > 1) {
122
+ return {
123
+ ok: false,
124
+ degraded: {
125
+ status: "degraded",
126
+ reason: "perception_contract_drift",
127
+ ownerStage: "perception",
128
+ sourceRefs: row.sourceRefs,
129
+ operatorNextAction: `relevanceScore ${row.relevance} out of range [0, 1]`,
130
+ retryable: false,
131
+ },
132
+ };
133
+ }
134
+ }
135
+ // Validate relevanceClass
136
+ if (row.relevanceClass && !CANONICAL_RELEVANCE_CLASSES.includes(row.relevanceClass)) {
137
+ return {
138
+ ok: false,
139
+ degraded: {
140
+ status: "degraded",
141
+ reason: "perception_contract_drift",
142
+ ownerStage: "perception",
143
+ sourceRefs: row.sourceRefs,
144
+ operatorNextAction: `relevanceClass "${row.relevanceClass}" is not canonical. Expected one of: ${CANONICAL_RELEVANCE_CLASSES.join(", ")}`,
145
+ retryable: false,
146
+ },
147
+ };
148
+ }
149
+ return { ok: true };
150
+ }
102
151
  export async function writePerceptionCard(db, row) {
103
152
  const validated = validateSourceRefs(row.sourceRefs, "perception");
104
153
  if (!validated.ok)
105
154
  return validated.degraded;
155
+ const canonicalCheck = validatePerceptionCardCanonical(row);
156
+ if (!canonicalCheck.ok)
157
+ return canonicalCheck.degraded;
106
158
  try {
107
159
  const record = {
108
160
  ...row,
@@ -258,6 +310,7 @@ export async function writeQuietDailyReview(db, row) {
258
310
  const record = {
259
311
  ...row,
260
312
  sourceRefsJson: serializeSourceRefs(validated.record),
313
+ closureRefsJson: row.closureRefs ? serializeSourceRefs(row.closureRefs) : null,
261
314
  };
262
315
  await db.db.insert(quietDailyReview).values(record);
263
316
  return { id: row.id };
@@ -366,6 +419,23 @@ export async function writeLongTermMemoryProjection(db, row) {
366
419
  return makeDegraded("state_unreadable", "projection", "Retry projection write after DB recovery", validated.record);
367
420
  }
368
421
  }
422
+ /**
423
+ * Update an existing projection's status — required for supersession lifecycle.
424
+ * Uses UPDATE instead of INSERT to avoid primary-key conflict.
425
+ */
426
+ export async function updateLongTermMemoryProjectionStatus(db, id, status, payloadJson) {
427
+ try {
428
+ const updateData = { status, lifecycleStatus: status };
429
+ if (payloadJson !== undefined) {
430
+ updateData.payloadJson = payloadJson;
431
+ }
432
+ await db.db.update(longTermMemoryProjection).set(updateData).where(eq(longTermMemoryProjection.id, id));
433
+ return { id };
434
+ }
435
+ catch {
436
+ return makeDegraded("state_unreadable", "projection", `Retry projection status update for ${id} after DB recovery`);
437
+ }
438
+ }
369
439
  export async function readMemoryProjectionsByStatus(db, status) {
370
440
  try {
371
441
  const rows = await db.db
@@ -398,6 +468,21 @@ export async function readMemoryProjectionsByTopic(db, topicKey) {
398
468
  };
399
469
  }
400
470
  }
471
+ export async function readLongTermMemoryProjectionById(db, id) {
472
+ try {
473
+ const rows = await db.db
474
+ .select()
475
+ .from(longTermMemoryProjection)
476
+ .where(eq(longTermMemoryProjection.id, id))
477
+ .limit(1);
478
+ return { row: rows[0] };
479
+ }
480
+ catch {
481
+ return {
482
+ degraded: makeDegraded("state_unreadable", "projection", `Check state database connectivity for projection ${id}`),
483
+ };
484
+ }
485
+ }
401
486
  // ───────────────────────────────────────────────────────────────
402
487
  // HeartbeatCycleTrace store
403
488
  // ───────────────────────────────────────────────────────────────
@@ -484,6 +569,125 @@ export async function readLoopStageEventsByStage(db, stage, limit = 100) {
484
569
  }
485
570
  }
486
571
  // ───────────────────────────────────────────────────────────────
572
+ // ImpulseContextArtifact store
573
+ // ───────────────────────────────────────────────────────────────
574
+ export async function writeImpulseContextArtifact(db, row) {
575
+ const validated = validateSourceRefs(row.sourceRefs, "projection");
576
+ if (!validated.ok)
577
+ return validated.degraded;
578
+ try {
579
+ const record = {
580
+ ...row,
581
+ sourceRefsJson: serializeSourceRefs(validated.record),
582
+ };
583
+ // Upsert: delete existing then insert (SQLite primary-key conflict)
584
+ await db.db.delete(impulseContextArtifact).where(eq(impulseContextArtifact.id, row.id));
585
+ await db.db.insert(impulseContextArtifact).values(record);
586
+ return { id: row.id };
587
+ }
588
+ catch {
589
+ return makeDegraded("state_unreadable", "projection", "Retry impulse context write after DB recovery", validated.record);
590
+ }
591
+ }
592
+ export async function readImpulseContextArtifact(db, sceneType, capabilityIntent, platformId) {
593
+ try {
594
+ const conditions = [eq(impulseContextArtifact.sceneType, sceneType)];
595
+ if (capabilityIntent) {
596
+ conditions.push(eq(impulseContextArtifact.capabilityIntent, capabilityIntent));
597
+ }
598
+ else {
599
+ conditions.push(isNull(impulseContextArtifact.capabilityIntent));
600
+ }
601
+ if (platformId) {
602
+ conditions.push(eq(impulseContextArtifact.platformId, platformId));
603
+ }
604
+ else {
605
+ conditions.push(isNull(impulseContextArtifact.platformId));
606
+ }
607
+ const rows = await db.db
608
+ .select()
609
+ .from(impulseContextArtifact)
610
+ .where(and(...conditions))
611
+ .orderBy(desc(impulseContextArtifact.updatedAt))
612
+ .limit(1);
613
+ return { row: rows[0] };
614
+ }
615
+ catch {
616
+ return {
617
+ degraded: makeDegraded("state_unreadable", "projection", "Check state database connectivity"),
618
+ };
619
+ }
620
+ }
621
+ // ───────────────────────────────────────────────────────────────
622
+ // DailyRhythmState store
623
+ // ───────────────────────────────────────────────────────────────
624
+ export async function writeDailyRhythmState(db, row) {
625
+ const validated = validateSourceRefs(row.sourceRefs, "dream");
626
+ if (!validated.ok)
627
+ return validated.degraded;
628
+ try {
629
+ const record = {
630
+ ...row,
631
+ sourceRefsJson: serializeSourceRefs(validated.record),
632
+ };
633
+ await db.db.delete(dailyRhythmState).where(eq(dailyRhythmState.id, row.id));
634
+ await db.db.insert(dailyRhythmState).values(record);
635
+ return { id: row.id };
636
+ }
637
+ catch {
638
+ return makeDegraded("state_unreadable", "dream", "Retry daily rhythm state write after DB recovery", validated.record);
639
+ }
640
+ }
641
+ export async function readDailyRhythmStateByDay(db, day) {
642
+ try {
643
+ const rows = await db.db
644
+ .select()
645
+ .from(dailyRhythmState)
646
+ .where(eq(dailyRhythmState.day, day))
647
+ .orderBy(desc(dailyRhythmState.updatedAt))
648
+ .limit(1);
649
+ return { row: rows[0] };
650
+ }
651
+ catch {
652
+ return {
653
+ degraded: makeDegraded("state_unreadable", "dream", `Check state database connectivity for day=${day}`),
654
+ };
655
+ }
656
+ }
657
+ export async function readConnectorCooldownState(db, platformId, capabilityId) {
658
+ try {
659
+ const rows = await db.db
660
+ .select()
661
+ .from(connectorCooldownState)
662
+ .where(and(eq(connectorCooldownState.platformId, platformId), eq(connectorCooldownState.capabilityId, capabilityId)))
663
+ .orderBy(desc(connectorCooldownState.updatedAt))
664
+ .limit(1);
665
+ return { row: rows[0] };
666
+ }
667
+ catch {
668
+ return {
669
+ degraded: makeDegraded("state_unreadable", "ingestion", `Check state database connectivity for cooldown ${platformId}:${capabilityId}`),
670
+ };
671
+ }
672
+ }
673
+ export async function writeConnectorCooldownState(db, row) {
674
+ const validated = validateSourceRefs(row.sourceRefs, "ingestion");
675
+ if (!validated.ok)
676
+ return validated.degraded;
677
+ try {
678
+ const record = {
679
+ ...row,
680
+ sourceRefsJson: serializeSourceRefs(validated.record),
681
+ };
682
+ await db.db.delete(connectorCooldownState).where(eq(connectorCooldownState.id, row.id));
683
+ await db.db.insert(connectorCooldownState).values(record);
684
+ return { id: row.id };
685
+ }
686
+ catch {
687
+ return makeDegraded("state_unreadable", "ingestion", "Retry connector cooldown state write after DB recovery", validated.record);
688
+ }
689
+ }
690
+ // ───────────────────────────────────────────────────────────────
487
691
  // SourceRef round-trip helper (for tests and consumers)
488
692
  // ───────────────────────────────────────────────────────────────
489
693
  export function extractSourceRefs(row) {